mirror of
https://github.com/bringout/oca-workflow-process.git
synced 2026-04-19 21:31:59 +02:00
Initial commit: OCA Workflow Process packages (456 packages)
This commit is contained in:
commit
d366e42934
18799 changed files with 1284507 additions and 0 deletions
|
|
@ -0,0 +1,11 @@
|
|||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
|
||||
|
||||
from . import purchase_request_allocation
|
||||
from . import orderpoint
|
||||
from . import purchase_request
|
||||
from . import purchase_request_line
|
||||
from . import stock_rule
|
||||
from . import product_template
|
||||
from . import purchase_order
|
||||
from . import stock_move
|
||||
from . import stock_move_line
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# Copyright 2018-2019 ForgeFlow, S.L.
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class Orderpoint(models.Model):
|
||||
_inherit = "stock.warehouse.orderpoint"
|
||||
|
||||
def _quantity_in_progress(self):
|
||||
res = super(Orderpoint, self)._quantity_in_progress()
|
||||
for prline in self.env["purchase.request.line"].search(
|
||||
[
|
||||
(
|
||||
"request_id.state",
|
||||
"in",
|
||||
("draft", "approved", "to_approve", "in_progress"),
|
||||
),
|
||||
("orderpoint_id", "in", self.ids),
|
||||
("purchase_state", "=", False),
|
||||
]
|
||||
):
|
||||
res[prline.orderpoint_id.id] += prline.product_uom_id._compute_quantity(
|
||||
prline.product_qty, prline.orderpoint_id.product_uom, round=False
|
||||
)
|
||||
return res
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# Copyright 2018-2019 ForgeFlow, S.L.
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = "product.template"
|
||||
|
||||
purchase_request = fields.Boolean(
|
||||
help="Check this box to generate Purchase Request instead of "
|
||||
"generating Requests For Quotation from procurement.",
|
||||
company_dependent=True,
|
||||
)
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
# Copyright 2018-2019 ForgeFlow, S.L.
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
|
||||
|
||||
from odoo import _, api, exceptions, fields, models
|
||||
|
||||
|
||||
class PurchaseOrder(models.Model):
|
||||
_inherit = "purchase.order"
|
||||
|
||||
def _purchase_request_confirm_message_content(self, request, request_dict=None):
|
||||
self.ensure_one()
|
||||
if not request_dict:
|
||||
request_dict = {}
|
||||
title = _("Order confirmation %(po_name)s for your Request %(pr_name)s") % {
|
||||
"po_name": self.name,
|
||||
"pr_name": request.name,
|
||||
}
|
||||
message = "<h3>%s</h3><ul>" % title
|
||||
message += _(
|
||||
"The following requested items from Purchase Request %(pr_name)s "
|
||||
"have now been confirmed in Purchase Order %(po_name)s:"
|
||||
) % {
|
||||
"po_name": self.name,
|
||||
"pr_name": request.name,
|
||||
}
|
||||
|
||||
for line in request_dict.values():
|
||||
message += _(
|
||||
"<li><b>%(prl_name)s</b>: Ordered quantity %(prl_qty)s %(prl_uom)s, "
|
||||
"Planned date %(prl_date_planned)s</li>"
|
||||
) % {
|
||||
"prl_name": line["name"],
|
||||
"prl_qty": line["product_qty"],
|
||||
"prl_uom": line["product_uom"],
|
||||
"prl_date_planned": line["date_planned"],
|
||||
}
|
||||
message += "</ul>"
|
||||
return message
|
||||
|
||||
def _purchase_request_confirm_message(self):
|
||||
request_obj = self.env["purchase.request"]
|
||||
for po in self:
|
||||
requests_dict = {}
|
||||
for line in po.order_line:
|
||||
for request_line in line.sudo().purchase_request_lines:
|
||||
request_id = request_line.request_id.id
|
||||
if request_id not in requests_dict:
|
||||
requests_dict[request_id] = {}
|
||||
date_planned = "%s" % line.date_planned
|
||||
data = {
|
||||
"name": request_line.name,
|
||||
"product_qty": line.product_qty,
|
||||
"product_uom": line.product_uom.name,
|
||||
"date_planned": date_planned,
|
||||
}
|
||||
requests_dict[request_id][request_line.id] = data
|
||||
for request_id in requests_dict:
|
||||
request = request_obj.sudo().browse(request_id)
|
||||
message = po._purchase_request_confirm_message_content(
|
||||
request, requests_dict[request_id]
|
||||
)
|
||||
request.message_post(
|
||||
body=message,
|
||||
subtype_id=self.env.ref(
|
||||
"purchase_request.mt_request_po_confirmed"
|
||||
).id,
|
||||
)
|
||||
return True
|
||||
|
||||
def _purchase_request_line_check(self):
|
||||
for po in self:
|
||||
for line in po.order_line:
|
||||
for request_line in line.purchase_request_lines:
|
||||
if request_line.sudo().purchase_state == "done":
|
||||
raise exceptions.UserError(
|
||||
_("Purchase Request %s has already been completed")
|
||||
% (request_line.request_id.name)
|
||||
)
|
||||
return True
|
||||
|
||||
def button_confirm(self):
|
||||
self._purchase_request_line_check()
|
||||
res = super(PurchaseOrder, self).button_confirm()
|
||||
self._purchase_request_confirm_message()
|
||||
return res
|
||||
|
||||
def unlink(self):
|
||||
alloc_to_unlink = self.env["purchase.request.allocation"]
|
||||
for rec in self:
|
||||
for alloc in (
|
||||
rec.order_line.mapped("purchase_request_lines")
|
||||
.mapped("purchase_request_allocation_ids")
|
||||
.filtered(
|
||||
lambda alloc, rec=rec: alloc.purchase_line_id.order_id.id == rec.id
|
||||
)
|
||||
):
|
||||
alloc_to_unlink += alloc
|
||||
res = super().unlink()
|
||||
alloc_to_unlink.unlink()
|
||||
return res
|
||||
|
||||
|
||||
class PurchaseOrderLine(models.Model):
|
||||
_inherit = "purchase.order.line"
|
||||
|
||||
purchase_request_lines = fields.Many2many(
|
||||
comodel_name="purchase.request.line",
|
||||
relation="purchase_request_purchase_order_line_rel",
|
||||
column1="purchase_order_line_id",
|
||||
column2="purchase_request_line_id",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
)
|
||||
|
||||
purchase_request_allocation_ids = fields.One2many(
|
||||
comodel_name="purchase.request.allocation",
|
||||
inverse_name="purchase_line_id",
|
||||
string="Purchase Request Allocation",
|
||||
copy=False,
|
||||
)
|
||||
|
||||
def action_open_request_line_tree_view(self):
|
||||
"""
|
||||
:return dict: dictionary value for created view
|
||||
"""
|
||||
request_line_ids = []
|
||||
for line in self:
|
||||
request_line_ids += line.purchase_request_lines.ids
|
||||
|
||||
domain = [("id", "in", request_line_ids)]
|
||||
|
||||
return {
|
||||
"name": _("Purchase Request Lines"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "purchase.request.line",
|
||||
"view_mode": "tree,form",
|
||||
"domain": domain,
|
||||
}
|
||||
|
||||
def _prepare_stock_moves(self, picking):
|
||||
self.ensure_one()
|
||||
val = super(PurchaseOrderLine, self)._prepare_stock_moves(picking)
|
||||
all_list = []
|
||||
for v in val:
|
||||
all_ids = self.env["purchase.request.allocation"].search(
|
||||
[("purchase_line_id", "=", v["purchase_line_id"])]
|
||||
)
|
||||
for all_id in all_ids:
|
||||
all_list.append((4, all_id.id))
|
||||
v["purchase_request_allocation_ids"] = all_list
|
||||
return val
|
||||
|
||||
def update_service_allocations(self, prev_qty_received):
|
||||
for rec in self:
|
||||
allocation = self.env["purchase.request.allocation"].search(
|
||||
[
|
||||
("purchase_line_id", "=", rec.id),
|
||||
("purchase_line_id.product_id.type", "=", "service"),
|
||||
]
|
||||
)
|
||||
if not allocation:
|
||||
return
|
||||
qty_left = rec.qty_received - prev_qty_received
|
||||
for alloc in allocation:
|
||||
allocated_product_qty = alloc.allocated_product_qty
|
||||
if not qty_left:
|
||||
alloc.purchase_request_line_id._compute_qty()
|
||||
break
|
||||
if alloc.open_product_qty <= qty_left:
|
||||
allocated_product_qty += alloc.open_product_qty
|
||||
qty_left -= alloc.open_product_qty
|
||||
alloc._notify_allocation(alloc.open_product_qty)
|
||||
else:
|
||||
allocated_product_qty += qty_left
|
||||
alloc._notify_allocation(qty_left)
|
||||
qty_left = 0
|
||||
alloc.write({"allocated_product_qty": allocated_product_qty})
|
||||
|
||||
message_data = self._prepare_request_message_data(
|
||||
alloc, alloc.purchase_request_line_id, allocated_product_qty
|
||||
)
|
||||
message = self._purchase_request_confirm_done_message_content(
|
||||
message_data
|
||||
)
|
||||
if message:
|
||||
alloc.purchase_request_line_id.request_id.message_post(
|
||||
body=message, subtype_id=self.env.ref("mail.mt_note").id
|
||||
)
|
||||
|
||||
alloc.purchase_request_line_id._compute_qty()
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def _purchase_request_confirm_done_message_content(self, message_data):
|
||||
title = _("Service confirmation for Request %s") % (
|
||||
message_data["request_name"]
|
||||
)
|
||||
message = "<h3>%s</h3>" % title
|
||||
message += _(
|
||||
"The following requested services from Purchase"
|
||||
" Request %(request_name)s requested by %(requestor)s "
|
||||
"have now been received:"
|
||||
) % {
|
||||
"request_name": message_data["request_name"],
|
||||
"requestor": message_data["requestor"],
|
||||
}
|
||||
message += "<ul>"
|
||||
message += _(
|
||||
"<li><b>%(product_name)s</b>: "
|
||||
"Received quantity %(product_qty)s %(product_uom)s</li>"
|
||||
) % {
|
||||
"product_name": message_data["product_name"],
|
||||
"product_qty": message_data["product_qty"],
|
||||
"product_uom": message_data["product_uom"],
|
||||
}
|
||||
message += "</ul>"
|
||||
return message
|
||||
|
||||
def _prepare_request_message_data(self, alloc, request_line, allocated_qty):
|
||||
return {
|
||||
"request_name": request_line.request_id.name,
|
||||
"product_name": request_line.product_id.name_get()[0][1],
|
||||
"product_qty": allocated_qty,
|
||||
"product_uom": alloc.product_uom_id.name,
|
||||
"requestor": request_line.request_id.requested_by.partner_id.name,
|
||||
}
|
||||
|
||||
def write(self, vals):
|
||||
# As services do not generate stock move this tweak is required
|
||||
# to allocate them.
|
||||
prev_qty_received = {}
|
||||
if vals.get("qty_received", False):
|
||||
service_lines = self.filtered(lambda l: l.product_id.type == "service")
|
||||
for line in service_lines:
|
||||
prev_qty_received[line.id] = line.qty_received
|
||||
res = super(PurchaseOrderLine, self).write(vals)
|
||||
if prev_qty_received:
|
||||
for line in service_lines:
|
||||
line.update_service_allocations(prev_qty_received[line.id])
|
||||
return res
|
||||
|
|
@ -0,0 +1,317 @@
|
|||
# Copyright 2018-2019 ForgeFlow, S.L.
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_STATES = [
|
||||
("draft", "Draft"),
|
||||
("to_approve", "To be approved"),
|
||||
("approved", "Approved"),
|
||||
("in_progress", "In progress"),
|
||||
("done", "Done"),
|
||||
("rejected", "Rejected"),
|
||||
]
|
||||
|
||||
|
||||
class PurchaseRequest(models.Model):
|
||||
|
||||
_name = "purchase.request"
|
||||
_description = "Purchase Request"
|
||||
_mail_post_access = "read"
|
||||
_inherit = ["mail.thread", "mail.activity.mixin"]
|
||||
_order = "id desc"
|
||||
|
||||
@api.model
|
||||
def _company_get(self):
|
||||
return self.env["res.company"].browse(self.env.company.id)
|
||||
|
||||
@api.model
|
||||
def _get_default_requested_by(self):
|
||||
return self.env["res.users"].browse(self.env.uid)
|
||||
|
||||
@api.model
|
||||
def _get_default_name(self):
|
||||
return self.env["ir.sequence"].next_by_code("purchase.request")
|
||||
|
||||
@api.model
|
||||
def _default_picking_type(self):
|
||||
type_obj = self.env["stock.picking.type"]
|
||||
company_id = self.env.context.get("company_id") or self.env.company.id
|
||||
types = type_obj.search(
|
||||
[("code", "=", "incoming"), ("warehouse_id.company_id", "=", company_id)]
|
||||
)
|
||||
if not types:
|
||||
types = type_obj.search(
|
||||
[("code", "=", "incoming"), ("warehouse_id", "=", False)]
|
||||
)
|
||||
return types[:1]
|
||||
|
||||
@api.depends("state")
|
||||
def _compute_is_editable(self):
|
||||
for rec in self:
|
||||
if rec.state in (
|
||||
"to_approve",
|
||||
"approved",
|
||||
"rejected",
|
||||
"in_progress",
|
||||
"done",
|
||||
):
|
||||
rec.is_editable = False
|
||||
else:
|
||||
rec.is_editable = True
|
||||
|
||||
name = fields.Char(
|
||||
string="Request Reference",
|
||||
required=True,
|
||||
default=lambda self: _("New"),
|
||||
tracking=True,
|
||||
)
|
||||
is_name_editable = fields.Boolean(
|
||||
default=lambda self: self.env.user.has_group("base.group_no_one"),
|
||||
)
|
||||
origin = fields.Char(string="Source Document")
|
||||
date_start = fields.Date(
|
||||
string="Creation date",
|
||||
help="Date when the user initiated the request.",
|
||||
default=fields.Date.context_today,
|
||||
tracking=True,
|
||||
)
|
||||
requested_by = fields.Many2one(
|
||||
comodel_name="res.users",
|
||||
required=True,
|
||||
copy=False,
|
||||
tracking=True,
|
||||
default=_get_default_requested_by,
|
||||
index=True,
|
||||
)
|
||||
assigned_to = fields.Many2one(
|
||||
comodel_name="res.users",
|
||||
string="Approver",
|
||||
tracking=True,
|
||||
domain=lambda self: [
|
||||
(
|
||||
"groups_id",
|
||||
"in",
|
||||
self.env.ref("purchase_request.group_purchase_request_manager").id,
|
||||
)
|
||||
],
|
||||
index=True,
|
||||
)
|
||||
description = fields.Text()
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.company",
|
||||
required=False,
|
||||
default=_company_get,
|
||||
tracking=True,
|
||||
)
|
||||
line_ids = fields.One2many(
|
||||
comodel_name="purchase.request.line",
|
||||
inverse_name="request_id",
|
||||
string="Products to Purchase",
|
||||
readonly=True,
|
||||
copy=True,
|
||||
tracking=True,
|
||||
states={"draft": [("readonly", False)]},
|
||||
)
|
||||
product_id = fields.Many2one(
|
||||
comodel_name="product.product",
|
||||
related="line_ids.product_id",
|
||||
string="Product",
|
||||
readonly=True,
|
||||
)
|
||||
state = fields.Selection(
|
||||
selection=_STATES,
|
||||
string="Status",
|
||||
index=True,
|
||||
tracking=True,
|
||||
required=True,
|
||||
copy=False,
|
||||
default="draft",
|
||||
)
|
||||
is_editable = fields.Boolean(compute="_compute_is_editable", readonly=True)
|
||||
to_approve_allowed = fields.Boolean(compute="_compute_to_approve_allowed")
|
||||
picking_type_id = fields.Many2one(
|
||||
comodel_name="stock.picking.type",
|
||||
string="Picking Type",
|
||||
required=True,
|
||||
default=_default_picking_type,
|
||||
)
|
||||
group_id = fields.Many2one(
|
||||
comodel_name="procurement.group",
|
||||
string="Procurement Group",
|
||||
copy=False,
|
||||
index=True,
|
||||
)
|
||||
line_count = fields.Integer(
|
||||
string="Purchase Request Line count",
|
||||
compute="_compute_line_count",
|
||||
readonly=True,
|
||||
)
|
||||
move_count = fields.Integer(
|
||||
string="Stock Move count", compute="_compute_move_count", readonly=True
|
||||
)
|
||||
purchase_count = fields.Integer(
|
||||
string="Purchases count", compute="_compute_purchase_count", readonly=True
|
||||
)
|
||||
currency_id = fields.Many2one(related="company_id.currency_id", readonly=True)
|
||||
estimated_cost = fields.Monetary(
|
||||
compute="_compute_estimated_cost",
|
||||
string="Total Estimated Cost",
|
||||
store=True,
|
||||
)
|
||||
|
||||
@api.depends("line_ids", "line_ids.estimated_cost")
|
||||
def _compute_estimated_cost(self):
|
||||
for rec in self:
|
||||
rec.estimated_cost = sum(rec.line_ids.mapped("estimated_cost"))
|
||||
|
||||
@api.depends("line_ids")
|
||||
def _compute_purchase_count(self):
|
||||
for rec in self:
|
||||
rec.purchase_count = len(rec.mapped("line_ids.purchase_lines.order_id"))
|
||||
|
||||
def action_view_purchase_order(self):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("purchase.purchase_rfq")
|
||||
lines = self.mapped("line_ids.purchase_lines.order_id")
|
||||
if len(lines) > 1:
|
||||
action["domain"] = [("id", "in", lines.ids)]
|
||||
elif lines:
|
||||
action["views"] = [
|
||||
(self.env.ref("purchase.purchase_order_form").id, "form")
|
||||
]
|
||||
action["res_id"] = lines.id
|
||||
return action
|
||||
|
||||
@api.depends("line_ids")
|
||||
def _compute_move_count(self):
|
||||
for rec in self:
|
||||
rec.move_count = len(
|
||||
rec.mapped("line_ids.purchase_request_allocation_ids.stock_move_id")
|
||||
)
|
||||
|
||||
def action_view_stock_picking(self):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id(
|
||||
"stock.action_picking_tree_all"
|
||||
)
|
||||
# remove default filters
|
||||
action["context"] = {}
|
||||
lines = self.mapped(
|
||||
"line_ids.purchase_request_allocation_ids.stock_move_id.picking_id"
|
||||
)
|
||||
if len(lines) > 1:
|
||||
action["domain"] = [("id", "in", lines.ids)]
|
||||
elif lines:
|
||||
action["views"] = [(self.env.ref("stock.view_picking_form").id, "form")]
|
||||
action["res_id"] = lines.id
|
||||
return action
|
||||
|
||||
@api.depends("line_ids")
|
||||
def _compute_line_count(self):
|
||||
for rec in self:
|
||||
rec.line_count = len(rec.mapped("line_ids"))
|
||||
|
||||
def action_view_purchase_request_line(self):
|
||||
action = (
|
||||
self.env.ref("purchase_request.purchase_request_line_form_action")
|
||||
.sudo()
|
||||
.read()[0]
|
||||
)
|
||||
lines = self.mapped("line_ids")
|
||||
if len(lines) > 1:
|
||||
action["domain"] = [("id", "in", lines.ids)]
|
||||
elif lines:
|
||||
action["views"] = [
|
||||
(self.env.ref("purchase_request.purchase_request_line_form").id, "form")
|
||||
]
|
||||
action["res_id"] = lines.ids[0]
|
||||
return action
|
||||
|
||||
@api.depends("state", "line_ids.product_qty", "line_ids.cancelled")
|
||||
def _compute_to_approve_allowed(self):
|
||||
for rec in self:
|
||||
rec.to_approve_allowed = rec.state == "draft" and any(
|
||||
not line.cancelled and line.product_qty for line in rec.line_ids
|
||||
)
|
||||
|
||||
def copy(self, default=None):
|
||||
default = dict(default or {})
|
||||
self.ensure_one()
|
||||
default.update({"state": "draft", "name": self._get_default_name()})
|
||||
return super(PurchaseRequest, self).copy(default)
|
||||
|
||||
@api.model
|
||||
def _get_partner_id(self, request):
|
||||
user_id = request.assigned_to or self.env.user
|
||||
return user_id.partner_id.id
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get("name", _("New")) == _("New"):
|
||||
vals["name"] = self._get_default_name()
|
||||
requests = super(PurchaseRequest, self).create(vals_list)
|
||||
for vals, request in zip(vals_list, requests):
|
||||
if vals.get("assigned_to"):
|
||||
partner_id = self._get_partner_id(request)
|
||||
request.message_subscribe(partner_ids=[partner_id])
|
||||
return requests
|
||||
|
||||
def write(self, vals):
|
||||
res = super(PurchaseRequest, self).write(vals)
|
||||
for request in self:
|
||||
if vals.get("assigned_to"):
|
||||
partner_id = self._get_partner_id(request)
|
||||
request.message_subscribe(partner_ids=[partner_id])
|
||||
return res
|
||||
|
||||
def _can_be_deleted(self):
|
||||
self.ensure_one()
|
||||
return self.state == "draft"
|
||||
|
||||
def unlink(self):
|
||||
for request in self:
|
||||
if not request._can_be_deleted():
|
||||
raise UserError(
|
||||
_("You cannot delete a purchase request which is not draft.")
|
||||
)
|
||||
return super(PurchaseRequest, self).unlink()
|
||||
|
||||
def button_draft(self):
|
||||
self.mapped("line_ids").do_uncancel()
|
||||
return self.write({"state": "draft"})
|
||||
|
||||
def button_to_approve(self):
|
||||
self.to_approve_allowed_check()
|
||||
return self.write({"state": "to_approve"})
|
||||
|
||||
def button_approved(self):
|
||||
return self.write({"state": "approved"})
|
||||
|
||||
def button_rejected(self):
|
||||
self.mapped("line_ids").do_cancel()
|
||||
return self.write({"state": "rejected"})
|
||||
|
||||
def button_in_progress(self):
|
||||
return self.write({"state": "in_progress"})
|
||||
|
||||
def button_done(self):
|
||||
return self.write({"state": "done"})
|
||||
|
||||
def check_auto_reject(self):
|
||||
"""When all lines are cancelled the purchase request should be
|
||||
auto-rejected."""
|
||||
for pr in self:
|
||||
if not pr.line_ids.filtered(lambda l: l.cancelled is False):
|
||||
pr.write({"state": "rejected"})
|
||||
|
||||
def to_approve_allowed_check(self):
|
||||
for rec in self:
|
||||
if not rec.to_approve_allowed:
|
||||
raise UserError(
|
||||
_(
|
||||
"You can't request an approval for a purchase request "
|
||||
"which is empty. (%s)"
|
||||
)
|
||||
% rec.name
|
||||
)
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
# Copyright 2019 ForgeFlow, S.L.
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class PurchaseRequestAllocation(models.Model):
|
||||
_name = "purchase.request.allocation"
|
||||
_description = "Purchase Request Allocation"
|
||||
|
||||
purchase_request_line_id = fields.Many2one(
|
||||
string="Purchase Request Line",
|
||||
comodel_name="purchase.request.line",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
copy=True,
|
||||
index=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
string="Company",
|
||||
comodel_name="res.company",
|
||||
readonly=True,
|
||||
related="purchase_request_line_id.request_id.company_id",
|
||||
store=True,
|
||||
index=True,
|
||||
)
|
||||
stock_move_id = fields.Many2one(
|
||||
string="Stock Move",
|
||||
comodel_name="stock.move",
|
||||
ondelete="cascade",
|
||||
index=True,
|
||||
)
|
||||
purchase_line_id = fields.Many2one(
|
||||
string="Purchase Line",
|
||||
comodel_name="purchase.order.line",
|
||||
copy=True,
|
||||
ondelete="cascade",
|
||||
help="Service Purchase Order Line",
|
||||
index=True,
|
||||
)
|
||||
product_id = fields.Many2one(
|
||||
string="Product",
|
||||
comodel_name="product.product",
|
||||
related="purchase_request_line_id.product_id",
|
||||
readonly=True,
|
||||
)
|
||||
product_uom_id = fields.Many2one(
|
||||
string="UoM",
|
||||
comodel_name="uom.uom",
|
||||
related="purchase_request_line_id.product_uom_id",
|
||||
readonly=True,
|
||||
required=True,
|
||||
)
|
||||
requested_product_uom_qty = fields.Float(
|
||||
string="Requested Quantity",
|
||||
help="Quantity of the purchase request line allocated to the"
|
||||
"stock move, in the UoM of the Purchase Request Line",
|
||||
)
|
||||
|
||||
allocated_product_qty = fields.Float(
|
||||
string="Allocated Quantity",
|
||||
copy=False,
|
||||
help="Quantity of the purchase request line allocated to the stock"
|
||||
"move, in the default UoM of the product",
|
||||
)
|
||||
open_product_qty = fields.Float(
|
||||
string="Open Quantity", compute="_compute_open_product_qty"
|
||||
)
|
||||
|
||||
purchase_state = fields.Selection(related="purchase_line_id.state")
|
||||
|
||||
@api.depends(
|
||||
"requested_product_uom_qty",
|
||||
"allocated_product_qty",
|
||||
"stock_move_id",
|
||||
"stock_move_id.state",
|
||||
"stock_move_id.product_uom_qty",
|
||||
"stock_move_id.move_line_ids.qty_done",
|
||||
"purchase_line_id",
|
||||
"purchase_line_id.qty_received",
|
||||
"purchase_state",
|
||||
)
|
||||
def _compute_open_product_qty(self):
|
||||
for rec in self:
|
||||
if rec.purchase_state in ["cancel", "done"]:
|
||||
rec.open_product_qty = 0.0
|
||||
else:
|
||||
rec.open_product_qty = (
|
||||
rec.requested_product_uom_qty - rec.allocated_product_qty
|
||||
)
|
||||
if rec.open_product_qty < 0.0:
|
||||
rec.open_product_qty = 0.0
|
||||
|
||||
@api.model
|
||||
def _purchase_request_confirm_done_message_content(self, message_data):
|
||||
message = ""
|
||||
message += _(
|
||||
"From last reception this quantity has been "
|
||||
"allocated to this purchase request"
|
||||
)
|
||||
message += "<ul>"
|
||||
message += _(
|
||||
"<li><b>%(product_name)s</b>: "
|
||||
"Received quantity %(product_qty)s %(product_uom)s</li>"
|
||||
) % {
|
||||
"product_name": message_data["product_name"],
|
||||
"product_qty": message_data["product_qty"],
|
||||
"product_uom": message_data["product_uom"],
|
||||
}
|
||||
message += "</ul>"
|
||||
return message
|
||||
|
||||
def _prepare_message_data(self, po_line, request, allocated_qty):
|
||||
return {
|
||||
"request_name": request.name,
|
||||
"po_name": po_line.order_id.name,
|
||||
"product_name": po_line.product_id.name_get()[0][1],
|
||||
"product_qty": allocated_qty,
|
||||
"product_uom": po_line.product_uom.name,
|
||||
}
|
||||
|
||||
def _notify_allocation(self, allocated_qty):
|
||||
if not allocated_qty:
|
||||
return
|
||||
for allocation in self:
|
||||
request = allocation.purchase_request_line_id.request_id
|
||||
po_line = allocation.purchase_line_id
|
||||
message_data = self._prepare_message_data(po_line, request, allocated_qty)
|
||||
message = self._purchase_request_confirm_done_message_content(message_data)
|
||||
request.message_post(
|
||||
body=message, subtype_id=self.env.ref("mail.mt_note").id
|
||||
)
|
||||
|
|
@ -0,0 +1,433 @@
|
|||
# Copyright 2018-2019 ForgeFlow, S.L.
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_STATES = [
|
||||
("draft", "Draft"),
|
||||
("to_approve", "To be approved"),
|
||||
("approved", "Approved"),
|
||||
("in_progress", "In progress"),
|
||||
("done", "Done"),
|
||||
("rejected", "Rejected"),
|
||||
]
|
||||
|
||||
|
||||
class PurchaseRequestLine(models.Model):
|
||||
|
||||
_name = "purchase.request.line"
|
||||
_description = "Purchase Request Line"
|
||||
_inherit = ["mail.thread", "mail.activity.mixin", "analytic.mixin"]
|
||||
_order = "id desc"
|
||||
|
||||
name = fields.Char(string="Description", tracking=True)
|
||||
product_uom_id = fields.Many2one(
|
||||
comodel_name="uom.uom",
|
||||
string="UoM",
|
||||
tracking=True,
|
||||
domain="[('category_id', '=', product_uom_category_id)]",
|
||||
)
|
||||
product_uom_category_id = fields.Many2one(related="product_id.uom_id.category_id")
|
||||
product_qty = fields.Float(
|
||||
string="Quantity", tracking=True, digits="Product Unit of Measure"
|
||||
)
|
||||
request_id = fields.Many2one(
|
||||
comodel_name="purchase.request",
|
||||
string="Purchase Request",
|
||||
ondelete="cascade",
|
||||
readonly=True,
|
||||
index=True,
|
||||
auto_join=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.company",
|
||||
related="request_id.company_id",
|
||||
string="Company",
|
||||
store=True,
|
||||
index=True,
|
||||
)
|
||||
requested_by = fields.Many2one(
|
||||
comodel_name="res.users",
|
||||
related="request_id.requested_by",
|
||||
string="Requested by",
|
||||
store=True,
|
||||
)
|
||||
assigned_to = fields.Many2one(
|
||||
comodel_name="res.users",
|
||||
related="request_id.assigned_to",
|
||||
string="Assigned to",
|
||||
store=True,
|
||||
)
|
||||
date_start = fields.Date(related="request_id.date_start", store=True)
|
||||
description = fields.Text(
|
||||
related="request_id.description",
|
||||
string="PR Description",
|
||||
store=True,
|
||||
readonly=False,
|
||||
)
|
||||
origin = fields.Char(
|
||||
related="request_id.origin", string="Source Document", store=True
|
||||
)
|
||||
date_required = fields.Date(
|
||||
string="Request Date",
|
||||
required=True,
|
||||
tracking=True,
|
||||
default=fields.Date.context_today,
|
||||
)
|
||||
is_editable = fields.Boolean(compute="_compute_is_editable", readonly=True)
|
||||
specifications = fields.Text()
|
||||
request_state = fields.Selection(
|
||||
string="Request state",
|
||||
related="request_id.state",
|
||||
store=True,
|
||||
)
|
||||
supplier_id = fields.Many2one(
|
||||
comodel_name="res.partner",
|
||||
string="Preferred supplier",
|
||||
compute="_compute_supplier_id",
|
||||
compute_sudo=True,
|
||||
store=True,
|
||||
)
|
||||
cancelled = fields.Boolean(readonly=True, default=False, copy=False)
|
||||
|
||||
purchased_qty = fields.Float(
|
||||
string="RFQ/PO Qty",
|
||||
digits="Product Unit of Measure",
|
||||
compute="_compute_purchased_qty",
|
||||
)
|
||||
purchase_lines = fields.Many2many(
|
||||
comodel_name="purchase.order.line",
|
||||
relation="purchase_request_purchase_order_line_rel",
|
||||
column1="purchase_request_line_id",
|
||||
column2="purchase_order_line_id",
|
||||
string="Purchase Order Lines",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
)
|
||||
purchase_state = fields.Selection(
|
||||
compute="_compute_purchase_state",
|
||||
string="Purchase Status",
|
||||
selection=lambda self: self.env["purchase.order"]
|
||||
._fields["state"]
|
||||
._description_selection(self.env),
|
||||
store=True,
|
||||
)
|
||||
move_dest_ids = fields.One2many(
|
||||
comodel_name="stock.move",
|
||||
inverse_name="created_purchase_request_line_id",
|
||||
string="Downstream Moves",
|
||||
)
|
||||
|
||||
orderpoint_id = fields.Many2one(
|
||||
comodel_name="stock.warehouse.orderpoint", string="Orderpoint"
|
||||
)
|
||||
purchase_request_allocation_ids = fields.One2many(
|
||||
comodel_name="purchase.request.allocation",
|
||||
inverse_name="purchase_request_line_id",
|
||||
string="Purchase Request Allocation",
|
||||
)
|
||||
|
||||
qty_in_progress = fields.Float(
|
||||
digits="Product Unit of Measure",
|
||||
readonly=True,
|
||||
compute="_compute_qty",
|
||||
store=True,
|
||||
help="Quantity in progress.",
|
||||
)
|
||||
qty_done = fields.Float(
|
||||
digits="Product Unit of Measure",
|
||||
readonly=True,
|
||||
compute="_compute_qty",
|
||||
store=True,
|
||||
help="Quantity completed",
|
||||
)
|
||||
qty_cancelled = fields.Float(
|
||||
digits="Product Unit of Measure",
|
||||
readonly=True,
|
||||
compute="_compute_qty_cancelled",
|
||||
store=True,
|
||||
help="Quantity cancelled",
|
||||
)
|
||||
qty_to_buy = fields.Boolean(
|
||||
compute="_compute_qty_to_buy",
|
||||
string="There is some pending qty to buy",
|
||||
store=True,
|
||||
)
|
||||
pending_qty_to_receive = fields.Float(
|
||||
compute="_compute_qty_to_buy",
|
||||
digits="Product Unit of Measure",
|
||||
copy=False,
|
||||
string="Pending Qty to Receive",
|
||||
store=True,
|
||||
)
|
||||
estimated_cost = fields.Monetary(
|
||||
currency_field="currency_id",
|
||||
default=0.0,
|
||||
help="Estimated cost of Purchase Request Line, not propagated to PO.",
|
||||
)
|
||||
currency_id = fields.Many2one(related="company_id.currency_id", readonly=True)
|
||||
product_id = fields.Many2one(
|
||||
comodel_name="product.product",
|
||||
string="Product",
|
||||
domain=[("purchase_ok", "=", True)],
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
"purchase_request_allocation_ids",
|
||||
"purchase_request_allocation_ids.stock_move_id.state",
|
||||
"purchase_request_allocation_ids.stock_move_id",
|
||||
"purchase_request_allocation_ids.purchase_line_id",
|
||||
"purchase_request_allocation_ids.purchase_line_id.state",
|
||||
"request_id.state",
|
||||
"product_qty",
|
||||
)
|
||||
def _compute_qty_to_buy(self):
|
||||
for pr in self:
|
||||
qty_to_buy = sum(pr.mapped("product_qty")) - sum(pr.mapped("qty_done"))
|
||||
pr.qty_to_buy = qty_to_buy > 0.0
|
||||
pr.pending_qty_to_receive = qty_to_buy
|
||||
|
||||
@api.depends(
|
||||
"purchase_request_allocation_ids",
|
||||
"purchase_request_allocation_ids.stock_move_id.state",
|
||||
"purchase_request_allocation_ids.stock_move_id",
|
||||
"purchase_request_allocation_ids.purchase_line_id.state",
|
||||
"purchase_request_allocation_ids.purchase_line_id",
|
||||
)
|
||||
def _compute_qty(self):
|
||||
for request in self:
|
||||
done_qty = sum(
|
||||
request.purchase_request_allocation_ids.mapped("allocated_product_qty")
|
||||
)
|
||||
open_qty = sum(
|
||||
request.purchase_request_allocation_ids.mapped("open_product_qty")
|
||||
)
|
||||
request.qty_done = done_qty
|
||||
request.qty_in_progress = open_qty
|
||||
|
||||
@api.depends(
|
||||
"purchase_request_allocation_ids",
|
||||
"purchase_request_allocation_ids.stock_move_id.state",
|
||||
"purchase_request_allocation_ids.stock_move_id",
|
||||
"purchase_request_allocation_ids.purchase_line_id.order_id.state",
|
||||
"purchase_request_allocation_ids.purchase_line_id",
|
||||
)
|
||||
def _compute_qty_cancelled(self):
|
||||
for request in self:
|
||||
if request.product_id.type != "service":
|
||||
qty_cancelled = sum(
|
||||
request.mapped("purchase_request_allocation_ids.stock_move_id")
|
||||
.filtered(lambda sm: sm.state == "cancel")
|
||||
.mapped("product_qty")
|
||||
)
|
||||
else:
|
||||
qty_cancelled = sum(
|
||||
request.mapped("purchase_request_allocation_ids.purchase_line_id")
|
||||
.filtered(lambda sm: sm.state == "cancel")
|
||||
.mapped("product_qty")
|
||||
)
|
||||
# done this way as i cannot track what was received before
|
||||
# cancelled the purchase order
|
||||
qty_cancelled -= request.qty_done
|
||||
if request.product_uom_id:
|
||||
request.qty_cancelled = (
|
||||
max(
|
||||
0,
|
||||
request.product_id.uom_id._compute_quantity(
|
||||
qty_cancelled, request.product_uom_id
|
||||
),
|
||||
)
|
||||
if request.purchase_request_allocation_ids
|
||||
else 0
|
||||
)
|
||||
else:
|
||||
request.qty_cancelled = qty_cancelled
|
||||
|
||||
@api.depends(
|
||||
"purchase_lines",
|
||||
"request_id.state",
|
||||
)
|
||||
def _compute_is_editable(self):
|
||||
for rec in self:
|
||||
if rec.request_id.state in (
|
||||
"to_approve",
|
||||
"approved",
|
||||
"rejected",
|
||||
"in_progress",
|
||||
"done",
|
||||
):
|
||||
rec.is_editable = False
|
||||
else:
|
||||
rec.is_editable = True
|
||||
for rec in self.filtered(lambda p: p.purchase_lines):
|
||||
rec.is_editable = False
|
||||
|
||||
@api.depends("product_id", "product_id.seller_ids")
|
||||
def _compute_supplier_id(self):
|
||||
for rec in self:
|
||||
sellers = rec.product_id.seller_ids.filtered(
|
||||
lambda si, rec=rec: not si.company_id or si.company_id == rec.company_id
|
||||
)
|
||||
rec.supplier_id = sellers[0].partner_id if sellers else False
|
||||
|
||||
@api.onchange("product_id")
|
||||
def onchange_product_id(self):
|
||||
if self.product_id:
|
||||
name = self.product_id.name
|
||||
if self.product_id.code:
|
||||
name = "[{}] {}".format(self.product_id.code, name)
|
||||
if self.product_id.description_purchase:
|
||||
name += "\n" + self.product_id.description_purchase
|
||||
self.product_uom_id = self.product_id.uom_id.id
|
||||
self.product_qty = 1
|
||||
self.name = name
|
||||
|
||||
def do_cancel(self):
|
||||
"""Actions to perform when cancelling a purchase request line."""
|
||||
self.write({"cancelled": True})
|
||||
|
||||
def do_uncancel(self):
|
||||
"""Actions to perform when uncancelling a purchase request line."""
|
||||
self.write({"cancelled": False})
|
||||
|
||||
def write(self, vals):
|
||||
res = super(PurchaseRequestLine, self).write(vals)
|
||||
if vals.get("cancelled"):
|
||||
requests = self.mapped("request_id")
|
||||
requests.check_auto_reject()
|
||||
return res
|
||||
|
||||
def _compute_purchased_qty(self):
|
||||
for rec in self:
|
||||
rec.purchased_qty = 0.0
|
||||
for line in rec.purchase_lines.filtered(lambda x: x.state != "cancel"):
|
||||
if rec.product_uom_id and line.product_uom != rec.product_uom_id:
|
||||
rec.purchased_qty += line.product_uom._compute_quantity(
|
||||
line.product_qty, rec.product_uom_id
|
||||
)
|
||||
else:
|
||||
rec.purchased_qty += line.product_qty
|
||||
|
||||
@api.depends("purchase_lines.state", "purchase_lines.order_id.state")
|
||||
def _compute_purchase_state(self):
|
||||
for rec in self:
|
||||
temp_purchase_state = False
|
||||
if rec.purchase_lines:
|
||||
if any(po_line.state == "done" for po_line in rec.purchase_lines):
|
||||
temp_purchase_state = "done"
|
||||
elif all(po_line.state == "cancel" for po_line in rec.purchase_lines):
|
||||
temp_purchase_state = "cancel"
|
||||
elif any(po_line.state == "purchase" for po_line in rec.purchase_lines):
|
||||
temp_purchase_state = "purchase"
|
||||
elif any(
|
||||
po_line.state == "to approve" for po_line in rec.purchase_lines
|
||||
):
|
||||
temp_purchase_state = "to approve"
|
||||
elif any(po_line.state == "sent" for po_line in rec.purchase_lines):
|
||||
temp_purchase_state = "sent"
|
||||
elif all(
|
||||
po_line.state in ("draft", "cancel")
|
||||
for po_line in rec.purchase_lines
|
||||
):
|
||||
temp_purchase_state = "draft"
|
||||
rec.purchase_state = temp_purchase_state
|
||||
|
||||
@api.model
|
||||
def _get_supplier_min_qty(self, product, partner_id=False):
|
||||
seller_min_qty = 0.0
|
||||
if partner_id:
|
||||
seller = product.seller_ids.filtered(
|
||||
lambda r: r.partner_id == partner_id
|
||||
).sorted(key=lambda r: r.min_qty)
|
||||
else:
|
||||
seller = product.seller_ids.sorted(key=lambda r: r.min_qty)
|
||||
if seller:
|
||||
seller_min_qty = seller[0].min_qty
|
||||
return seller_min_qty
|
||||
|
||||
@api.model
|
||||
def _calc_new_qty(self, request_line, po_line=None, new_pr_line=False):
|
||||
purchase_uom = po_line.product_uom or request_line.product_id.uom_po_id
|
||||
# TODO: Not implemented yet.
|
||||
# Make sure we use the minimum quantity of the partner corresponding
|
||||
# to the PO. This does not apply in case of dropshipping
|
||||
supplierinfo_min_qty = 0.0
|
||||
if not po_line.order_id.dest_address_id:
|
||||
supplierinfo_min_qty = self._get_supplier_min_qty(
|
||||
po_line.product_id, po_line.order_id.partner_id
|
||||
)
|
||||
|
||||
rl_qty = 0.0
|
||||
# Recompute quantity by adding existing running procurements.
|
||||
if new_pr_line:
|
||||
rl_qty = po_line.product_uom_qty
|
||||
else:
|
||||
for prl in po_line.purchase_request_lines:
|
||||
for alloc in prl.purchase_request_allocation_ids:
|
||||
rl_qty += alloc.product_uom_id._compute_quantity(
|
||||
alloc.requested_product_uom_qty, purchase_uom
|
||||
)
|
||||
qty = max(rl_qty, supplierinfo_min_qty)
|
||||
return qty
|
||||
|
||||
def _can_be_deleted(self):
|
||||
self.ensure_one()
|
||||
return self.request_state == "draft"
|
||||
|
||||
def unlink(self):
|
||||
if self.mapped("purchase_lines"):
|
||||
raise UserError(
|
||||
_("You cannot delete a record that refers to purchase lines!")
|
||||
)
|
||||
for line in self:
|
||||
if not line._can_be_deleted():
|
||||
raise UserError(
|
||||
_(
|
||||
"You can only delete a purchase request line "
|
||||
"if the purchase request is in draft state."
|
||||
)
|
||||
)
|
||||
return super(PurchaseRequestLine, self).unlink()
|
||||
|
||||
def action_show_details(self):
|
||||
self.ensure_one()
|
||||
view = self.env.ref("purchase_request.view_purchase_request_line_details")
|
||||
return {
|
||||
"name": _("Detailed Line"),
|
||||
"type": "ir.actions.act_window",
|
||||
"view_mode": "form",
|
||||
"res_model": "purchase.request.line",
|
||||
"views": [(view.id, "form")],
|
||||
"view_id": view.id,
|
||||
"target": "new",
|
||||
"res_id": self.id,
|
||||
"context": dict(
|
||||
self.env.context,
|
||||
),
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _get_analytic_name(self):
|
||||
return (
|
||||
[
|
||||
"%(name)s (%(value)s)"
|
||||
% {
|
||||
"name": self.env["account.analytic.account"]
|
||||
.browse(int(key))
|
||||
.display_name,
|
||||
"value": value,
|
||||
}
|
||||
for key, value in self.analytic_distribution.items()
|
||||
]
|
||||
if self.analytic_distribution
|
||||
else [""]
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _get_analytic_distribution(self):
|
||||
self.ensure_one()
|
||||
|
||||
name = ", ".join(filter(None, self._get_analytic_name()))
|
||||
return name
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
# Copyright 2018-2019 ForgeFlow, S.L.
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import float_compare
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = "stock.move"
|
||||
|
||||
created_purchase_request_line_id = fields.Many2one(
|
||||
comodel_name="purchase.request.line",
|
||||
string="Created Purchase Request Line",
|
||||
ondelete="set null",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
index=True,
|
||||
)
|
||||
|
||||
purchase_request_allocation_ids = fields.One2many(
|
||||
comodel_name="purchase.request.allocation",
|
||||
inverse_name="stock_move_id",
|
||||
copy=False,
|
||||
string="Purchase Request Allocation",
|
||||
)
|
||||
|
||||
purchase_request_ids = fields.One2many(
|
||||
comodel_name="purchase.request",
|
||||
string="Purchase Requests",
|
||||
compute="_compute_purchase_request_ids",
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _prepare_merge_moves_distinct_fields(self):
|
||||
distinct_fields = super(StockMove, self)._prepare_merge_moves_distinct_fields()
|
||||
distinct_fields += ["created_purchase_request_line_id"]
|
||||
return distinct_fields
|
||||
|
||||
def _action_cancel_create_mail_activity(self):
|
||||
"""Create an activity on the request for the cancelled procurement move"""
|
||||
for move in self:
|
||||
if move.created_purchase_request_line_id:
|
||||
try:
|
||||
activity_type_id = self.env.ref("mail.mail_activity_data_todo").id
|
||||
except ValueError:
|
||||
activity_type_id = False
|
||||
pr_line = move.created_purchase_request_line_id
|
||||
if pr_line.product_id.responsible_id:
|
||||
activity_user = pr_line.product_id.responsible_id
|
||||
elif pr_line.request_id.assigned_to:
|
||||
activity_user = pr_line.request_id.assigned_to
|
||||
elif move.picking_id.user_id:
|
||||
activity_user = move.picking_id.user_id
|
||||
else:
|
||||
activity_user = self.env.user
|
||||
self.env["mail.activity"].sudo().create(
|
||||
{
|
||||
"activity_type_id": activity_type_id,
|
||||
"note": _(
|
||||
"A sale/manufacturing order that generated this "
|
||||
"purchase request has been cancelled/deleted. "
|
||||
"Check if an action is needed."
|
||||
),
|
||||
"user_id": activity_user.id,
|
||||
"res_id": pr_line.request_id.id,
|
||||
"res_model_id": self.env.ref(
|
||||
"purchase_request.model_purchase_request"
|
||||
).id,
|
||||
}
|
||||
)
|
||||
|
||||
def _action_cancel(self):
|
||||
self._action_cancel_create_mail_activity()
|
||||
return super(StockMove, self)._action_cancel()
|
||||
|
||||
@api.depends("purchase_request_allocation_ids")
|
||||
def _compute_purchase_request_ids(self):
|
||||
for rec in self:
|
||||
rec.purchase_request_ids = (
|
||||
rec.purchase_request_allocation_ids.purchase_request_line_id.request_id
|
||||
)
|
||||
|
||||
def _merge_moves_fields(self):
|
||||
res = super(StockMove, self)._merge_moves_fields()
|
||||
res["purchase_request_allocation_ids"] = [
|
||||
(4, m.id) for m in self.mapped("purchase_request_allocation_ids")
|
||||
]
|
||||
return res
|
||||
|
||||
@api.constrains("company_id")
|
||||
def _check_company_purchase_request(self):
|
||||
if not self.ids:
|
||||
return
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM purchase_request_allocation pra
|
||||
INNER JOIN stock_move sm
|
||||
ON sm.id=pra.stock_move_id
|
||||
WHERE pra.company_id != sm.company_id
|
||||
AND sm.id IN %s
|
||||
LIMIT 1
|
||||
""",
|
||||
(tuple(self.ids),),
|
||||
)
|
||||
if self.env.cr.fetchone():
|
||||
raise ValidationError(
|
||||
_(
|
||||
"The company of the purchase request must match with "
|
||||
"that of the location."
|
||||
)
|
||||
)
|
||||
|
||||
def copy_data(self, default=None):
|
||||
"""Propagate request allocation on copy.
|
||||
|
||||
If this move is being split, or if this move is processed and there is
|
||||
a remaining allocation, move the appropriate quantity over to the new move.
|
||||
"""
|
||||
if default is None:
|
||||
default = {}
|
||||
if not default.get("purchase_request_allocation_ids") and (
|
||||
default.get("product_uom_qty") or self.state in ("done", "cancel")
|
||||
):
|
||||
default["purchase_request_allocation_ids"] = []
|
||||
new_move_qty = default.get("product_uom_qty") or self.product_uom_qty
|
||||
rounding = self.product_id.uom_id.rounding
|
||||
for alloc in self.purchase_request_allocation_ids.filtered(
|
||||
"open_product_qty"
|
||||
):
|
||||
if (
|
||||
float_compare(
|
||||
new_move_qty,
|
||||
0,
|
||||
precision_rounding=self.product_id.uom_id.rounding,
|
||||
)
|
||||
<= 0
|
||||
or float_compare(
|
||||
alloc.open_product_qty, 0, precision_rounding=rounding
|
||||
)
|
||||
<= 0
|
||||
):
|
||||
break
|
||||
open_qty = min(new_move_qty, alloc.open_product_qty)
|
||||
new_move_qty -= open_qty
|
||||
default["purchase_request_allocation_ids"].append(
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"purchase_request_line_id": alloc.purchase_request_line_id.id,
|
||||
"requested_product_uom_qty": open_qty,
|
||||
},
|
||||
)
|
||||
)
|
||||
alloc.requested_product_uom_qty -= open_qty
|
||||
return super(StockMove, self).copy_data(default)
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
# Copyright 2017 ForgeFlow, S.L.
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
|
||||
|
||||
from odoo import _, api, models
|
||||
|
||||
|
||||
class StockMoveLine(models.Model):
|
||||
_inherit = "stock.move.line"
|
||||
|
||||
@api.model
|
||||
def _purchase_request_confirm_done_message_content(self, message_data):
|
||||
title = _(
|
||||
"Receipt confirmation %(picking_name)s for your Request %(request_name)s"
|
||||
) % {
|
||||
"picking_name": message_data["picking_name"],
|
||||
"request_name": message_data["request_name"],
|
||||
}
|
||||
message = "<h3>%s</h3>" % title
|
||||
message += _(
|
||||
"The following requested items from Purchase Request %(request_name)s "
|
||||
"have now been received in %(location_name)s using Picking %(picking_name)s:"
|
||||
) % {
|
||||
"request_name": message_data["request_name"],
|
||||
"location_name": message_data["location_name"],
|
||||
"picking_name": message_data["picking_name"],
|
||||
}
|
||||
message += "<ul>"
|
||||
message += _(
|
||||
"<li><b>%(product_name)s</b>: "
|
||||
"Transferred quantity %(product_qty)s %(product_uom)s</li>"
|
||||
) % {
|
||||
"product_name": message_data["product_name"],
|
||||
"product_qty": message_data["product_qty"],
|
||||
"product_uom": message_data["product_uom"],
|
||||
}
|
||||
message += "</ul>"
|
||||
return message
|
||||
|
||||
@api.model
|
||||
def _picking_confirm_done_message_content(self, message_data):
|
||||
title = _("Receipt confirmation for Request %s") % (
|
||||
message_data["request_name"]
|
||||
)
|
||||
message = "<h3>%s</h3>" % title
|
||||
message += _(
|
||||
"The following requested items from Purchase Request %(request_name)s "
|
||||
"requested by %(requestor)s "
|
||||
"have now been received in %(location_name)s:"
|
||||
) % {
|
||||
"request_name": message_data["request_name"],
|
||||
"requestor": message_data["requestor"],
|
||||
"location_name": message_data["location_name"],
|
||||
}
|
||||
message += "<ul>"
|
||||
message += _(
|
||||
"<li><b>%(product_name)s</b>: "
|
||||
"Transferred quantity %(product_qty)s %(product_uom)s</li>"
|
||||
) % {
|
||||
"product_name": message_data["product_name"],
|
||||
"product_qty": message_data["product_qty"],
|
||||
"product_uom": message_data["product_uom"],
|
||||
}
|
||||
message += "</ul>"
|
||||
return message
|
||||
|
||||
def _prepare_message_data(self, ml, request, allocated_qty):
|
||||
return {
|
||||
"request_name": request.name,
|
||||
"picking_name": ml.picking_id.name,
|
||||
"product_name": ml.product_id.name_get()[0][1],
|
||||
"product_qty": allocated_qty,
|
||||
"product_uom": ml.product_uom_id.name,
|
||||
"location_name": ml.location_dest_id.name_get()[0][1],
|
||||
"requestor": request.requested_by.partner_id.name,
|
||||
}
|
||||
|
||||
def allocate(self):
|
||||
for ml in self.filtered(
|
||||
lambda m: m.exists() and m.move_id.purchase_request_allocation_ids
|
||||
):
|
||||
|
||||
# We do sudo because potentially the user that completes the move
|
||||
# may not have permissions for purchase.request.
|
||||
to_allocate_qty = ml.qty_done
|
||||
to_allocate_uom = ml.product_uom_id
|
||||
for allocation in ml.move_id.purchase_request_allocation_ids.sudo():
|
||||
allocated_qty = 0.0
|
||||
if allocation.open_product_qty and to_allocate_qty:
|
||||
to_allocate_uom_qty = to_allocate_uom._compute_quantity(
|
||||
to_allocate_qty, allocation.product_uom_id
|
||||
)
|
||||
allocated_qty = min(
|
||||
allocation.open_product_qty, to_allocate_uom_qty
|
||||
)
|
||||
allocation.allocated_product_qty += allocated_qty
|
||||
to_allocate_uom_qty -= allocated_qty
|
||||
to_allocate_qty = allocation.product_uom_id._compute_quantity(
|
||||
to_allocate_uom_qty, to_allocate_uom
|
||||
)
|
||||
|
||||
request = allocation.purchase_request_line_id.request_id
|
||||
if allocated_qty:
|
||||
message_data = self._prepare_message_data(
|
||||
ml, request, allocated_qty
|
||||
)
|
||||
message = self._purchase_request_confirm_done_message_content(
|
||||
message_data
|
||||
)
|
||||
if message:
|
||||
request.message_post(
|
||||
body=message,
|
||||
subtype_id=self.env.ref(
|
||||
"purchase_request.mt_request_picking_done"
|
||||
).id,
|
||||
)
|
||||
|
||||
picking_message = self._picking_confirm_done_message_content(
|
||||
message_data
|
||||
)
|
||||
if picking_message:
|
||||
ml.move_id.picking_id.message_post(
|
||||
body=picking_message,
|
||||
subtype_id=self.env.ref("mail.mt_note").id,
|
||||
)
|
||||
|
||||
allocation._compute_open_product_qty()
|
||||
|
||||
def _action_done(self):
|
||||
res = super(StockMoveLine, self)._action_done()
|
||||
self.allocate()
|
||||
return res
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
# Copyright 2018-2020 ForgeFlow, S.L.
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class StockRule(models.Model):
|
||||
_inherit = "stock.rule"
|
||||
|
||||
@api.model
|
||||
def _prepare_purchase_request_line(self, request_id, procurement):
|
||||
procurement_uom_po_qty = procurement.product_uom._compute_quantity(
|
||||
procurement.product_qty, procurement.product_id.uom_po_id
|
||||
)
|
||||
return {
|
||||
"product_id": procurement.product_id.id,
|
||||
"name": procurement.product_id.name,
|
||||
"date_required": "date_planned" in procurement.values
|
||||
and procurement.values["date_planned"]
|
||||
or fields.Datetime.now(),
|
||||
"product_uom_id": procurement.product_id.uom_po_id.id,
|
||||
"product_qty": procurement_uom_po_qty,
|
||||
"request_id": request_id.id,
|
||||
"move_dest_ids": [
|
||||
(4, x.id) for x in procurement.values.get("move_dest_ids", [])
|
||||
],
|
||||
"orderpoint_id": procurement.values.get("orderpoint_id", False)
|
||||
and procurement.values.get("orderpoint_id").id,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _prepare_purchase_request(self, origin, values):
|
||||
gpo = self.group_propagation_option
|
||||
group_id = (
|
||||
(gpo == "fixed" and self.group_id.id)
|
||||
or (gpo == "propagate" and values.get("group_id") and values["group_id"].id)
|
||||
or False
|
||||
)
|
||||
return {
|
||||
"origin": origin,
|
||||
"company_id": values["company_id"].id,
|
||||
"picking_type_id": self.picking_type_id.id,
|
||||
"group_id": group_id or False,
|
||||
"requested_by": self.env.context.get("uid", self.env.uid),
|
||||
"assigned_to": False,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _make_pr_get_domain(self, values):
|
||||
"""
|
||||
This method is to be implemented by other modules that can
|
||||
provide a criteria to select the appropriate purchase request to be
|
||||
extended.
|
||||
:return: False
|
||||
"""
|
||||
domain = (
|
||||
("state", "=", "draft"),
|
||||
("picking_type_id", "=", self.picking_type_id.id),
|
||||
("company_id", "=", values["company_id"].id),
|
||||
)
|
||||
gpo = self.group_propagation_option
|
||||
group_id = (
|
||||
(gpo == "fixed" and self.group_id.id)
|
||||
or (gpo == "propagate" and values["group_id"].id)
|
||||
or False
|
||||
)
|
||||
if group_id:
|
||||
domain += (("group_id", "=", group_id),)
|
||||
return domain
|
||||
|
||||
def is_create_purchase_request_allowed(self, procurement):
|
||||
"""
|
||||
Tell if current procurement order should
|
||||
create a purchase request or not.
|
||||
:return: boolean
|
||||
"""
|
||||
return (
|
||||
procurement[1].action == "buy"
|
||||
and procurement[0].product_id.purchase_request
|
||||
)
|
||||
|
||||
def _run_buy(self, procurements):
|
||||
indexes_to_pop = []
|
||||
for i, procurement in enumerate(procurements):
|
||||
if self.is_create_purchase_request_allowed(procurement):
|
||||
self.create_purchase_request(procurement)
|
||||
indexes_to_pop.append(i)
|
||||
if indexes_to_pop:
|
||||
indexes_to_pop.reverse()
|
||||
for index in indexes_to_pop:
|
||||
procurements.pop(index)
|
||||
if not procurements:
|
||||
return
|
||||
return super(StockRule, self)._run_buy(procurements)
|
||||
|
||||
def create_purchase_request(self, procurement_group):
|
||||
"""
|
||||
Create a purchase request containing procurement order product.
|
||||
"""
|
||||
procurement = procurement_group[0]
|
||||
rule = procurement_group[1]
|
||||
purchase_request_model = self.env["purchase.request"]
|
||||
purchase_request_line_model = self.env["purchase.request.line"]
|
||||
cache = {}
|
||||
pr = self.env["purchase.request"]
|
||||
domain = rule._make_pr_get_domain(procurement.values)
|
||||
if domain in cache:
|
||||
pr = cache[domain]
|
||||
elif domain:
|
||||
pr = self.env["purchase.request"].search([dom for dom in domain])
|
||||
pr = pr[0] if pr else False
|
||||
cache[domain] = pr
|
||||
if not pr:
|
||||
request_data = rule._prepare_purchase_request(
|
||||
procurement.origin, procurement.values
|
||||
)
|
||||
pr = purchase_request_model.create(request_data)
|
||||
cache[domain] = pr
|
||||
elif (
|
||||
not pr.origin
|
||||
or procurement.origin not in pr.origin.split(", ")
|
||||
and procurement.origin != "/"
|
||||
):
|
||||
if pr.origin:
|
||||
if procurement.origin:
|
||||
pr.write({"origin": pr.origin + ", " + procurement.origin})
|
||||
else:
|
||||
pr.write({"origin": procurement.origin})
|
||||
# Create Line
|
||||
request_line_data = rule._prepare_purchase_request_line(pr, procurement)
|
||||
purchase_request_line_model.create(request_line_data)
|
||||
Loading…
Add table
Add a link
Reference in a new issue