diff --git a/purchase_order_generator/README.rst b/purchase_order_generator/README.rst new file mode 100644 index 0000000..5d438e1 --- /dev/null +++ b/purchase_order_generator/README.rst @@ -0,0 +1,58 @@ +======================== +Purchase Order Generator +======================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-beescoop%2Fobeesdoo-lightgray.png?logo=github + :target: https://github.com/beescoop/obeesdoo/tree/12.0/purchase_order_generator + :alt: beescoop/obeesdoo + +|badge1| |badge2| |badge3| + +Generate purchase order from a product selection. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Coop IT Easy SCRLfs + +Contributors +~~~~~~~~~~~~ + +* Robin Keunen +* Vincent Van Rossem + +Maintainers +~~~~~~~~~~~ + +This module is part of the `beescoop/obeesdoo `_ project on GitHub. + +You are welcome to contribute. diff --git a/purchase_order_generator/__init__.py b/purchase_order_generator/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/purchase_order_generator/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/purchase_order_generator/__manifest__.py b/purchase_order_generator/__manifest__.py new file mode 100644 index 0000000..5587487 --- /dev/null +++ b/purchase_order_generator/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2020 Coop IT Easy SCRL fs +# Robin Keunen +# Vincent Van Rossem +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Purchase Order Generator", + "version": "12.0.1.0.0", + "category": "Purchase Order", + "summary": "Generate purchase order from a product selection", + "author": "Coop IT Easy SCRLfs", + "website": "https://github.com/beescoop/obeesdoo/", + "license": "AGPL-3", + "depends": ["purchase", "beesdoo_stock_coverage"], + "data": [ + "security/ir.model.access.csv", + "views/purchase_order_generator.xml", + "views/purchase_order.xml", + ], +} diff --git a/purchase_order_generator/models/__init__.py b/purchase_order_generator/models/__init__.py new file mode 100644 index 0000000..78eefbf --- /dev/null +++ b/purchase_order_generator/models/__init__.py @@ -0,0 +1,4 @@ +from . import purchase_order +from . import purchase_order_generator +from . import purchase_order_generator_line +from . import product_template diff --git a/purchase_order_generator/models/product_template.py b/purchase_order_generator/models/product_template.py new file mode 100644 index 0000000..5cd0545 --- /dev/null +++ b/purchase_order_generator/models/product_template.py @@ -0,0 +1,31 @@ +# Copyright 2020 Coop IT Easy SCRL fs +# Robin Keunen +# Vincent Van Rossem +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import models, fields, api + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + main_supplier_id = fields.Many2one( + "res.partner", compute="_compute_main_supplier_id", store=True + ) + + def _get_sorted_supplierinfo(self): + return self.seller_ids.sorted( + key=lambda seller: seller.date_start, reverse=True + ) + + @api.multi + @api.depends("seller_ids", "seller_ids.date_start") + def _compute_main_supplier_id(self): + for pt in self: + sellers_ids = pt.seller_ids.sorted( + key=lambda seller: seller.date_start, reverse=True + ) + if sellers_ids: + pt.main_supplier_id = sellers_ids[0].name + else: + pt.main_supplier_id = False diff --git a/purchase_order_generator/models/purchase_order.py b/purchase_order_generator/models/purchase_order.py new file mode 100644 index 0000000..b17a488 --- /dev/null +++ b/purchase_order_generator/models/purchase_order.py @@ -0,0 +1,45 @@ +# Copyright 2020 Coop IT Easy SCRL fs +# Robin Keunen +# Vincent Van Rossem +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import models, fields, api, SUPERUSER_ID + + +class PurchaseOrder(models.Model): + _inherit = "purchase.order" + + original_cpo_id = fields.Many2one( + "purchase.order.generator", + string="Original POG", + help="POG used to generate this Purchase Order", + ) + + +class PurchaseOrderLine(models.Model): + _inherit = "purchase.order.line" + + @api.multi + def compute_taxes_id(self): + for pol in self: + if self.env.uid == SUPERUSER_ID: + company_id = self.env.user.company_id.id + else: + company_id = self.company_id.id + + fpos_id = ( + self.env["account.fiscal.position"] + .with_context(company_id=company_id) + .get_fiscal_position(pol.partner_id.id) + ) + fpos = self.env["account.fiscal.position"].browse(fpos_id) + pol.order_id.fiscal_position_id = fpos + + taxes = self.product_id.supplier_taxes_id + taxes_id = fpos.map_tax(taxes) if fpos else taxes + + if taxes_id: + taxes_id = taxes_id.filtered( + lambda t: t.company_id.id == company_id + ) + + pol.taxes_id = taxes_id diff --git a/purchase_order_generator/models/purchase_order_generator.py b/purchase_order_generator/models/purchase_order_generator.py new file mode 100644 index 0000000..7c092c8 --- /dev/null +++ b/purchase_order_generator/models/purchase_order_generator.py @@ -0,0 +1,166 @@ +# Copyright 2020 Coop IT Easy SCRL fs +# Robin Keunen +# Vincent Van Rossem +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + + +class PurchaseOrderGenerator(models.Model): + _description = "Purchase Order Generator" + _name = "purchase.order.generator" + _order = "id desc" + + name = fields.Char(string="POG Reference", default=_("New")) + order_date = fields.Datetime( + string="Purchase Order Date", + default=fields.Datetime.now, + help="Date at which the Quotation should be validated and " + "converted into a purchase order.", + ) + date_planned = fields.Datetime( + string="Date Planned", + default=fields.Datetime.now, + ) + supplier_id = fields.Many2one( + comodel_name="res.partner", + string="Supplier", + readonly=True, + help="Supplier of the purchase order.", + ) + pog_line_ids = fields.One2many( + comodel_name="purchase.order.generator.line", + inverse_name="cpo_id", + string="Order Lines", + ) + total_amount = fields.Float( + string="Total Amount (w/o VAT)", compute="compute_pog_total" + ) + generated_purchase_order_ids = fields.One2many( + comodel_name="purchase.order", + inverse_name="original_cpo_id", + string="Generated Purchase Orders", + ) + generated_po_count = fields.Integer( + string="Generated Purchase Order count", + compute="_compute_generated_po_count", + ) + + @api.multi + @api.depends("pog_line_ids", "pog_line_ids.purchase_quantity") + def compute_pog_total(self): + for cpo in self: + total_amount = sum(cpol.subtotal for cpol in cpo.pog_line_ids) + cpo.total_amount = total_amount + + @api.model + def _get_selected_supplier(self): + product_ids = self.env.context.get("active_ids", []) + products = self.env["product.template"].browse(product_ids) + suppliers = products.mapped("main_supplier_id") + + if not suppliers: + raise ValidationError("No supplier is set for selected articles.") + elif len(suppliers) == 1: + return suppliers + else: + raise ValidationError( + "You must select article from a single supplier." + ) + + @api.model + def generate_cpo(self): + order_line_obj = self.env["purchase.order.generator.line"] + product_ids = self.env.context.get("active_ids", []) + + supplier = self._get_selected_supplier() + name = "POG {} {}".format(supplier.name, fields.Date.today()) + cpo = self.create({"name": name, "supplier_id": supplier.id}) + + for product_id in product_ids: + supplierinfo = self.env["product.supplierinfo"].search( + [ + ("product_tmpl_id", "=", product_id), + ("name", "=", supplier.id), + ] + ) + min_qty = supplierinfo.min_qty if supplierinfo else 0 + order_line_obj.create( + { + "cpo_id": cpo.id, + "product_template_id": product_id, + "purchase_quantity": min_qty, + } + ) + action = { + "type": "ir.actions.act_window", + "res_model": "purchase.order.generator", + "res_id": cpo.id, + "view_type": "form", + "view_mode": "form,tree", + "target": "current", + } + return action + + @api.multi + def create_purchase_order(self): + self.ensure_one() + + if sum(self.pog_line_ids.mapped("purchase_quantity")) == 0: + raise ValidationError( + "You need at least a product to generate " "a Purchase Order" + ) + + purchase_order = self.env["purchase.order"].create( + { + "date_order": self.order_date, + "partner_id": self.supplier_id.id, + "date_planned": self.date_planned, + } + ) + + for cpo_line in self.pog_line_ids: + if cpo_line.purchase_quantity > 0: + pol = self.env["purchase.order.line"].create( + { + "name": cpo_line.name, + "product_id": cpo_line.product_template_id.product_variant_id.id, + "product_qty": cpo_line.purchase_quantity, + "price_unit": cpo_line.product_price, + "product_uom": cpo_line.uom_po_id.id, + "order_id": purchase_order.id, + "date_planned": self.date_planned, + } + ) + pol.compute_taxes_id() + + self.generated_purchase_order_ids += purchase_order + + action = { + "type": "ir.actions.act_window", + "res_model": "purchase.order", + "res_id": purchase_order.id, + "view_type": "form", + "view_mode": "form,tree", + "target": "current", + } + return action + + @api.multi + @api.depends("generated_purchase_order_ids") + def _compute_generated_po_count(self): + for cpo in self: + cpo.generated_po_count = len(cpo.generated_purchase_order_ids) + + @api.multi + def get_generated_po_action(self): + self.ensure_one() + action = { + "type": "ir.actions.act_window", + "res_model": "purchase.order", + "view_mode": "tree,form,kanban", + "target": "current", + "domain": [("id", "in", self.generated_purchase_order_ids.ids)], + } + return action diff --git a/purchase_order_generator/models/purchase_order_generator_line.py b/purchase_order_generator/models/purchase_order_generator_line.py new file mode 100644 index 0000000..710b7ba --- /dev/null +++ b/purchase_order_generator/models/purchase_order_generator_line.py @@ -0,0 +1,181 @@ +# Copyright 2020 Coop IT Easy SCRL fs +# Robin Keunen +# Vincent Van Rossem +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import logging + +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class PurchaseOrderGeneratorLine(models.Model): + _description = "Purchase Order Generator Line" + _name = "purchase.order.generator.line" + + name = fields.Char(string="Product Name", compute="_compute_name") + cpo_id = fields.Many2one( + comodel_name="purchase.order.generator", + string="Purchase Order Generator", + ) + product_template_id = fields.Many2one( + comodel_name="product.template", + string="Linked Product Template", + required=True, + help="Product", + ) + purchase_quantity = fields.Float(string="Purchase Quantity", default=0.0) + category_id = fields.Many2one( + comodel_name="product.category", + string="Internal Category", + related="product_template_id.categ_id", + read_only=True, + ) + uom_id = fields.Many2one( + comodel_name="uom.uom", + string="Unit of Measure", + read_only=True, + related="product_template_id.uom_id", + help="Default Unit of Measure used for all stock operation.", + ) + qty_available = fields.Float( + string="Stock Quantity", + related="product_template_id.qty_available", + read_only=True, + help="Quantity currently in stock. Does not take " + "into account incoming orders.", + ) + virtual_available = fields.Float( + string="Forecast Quantity", + related="product_template_id.virtual_available", + read_only=True, + help="Virtual quantity taking into account current stock, incoming " + "orders and outgoing sales.", + ) + daily_sales = fields.Float( + string="Average Consumption", + related="product_template_id.daily_sales", + read_only=True, + ) + stock_coverage = fields.Float( + string="Stock Coverage", + related="product_template_id.stock_coverage", + read_only=True, + ) + uom_po_id = fields.Many2one( + comodel_name="uom.uom", + string="Purchase Unit of Measure", + read_only=True, + related="product_template_id.uom_po_id", + help="Default Unit of Measure used for all stock operation.", + ) + supplierinfo_id = fields.Many2one( + comodel_name="product.supplierinfo", + string="Supplier information", + compute="_compute_supplierinfo", + store=True, + readonly=True, + ) + minimum_purchase_qty = fields.Float( + string="Minimum Purchase Quantity", related="supplierinfo_id.min_qty" + ) + product_price = fields.Float( + string="Product Price (w/o VAT)", + related="supplierinfo_id.price", + help="Supplier Product Price by buying unit. Price is without VAT", + ) + virtual_coverage = fields.Float( + string="Expected Stock Coverage", + compute="_compute_coverage_and_subtotal", + help="Expected stock coverage (in days) based on current stocks and " + "average daily consumption", + ) + subtotal = fields.Float( + string="Subtotal (w/o VAT)", compute="_compute_coverage_and_subtotal" + ) + + @api.multi + @api.depends("supplierinfo_id") + def _compute_name(self): + for cpol in self: + if cpol.supplierinfo_id and cpol.supplierinfo_id.product_code: + product_code = cpol.supplierinfo_id.product_code + product_name = cpol.product_template_id.name + cpol_name = "[%s] %s" % (product_code, product_name) + else: + cpol_name = cpol.product_template_id.name + cpol.name = cpol_name + + @api.multi + @api.onchange("product_template_id") + def _onchange_purchase_quantity(self): + for cpol in self: + cpol.purchase_quantity = cpol.minimum_purchase_qty + + @api.multi + @api.depends("purchase_quantity") + def _compute_coverage_and_subtotal(self): + for cpol in self: + cpol.subtotal = cpol.product_price * cpol.purchase_quantity + avg = cpol.daily_sales + if avg > 0: + qty = (cpol.virtual_available / cpol.uom_id.factor) + ( + cpol.purchase_quantity / cpol.uom_po_id.factor + ) + cpol.virtual_coverage = qty / avg + else: + # todo what would be a good default value? (not float(inf)) + cpol.virtual_coverage = 9999 + + return True + + @api.multi + @api.depends("product_template_id") + def _compute_supplierinfo(self): + for cpol in self: + if not cpol.product_template_id: + continue + + si = self.env["product.supplierinfo"].search( + [ + ("product_tmpl_id", "=", cpol.product_template_id.id), + ("name", "=", cpol.cpo_id.supplier_id.id), + ] + ) + + if len(si) == 0: + raise ValidationError( + _("POG supplier does not sell product {name}").format( + name=cpol.product_template_id.name + ) + ) + elif len(si) > 1: + _logger.warning( + "product {name} has several supplier info set, chose last".format( + name=cpol.product_template_id.name + ) + ) + si = si.sorted(key=lambda r: r.create_date, reverse=True) + cpol.supplierinfo_id = si[0] + + @api.constrains("purchase_quantity") + def _check_minimum_purchase_quantity(self): + for cpol in self: + if cpol.purchase_quantity < 0: + raise ValidationError( + _( + "Purchase quantity for {product_name} " + "must be greater than 0" + ).format(product_name=cpol.product_template_id.name) + ) + elif 0 < cpol.purchase_quantity < cpol.minimum_purchase_qty: + raise ValidationError( + _( + "Purchase quantity for {product_name} " + "must be greater than {min_qty}" + ).format( + product_name=cpol.product_template_id.name, + min_qty=cpol.minimum_purchase_qty, + ) + ) diff --git a/purchase_order_generator/readme/CONTRIBUTORS.rst b/purchase_order_generator/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..4eeddcc --- /dev/null +++ b/purchase_order_generator/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Robin Keunen +* Vincent Van Rossem diff --git a/purchase_order_generator/readme/DESCRIPTION.rst b/purchase_order_generator/readme/DESCRIPTION.rst new file mode 100644 index 0000000..ea924d5 --- /dev/null +++ b/purchase_order_generator/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Generate purchase order from a product selection. diff --git a/purchase_order_generator/readme/USAGE.rst b/purchase_order_generator/readme/USAGE.rst new file mode 100644 index 0000000..e69de29 diff --git a/purchase_order_generator/security/ir.model.access.csv b/purchase_order_generator/security/ir.model.access.csv new file mode 100644 index 0000000..dbb4063 --- /dev/null +++ b/purchase_order_generator/security/ir.model.access.csv @@ -0,0 +1,11 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink + +access_purchase_order,purchase.order,model_purchase_order_generator,purchase.group_purchase_user,1,1,1,1 +access_purchase_order_manager,purchase.order,model_purchase_order_generator,purchase.group_purchase_manager,1,1,1,1 +access_purchase_order_stock_worker,purchase.order,model_purchase_order_generator,stock.group_stock_user,1,0,0,0 +access_purchase_order_invoicing_payments,purchase.order,model_purchase_order_generator,account.group_account_invoice,1,1,0,0 + +access_purchase_order_line,purchase.order.line user,model_purchase_order_generator_line,purchase.group_purchase_user,1,1,1,1 +access_purchase_order_line_manager,purchase.order.line manager,model_purchase_order_generator_line,purchase.group_purchase_manager,1,1,1,1 +access_purchase_order_line_stock_worker,purchase.order.line,model_purchase_order_generator_line,stock.group_stock_user,1,0,0,0 +access_purchase_order_line_invoicing_payments,purchase.order.line,model_purchase_order_generator_line,account.group_account_invoice,1,1,0,0 diff --git a/purchase_order_generator/static/description/index.html b/purchase_order_generator/static/description/index.html new file mode 100644 index 0000000..bf49414 --- /dev/null +++ b/purchase_order_generator/static/description/index.html @@ -0,0 +1,415 @@ + + + + + + +Purchase Order Generator + + + +
+

Purchase Order Generator

+ + +

Beta License: AGPL-3 beescoop/obeesdoo

+

Generate purchase order from a product selection.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Coop IT Easy SCRLfs
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the beescoop/obeesdoo project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/purchase_order_generator/tests/__init__.py b/purchase_order_generator/tests/__init__.py new file mode 100644 index 0000000..ef21616 --- /dev/null +++ b/purchase_order_generator/tests/__init__.py @@ -0,0 +1 @@ +from . import test_pog diff --git a/purchase_order_generator/tests/test_pog.py b/purchase_order_generator/tests/test_pog.py new file mode 100644 index 0000000..4a5f9d4 --- /dev/null +++ b/purchase_order_generator/tests/test_pog.py @@ -0,0 +1,84 @@ +# Copyright (C) 2019 - Today: GRAP (http://www.grap.coop) +# @author: Robin Keunen +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.tests.common import TransactionCase, Form + + +class TestCPO(TransactionCase): + def setUp(self): + super().setUp() + + self.supplier = self.browse_ref("base.res_partner_1") + self.pproduct1 = self.browse_ref("product.product_product_25") + self.ptemplate1 = self.pproduct1.product_tmpl_id + self.pproduct2 = self.browse_ref("product.product_delivery_02") + self.ptemplate2 = self.pproduct2.product_tmpl_id + + def test_generate_cpo(self): + supplierinfo_obj = self.env["product.supplierinfo"] + supplierinfo = supplierinfo_obj.search( + [ + ("name", "=", self.supplier.id), + ("product_tmpl_id", "=", self.ptemplate1.id), + ] + ) + supplierinfo2 = supplierinfo_obj.search( + [ + ("name", "=", self.supplier.id), + ("product_tmpl_id", "=", self.ptemplate2.id), + ] + ) + + pog_obj = self.env["purchase.order.generator"] + pog_action = pog_obj.with_context( + active_ids=[self.ptemplate1.id] + ).generate_cpo() + pog = pog_obj.browse(pog_action["res_id"]) + pogl = pog.pog_line_ids # expect one line + + self.assertEquals(pog.supplier_id, self.supplier) + self.assertEquals(pogl.product_template_id, self.ptemplate1) + self.assertEquals(pogl.product_price, supplierinfo.price) + self.assertEquals(pogl.purchase_quantity, supplierinfo.min_qty) + + # testing triggers + expected_subtotal = supplierinfo.price * supplierinfo.min_qty + self.assertEquals(pogl.subtotal, expected_subtotal) + + pogl.purchase_quantity = 4 + expected_subtotal = supplierinfo.price * 4 + self.assertEquals(pogl.subtotal, expected_subtotal) + + pog_form = Form(pog) + with pog_form.pog_line_ids.edit(index=0) as line_form: + line_form.product_template_id = self.ptemplate2 + self.assertEquals(line_form.product_template_id, self.ptemplate2) + pog = pog_form.save() + pogl = pog.pog_line_ids + + expected_subtotal = supplierinfo2.price * supplierinfo2.min_qty + self.assertEquals(pogl.product_price, supplierinfo2.price) + self.assertEquals(pogl.purchase_quantity, supplierinfo2.min_qty) + self.assertEquals(pogl.subtotal, expected_subtotal) + + def test_generate_po(self): + cpo_obj = self.env["purchase.order.generator"] + cpo_action = cpo_obj.with_context( + active_ids=[self.ptemplate1.id, self.ptemplate2.id] + ).generate_cpo() + cpo = cpo_obj.browse(cpo_action["res_id"]) + po_action = cpo.create_purchase_order() + po = self.env["purchase.order"].browse(po_action["res_id"]) + + self.assertEquals(cpo.supplier_id, po.partner_id) + self.assertEquals(len(cpo.pog_line_ids), len(po.order_line)) + lines = zip( + cpo.pog_line_ids.sorted(lambda l: l.product_template_id), + po.order_line.sorted(lambda l: l.product_id.product_tmpl_id), + ) + for cpol, pol in lines: + self.assertEquals( + cpol.product_template_id, pol.product_id.product_tmpl_id + ) + self.assertEquals(cpol.purchase_quantity, pol.product_qty) diff --git a/purchase_order_generator/views/product_template.xml b/purchase_order_generator/views/product_template.xml new file mode 100644 index 0000000..b326eec --- /dev/null +++ b/purchase_order_generator/views/product_template.xml @@ -0,0 +1,23 @@ + + + + + product.template.tree + product.template + + + + + + + + + + + + + + + + + diff --git a/purchase_order_generator/views/purchase_order.xml b/purchase_order_generator/views/purchase_order.xml new file mode 100644 index 0000000..75dc354 --- /dev/null +++ b/purchase_order_generator/views/purchase_order.xml @@ -0,0 +1,13 @@ + + + + purchase.order.form.inherit + purchase.order + + + + + + + + diff --git a/purchase_order_generator/views/purchase_order_generator.xml b/purchase_order_generator/views/purchase_order_generator.xml new file mode 100644 index 0000000..ad5c78d --- /dev/null +++ b/purchase_order_generator/views/purchase_order_generator.xml @@ -0,0 +1,113 @@ + + + + + + + purchase.order.generator.tree + purchase.order.generator + + + + + + + + + + + + + + purchase.order.generator.form + purchase.order.generator + +
+
+
+ + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + +
+ + +
+
+
+ + + + purchase.order.generator + + + + + + + + + + + Purchase Order Generators + purchase.order.generator + + + + + + + + Generate Purchase Order + + + code + + action = model.generate_cpo() + + +