From bc01c7bcec974ee095f5855f225667645ef40213 Mon Sep 17 00:00:00 2001 From: svs-odoo Date: Thu, 14 Sep 2023 15:28:44 +0200 Subject: [PATCH] [IMP] product,purchase,sale,*: product catalog This commit makes two major things: 1. Move the product catalog from `sale` to `product`. It moves the product catalog's code from `sale` to `product` module in order to be able to use it in other modules. A mixin, `product.catalog.mixin`, was created for to make other models catalog compatible. 2. It enables the product's catalog in `purchase`. In `purchase`, the catalog differs a little bit from `sale`: - If Unit of Measure is enable, the UoM for each product will appear in the catalog; - If the product purchase's UoM or the purchase order line's UoM is different than the default product's UoM, the former one will be displayed (in bold so the user can know it's the UoM to refer); - When opening the product's catalog from a purchase order, the products will be filtered by the PO's vendor; - Some data from the vendor list will be used if appliant: the minimum quantity and the price; - If there is at least one product's packaging, a button will be displayed to increase the qty by the packaging's one. If a packaging is set on the purchase order line, this one will be used. task-3373589 Part-of: odoo/odoo#135502 --- addons/delivery/__init__.py | 1 - addons/delivery/controllers/__init__.py | 3 - addons/delivery/controllers/catalog.py | 28 ---- addons/delivery/models/sale_order.py | 13 ++ addons/product/__manifest__.py | 3 + addons/product/controllers/__init__.py | 1 + addons/product/controllers/catalog.py | 42 ++++++ addons/product/models/__init__.py | 1 + .../product/models/product_catalog_mixin.py | 124 ++++++++++++++++ addons/product/models/product_product.py | 4 +- .../src}/product_catalog/kanban_controller.js | 9 +- .../product_catalog/kanban_controller.xml | 2 +- .../src}/product_catalog/kanban_model.js | 5 +- .../src}/product_catalog/kanban_record.js | 48 +++++-- .../src}/product_catalog/kanban_record.xml | 6 +- .../src}/product_catalog/kanban_renderer.js | 2 +- .../src}/product_catalog/kanban_renderer.xml | 4 +- .../src}/product_catalog/kanban_view.js | 2 +- .../product_catalog/order_line/order_line.js} | 16 ++- .../order_line/order_line.scss} | 4 +- .../order_line/order_line.xml} | 24 ++-- .../product_catalog/search/search_panel.js | 2 +- .../product_catalog/search/search_panel.xml | 2 +- addons/product/views/product_views.xml | 91 ++++++++++++ addons/purchase/__manifest__.py | 1 + addons/purchase/models/purchase_order.py | 134 +++++++++++++++++- addons/purchase/models/purchase_order_line.py | 68 +++++++++ .../src/product_catalog/kanban_record.js | 45 ++++++ .../purchase_order_line.js | 21 +++ .../purchase_order_line.xml | 26 ++++ addons/purchase/views/product_views.xml | 44 ++++-- addons/purchase/views/purchase_views.xml | 1 + .../purchase_stock/models/purchase_order.py | 7 + addons/purchase_stock/views/product_views.xml | 17 +++ addons/sale/__manifest__.py | 2 - addons/sale/controllers/__init__.py | 1 - addons/sale/controllers/catalog.py | 97 ------------- addons/sale/models/sale_order.py | 77 ++++++++-- addons/sale/models/sale_order_line.py | 26 +--- addons/sale/views/product_product_views.xml | 97 ------------- addons/sale/views/product_views.xml | 25 ++++ addons/sale_stock/__manifest__.py | 1 - addons/sale_stock/models/sale_order_line.py | 4 +- .../src/product_catalog/kanban_record.js | 20 ++- .../sale_order_line/sale_order_line.js | 20 ++- .../views/sale_product_kanban_views.xml | 24 ---- addons/stock/views/product_views.xml | 22 +++ 47 files changed, 850 insertions(+), 367 deletions(-) delete mode 100644 addons/delivery/controllers/__init__.py delete mode 100644 addons/delivery/controllers/catalog.py create mode 100644 addons/product/controllers/catalog.py create mode 100644 addons/product/models/product_catalog_mixin.py rename addons/{sale/static/src/js => product/static/src}/product_catalog/kanban_controller.js (83%) rename addons/{sale/static/src/js => product/static/src}/product_catalog/kanban_controller.xml (76%) rename addons/{sale/static/src/js => product/static/src}/product_catalog/kanban_model.js (80%) rename addons/{sale/static/src/js => product/static/src}/product_catalog/kanban_record.js (67%) rename addons/{sale/static/src/js => product/static/src}/product_catalog/kanban_record.xml (72%) rename addons/{sale/static/src/js => product/static/src}/product_catalog/kanban_renderer.js (94%) rename addons/{sale/static/src/js => product/static/src}/product_catalog/kanban_renderer.xml (77%) rename addons/{sale/static/src/js => product/static/src}/product_catalog/kanban_view.js (88%) rename addons/{sale/static/src/js/product_catalog/sale_order_line/sale_order_line.js => product/static/src/product_catalog/order_line/order_line.js} (51%) rename addons/{sale/static/src/js/product_catalog/sale_order_line/sale_order_line.scss => product/static/src/product_catalog/order_line/order_line.scss} (73%) rename addons/{sale/static/src/js/product_catalog/sale_order_line/sale_order_line.xml => product/static/src/product_catalog/order_line/order_line.xml} (65%) rename addons/{sale/static/src/js => product/static/src}/product_catalog/search/search_panel.js (95%) rename addons/{sale/static/src/js => product/static/src}/product_catalog/search/search_panel.xml (92%) create mode 100644 addons/purchase/static/src/product_catalog/kanban_record.js create mode 100644 addons/purchase/static/src/product_catalog/purchase_order_line/purchase_order_line.js create mode 100644 addons/purchase/static/src/product_catalog/purchase_order_line/purchase_order_line.xml delete mode 100644 addons/sale/controllers/catalog.py delete mode 100644 addons/sale/views/product_product_views.xml delete mode 100644 addons/sale_stock/views/sale_product_kanban_views.xml diff --git a/addons/delivery/__init__.py b/addons/delivery/__init__.py index 5b75d1ea8c5..33bbab569d0 100644 --- a/addons/delivery/__init__.py +++ b/addons/delivery/__init__.py @@ -1,5 +1,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import controllers from . import models from . import wizard diff --git a/addons/delivery/controllers/__init__.py b/addons/delivery/controllers/__init__.py deleted file mode 100644 index 843112eeb52..00000000000 --- a/addons/delivery/controllers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from . import catalog diff --git a/addons/delivery/controllers/catalog.py b/addons/delivery/controllers/catalog.py deleted file mode 100644 index cbf19fa7287..00000000000 --- a/addons/delivery/controllers/catalog.py +++ /dev/null @@ -1,28 +0,0 @@ -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo.http import request, route -from odoo.addons.sale.controllers.catalog import CatalogController - - -class CatalogControllerDelivery(CatalogController): - - @route() - def sale_product_catalog_update_sale_order_line_info( - self, order_id, product_id, quantity, **kwargs - ): - """ Override of `sale` to recompute the delivery prices. - - :param int order_id: The sale order, as a `sale.order` id. - :param int product_id: The product, as a `product.product` id. - :param float quantity: The quantity selected in the product catalog. - :return: The unit price price of the product, based on the pricelist of the sale order and - the quantity selected. - :rtype: float - """ - price_unit = super().sale_product_catalog_update_sale_order_line_info( - order_id, product_id, quantity, **kwargs - ) - order = request.env['sale.order'].browse(order_id) - if order: - order.onchange_order_line() - return price_unit diff --git a/addons/delivery/models/sale_order.py b/addons/delivery/models/sale_order.py index e72971b9da5..5a6a3a30711 100644 --- a/addons/delivery/models/sale_order.py +++ b/addons/delivery/models/sale_order.py @@ -161,3 +161,16 @@ class SaleOrder(models.Model): for order_line in self.order_line.filtered(lambda l: l.product_id.type in ['product', 'consu'] and not l.is_delivery and not l.display_type and l.product_uom_qty > 0): weight += order_line.product_qty * order_line.product_id.weight return weight + + def _update_order_line_info(self, product_id, quantity, **kwargs): + """ Override of `sale` to recompute the delivery prices. + + :param int product_id: The product, as a `product.product` id. + :return: The unit price price of the product, based on the pricelist of the sale order and + the quantity selected. + :rtype: float + """ + price_unit = super()._update_order_line_info(product_id, quantity, **kwargs) + if self: + self.onchange_order_line() + return price_unit diff --git a/addons/product/__manifest__.py b/addons/product/__manifest__.py index 8bb0d8efdc7..145110b9608 100644 --- a/addons/product/__manifest__.py +++ b/addons/product/__manifest__.py @@ -61,6 +61,9 @@ Print product labels with barcode. 'assets': { 'web.assets_backend': [ 'product/static/src/js/**/*', + 'product/static/src/product_catalog/**/*.js', + 'product/static/src/product_catalog/**/*.xml', + 'product/static/src/product_catalog/**/*.scss', ], 'web.report_assets_common': [ 'product/static/src/scss/report_label_sheet.scss', diff --git a/addons/product/controllers/__init__.py b/addons/product/controllers/__init__.py index 51c0b8196aa..43daf3678cf 100644 --- a/addons/product/controllers/__init__.py +++ b/addons/product/controllers/__init__.py @@ -1 +1,2 @@ +from . import catalog from . import product_document diff --git a/addons/product/controllers/catalog.py b/addons/product/controllers/catalog.py new file mode 100644 index 00000000000..f0bb0e0d4ff --- /dev/null +++ b/addons/product/controllers/catalog.py @@ -0,0 +1,42 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.http import request, route, Controller + + +class ProductCatalogController(Controller): + + @route('/product/catalog/order_lines_info', auth='user', type='json') + def product_catalog_get_order_lines_info(self, res_model, order_id, product_ids, **kwargs): + """ Returns products information to be shown in the catalog. + + :param string res_model: The order model. + :param int order_id: The order id. + :param list product_ids: The products currently displayed in the product catalog, as a list + of `product.product` ids. + :rtype: dict + :return: A dict with the following structure: + { + product.id: { + 'productId': int + 'quantity': float (optional) + 'price': float + 'readOnly': bool (optional) + } + } + """ + order = request.env[res_model].browse(order_id) + return order._get_product_catalog_order_line_info(product_ids, **kwargs) + + @route('/product/catalog/update_order_line_info', auth='user', type='json') + def product_catalog_update_order_line_info(self, res_model, order_id, product_id, quantity=0, **kwargs): + """ Update order line information on a given order for a given product. + + :param string res_model: The order model. + :param int order_id: The order id. + :param int product_id: The product, as a `product.product` id. + :return: The unit price price of the product, based on the pricelist of the order and + the quantity selected. + :rtype: float + """ + order = request.env[res_model].browse(order_id) + return order._update_order_line_info(product_id, quantity, **kwargs) diff --git a/addons/product/models/__init__.py b/addons/product/models/__init__.py index 6e9141c38b3..a8cc113443f 100644 --- a/addons/product/models/__init__.py +++ b/addons/product/models/__init__.py @@ -13,6 +13,7 @@ from . import ir_attachment from . import product_attribute from . import product_attribute_custom_value from . import product_attribute_value +from . import product_catalog_mixin from . import product_category from . import product_document from . import product_packaging diff --git a/addons/product/models/product_catalog_mixin.py b/addons/product/models/product_catalog_mixin.py new file mode 100644 index 00000000000..16d0e4b0ce4 --- /dev/null +++ b/addons/product/models/product_catalog_mixin.py @@ -0,0 +1,124 @@ +from odoo import _, models + + + +class ProductCatalogMixin(models.AbstractModel): + """ This mixin should be inherited when the model should be able to work + with the product catalog. + It assumes the model using this mixin has a O2M field where the products are added/removed and + this field's co-related model should has a method named `_get_product_catalog_lines_data`. + """ + _name = 'product.catalog.mixin' + _description = 'Product Catalog Mixin' + + def action_add_from_catalog(self): + kanban_view_id = self.env.ref('product.product_view_kanban_catalog').id + search_view_id = self.env.ref('product.product_view_search_catalog').id + additional_context = self._get_action_add_from_catalog_extra_context() + return { + 'type': 'ir.actions.act_window', + 'name': _('Products'), + 'res_model': 'product.product', + 'views': [(kanban_view_id, 'kanban'), (False, 'form')], + 'search_view_id': [search_view_id, 'search'], + 'domain': self._get_product_catalog_domain(), + 'context': {**self.env.context, **additional_context}, + } + + def _default_order_line_values(self): + return { + 'quantity': 0, + 'readOnly': self._is_readonly() if self else False, + } + + def _get_product_catalog_domain(self): + """Get the domain to search for products in the catalog. + + For a model that uses products that has to be hidden in the catalog, it + must override this method and extend the appropriate domain. + :returns: A list of tuples that represents a domain. + :rtype: list + """ + return [('company_id', 'in', [self.company_id.id, False])] + + def _get_product_catalog_record_lines(self, product_ids): + """ Returns the record's lines grouped by product. + Must be overrided by each model using this mixin. + + :param list product_ids: The ids of the products currently displayed in the product catalog. + :rtype: dict + """ + return {} + + def _get_product_catalog_order_data(self, products, **kwargs): + """ Returns a dict containing the products' data. Those data are for products who aren't in + the record yet. For products already in the record, see `_get_product_catalog_lines_data`. + + For each product, its id is the key and the value is another dict with all needed data. + By default, the price is the only needed data but each model is free to add more data. + Must be overrided by each model using this mixin. + + :param products: Recordset of `product.product`. + :param dict kwargs: additional values given for inherited models. + :rtype: dict + :return: A dict with the following structure: + { + 'productId': int + 'quantity': float (optional) + 'price': float + 'readOnly': bool (optional) + } + """ + return {} + + def _get_product_catalog_order_line_info(self, product_ids, **kwargs): + """ Returns products information to be shown in the catalog. + :param list product_ids: The products currently displayed in the product catalog, as a list + of `product.product` ids. + :param dict kwargs: additional values given for inherited models. + :rtype: dict + :return: A dict with the following structure: + { + 'productId': int + 'quantity': float (optional) + 'price': float + 'readOnly': bool (optional) + } + """ + order_line_info = {} + default_data = self._default_order_line_values() + + for product, record_lines in self._get_product_catalog_record_lines(product_ids).items(): + order_line_info[product.id] = record_lines._get_product_catalog_lines_data(**kwargs) + product_ids.remove(product.id) + + products = self.env['product.product'].browse(product_ids) + product_data = self._get_product_catalog_order_data(products, **kwargs) + for product_id, data in product_data.items(): + order_line_info[product_id] = {**default_data, **data} + return order_line_info + + def _get_action_add_from_catalog_extra_context(self): + return { + 'product_catalog_order_id': self.id, + 'product_catalog_order_model': self._name, + } + + def _is_readonly(self): + """ Must be overrided by each model using this mixin. + :return: Whether the record is read-only or not. + :rtype: bool + """ + return False + + def _update_order_line_info(self, product_id, quantity, **kwargs): + """ Update the line information for a given product or create a new one if none exists yet. + Must be overrided by each model using this mixin. + :param int product_id: The product, as a `product.product` id. + :param int quantity: The product's quantity. + :param dict kwargs: additional values given for inherited models. + :return: The unit price of the product, based on the pricelist of the + purchase order and the quantity selected. + :rtype: float + """ + return 0 diff --git a/addons/product/models/product_product.py b/addons/product/models/product_product.py index dcd03008955..5a607058936 100644 --- a/addons/product/models/product_product.py +++ b/addons/product/models/product_product.py @@ -676,13 +676,13 @@ class ProductProduct(models.Model): sellers |= seller return sellers - def _select_seller(self, partner_id=False, quantity=0.0, date=None, uom_id=False, params=False): + def _select_seller(self, partner_id=False, quantity=0.0, date=None, uom_id=False, ordered_by='price', params=False): sellers = self._get_filtered_sellers(partner_id=partner_id, quantity=quantity, date=date, uom_id=uom_id, params=params) res = self.env['product.supplierinfo'] for seller in sellers: if not res or res.partner_id == seller.partner_id: res |= seller - return res and res.sorted('price')[:1] + return res and res.sorted(ordered_by)[:1] def _get_product_price_context(self, combination): self.ensure_one() diff --git a/addons/sale/static/src/js/product_catalog/kanban_controller.js b/addons/product/static/src/product_catalog/kanban_controller.js similarity index 83% rename from addons/sale/static/src/js/product_catalog/kanban_controller.js rename to addons/product/static/src/product_catalog/kanban_controller.js index af4ab311048..693d76adb5d 100644 --- a/addons/sale/static/src/js/product_catalog/kanban_controller.js +++ b/addons/product/static/src/product_catalog/kanban_controller.js @@ -6,13 +6,14 @@ import { useService } from "@web/core/utils/hooks"; import { _t } from "@web/core/l10n/translation"; export class ProductCatalogKanbanController extends KanbanController { - static template = "sale.ProductCatalogKanbanController"; + static template = "ProductCatalogKanbanController"; setup() { super.setup(); this.action = useService("action"); this.orm = useService("orm"); this.orderId = this.props.context.order_id; + this.orderResModel = this.props.context.product_catalog_order_model; onWillStart(async () => this._defineButtonContent()); } @@ -23,9 +24,9 @@ export class ProductCatalogKanbanController extends KanbanController { } async _defineButtonContent() { - // Define the content of the button. + // Define the button's label depending of the order's state. const orderStateInfo = await this.orm.searchRead( - "sale.order", [["id", "=", this.orderId]], ["state"] + this.orderResModel, [["id", "=", this.orderId]], ["state"] ); const orderIsQuotation = ["draft", "sent"].includes(orderStateInfo[0].state); if (orderIsQuotation) { @@ -44,7 +45,7 @@ export class ProductCatalogKanbanController extends KanbanController { } else { await this.action.doAction({ type: "ir.actions.act_window", - res_model: "sale.order", + res_model: this.orderResModel, views: [[false, "form"]], view_mode: "form", res_id: this.orderId, diff --git a/addons/sale/static/src/js/product_catalog/kanban_controller.xml b/addons/product/static/src/product_catalog/kanban_controller.xml similarity index 76% rename from addons/sale/static/src/js/product_catalog/kanban_controller.xml rename to addons/product/static/src/product_catalog/kanban_controller.xml index 3b97e40ed5d..bff7da35236 100644 --- a/addons/sale/static/src/js/product_catalog/kanban_controller.xml +++ b/addons/product/static/src/product_catalog/kanban_controller.xml @@ -1,6 +1,6 @@ - + +

You must define a product for everything you sell or purchase, diff --git a/addons/sale/static/src/js/product_catalog/kanban_view.js b/addons/product/static/src/product_catalog/kanban_view.js similarity index 88% rename from addons/sale/static/src/js/product_catalog/kanban_view.js rename to addons/product/static/src/product_catalog/kanban_view.js index b4856a325ee..93328136883 100644 --- a/addons/sale/static/src/js/product_catalog/kanban_view.js +++ b/addons/product/static/src/product_catalog/kanban_view.js @@ -17,4 +17,4 @@ export const productCatalogKanbanView = { SearchPanel: ProductCatalogSearchPanel, }; -registry.category("views").add("sale_product_kanban", productCatalogKanbanView); +registry.category("views").add("product_kanban_catalog", productCatalogKanbanView); diff --git a/addons/sale/static/src/js/product_catalog/sale_order_line/sale_order_line.js b/addons/product/static/src/product_catalog/order_line/order_line.js similarity index 51% rename from addons/sale/static/src/js/product_catalog/sale_order_line/sale_order_line.js rename to addons/product/static/src/product_catalog/order_line/order_line.js index 7bf0edff90e..d1363221a0d 100644 --- a/addons/sale/static/src/js/product_catalog/sale_order_line/sale_order_line.js +++ b/addons/product/static/src/product_catalog/order_line/order_line.js @@ -1,9 +1,9 @@ /** @odoo-module */ import { Component } from "@odoo/owl"; -import { formatMonetary } from "@web/views/fields/formatters"; +import { formatFloat, formatMonetary } from "@web/views/fields/formatters"; -export class ProductCatalogSOL extends Component { - static template = "sale.ProductCatalogSOL"; +export class ProductCatalogOrderLine extends Component { + static template = "ProductCatalogOrderLine"; static props = { productId: Number, quantity: Number, @@ -15,7 +15,7 @@ export class ProductCatalogSOL extends Component { // Private //-------------------------------------------------------------------------- - isInSaleOrder() { + isInOrder() { return this.props.quantity !== 0; } @@ -28,7 +28,13 @@ export class ProductCatalogSOL extends Component { } get price() { - return formatMonetary(this.props.price, { currencyId: this.env.currencyId, digits: this.env.digits }); + const { currencyId, digits } = this.env; + return formatMonetary(this.props.price, { currencyId, digits }); } + get quantity() { + const digits = [false, this.env.precision]; + const options = { digits, decimalPoint: ".", thousandsSep: "" }; + return parseFloat(formatFloat(this.props.quantity, options)); + } } diff --git a/addons/sale/static/src/js/product_catalog/sale_order_line/sale_order_line.scss b/addons/product/static/src/product_catalog/order_line/order_line.scss similarity index 73% rename from addons/sale/static/src/js/product_catalog/sale_order_line/sale_order_line.scss rename to addons/product/static/src/product_catalog/order_line/order_line.scss index 1e23517befd..757ef519de9 100644 --- a/addons/sale/static/src/js/product_catalog/sale_order_line/sale_order_line.scss +++ b/addons/product/static/src/product_catalog/order_line/order_line.scss @@ -1,4 +1,4 @@ -div.o_sale_product_catalog_quantity { +div.o_product_catalog_quantity { input[type="number"] { // Remove arrow buttons input type="number" appearance: textfield; @@ -10,7 +10,7 @@ div.o_sale_product_catalog_quantity { } } -.o_sale_product_kanban_view .o_kanban_renderer .o_kanban_record .o_sale_product_added { +.o_product_kanban_view .o_kanban_renderer .o_kanban_record .o_product_added { background-color: $o-component-active-bg; border-color: $o-component-active-border; } diff --git a/addons/sale/static/src/js/product_catalog/sale_order_line/sale_order_line.xml b/addons/product/static/src/product_catalog/order_line/order_line.xml similarity index 65% rename from addons/sale/static/src/js/product_catalog/sale_order_line/sale_order_line.xml rename to addons/product/static/src/product_catalog/order_line/order_line.xml index 6546b61b889..ead4e0ebc8c 100644 --- a/addons/sale/static/src/js/product_catalog/sale_order_line/sale_order_line.xml +++ b/addons/product/static/src/product_catalog/order_line/order_line.xml @@ -1,41 +1,41 @@ - + - - Unit price: + + Unit price: You can't edit this product in the catalog.

-
+
-
-
- + + + diff --git a/addons/purchase/views/product_views.xml b/addons/purchase/views/product_views.xml index afa4b561ba0..3f23a37f8fe 100644 --- a/addons/purchase/views/product_views.xml +++ b/addons/purchase/views/product_views.xml @@ -139,15 +139,39 @@ - - product.template.search.purchase - product.template - - - - - - - + + product.template.search.purchase + product.template + + + + + + + + + + + product.view.kanban.catalog.purchase + product.product + + primary + + + + + + + + + product.view.search.catalog.inherit.purchase + product.product + + + + + + + diff --git a/addons/purchase/views/purchase_views.xml b/addons/purchase/views/purchase_views.xml index 520f4e11f25..8bea6a34beb 100644 --- a/addons/purchase/views/purchase_views.xml +++ b/addons/purchase/views/purchase_views.xml @@ -218,6 +218,7 @@ +