diff --git a/purchase_quick/README.rst b/purchase_quick/README.rst new file mode 100644 index 00000000000..027db5514a0 --- /dev/null +++ b/purchase_quick/README.rst @@ -0,0 +1,132 @@ +==================== +Quick Purchase order +==================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:93c58ee58e1fb9df5ff7d7c7b4d55d0461805db2aee1def2d8605b2e11ea3818 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-OCA%2Fpurchase--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/purchase-workflow/tree/16.0/purchase_quick + :alt: OCA/purchase-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/purchase-workflow-16-0/purchase-workflow-16-0-purchase_quick + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/purchase-workflow&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to add easily products into the purchase order (mass line add/update). + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + + +Inside a purchase order, you can click on "Add products", to open a product tree view, then update "qty to purchase" field. + +.. image:: https://raw.githubusercontent.com/OCA/purchase-workflow/16.0/purchase_quick/static/description/01_purchase_order_create.png + :width: 800 px + :alt: Purchase order create + +| + +.. image:: https://raw.githubusercontent.com/OCA/purchase-workflow/16.0/purchase_quick/static/description/02_purchase_order_add_product.png + :width: 800 px + :alt: Purchase order Add product + +| + +The update of "qty to purchase" will add new purchase line or update the existing line. If qty to purchase is 0 it purchase line will deleted if it exists. + +| + +.. image:: https://raw.githubusercontent.com/OCA/purchase-workflow/16.0/purchase_quick/static/description/03_purchase_order_updated.png + :width: 800 px + :alt: Purchase order updated + +| + +.. image:: https://raw.githubusercontent.com/OCA/purchase-workflow/16.0/purchase_quick/static/description/04_purchase_order_update_product_qty.png + :width: 800 px + :alt: Purchase order update product qty. + +Known issues / Roadmap +====================== + +A note on dependencies: this module depends on stock. Mainly, for displaying qty_available of a product. +To avoid this dependency, this module could be split. + +Compatibility note: purchase_order_type could be compatible as far as functionality goes, but not +for tests (adding a new required field breaks our usage of Form). + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* Sébastien BEAU +* Mourad EL HADJ MIMOUNE +* Pierrick Brun +* Kevin Khao + +* `Sygel `_: + + * Ángel García de la Chica Herrera + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-legalsylvain| image:: https://github.com/legalsylvain.png?size=40px + :target: https://github.com/legalsylvain + :alt: legalsylvain + +Current `maintainer `__: + +|maintainer-legalsylvain| + +This module is part of the `OCA/purchase-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/purchase_quick/__init__.py b/purchase_quick/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/purchase_quick/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/purchase_quick/__manifest__.py b/purchase_quick/__manifest__.py new file mode 100644 index 00000000000..b37a7f814e9 --- /dev/null +++ b/purchase_quick/__manifest__.py @@ -0,0 +1,17 @@ +# © 2014 Today Akretion +# @author Sébastien BEAU +# @author Pierrick Brun +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Quick Purchase order", + "version": "16.0.1.0.0", + "author": "Akretion, Odoo Community Association (OCA)", + "maintainers": ["legalsylvain"], + "website": "https://github.com/OCA/purchase-workflow", + "license": "AGPL-3", + "category": "Purchase", + "depends": ["base_product_mass_addition", "purchase"], + "data": ["views/purchase_view.xml", "views/product_view.xml"], + "installable": True, +} diff --git a/purchase_quick/i18n/es.po b/purchase_quick/i18n/es.po new file mode 100644 index 00000000000..6e78357217d --- /dev/null +++ b/purchase_quick/i18n/es.po @@ -0,0 +1,89 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_quick +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-04-12 10:07+0000\n" +"Last-Translator: luis-ron \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.14.1\n" + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.view_purchase_form +msgid "Add" +msgstr "Añadir" + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.product_search_form_view +msgid "Filter products supplied by the supplier of the current parent object" +msgstr "" +"Filtrar los productos suministrados por el proveedor del objeto padre actual" + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.product_search_form_view +msgid "For current supplier" +msgstr "Para el proveedor actual" + +#. module: purchase_quick +#: code:addons/purchase_quick/models/purchase_order.py:0 +#, python-format +msgid "" +"Must have only 1 line per product for mass addition, but there are {nr_lines}" +"s lines for the product %(product)s" +msgstr "" +"Sólo debe haber 1 línea por producto para la adición en masa, pero hay " +"{nr_lines}s líneas para el producto %(product)s" + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.product_tree_view4purchase +msgid "Open" +msgstr "Abrir" + +#. module: purchase_quick +#: model:ir.model.fields,field_description:purchase_quick.field_product_product__po_line_ids +msgid "Po Line" +msgstr "Línea Po" + +#. module: purchase_quick +#: model:ir.model,name:purchase_quick.model_product_product +msgid "Product" +msgstr "Producto" + +#. module: purchase_quick +#: code:addons/purchase_quick/models/purchase_order.py:0 +#, python-format +msgid "Product Variants" +msgstr "Variantes de Productos" + +#. module: purchase_quick +#: model:ir.model,name:purchase_quick.model_purchase_order +msgid "Purchase Order" +msgstr "Orden de Compra" + +#. module: purchase_quick +#: model:ir.model.fields,field_description:purchase_quick.field_product_product__seller_price +msgid "Seller Price" +msgstr "Precio Vendedor" + +#. module: purchase_quick +#: model:ir.model.fields,help:purchase_quick.field_product_product__po_line_ids +msgid "Technical: used to compute quantities to purchase." +msgstr "Técnico: utilizado para calcular las cantidades a comprar." + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.product_tree_view4purchase +msgid "UoM" +msgstr "UdM" + +#. module: purchase_quick +#: model:ir.model.fields,field_description:purchase_quick.field_product_product__variant_specific_seller_ids +msgid "Variant Specific Seller" +msgstr "Vendedor Específico de Variantes" diff --git a/purchase_quick/i18n/fr.po b/purchase_quick/i18n/fr.po new file mode 100644 index 00000000000..63a774c2999 --- /dev/null +++ b/purchase_quick/i18n/fr.po @@ -0,0 +1,90 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_quick +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-02-09 22:03+0000\n" +"PO-Revision-Date: 2024-02-09 22:03+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.view_purchase_form +msgid "Add" +msgstr "Ajout Rapide" + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.product_search_form_view +msgid "Filter products supplied by the supplier of the current parent object" +msgstr "" +"Afficher uniquement les articles du fournisseur de la commande d'achat en " +"cours" + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.product_search_form_view +msgid "For current supplier" +msgstr "Ce Fournisseur" + +#. module: purchase_quick +#. odoo-python +#: code:addons/purchase_quick/models/purchase_order.py:0 +#, python-format +msgid "" +"Must have only 1 line per product for mass addition, but there are " +"%(nr_lines)s lines for the product %(product_name)s" +msgstr "Il doit y avoir seulement une ligne par produit pour l'addition en masse, mais il y a" +" %(nr_lines)s lignes pour le produit %(product_name)s" + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.product_tree_view4purchase +msgid "Open" +msgstr "Ouvrir" + +#. module: purchase_quick +#: model:ir.model.fields,field_description:purchase_quick.field_product_product__po_line_ids +msgid "Po Line" +msgstr "Ligne de Commande" + +#. module: purchase_quick +#: model:ir.model,name:purchase_quick.model_product_product +msgid "Product Variant" +msgstr "Variante de produit" + +#. module: purchase_quick +#. odoo-python +#: code:addons/purchase_quick/models/purchase_order.py:0 +#, python-format +msgid "Product Variants" +msgstr "Articles" + +#. module: purchase_quick +#: model:ir.model,name:purchase_quick.model_purchase_order +msgid "Purchase Order" +msgstr "Bon de commande" + +#. module: purchase_quick +#: model:ir.model.fields,field_description:purchase_quick.field_product_product__seller_price +msgid "Seller Price" +msgstr "Prix fournisseur" + +#. module: purchase_quick +#: model:ir.model.fields,help:purchase_quick.field_product_product__po_line_ids +msgid "Technical: used to compute quantities to purchase." +msgstr "Technique : utilisé pour calculer les quantités à acheter." + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.product_tree_view4purchase +msgid "UoM" +msgstr "UdM" + +#. module: purchase_quick +#: model:ir.model.fields,field_description:purchase_quick.field_product_product__variant_specific_seller_ids +msgid "Variant Specific Seller" +msgstr "Fournisseur spécifique pour cette variante" diff --git a/purchase_quick/i18n/it.po b/purchase_quick/i18n/it.po new file mode 100644 index 00000000000..8880efaa8d4 --- /dev/null +++ b/purchase_quick/i18n/it.po @@ -0,0 +1,106 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_quick +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-04-18 12:34+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.14.1\n" + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.view_purchase_form +msgid "Add" +msgstr "Aggiungi" + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.product_search_form_view +msgid "Filter products supplied by the supplier of the current parent object" +msgstr "Filtra prodotti forniti dal fornitore dell'oggetto genitore corrente" + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.product_search_form_view +msgid "For current supplier" +msgstr "Per il fornitore selezionato" + +#. module: purchase_quick +#: code:addons/purchase_quick/models/purchase_order.py:0 +#, python-format +msgid "" +"Must have only 1 line per product for mass addition, but there are {nr_lines}" +"s lines for the product %(product)s" +msgstr "" + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.product_tree_view4purchase +msgid "Open" +msgstr "Apri" + +#. module: purchase_quick +#: model:ir.model.fields,field_description:purchase_quick.field_product_product__po_line_ids +msgid "Po Line" +msgstr "Riga PO" + +#. module: purchase_quick +#: model:ir.model,name:purchase_quick.model_product_product +msgid "Product" +msgstr "Prodotto" + +#. module: purchase_quick +#: code:addons/purchase_quick/models/purchase_order.py:0 +#, python-format +msgid "Product Variants" +msgstr "Varianti prodotto" + +#. module: purchase_quick +#: model:ir.model,name:purchase_quick.model_purchase_order +msgid "Purchase Order" +msgstr "Ordine di acquisto" + +#. module: purchase_quick +#: model:ir.model.fields,field_description:purchase_quick.field_product_product__seller_price +msgid "Seller Price" +msgstr "Prezzo Fornitore" + +#. module: purchase_quick +#: model:ir.model.fields,help:purchase_quick.field_product_product__po_line_ids +msgid "Technical: used to compute quantities to purchase." +msgstr "Tecnico: utilizzato per calcolare quantità da acquistare." + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.product_tree_view4purchase +msgid "UoM" +msgstr "UdM" + +#. module: purchase_quick +#: model:ir.model.fields,field_description:purchase_quick.field_product_product__variant_specific_seller_ids +msgid "Variant Specific Seller" +msgstr "Fornitore specifica variante" + +#~ msgid "Display Name" +#~ msgstr "Nome visualizzato" + +#~ msgid "ID" +#~ msgstr "ID" + +#~ msgid "Last Modified on" +#~ msgstr "Ultima modifica il" + +#, python-format +#~ msgid "" +#~ "Must have only 1 line per product for mass addition, but there are %s " +#~ "lines for the product %s" +#~ msgstr "" +#~ "Dev'esserci solo 1 riga per prodotto per aggiungerli massivamente, ma ci " +#~ "sono %s righe per il prodotto %s" + +#~ msgid "Products for purchase" +#~ msgstr "Prodotti da acquistare" diff --git a/purchase_quick/i18n/purchase_quick.pot b/purchase_quick/i18n/purchase_quick.pot new file mode 100644 index 00000000000..2a1f25b60c0 --- /dev/null +++ b/purchase_quick/i18n/purchase_quick.pot @@ -0,0 +1,87 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_quick +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-02-09 22:06+0000\n" +"PO-Revision-Date: 2024-02-09 22:06+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.view_purchase_form +msgid "Add" +msgstr "" + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.product_search_form_view +msgid "Filter products supplied by the supplier of the current parent object" +msgstr "" + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.product_search_form_view +msgid "For current supplier" +msgstr "" + +#. module: purchase_quick +#. odoo-python +#: code:addons/purchase_quick/models/purchase_order.py:0 +#, python-format +msgid "" +"Must have only 1 line per product for mass addition, but there are " +"%(nr_lines)s lines for the product %(product_name)s" +msgstr "" + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.product_tree_view4purchase +msgid "Open" +msgstr "" + +#. module: purchase_quick +#: model:ir.model.fields,field_description:purchase_quick.field_product_product__po_line_ids +msgid "Po Line" +msgstr "" + +#. module: purchase_quick +#: model:ir.model,name:purchase_quick.model_product_product +msgid "Product Variant" +msgstr "" + +#. module: purchase_quick +#. odoo-python +#: code:addons/purchase_quick/models/purchase_order.py:0 +#, python-format +msgid "Product Variants" +msgstr "" + +#. module: purchase_quick +#: model:ir.model,name:purchase_quick.model_purchase_order +msgid "Purchase Order" +msgstr "" + +#. module: purchase_quick +#: model:ir.model.fields,field_description:purchase_quick.field_product_product__seller_price +msgid "Seller Price" +msgstr "" + +#. module: purchase_quick +#: model:ir.model.fields,help:purchase_quick.field_product_product__po_line_ids +msgid "Technical: used to compute quantities to purchase." +msgstr "" + +#. module: purchase_quick +#: model_terms:ir.ui.view,arch_db:purchase_quick.product_tree_view4purchase +msgid "UoM" +msgstr "" + +#. module: purchase_quick +#: model:ir.model.fields,field_description:purchase_quick.field_product_product__variant_specific_seller_ids +msgid "Variant Specific Seller" +msgstr "" \ No newline at end of file diff --git a/purchase_quick/models/__init__.py b/purchase_quick/models/__init__.py new file mode 100644 index 00000000000..336337e8f31 --- /dev/null +++ b/purchase_quick/models/__init__.py @@ -0,0 +1,2 @@ +from . import purchase_order +from . import product_product diff --git a/purchase_quick/models/product_product.py b/purchase_quick/models/product_product.py new file mode 100644 index 00000000000..31be6316e9a --- /dev/null +++ b/purchase_quick/models/product_product.py @@ -0,0 +1,93 @@ +# © 2014 Today Akretion +# @author Sébastien BEAU +# @author Mourad EL HADJ MIMOUNE +# @author Pierrick Brun +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +from odoo import api, fields, models + + +class ProductProduct(models.Model): + _inherit = "product.product" + + # for searching purpose + variant_specific_seller_ids = fields.One2many("product.supplierinfo", "product_id") + po_line_ids = fields.One2many( + comodel_name="purchase.order.line", + inverse_name="product_id", + help="Technical: used to compute quantities to purchase.", + ) + seller_price = fields.Float(compute="_compute_seller_price") + + def _compute_seller_price(self): + po = self.pma_parent + for record in self: + seller = record._select_seller( + partner_id=po.partner_id, + quantity=record.qty_to_process or 1, + uom_id=record.quick_uom_id, + ) + price_unit = record.uom_id._compute_price( + seller.price, + record.quick_uom_id, + ) + if self.pma_parent.currency_id != seller.currency_id: + price_unit = seller.currency_id._convert( + price_unit, po.currency_id, po.company_id, po.date_order.date() + ) + record.seller_price = price_unit + + def _default_quick_uom_id(self): + if self.env.context.get("parent_model", False) == "purchase.order": + return self.uom_po_id + return super()._default_quick_uom_id() + + def _compute_process_qty_purchase(self): + po_lines = self.env["purchase.order.line"].search( + [("order_id", "=", self.env.context.get("parent_id"))] + ) + for product in self: + product.qty_to_process = sum( + po_lines.filtered(lambda l: l.product_id == product).mapped( + "product_qty" + ) + ) + + @api.depends("po_line_ids") + def _compute_process_qty(self): + res = super()._compute_process_qty() + if self.env.context.get("parent_model", False) == "purchase.order": + self._compute_process_qty_purchase() + return res + + @api.model + def search(self, args, offset=0, limit=None, order=None, count=False): + purchase = self.env["purchase.order"].browse(self.env.context.get("parent_id")) + if self.env.context.get("in_current_parent") and purchase: + po_lines = self.env["purchase.order.line"].search( + [("order_id", "=", purchase.id)] + ) + args.append(("id", "in", po_lines.mapped("product_id").ids)) + if self.env.context.get("for_current_supplier") and purchase: + seller = purchase.partner_id + seller = seller.commercial_partner_id or seller + args += [ + "|", + ("variant_specific_seller_ids.partner_id", "=", seller.id), + "&", + ("seller_ids.partner_id", "=", seller.id), + ("product_variant_ids", "!=", False), + ] + return super().search( + args, offset=offset, limit=limit, order=order, count=count + ) + + @api.model + def check_access_rights(self, operation, raise_exception=True): + """hijack product edition rights if we're in the mass edition menu""" + if self.env.context.get("quick_access_rights_purchase"): + return self.env["purchase.order.line"].check_access_rights( + operation, raise_exception + ) + return super().check_access_rights(operation, raise_exception) diff --git a/purchase_quick/models/purchase_order.py b/purchase_quick/models/purchase_order.py new file mode 100644 index 00000000000..074e68b4609 --- /dev/null +++ b/purchase_quick/models/purchase_order.py @@ -0,0 +1,71 @@ +# © 2014 Today Akretion +# @author Sébastien BEAU +# @author Mourad EL HADJ MIMOUNE +# @author Pierrick Brun +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo import _, models +from odoo.exceptions import ValidationError + + +class PurchaseOrder(models.Model): + _name = "purchase.order" + _inherit = ["purchase.order", "product.mass.addition"] + + def add_product(self): + self.ensure_one() + res = self._common_action_keys() + res["context"].update( + { + "search_default_filter_to_purchase": 1, + "search_default_filter_for_current_supplier": 1, + "quick_access_rights_purchase": 1, + } + ) + commercial = self.partner_id.commercial_partner_id.name + res["name"] = "🔙 {} ({})".format(_("Product Variants"), commercial) + res["view_id"] = (self.env.ref("purchase_quick.product_tree_view4purchase").id,) + res["search_view_id"] = ( + self.env.ref("purchase_quick.product_search_form_view").id, + ) + return res + + def _get_quick_line(self, product): + result = self.env["purchase.order.line"].search( + [("product_id", "=", product.id), ("order_id", "=", self.id)] + ) + nr_lines = len(result.ids) + if nr_lines > 1: + raise ValidationError( + _( + "Must have only 1 line per product for mass addition, but " + "there are %(nr_lines)s lines for the product %(product_name)s", + nr_lines=nr_lines, + product_name=product.display_name, + ) + ) + return result + + def _get_quick_line_qty_vals(self, product): + return { + "product_id": None, + "product_uom": product.quick_uom_id.id, + "product_qty": product.qty_to_process, + } + + def _complete_quick_line_vals(self, vals, lines_key=""): + # This params are need for playing correctly the onchange + vals_to_add = { + "order_id": self.id, + "partner_id": self.partner_id.id, + } + vals_to_add.update(vals) + vals = vals_to_add + return super(PurchaseOrder, self)._complete_quick_line_vals( + vals, lines_key="order_line" + ) + + def _add_quick_line(self, product, lines_key=""): + return super(PurchaseOrder, self)._add_quick_line( + product, lines_key="order_line" + ) diff --git a/purchase_quick/readme/CONTRIBUTORS.rst b/purchase_quick/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..d71da740172 --- /dev/null +++ b/purchase_quick/readme/CONTRIBUTORS.rst @@ -0,0 +1,8 @@ +* Sébastien BEAU +* Mourad EL HADJ MIMOUNE +* Pierrick Brun +* Kevin Khao + +* `Sygel `_: + + * Ángel García de la Chica Herrera diff --git a/purchase_quick/readme/DESCRIPTION.rst b/purchase_quick/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..dcaf1af8fe7 --- /dev/null +++ b/purchase_quick/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module allows to add easily products into the purchase order (mass line add/update). diff --git a/purchase_quick/readme/ROADMAP.rst b/purchase_quick/readme/ROADMAP.rst new file mode 100644 index 00000000000..7ce32408133 --- /dev/null +++ b/purchase_quick/readme/ROADMAP.rst @@ -0,0 +1,5 @@ +A note on dependencies: this module depends on stock. Mainly, for displaying qty_available of a product. +To avoid this dependency, this module could be split. + +Compatibility note: purchase_order_type could be compatible as far as functionality goes, but not +for tests (adding a new required field breaks our usage of Form). diff --git a/purchase_quick/readme/USAGE.rst b/purchase_quick/readme/USAGE.rst new file mode 100644 index 00000000000..733ed9ea1ba --- /dev/null +++ b/purchase_quick/readme/USAGE.rst @@ -0,0 +1,28 @@ + +Inside a purchase order, you can click on "Add products", to open a product tree view, then update "qty to purchase" field. + +.. image:: ../static/description/01_purchase_order_create.png + :width: 800 px + :alt: Purchase order create + +| + +.. image:: ../static/description/02_purchase_order_add_product.png + :width: 800 px + :alt: Purchase order Add product + +| + +The update of "qty to purchase" will add new purchase line or update the existing line. If qty to purchase is 0 it purchase line will deleted if it exists. + +| + +.. image:: ../static/description/03_purchase_order_updated.png + :width: 800 px + :alt: Purchase order updated + +| + +.. image:: ../static/description/04_purchase_order_update_product_qty.png + :width: 800 px + :alt: Purchase order update product qty. diff --git a/purchase_quick/static/description/01_purchase_order_create.png b/purchase_quick/static/description/01_purchase_order_create.png new file mode 100644 index 00000000000..d200056c5fa Binary files /dev/null and b/purchase_quick/static/description/01_purchase_order_create.png differ diff --git a/purchase_quick/static/description/02_purchase_order_add_product.png b/purchase_quick/static/description/02_purchase_order_add_product.png new file mode 100644 index 00000000000..5bd97e6f2ab Binary files /dev/null and b/purchase_quick/static/description/02_purchase_order_add_product.png differ diff --git a/purchase_quick/static/description/03_purchase_order_updated.png b/purchase_quick/static/description/03_purchase_order_updated.png new file mode 100644 index 00000000000..0de514f801f Binary files /dev/null and b/purchase_quick/static/description/03_purchase_order_updated.png differ diff --git a/purchase_quick/static/description/04_purchase_order_update_product_qty.png b/purchase_quick/static/description/04_purchase_order_update_product_qty.png new file mode 100644 index 00000000000..c8b19741b7d Binary files /dev/null and b/purchase_quick/static/description/04_purchase_order_update_product_qty.png differ diff --git a/purchase_quick/static/description/filter.png b/purchase_quick/static/description/filter.png new file mode 100644 index 00000000000..12342418cb0 Binary files /dev/null and b/purchase_quick/static/description/filter.png differ diff --git a/purchase_quick/static/description/icon.png b/purchase_quick/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/purchase_quick/static/description/icon.png differ diff --git a/purchase_quick/static/description/index.html b/purchase_quick/static/description/index.html new file mode 100644 index 00000000000..ee3f97da3c0 --- /dev/null +++ b/purchase_quick/static/description/index.html @@ -0,0 +1,462 @@ + + + + + +Quick Purchase order + + + +
+

Quick Purchase order

+ + +

Beta License: AGPL-3 OCA/purchase-workflow Translate me on Weblate Try me on Runboat

+

This module allows to add easily products into the purchase order (mass line add/update).

+

Table of contents

+ +
+

Usage

+

Inside a purchase order, you can click on “Add products”, to open a product tree view, then update “qty to purchase” field.

+Purchase order create +
+

+
+Purchase order Add product +
+

+
+

The update of “qty to purchase” will add new purchase line or update the existing line. If qty to purchase is 0 it purchase line will deleted if it exists.

+
+

+
+Purchase order updated +
+

+
+Purchase order update product qty. +
+
+

Known issues / Roadmap

+

A note on dependencies: this module depends on stock. Mainly, for displaying qty_available of a product. +To avoid this dependency, this module could be split.

+

Compatibility note: purchase_order_type could be compatible as far as functionality goes, but not +for tests (adding a new required field breaks our usage of Form).

+
+
+

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 to smash it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

legalsylvain

+

This module is part of the OCA/purchase-workflow project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/purchase_quick/static/description/po-product.png b/purchase_quick/static/description/po-product.png new file mode 100644 index 00000000000..14a3d1c50fc Binary files /dev/null and b/purchase_quick/static/description/po-product.png differ diff --git a/purchase_quick/static/description/po.png b/purchase_quick/static/description/po.png new file mode 100644 index 00000000000..e2a5e7d9dc5 Binary files /dev/null and b/purchase_quick/static/description/po.png differ diff --git a/purchase_quick/tests/__init__.py b/purchase_quick/tests/__init__.py new file mode 100644 index 00000000000..02c358d02bf --- /dev/null +++ b/purchase_quick/tests/__init__.py @@ -0,0 +1 @@ +from . import test_quick_purchase diff --git a/purchase_quick/tests/test_quick_purchase.py b/purchase_quick/tests/test_quick_purchase.py new file mode 100644 index 00000000000..b59948cc4ee --- /dev/null +++ b/purchase_quick/tests/test_quick_purchase.py @@ -0,0 +1,216 @@ +# @author Mourad EL HADJ MIMOUNE +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.exceptions import ValidationError +from odoo.tests import tagged +from odoo.tests.common import Form, TransactionCase + + +@tagged("post_install", "-at_install") +class TestQuickPurchase(TransactionCase): + @classmethod + def _add_seller(cls, product, prices): + # drop existing seller + product.seller_ids.filtered(lambda s: s.partner_id == cls.partner).unlink() + for min_qty, price in prices: + cls.env["product.supplierinfo"].create( + { + "product_tmpl_id": product.product_tmpl_id.id, + "partner_id": cls.partner.id, + "price": price, + "min_qty": min_qty, + } + ) + + @classmethod + def _setUpBasicSaleOrder(cls): + vals = {"partner_id": cls.partner.id} + if hasattr(cls.env["purchase.order"], "order_type"): + vals["order_type"] = cls.env.ref("purchase_order_type.po_type_blanket").id + cls.po = cls.env["purchase.order"].create(vals) + with Form(cls.po, "purchase.purchase_order_form") as po_form: + po_form.partner_id = cls.partner + ctx = {"parent_id": cls.po.id, "parent_model": "purchase.order"} + cls.product_1 = cls.product_1.with_context(**ctx) + cls.product_2 = cls.product_2.with_context(**ctx) + cls.product_1.qty_to_process = 5.0 + cls.product_2.qty_to_process = 6.0 + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env.ref("base.res_partner_1") + cls.uom_unit = cls.env.ref("uom.product_uom_unit") + cls.uom_dozen = cls.env.ref("uom.product_uom_dozen") + cls.product_1 = cls.env.ref("product.product_product_8") + cls.product_2 = cls.env.ref("product.product_product_11") + cls._add_seller(cls.product_1, [(0, 10), (10, 8)]) + cls._add_seller(cls.product_2, [(0, 5), (10, 4)]) + cls._setUpBasicSaleOrder() + + def test_product_seller_price(self): + self.assertEqual(self.product_1.seller_price, 10) + self.product_1.qty_to_process = 10.0 + self.assertEqual(self.product_1.seller_price, 8) + self.product_1.quick_uom_id = self.uom_dozen + self.assertEqual(self.product_1.seller_price, 96) + + def test_product_seller_price_with_currency(self): + self.po.currency_id = self.env.ref("base.EUR") + usd = self.env.ref("base.USD") + usd.rate_ids[1:].unlink() + usd.rate_ids.name = self.po.date_order.date() + usd.rate_ids.rate = 2 + self.assertEqual(self.product_1.seller_price, 5) + self.product_1.qty_to_process = 10.0 + self.assertEqual(self.product_1.seller_price, 4) + self.product_1.quick_uom_id = self.uom_dozen + self.assertEqual(self.product_1.seller_price, 48) + + def test_quick_line_add_1(self): + """ + set non-null quantity to any product with no PO line: + -> a new PO line is created with that quantity + """ + line_1, line_2 = self.po.order_line + self.assertAlmostEqual(line_1.product_uom_qty, 5.0) + self.assertAlmostEqual(line_1.price_unit, 10) + self.assertAlmostEqual(line_2.product_uom_qty, 6.0) + self.assertAlmostEqual(line_2.price_unit, 5) + + def test_quick_line_add_2(self): + """ + same as previous, but include a different UoM as well + We duplicate _setUpBasicSaleOrder except we ~simultaneously~ + write on qty_to_process as well as quick_uom_id + (we want to make sure to test _inverse function when it is triggered twice) + """ + vals = {"partner_id": self.partner.id} + if hasattr(self.env["purchase.order"], "order_type"): + vals["order_type"] = self.env.ref("purchase_order_type.po_type_blanket").id + po = self.env["purchase.order"].create(vals) + with Form(po, "purchase.purchase_order_form") as po_form: + po_form.partner_id = self.partner + ctx = {"parent_id": self.po.id, "parent_model": "purchase.order"} + self.product_1 = self.product_1.with_context(**ctx) + self.product_2 = self.product_2.with_context(**ctx) + self.product_1.write({"qty_to_process": 5.0, "quick_uom_id": self.uom_unit.id}) + self.product_2.write({"qty_to_process": 6.0, "quick_uom_id": self.uom_dozen.id}) + + line_1, line_2 = self.po.order_line + self.assertAlmostEqual(line_1.product_uom_qty, 5.0) + self.assertAlmostEqual(line_1.product_qty, 5.0) + self.assertEqual(line_1.product_uom, self.uom_unit) + self.assertAlmostEqual(line_1.price_unit, 10) + + self.assertAlmostEqual(line_2.product_uom_qty, 72.0) # 12 * 6 + self.assertAlmostEqual(line_2.product_qty, 6.0) + self.assertEqual(line_2.product_uom, self.uom_dozen) + self.assertAlmostEqual(line_2.price_unit, 48) # 12 * 4 + + def test_quick_line_update_1(self): + """ + set non-null quantity to any product with an already existing PO line: + -> same PO line is updated with that quantity + """ + self.product_1.qty_to_process = 7.0 + self.product_2.qty_to_process = 13.0 + line_1, line_2 = self.po.order_line + self.assertAlmostEqual(line_1.product_qty, 7.0) + self.assertAlmostEqual(line_1.price_unit, 10.0) + self.assertAlmostEqual(line_2.product_qty, 13.0) + self.assertAlmostEqual(line_2.price_unit, 4.0) + + def test_quick_line_update_2(self): + """ + same as previous update only UoM in isolation, not qty + """ + self.product_1.quick_uom_id = self.uom_dozen + self.product_2.quick_uom_id = self.uom_unit + line_1, line_2 = self.po.order_line + + self.assertEqual(line_1.product_uom, self.uom_dozen) + self.assertAlmostEqual(line_1.product_qty, 5.0) + self.assertAlmostEqual(line_1.product_uom_qty, 60.0) + self.assertAlmostEqual(line_1.price_unit, 96) + + self.assertEqual(line_2.product_uom, self.uom_unit) + self.assertAlmostEqual(line_2.product_qty, 6.0) + self.assertAlmostEqual(line_2.product_uom_qty, 6.0) + self.assertAlmostEqual(line_2.price_unit, 5.0) + + def test_quick_line_update_3(self): + """ + same as previous 2 tests combined: we do simultaneous qty + uom updates + """ + self.product_1.qty_to_process = 7.0 + self.product_2.qty_to_process = 13.0 + self.product_1.quick_uom_id = self.uom_dozen + self.product_2.quick_uom_id = self.uom_unit + + line_1, line_2 = self.po.order_line + self.assertEqual(line_1.product_uom, self.uom_dozen) + self.assertEqual(line_2.product_uom, self.uom_unit) + + self.assertEqual(line_1.product_uom, self.uom_dozen) + self.assertAlmostEqual(line_1.product_qty, 7.0) + self.assertAlmostEqual(line_1.product_uom_qty, 84.0) + self.assertAlmostEqual(line_1.price_unit, 96) + + self.assertEqual(line_2.product_uom, self.uom_unit) + self.assertAlmostEqual(line_2.product_qty, 13.0) + self.assertAlmostEqual(line_2.product_uom_qty, 13.0) + self.assertAlmostEqual(line_2.price_unit, 4.0) + + def test_quick_line_delete(self): + """ + set null quantity to any product with existing PO line: + -> PO line is deleted + """ + self.product_1.qty_to_process = 0.0 + self.product_2.qty_to_process = 0.0 + self.assertEqual(len(self.po.order_line), 0) + + def test_open_quick_view(self): + """ + Test that the "Add" button opens the right action + """ + product_act_from_po = self.po.add_product() + self.assertEqual(product_act_from_po["type"], "ir.actions.act_window") + self.assertEqual(product_act_from_po["res_model"], "product.product") + self.assertEqual(product_act_from_po["view_mode"], "tree") + self.assertEqual(product_act_from_po["target"], "current") + self.assertEqual( + product_act_from_po["view_id"][0], + self.env.ref("purchase_quick.product_tree_view4purchase").id, + ) + self.assertEqual(product_act_from_po["context"]["parent_id"], self.po.id) + + def test_several_po_for_one_product(self): + """ + Test that when we try to mass add a product that already has + several lines with the same product we get a raise + """ + self.po.order_line[0].copy() + with self.assertRaises(ValidationError): + self.product_1.qty_to_process = 3.0 + + def test_no_pricelist_for_the_min_qty(self): + """ + Checks that if you enter a qty_to_process lower than the seller's min_qty, + the price_unit in de pusrchase.order.line is the standard_price. + """ + po = self.env["purchase.order"].create({"partner_id": self.partner.id}) + ctx = { + "parent_id": po.id, + "parent_model": "purchase.order", + "quick_access_rights_purchase": 1, + } + product_3 = self.env.ref("product.product_product_5") + self._add_seller(product_3, [(5, 5), (10, 1)]) + product_3 = product_3.with_context(**ctx) + product_3.write({"qty_to_process": 1.0, "quick_uom_id": self.uom_unit.id}) + line_1 = po.order_line + self.assertEqual(line_1.product_qty, 1.0) + self.assertEqual(line_1.product_uom, self.uom_unit) + self.assertEqual(line_1.price_unit, product_3.standard_price) diff --git a/purchase_quick/views/product_view.xml b/purchase_quick/views/product_view.xml new file mode 100644 index 00000000000..2c9b2dcd75f --- /dev/null +++ b/purchase_quick/views/product_view.xml @@ -0,0 +1,46 @@ + + + + + + product.product + + + + + + + + + + +