diff --git a/addons/sale/controllers/catalog.py b/addons/sale/controllers/catalog.py
index 91670e9403b..f17587d6fbd 100644
--- a/addons/sale/controllers/catalog.py
+++ b/addons/sale/controllers/catalog.py
@@ -30,27 +30,22 @@ class CatalogController(Controller):
):
if(product.id not in product_ids):
continue
- if (len(lines) > 1) or lines[0].product_uom != product.uom_id:
- price_unit = order.pricelist_id._get_product_price(
- product=product,
- quantity=1.0,
- currency=order.currency_id,
- date=order.date_order,
- )
- sale_order_line_info[product.id] = dict(
- readOnly=True,
- price=price_unit,
- )
- else:
- line = lines[0]
- sale_order_line_info[product.id] = dict(
- quantity=line.product_uom_qty,
- price=line.price_unit,
- )
+
+ sale_order_line_info[product.id] = request.env['sale.order.line'].browse(
+ line.id for line in lines # groupby gives a list and not a recordset
+ )._get_catalog_info()
+
product_ids.remove(product.id)
+ default_data = request.env['sale.order.line']._get_catalog_info()
+ default_data['readOnly'] = order._is_readonly()
+
sale_order_line_info.update({
- id: dict(price=price) for id, price in order.pricelist_id._get_products_price(
+ id: {
+ **default_data,
+ 'price': price,
+ }
+ for id, price in order.pricelist_id._get_products_price(
quantity=1.0,
products=request.env['product.product'].browse(product_ids),
currency=order.currency_id,
diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py
index 703ca3f474c..0bb65c9a66c 100644
--- a/addons/sale/models/sale_order.py
+++ b/addons/sale/models/sale_order.py
@@ -1658,3 +1658,17 @@ class SaleOrder(models.Model):
# Override for correct taxcloud computation
# when using coupon and delivery
return True
+
+ #=== TOOLING ===#
+
+ def _is_readonly(self):
+ """ Return Whether the sale order is read-only or not based on the state or the lock status.
+
+ A sale order is considered read-only if its state is 'cancel' or if the sale order is
+ locked.
+
+ :return: Whether the sale order is read-only or not.
+ :rtype: bool
+ """
+ self.ensure_one()
+ return self.state == 'cancel' or self.locked
diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py
index 4ad5c4c99fc..1ab06528b22 100644
--- a/addons/sale/models/sale_order_line.py
+++ b/addons/sale/models/sale_order_line.py
@@ -1188,6 +1188,62 @@ class SaleOrderLine(models.Model):
product_catalog_digits=order.order_line._fields['price_unit'].get_digits(order.env),
)
+ def _get_catalog_info(self):
+ """ Return information about sale order lines in `self`.
+
+ If `self` is empty, this method returns only the default value(s) needed for the product
+ catalog. In this case, the quantity that equals 0.
+
+ Otherwise, it returns a quantity and a price based on the product of the SOL(s) and whether
+ the product is read-only or not.
+
+ A product is considered read-only if the order is considered read-only (see
+ ``SaleOrder._is_readonly`` for more details) or if `self` contains multiple records.
+
+ Note: This method cannot be called with multiple records that have different products linked.
+
+ :raise odoo.exceptions.ValueError: ``len(self.product_id) != 1``
+ :rtype: dict
+ :return: A dict with the following structure:
+ {
+ 'quantity': float,
+ 'price': float,
+ 'readOnly': bool,
+ }
+ """
+ if len(self) == 1:
+ return {
+ 'quantity': self.product_uom_qty,
+ 'price': self.price_unit,
+ 'readOnly': self.order_id._is_readonly(),
+ }
+ elif self:
+ self.product_id.ensure_one()
+ order_line = self[0]
+ order = order_line.order_id
+ return {
+ 'readOnly': True,
+ 'price': order.pricelist._get_product_price(
+ product=order_line.product_id,
+ quantity=1.0,
+ currency=order.currency_id,
+ date=order.date_order,
+ ),
+ 'quantity': sum(
+ self.mapped(
+ lambda line: line.product_uom._compute_quantity(
+ qty=line.product_uom_qty,
+ to_unit=line.product_id.uom_id,
+ )
+ )
+ ),
+ }
+ else:
+ return {
+ 'quantity': 0,
+ # price will be computed in batch with pricelist utils so not given here
+ }
+
#=== TOOLING ===#
def _convert_to_sol_currency(self, amount, currency):
diff --git a/addons/sale/static/src/js/product_catalog/kanban_model.js b/addons/sale/static/src/js/product_catalog/kanban_model.js
index 9f3ff0e7050..889f4842ee4 100644
--- a/addons/sale/static/src/js/product_catalog/kanban_model.js
+++ b/addons/sale/static/src/js/product_catalog/kanban_model.js
@@ -23,7 +23,6 @@ export class ProductCatalogKanbanModel extends RelationalModel {
});
for (const record of result.records) {
record.productCatalogData = saleOrderLinesInfo[record.id];
- record.productCatalogData.quantity = record.productCatalogData.quantity || 0;
}
return result;
}
diff --git a/addons/sale/static/src/js/product_catalog/kanban_record.xml b/addons/sale/static/src/js/product_catalog/kanban_record.xml
index ccc37639ae1..a54b48ddd63 100644
--- a/addons/sale/static/src/js/product_catalog/kanban_record.xml
+++ b/addons/sale/static/src/js/product_catalog/kanban_record.xml
@@ -11,10 +11,7 @@
t-att-class="{'text-bg-primary': productCatalogData.quantity || productCatalogData.readOnly}">
-
+
diff --git a/addons/sale_stock/models/sale_order_line.py b/addons/sale_stock/models/sale_order_line.py
index 869bd0be0e5..8a9d521c8f7 100644
--- a/addons/sale_stock/models/sale_order_line.py
+++ b/addons/sale_stock/models/sale_order_line.py
@@ -331,7 +331,32 @@ class SaleOrderLine(models.Model):
raise UserError(_('The ordered quantity cannot be decreased below the amount already delivered. Instead, create a return in your inventory.'))
super(SaleOrderLine, self)._update_line_quantity(values)
+ #=== HOOKS ===#
+
def _get_action_add_from_catalog_extra_context(self, order):
extra_context = super()._get_action_add_from_catalog_extra_context(order)
extra_context.update(warehouse=order.warehouse_id.id)
return extra_context
+
+ def _get_catalog_info(self):
+ """ Override of `sale` to add the delivered quantity.
+
+ :rtype: dict
+ :return: A dict with the following structure:
+ {
+ 'deliveredQty': float,
+ 'quantity': float,
+ 'price': float,
+ 'readOnly': bool,
+ }
+ """
+ res = super()._get_catalog_info()
+ res['deliveredQty'] = sum(
+ self.mapped(
+ lambda line: line.product_uom._compute_quantity(
+ qty=line.qty_delivered,
+ to_unit=line.product_id.uom_id,
+ )
+ )
+ )
+ return res
diff --git a/addons/sale_stock/static/src/product_catalog/sale_order_line/sale_order_line.js b/addons/sale_stock/static/src/product_catalog/sale_order_line/sale_order_line.js
new file mode 100644
index 00000000000..e99e2cbbfba
--- /dev/null
+++ b/addons/sale_stock/static/src/product_catalog/sale_order_line/sale_order_line.js
@@ -0,0 +1,11 @@
+/** @odoo-module */
+
+import { ProductCatalogSOL } from "@sale/js/product_catalog/sale_order_line/sale_order_line";
+import { patch } from "@web/core/utils/patch";
+
+patch(ProductCatalogSOL, {
+ props: {
+ ...ProductCatalogSOL.props,
+ deliveredQty: Number,
+ },
+});