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 @@ +