Add oca-purchase submodule with 96 purchase modules moved from oca-workflow-process

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ernad Husremovic 2025-08-30 18:00:40 +02:00
parent b0628ee8ea
commit 7378b233e9
3994 changed files with 334316 additions and 0 deletions

View file

@ -0,0 +1,3 @@
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)
from . import purchase_request_line_make_purchase_order

View file

@ -0,0 +1,398 @@
# Copyright 2018-2019 ForgeFlow, S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0).
from datetime import datetime
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tools import get_lang
class PurchaseRequestLineMakePurchaseOrder(models.TransientModel):
_name = "purchase.request.line.make.purchase.order"
_description = "Purchase Request Line Make Purchase Order"
supplier_id = fields.Many2one(
comodel_name="res.partner",
string="Supplier",
required=True,
context={"res_partner_search_mode": "supplier"},
)
item_ids = fields.One2many(
comodel_name="purchase.request.line.make.purchase.order.item",
inverse_name="wiz_id",
string="Items",
)
purchase_order_id = fields.Many2one(
comodel_name="purchase.order",
string="Purchase Order",
domain=[("state", "=", "draft")],
)
sync_data_planned = fields.Boolean(
string="Match existing PO lines by Scheduled Date",
help=(
"When checked, PO lines on the selected purchase order are only reused "
"if the scheduled date matches as well."
),
)
@api.model
def _prepare_item(self, line):
return {
"line_id": line.id,
"request_id": line.request_id.id,
"product_id": line.product_id.id,
"name": line.name or line.product_id.name,
"product_qty": line.pending_qty_to_receive,
"product_uom_id": line.product_uom_id.id,
"estimated_cost": line.estimated_cost,
}
@api.model
def _check_valid_request_line(self, request_line_ids):
picking_type = False
company_id = False
for line in self.env["purchase.request.line"].browse(request_line_ids):
if line.request_id.state == "done":
raise UserError(_("The purchase has already been completed."))
if line.request_id.state not in ["approved", "in_progress"]:
raise UserError(
_("Purchase Request %s is not approved or in progress")
% line.request_id.name
)
if line.purchase_state == "done":
raise UserError(_("The purchase has already been completed."))
line_company_id = line.company_id and line.company_id.id or False
if company_id is not False and line_company_id != company_id:
raise UserError(_("You have to select lines from the same company."))
else:
company_id = line_company_id
line_picking_type = line.request_id.picking_type_id or False
if not line_picking_type:
raise UserError(_("You have to enter a Picking Type."))
if picking_type is not False and line_picking_type != picking_type:
raise UserError(
_("You have to select lines from the same Picking Type.")
)
else:
picking_type = line_picking_type
@api.model
def check_group(self, request_lines):
if len(list(set(request_lines.mapped("request_id.group_id")))) > 1:
raise UserError(
_(
"You cannot create a single purchase order from "
"purchase requests that have different procurement group."
)
)
@api.model
def get_items(self, request_line_ids):
request_line_obj = self.env["purchase.request.line"]
items = []
request_lines = request_line_obj.browse(request_line_ids)
self._check_valid_request_line(request_line_ids)
self.check_group(request_lines)
for line in request_lines:
items.append([0, 0, self._prepare_item(line)])
return items
@api.model
def default_get(self, fields):
res = super().default_get(fields)
active_model = self.env.context.get("active_model", False)
request_line_ids = []
if active_model == "purchase.request.line":
request_line_ids += self.env.context.get("active_ids", [])
elif active_model == "purchase.request":
request_ids = self.env.context.get("active_ids", False)
request_line_ids += (
self.env[active_model].browse(request_ids).mapped("line_ids.id")
)
if not request_line_ids:
return res
res["item_ids"] = self.get_items(request_line_ids)
request_lines = self.env["purchase.request.line"].browse(request_line_ids)
supplier_ids = request_lines.mapped("supplier_id").ids
if len(supplier_ids) == 1:
res["supplier_id"] = supplier_ids[0]
return res
@api.model
def _prepare_purchase_order(self, picking_type, group_id, company, origin):
if not self.supplier_id:
raise UserError(_("Enter a supplier."))
supplier = self.supplier_id
data = {
"origin": origin,
"partner_id": self.supplier_id.id,
"payment_term_id": self.supplier_id.property_supplier_payment_term_id.id,
"fiscal_position_id": supplier.property_account_position_id
and supplier.property_account_position_id.id
or False,
"picking_type_id": picking_type.id,
"company_id": company.id,
"group_id": group_id.id,
}
return data
def create_allocation(self, po_line, pr_line, new_qty, alloc_uom):
vals = {
"requested_product_uom_qty": new_qty,
"product_uom_id": alloc_uom.id,
"purchase_request_line_id": pr_line.id,
"purchase_line_id": po_line.id,
}
return self.env["purchase.request.allocation"].create(vals)
@api.model
def _prepare_purchase_order_line(self, po, item):
if not item.product_id:
raise UserError(_("Please select a product for all lines"))
product = item.product_id
# Keep the standard product UOM for purchase order so we should
# convert the product quantity to this UOM
qty = item.product_uom_id._compute_quantity(
item.product_qty, product.uom_po_id or product.uom_id
)
# Suggest the supplier min qty as it's done in Odoo core
min_qty = item.line_id._get_supplier_min_qty(product, po.partner_id)
qty = max(qty, min_qty)
date_required = item.line_id.date_required
return {
"order_id": po.id,
"product_id": product.id,
"product_uom": product.uom_po_id.id or product.uom_id.id,
"price_unit": 0.0,
"product_qty": qty,
"analytic_distribution": item.line_id.analytic_distribution,
"purchase_request_lines": [(4, item.line_id.id)],
"date_planned": datetime(
date_required.year, date_required.month, date_required.day
),
"move_dest_ids": [(4, x.id) for x in item.line_id.move_dest_ids],
}
@api.model
def _get_purchase_line_name(self, order, line):
"""Fetch the product name as per supplier settings"""
product_lang = line.product_id.with_context(
lang=get_lang(self.env, self.supplier_id.lang).code,
partner_id=self.supplier_id.id,
company_id=order.company_id.id,
)
name = product_lang.display_name
if product_lang.description_purchase:
name += "\n" + product_lang.description_purchase
return name
@api.model
def _get_order_line_search_domain(self, order, item):
vals = self._prepare_purchase_order_line(order, item)
name = self._get_purchase_line_name(order, item)
order_line_data = [
("order_id", "=", order.id),
("name", "=", name),
("product_id", "=", item.product_id.id),
("product_uom", "=", vals["product_uom"]),
("analytic_distribution", "=?", item.line_id.analytic_distribution),
]
if self.sync_data_planned:
date_required = item.line_id.date_required
order_line_data += [
(
"date_planned",
"=",
datetime(
date_required.year, date_required.month, date_required.day
),
)
]
if not item.product_id:
order_line_data.append(("name", "=", item.name))
return order_line_data
def make_purchase_order(self):
res = []
purchase_obj = self.env["purchase.order"]
po_line_obj = self.env["purchase.order.line"]
purchase = False
for item in self.item_ids:
line = item.line_id
if item.product_qty <= 0.0:
raise UserError(_("Enter a positive quantity."))
if self.purchase_order_id:
purchase = self.purchase_order_id
if not purchase:
po_data = self._prepare_purchase_order(
line.request_id.picking_type_id,
line.request_id.group_id,
line.company_id,
line.origin,
)
purchase = purchase_obj.create(po_data)
# Look for any other PO line in the selected PO with same
# product and UoM to sum quantities instead of creating a new
# po line
domain = self._get_order_line_search_domain(purchase, item)
available_po_lines = po_line_obj.search(domain)
new_pr_line = True
# If Unit of Measure is not set, update from wizard.
if not line.product_uom_id:
line.product_uom_id = item.product_uom_id
# Allocation UoM has to be the same as PR line UoM
alloc_uom = line.product_uom_id
wizard_uom = item.product_uom_id
if (
available_po_lines
and not item.keep_description
and not item.keep_estimated_cost
):
new_pr_line = False
po_line = available_po_lines[0]
po_line.purchase_request_lines = [(4, line.id)]
po_line.move_dest_ids |= line.move_dest_ids
po_line_product_uom_qty = po_line.product_uom._compute_quantity(
po_line.product_uom_qty, alloc_uom
)
wizard_product_uom_qty = wizard_uom._compute_quantity(
item.product_qty, alloc_uom
)
all_qty = min(po_line_product_uom_qty, wizard_product_uom_qty)
self.create_allocation(po_line, line, all_qty, alloc_uom)
else:
po_line_data = self._prepare_purchase_order_line(purchase, item)
if item.keep_description:
po_line_data["name"] = item.name
po_line = po_line_obj.create(po_line_data)
po_line_product_uom_qty = po_line.product_uom._compute_quantity(
po_line.product_uom_qty, alloc_uom
)
wizard_product_uom_qty = wizard_uom._compute_quantity(
item.product_qty, alloc_uom
)
all_qty = min(po_line_product_uom_qty, wizard_product_uom_qty)
self.create_allocation(po_line, line, all_qty, alloc_uom)
self._post_process_po_line(item, po_line, new_pr_line)
res.append(purchase.id)
purchase_requests = self.item_ids.mapped("request_id")
purchase_requests.button_in_progress()
return {
"domain": [("id", "in", res)],
"name": _("RFQ"),
"view_mode": "tree,form",
"res_model": "purchase.order",
"view_id": False,
"context": False,
"type": "ir.actions.act_window",
}
def _post_process_po_line(self, item, po_line, new_pr_line):
self.ensure_one()
line = item.line_id
# TODO: Check propagate_uom compatibility:
price_unit = item.estimated_cost / item.product_qty
new_qty = self.env["purchase.request.line"]._calc_new_qty(
line, po_line=po_line, new_pr_line=new_pr_line
)
po_line.product_qty = new_qty
if item.keep_estimated_cost:
po_line.price_unit = price_unit
po_line._compute_amount()
# The quantity update triggers a compute method that alters the
# unit price (which is what we want, to honor graduate pricing)
# but also the scheduled date which is what we don't want.
date_required = line.date_required
po_line.date_planned = datetime(
date_required.year, date_required.month, date_required.day
)
class PurchaseRequestLineMakePurchaseOrderItem(models.TransientModel):
_name = "purchase.request.line.make.purchase.order.item"
_description = "Purchase Request Line Make Purchase Order Item"
wiz_id = fields.Many2one(
comodel_name="purchase.request.line.make.purchase.order",
string="Wizard",
required=True,
ondelete="cascade",
readonly=True,
)
line_id = fields.Many2one(
comodel_name="purchase.request.line", string="Purchase Request Line"
)
request_id = fields.Many2one(
comodel_name="purchase.request",
related="line_id.request_id",
string="Purchase Request",
readonly=False,
)
product_id = fields.Many2one(
comodel_name="product.product",
string="Product",
related="line_id.product_id",
readonly=False,
)
name = fields.Char(string="Description", required=True)
product_qty = fields.Float(
string="Quantity to purchase", digits="Product Unit of Measure"
)
product_uom_id = fields.Many2one(
comodel_name="uom.uom", string="UoM", required=True
)
estimated_cost = fields.Monetary(currency_field="currency_id")
currency_id = fields.Many2one(
"res.currency", string="Currency", related="line_id.currency_id", readonly=True
)
keep_description = fields.Boolean(
string="Copy descriptions to new PO",
help="Set true if you want to keep the "
"descriptions provided in the "
"wizard in the new PO.",
)
keep_estimated_cost = fields.Boolean(
string="Copy estimative cost to new PO",
help="Set true if you want to keep the "
"estimated cost provided in the "
"wizard in the new PO.",
)
@api.onchange("product_id")
def onchange_product_id(self):
if self.product_id:
if not self.keep_description:
name = self.product_id.name
code = self.product_id.code
sup_info_id = self.env["product.supplierinfo"].search(
[
"|",
("product_id", "=", self.product_id.id),
("product_tmpl_id", "=", self.product_id.product_tmpl_id.id),
("partner_id", "=", self.wiz_id.supplier_id.id),
]
)
if sup_info_id:
p_code = sup_info_id[0].product_code
p_name = sup_info_id[0].product_name
name = "[{}] {}".format(
p_code if p_code else code, p_name if p_name else name
)
else:
if code:
name = "[{}] {}".format(
code, self.name if self.keep_description else name
)
if self.product_id.description_purchase and not self.keep_description:
name += "\n" + self.product_id.description_purchase
self.product_uom_id = self.product_id.uom_id.id
if name:
self.name = name

View file

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2018-2019 ForgeFlow, S.L.
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) -->
<odoo>
<record id="view_purchase_request_line_make_purchase_order" model="ir.ui.view">
<field name="name">Purchase Request Line Make Purchase Order</field>
<field name="model">purchase.request.line.make.purchase.order</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Create RFQ">
<separator string="Existing RFQ to update:" />
<newline />
<group>
<field
name="purchase_order_id"
domain="[('partner_id', '=', supplier_id)]"
/>
<field name="sync_data_planned" />
</group>
<newline />
<separator string="New PO details:" />
<newline />
<group>
<field name="supplier_id" />
</group>
<newline />
<group>
<field name="item_ids" nolabel="1" colspan="2">
<tree name="Details" create="false" editable="bottom">
<field
name="line_id"
options="{'no_open': true}"
invisible="1"
/>
<field name="request_id" attrs="{'readonly': True}" />
<field name="product_id" />
<field name="name" />
<field name="estimated_cost" />
<field name="product_qty" />
<field
name="product_uom_id"
invisible="1"
groups="!uom.group_uom"
/>
<field name="product_uom_id" groups="uom.group_uom" />
<field
name="keep_description"
widget="boolean_toggle"
options="{'autosave': False}"
/>
<field
name="keep_estimated_cost"
widget="boolean_toggle"
options="{'autosave': False}"
/>
</tree>
</field>
</group>
<newline />
<footer>
<button
name="make_purchase_order"
string="Create RFQ"
type="object"
class="oe_highlight"
/>
<button special="cancel" string="Cancel" class="oe_link" />
</footer>
</form>
</field>
</record>
<record
id="action_purchase_request_line_make_purchase_order"
model="ir.actions.act_window"
>
<field name="name">Create RFQ</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">purchase.request.line.make.purchase.order</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_purchase_request_line_make_purchase_order" />
<field name="target">new</field>
<field
name="binding_model_id"
ref="purchase_request.model_purchase_request_line"
/>
</record>
</odoo>