diff --git a/addons/delivery/__init__.py b/addons/delivery/__init__.py index 2ae6446f9dc..33bbab569d0 100644 --- a/addons/delivery/__init__.py +++ b/addons/delivery/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import models diff --git a/addons/delivery/__manifest__.py b/addons/delivery/__manifest__.py index abe3fa5ad9a..ae142082a46 100644 --- a/addons/delivery/__manifest__.py +++ b/addons/delivery/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. diff --git a/addons/delivery/models/__init__.py b/addons/delivery/models/__init__.py index 7920cde6c7c..0a096042e15 100644 --- a/addons/delivery/models/__init__.py +++ b/addons/delivery/models/__init__.py @@ -1,12 +1,13 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import delivery_carrier -from . import delivery_grid +from . import delivery_price_rule from . import delivery_zip_prefix from . import product_template +from . import res_partner from . import sale_order -from . import partner +from . import sale_order_line from . import stock_move -from . import stock_picking from . import stock_package_type +from . import stock_picking +from . import stock_quant_package diff --git a/addons/delivery/models/delivery_carrier.py b/addons/delivery/models/delivery_carrier.py index 3fc79df1fbc..3490981f4e8 100644 --- a/addons/delivery/models/delivery_carrier.py +++ b/addons/delivery/models/delivery_carrier.py @@ -1,12 +1,12 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import psycopg2 import re -from odoo import api, fields, models, registry, SUPERUSER_ID, _ -from odoo.tools.float_utils import float_round -from odoo.exceptions import UserError +from odoo import _, api, fields, models, registry, SUPERUSER_ID +from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_round +from odoo.tools.safe_eval import safe_eval from .delivery_request_objects import DeliveryCommodity, DeliveryPackage @@ -40,7 +40,12 @@ class DeliveryCarrier(models.Model): active = fields.Boolean(default=True) sequence = fields.Integer(help="Determine the display order", default=10) # This field will be overwritten by internal shipping providers by adding their own type (ex: 'fedex') - delivery_type = fields.Selection([('fixed', 'Fixed Price')], string='Provider', default='fixed', required=True) + delivery_type = fields.Selection( + [('base_on_rule', 'Based on Rules'), ('fixed', 'Fixed Price')], + string='Provider', + default='fixed', + required=True, + ) integration_level = fields.Selection([('rate', 'Get Rate'), ('rate_and_ship', 'Get Rate and Create Shipment')], string="Integration Level", default='rate_and_ship', help="Action while validating Delivery Orders") prod_environment = fields.Boolean("Environment", help="Set to True if your credentials are certified for production.") debug_logging = fields.Boolean('Debug logging', help="Log requests in order to ease debugging") @@ -78,6 +83,10 @@ class DeliveryCarrier(models.Model): default=0 ) + price_rule_ids = fields.One2many( + 'delivery.price.rule', 'carrier_id', 'Pricing Rules', copy=True + ) + _sql_constraints = [ ('margin_not_under_100_percent', 'CHECK (margin >= -100)', 'Margin cannot be lower than -100%'), ('shipping_insurance_is_percentage', 'CHECK(shipping_insurance >= 0 AND shipping_insurance <= 100)', "The shipping insurance must be a percentage between 0 and 100."), @@ -316,6 +325,120 @@ class DeliveryCarrier(models.Model): def fixed_cancel_shipment(self, pickings): raise NotImplementedError() + # ----------------------------------- # + # Based on rule delivery type methods # + # ----------------------------------- # + + def base_on_rule_rate_shipment(self, order): + carrier = self._match_address(order.partner_shipping_id) + if not carrier: + return {'success': False, + 'price': 0.0, + 'error_message': _('Error: this delivery method is not available for this address.'), + 'warning_message': False} + + try: + price_unit = self._get_price_available(order) + except UserError as e: + return {'success': False, + 'price': 0.0, + 'error_message': e.args[0], + 'warning_message': False} + + price_unit = self._compute_currency(order, price_unit, 'company_to_pricelist') + + return {'success': True, + 'price': price_unit, + 'error_message': False, + 'warning_message': False} + + def _get_conversion_currencies(self, order, conversion): + if conversion == 'company_to_pricelist': + from_currency, to_currency = order.company_id.currency_id, order.pricelist_id.currency_id + elif conversion == 'pricelist_to_company': + from_currency, to_currency = order.currency_id, order.company_id.currency_id + + return from_currency, to_currency + + def _compute_currency(self, order, price, conversion): + from_currency, to_currency = self._get_conversion_currencies(order, conversion) + if from_currency.id == to_currency.id: + return price + return from_currency._convert(price, to_currency, order.company_id, order.date_order or fields.Date.today()) + + def _get_price_available(self, order): + self.ensure_one() + self = self.sudo() + order = order.sudo() + total = weight = volume = quantity = 0 + total_delivery = 0.0 + for line in order.order_line: + if line.state == 'cancel': + continue + if line.is_delivery: + total_delivery += line.price_total + if not line.product_id or line.is_delivery: + continue + if line.product_id.type == "service": + continue + qty = line.product_uom._compute_quantity(line.product_uom_qty, line.product_id.uom_id) + weight += (line.product_id.weight or 0.0) * qty + volume += (line.product_id.volume or 0.0) * qty + quantity += qty + total = (order.amount_total or 0.0) - total_delivery + + total = self._compute_currency(order, total, 'pricelist_to_company') + # weight is either, + # 1- weight chosen by user in choose.delivery.carrier wizard passed by context + # 2- saved weight to use on sale order + # 3- total order line weight as fallback + weight = self.env.context.get('order_weight') or order.shipping_weight or weight + return self._get_price_from_picking(total, weight, volume, quantity) + + def _get_price_dict(self, total, weight, volume, quantity): + '''Hook allowing to retrieve dict to be used in _get_price_from_picking() function. + Hook to be overridden when we need to add some field to product and use it in variable factor from price rules. ''' + return { + 'price': total, + 'volume': volume, + 'weight': weight, + 'wv': volume * weight, + 'quantity': quantity + } + + def _get_price_from_picking(self, total, weight, volume, quantity): + price = 0.0 + criteria_found = False + price_dict = self._get_price_dict(total, weight, volume, quantity) + if self.free_over and total >= self.amount: + return 0 + for line in self.price_rule_ids: + test = safe_eval(line.variable + line.operator + str(line.max_value), price_dict) + if test: + price = line.list_base_price + line.list_price * price_dict[line.variable_factor] + criteria_found = True + break + if not criteria_found: + raise UserError(_("No price rule matching this order; delivery cost cannot be computed.")) + + return price + + def base_on_rule_send_shipping(self, pickings): + res = [] + for p in pickings: + carrier = self._match_address(p.partner_id) + if not carrier: + raise ValidationError(_('There is no matching delivery rule.')) + res = res + [{'exact_price': p.carrier_id._get_price_available(p.sale_id) if p.sale_id else 0.0, # TODO cleanme + 'tracking_number': False}] + return res + + def base_on_rule_get_tracking_link(self, picking): + return False + + def base_on_rule_cancel_shipment(self, pickings): + raise NotImplementedError() + # -------------------------------- # # get default packages/commodities # # -------------------------------- # diff --git a/addons/delivery/models/delivery_grid.py b/addons/delivery/models/delivery_grid.py deleted file mode 100644 index cdb8cd7df74..00000000000 --- a/addons/delivery/models/delivery_grid.py +++ /dev/null @@ -1,156 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import models, fields, api, _ -from odoo.tools.safe_eval import safe_eval -from odoo.exceptions import UserError, ValidationError - - -class PriceRule(models.Model): - _name = "delivery.price.rule" - _description = "Delivery Price Rules" - _order = 'sequence, list_price, id' - - @api.depends('variable', 'operator', 'max_value', 'list_base_price', 'list_price', 'variable_factor') - def _compute_name(self): - for rule in self: - name = 'if %s %s %.02f then' % (rule.variable, rule.operator, rule.max_value) - if rule.list_base_price and not rule.list_price: - name = '%s fixed price %.02f' % (name, rule.list_base_price) - elif rule.list_price and not rule.list_base_price: - name = '%s %.02f times %s' % (name, rule.list_price, rule.variable_factor) - else: - name = '%s fixed price %.02f plus %.02f times %s' % (name, rule.list_base_price, rule.list_price, rule.variable_factor) - rule.name = name - - name = fields.Char(compute='_compute_name') - sequence = fields.Integer(required=True, default=10) - carrier_id = fields.Many2one('delivery.carrier', 'Carrier', required=True, ondelete='cascade') - - variable = fields.Selection([('weight', 'Weight'), ('volume', 'Volume'), ('wv', 'Weight * Volume'), ('price', 'Price'), ('quantity', 'Quantity')], required=True, default='weight') - operator = fields.Selection([('==', '='), ('<=', '<='), ('<', '<'), ('>=', '>='), ('>', '>')], required=True, default='<=') - max_value = fields.Float('Maximum Value', required=True) - list_base_price = fields.Float(string='Sale Base Price', digits='Product Price', required=True, default=0.0) - list_price = fields.Float('Sale Price', digits='Product Price', required=True, default=0.0) - variable_factor = fields.Selection([('weight', 'Weight'), ('volume', 'Volume'), ('wv', 'Weight * Volume'), ('price', 'Price'), ('quantity', 'Quantity')], 'Variable Factor', required=True, default='weight') - - -class ProviderGrid(models.Model): - _inherit = 'delivery.carrier' - - delivery_type = fields.Selection(selection_add=[ - ('base_on_rule', 'Based on Rules'), - ], ondelete={'base_on_rule': lambda recs: recs.write({ - 'delivery_type': 'fixed', 'fixed_price': 0, - })}) - price_rule_ids = fields.One2many('delivery.price.rule', 'carrier_id', 'Pricing Rules', copy=True) - - def base_on_rule_rate_shipment(self, order): - carrier = self._match_address(order.partner_shipping_id) - if not carrier: - return {'success': False, - 'price': 0.0, - 'error_message': _('Error: this delivery method is not available for this address.'), - 'warning_message': False} - - try: - price_unit = self._get_price_available(order) - except UserError as e: - return {'success': False, - 'price': 0.0, - 'error_message': e.args[0], - 'warning_message': False} - - price_unit = self._compute_currency(order, price_unit, 'company_to_pricelist') - - return {'success': True, - 'price': price_unit, - 'error_message': False, - 'warning_message': False} - - def _get_conversion_currencies(self, order, conversion): - if conversion == 'company_to_pricelist': - from_currency, to_currency = order.company_id.currency_id, order.pricelist_id.currency_id - elif conversion == 'pricelist_to_company': - from_currency, to_currency = order.currency_id, order.company_id.currency_id - - return from_currency, to_currency - - def _compute_currency(self, order, price, conversion): - from_currency, to_currency = self._get_conversion_currencies(order, conversion) - if from_currency.id == to_currency.id: - return price - return from_currency._convert(price, to_currency, order.company_id, order.date_order or fields.Date.today()) - - def _get_price_available(self, order): - self.ensure_one() - self = self.sudo() - order = order.sudo() - total = weight = volume = quantity = 0 - total_delivery = 0.0 - for line in order.order_line: - if line.state == 'cancel': - continue - if line.is_delivery: - total_delivery += line.price_total - if not line.product_id or line.is_delivery: - continue - if line.product_id.type == "service": - continue - qty = line.product_uom._compute_quantity(line.product_uom_qty, line.product_id.uom_id) - weight += (line.product_id.weight or 0.0) * qty - volume += (line.product_id.volume or 0.0) * qty - quantity += qty - total = (order.amount_total or 0.0) - total_delivery - - total = self._compute_currency(order, total, 'pricelist_to_company') - # weight is either, - # 1- weight chosen by user in choose.delivery.carrier wizard passed by context - # 2- saved weight to use on sale order - # 3- total order line weight as fallback - weight = self.env.context.get('order_weight') or order.shipping_weight or weight - return self._get_price_from_picking(total, weight, volume, quantity) - - def _get_price_dict(self, total, weight, volume, quantity): - '''Hook allowing to retrieve dict to be used in _get_price_from_picking() function. - Hook to be overridden when we need to add some field to product and use it in variable factor from price rules. ''' - return { - 'price': total, - 'volume': volume, - 'weight': weight, - 'wv': volume * weight, - 'quantity': quantity - } - - def _get_price_from_picking(self, total, weight, volume, quantity): - price = 0.0 - criteria_found = False - price_dict = self._get_price_dict(total, weight, volume, quantity) - if self.free_over and total >= self.amount: - return 0 - for line in self.price_rule_ids: - test = safe_eval(line.variable + line.operator + str(line.max_value), price_dict) - if test: - price = line.list_base_price + line.list_price * price_dict[line.variable_factor] - criteria_found = True - break - if not criteria_found: - raise UserError(_("No price rule matching this order; delivery cost cannot be computed.")) - - return price - - def base_on_rule_send_shipping(self, pickings): - res = [] - for p in pickings: - carrier = self._match_address(p.partner_id) - if not carrier: - raise ValidationError(_('There is no matching delivery rule.')) - res = res + [{'exact_price': p.carrier_id._get_price_available(p.sale_id) if p.sale_id else 0.0, # TODO cleanme - 'tracking_number': False}] - return res - - def base_on_rule_get_tracking_link(self, picking): - return False - - def base_on_rule_cancel_shipment(self, pickings): - raise NotImplementedError() diff --git a/addons/delivery/models/delivery_price_rule.py b/addons/delivery/models/delivery_price_rule.py new file mode 100644 index 00000000000..1583e9d99ae --- /dev/null +++ b/addons/delivery/models/delivery_price_rule.py @@ -0,0 +1,32 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class PriceRule(models.Model): + _name = "delivery.price.rule" + _description = "Delivery Price Rules" + _order = 'sequence, list_price, id' + + @api.depends('variable', 'operator', 'max_value', 'list_base_price', 'list_price', 'variable_factor') + def _compute_name(self): + for rule in self: + name = 'if %s %s %.02f then' % (rule.variable, rule.operator, rule.max_value) + if rule.list_base_price and not rule.list_price: + name = '%s fixed price %.02f' % (name, rule.list_base_price) + elif rule.list_price and not rule.list_base_price: + name = '%s %.02f times %s' % (name, rule.list_price, rule.variable_factor) + else: + name = '%s fixed price %.02f plus %.02f times %s' % (name, rule.list_base_price, rule.list_price, rule.variable_factor) + rule.name = name + + name = fields.Char(compute='_compute_name') + sequence = fields.Integer(required=True, default=10) + carrier_id = fields.Many2one('delivery.carrier', 'Carrier', required=True, ondelete='cascade') + + variable = fields.Selection([('weight', 'Weight'), ('volume', 'Volume'), ('wv', 'Weight * Volume'), ('price', 'Price'), ('quantity', 'Quantity')], required=True, default='weight') + operator = fields.Selection([('==', '='), ('<=', '<='), ('<', '<'), ('>=', '>='), ('>', '>')], required=True, default='<=') + max_value = fields.Float('Maximum Value', required=True) + list_base_price = fields.Float(string='Sale Base Price', digits='Product Price', required=True, default=0.0) + list_price = fields.Float('Sale Price', digits='Product Price', required=True, default=0.0) + variable_factor = fields.Selection([('weight', 'Weight'), ('volume', 'Volume'), ('wv', 'Weight * Volume'), ('price', 'Price'), ('quantity', 'Quantity')], 'Variable Factor', required=True, default='weight') diff --git a/addons/delivery/models/delivery_request_objects.py b/addons/delivery/models/delivery_request_objects.py index ae614c4d3cf..962c7338ccc 100644 --- a/addons/delivery/models/delivery_request_objects.py +++ b/addons/delivery/models/delivery_request_objects.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. class DeliveryPackage: diff --git a/addons/delivery/models/delivery_zip_prefix.py b/addons/delivery/models/delivery_zip_prefix.py index 2d56f66d232..e216f5948bb 100644 --- a/addons/delivery/models/delivery_zip_prefix.py +++ b/addons/delivery/models/delivery_zip_prefix.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models @@ -18,7 +17,7 @@ class DeliveryZipPrefix(models.Model): # we cannot easily convert a list of prefix names into upper to compare with partner zips # later on, so let's ensure they are always upper vals['name'] = vals['name'].upper() - return super(DeliveryZipPrefix, self).create(vals_list) + return super().create(vals_list) def write(self, vals): vals['name'] = vals['name'].upper() diff --git a/addons/delivery/models/product_template.py b/addons/delivery/models/product_template.py index b7b5db98e5c..37ca03408de 100644 --- a/addons/delivery/models/product_template.py +++ b/addons/delivery/models/product_template.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import models, fields +from odoo import fields, models class ProductTemplate(models.Model): diff --git a/addons/delivery/models/partner.py b/addons/delivery/models/res_partner.py similarity index 93% rename from addons/delivery/models/partner.py rename to addons/delivery/models/res_partner.py index 5b19d154a7c..42a49c5ae21 100644 --- a/addons/delivery/models/partner.py +++ b/addons/delivery/models/res_partner.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import fields, models diff --git a/addons/delivery/models/sale_order.py b/addons/delivery/models/sale_order.py index e49f5de1d51..0a8d3909756 100644 --- a/addons/delivery/models/sale_order.py +++ b/addons/delivery/models/sale_order.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import models, fields, api, _ +from odoo import _, api, fields, models from odoo.exceptions import UserError @@ -163,46 +162,3 @@ 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): weight += order_line.product_qty * order_line.product_id.weight return weight - - -class SaleOrderLine(models.Model): - _inherit = 'sale.order.line' - - is_delivery = fields.Boolean(string="Is a Delivery", default=False) - product_qty = fields.Float(compute='_compute_product_qty', string='Product Qty', digits='Product Unit of Measure') - recompute_delivery_price = fields.Boolean(related='order_id.recompute_delivery_price') - - def _is_not_sellable_line(self): - return self.is_delivery or super(SaleOrderLine, self)._is_not_sellable_line() - - @api.depends('product_id', 'product_uom', 'product_uom_qty') - def _compute_product_qty(self): - for line in self: - if not line.product_id or not line.product_uom or not line.product_uom_qty: - line.product_qty = 0.0 - continue - line.product_qty = line.product_uom._compute_quantity(line.product_uom_qty, line.product_id.uom_id) - - def unlink(self): - for line in self: - if line.is_delivery: - line.order_id.carrier_id = False - return super(SaleOrderLine, self).unlink() - - def _is_delivery(self): - self.ensure_one() - return self.is_delivery - - # override to allow deletion of delivery line in a confirmed order - def _check_line_unlink(self): - """ - Extend the allowed deletion policy of SO lines. - - Lines that are delivery lines can be deleted from a confirmed order. - - :rtype: recordset sale.order.line - :returns: set of lines that cannot be deleted - """ - - undeletable_lines = super()._check_line_unlink() - return undeletable_lines.filtered(lambda line: not line.is_delivery) diff --git a/addons/delivery/models/sale_order_line.py b/addons/delivery/models/sale_order_line.py new file mode 100644 index 00000000000..3b3dc725215 --- /dev/null +++ b/addons/delivery/models/sale_order_line.py @@ -0,0 +1,50 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + is_delivery = fields.Boolean(string="Is a Delivery", default=False) + product_qty = fields.Float( + string='Product Qty', compute='_compute_product_qty', digits='Product Unit of Measure' + ) + recompute_delivery_price = fields.Boolean(related='order_id.recompute_delivery_price') + + def _is_not_sellable_line(self): + return self.is_delivery or super()._is_not_sellable_line() + + @api.depends('product_id', 'product_uom', 'product_uom_qty') + def _compute_product_qty(self): + for line in self: + if not line.product_id or not line.product_uom or not line.product_uom_qty: + line.product_qty = 0.0 + continue + line.product_qty = line.product_uom._compute_quantity( + line.product_uom_qty, line.product_id.uom_id + ) + + def unlink(self): + for line in self: + if line.is_delivery: + line.order_id.carrier_id = False + return super().unlink() + + def _is_delivery(self): + self.ensure_one() + return self.is_delivery + + # override to allow deletion of delivery line in a confirmed order + def _check_line_unlink(self): + """ + Extend the allowed deletion policy of SO lines. + + Lines that are delivery lines can be deleted from a confirmed order. + + :rtype: recordset sale.order.line + :returns: set of lines that cannot be deleted + """ + + undeletable_lines = super()._check_line_unlink() + return undeletable_lines.filtered(lambda line: not line.is_delivery) diff --git a/addons/delivery/models/stock_move.py b/addons/delivery/models/stock_move.py index 4e5971be702..d0d315e1dd5 100644 --- a/addons/delivery/models/stock_move.py +++ b/addons/delivery/models/stock_move.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models diff --git a/addons/delivery/models/stock_package_type.py b/addons/delivery/models/stock_package_type.py index ad2bb25a00d..e63dd90972d 100644 --- a/addons/delivery/models/stock_package_type.py +++ b/addons/delivery/models/stock_package_type.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import models, fields, api +from odoo import api, fields, models class PackageType(models.Model): diff --git a/addons/delivery/models/stock_picking.py b/addons/delivery/models/stock_picking.py index 5f482bb8593..30fec3eb0ac 100644 --- a/addons/delivery/models/stock_picking.py +++ b/addons/delivery/models/stock_picking.py @@ -1,66 +1,13 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import json from collections import defaultdict +import json -from odoo import models, fields, api, _ +from odoo import _, api, fields, models from odoo.exceptions import UserError from odoo.tools.sql import column_exists, create_column -class StockQuantPackage(models.Model): - _inherit = "stock.quant.package" - - @api.depends('quant_ids', 'package_type_id') - def _compute_weight(self): - if self.env.context.get('picking_id'): - package_weights = defaultdict(float) - # Ordering by qty_done prevents the default ordering by groupby fields that can inject multiple Left Joins in the resulting query. - res_groups = self.env['stock.move.line'].read_group( - [('result_package_id', 'in', self.ids), ('product_id', '!=', False), ('picking_id', '=', self.env.context['picking_id'])], - ['id:count'], - ['result_package_id', 'product_id', 'product_uom_id', 'qty_done'], - lazy=False, orderby='qty_done asc' - ) - for res_group in res_groups: - product_id = self.env['product.product'].browse(res_group['product_id'][0]) - product_uom_id = self.env['uom.uom'].browse(res_group['product_uom_id'][0]) - package_weights[res_group['result_package_id'][0]] += ( - res_group['__count'] - * product_uom_id._compute_quantity(res_group['qty_done'], product_id.uom_id) - * product_id.weight - ) - for package in self: - weight = package.package_type_id.base_weight or 0.0 - if self.env.context.get('picking_id'): - package.weight = weight + package_weights[package.id] - else: - for quant in package.quant_ids: - weight += quant.quantity * quant.product_id.weight - package.weight = weight - - def _get_default_weight_uom(self): - return self.env['product.template']._get_weight_uom_name_from_ir_config_parameter() - - def _compute_weight_uom_name(self): - for package in self: - package.weight_uom_name = self.env['product.template']._get_weight_uom_name_from_ir_config_parameter() - - def _compute_weight_is_kg(self): - self.weight_is_kg = False - uom_id = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter() - if uom_id == self.env.ref('uom.product_uom_kgm'): - self.weight_is_kg = True - self.weight_uom_rounding = uom_id.rounding - - weight = fields.Float(compute='_compute_weight', digits='Stock Weight', help="Total weight of all the products contained in the package.") - weight_uom_name = fields.Char(string='Weight unit of measure label', compute='_compute_weight_uom_name', readonly=True, default=_get_default_weight_uom) - weight_is_kg = fields.Boolean("Technical field indicating whether weight uom is kg or not (i.e. lb)", compute="_compute_weight_is_kg") - weight_uom_rounding = fields.Float("Technical field indicating weight's number of decimal places", compute="_compute_weight_is_kg") - shipping_weight = fields.Float(string='Shipping Weight', help="Total weight of the package.") - - class StockPicking(models.Model): _inherit = 'stock.picking' @@ -319,16 +266,3 @@ class StockPicking(models.Model): def _should_generate_commercial_invoice(self): self.ensure_one() return self.picking_type_id.warehouse_id.partner_id.country_id != self.partner_id.country_id - - -class StockReturnPicking(models.TransientModel): - _inherit = 'stock.return.picking' - - def _create_returns(self): - # Prevent copy of the carrier and carrier price when generating return picking - # (we have no integration of returns for now) - new_picking, pick_type_id = super(StockReturnPicking, self)._create_returns() - picking = self.env['stock.picking'].browse(new_picking) - picking.write({'carrier_id': False, - 'carrier_price': 0.0}) - return new_picking, pick_type_id diff --git a/addons/delivery/models/stock_quant_package.py b/addons/delivery/models/stock_quant_package.py new file mode 100644 index 00000000000..7e61fab7114 --- /dev/null +++ b/addons/delivery/models/stock_quant_package.py @@ -0,0 +1,57 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from collections import defaultdict + +from odoo import api, fields, models + + +class StockQuantPackage(models.Model): + _inherit = "stock.quant.package" + + @api.depends('quant_ids', 'package_type_id') + def _compute_weight(self): + if self.env.context.get('picking_id'): + package_weights = defaultdict(float) + # Ordering by qty_done prevents the default ordering by groupby fields that can inject multiple Left Joins in the resulting query. + res_groups = self.env['stock.move.line'].read_group( + [('result_package_id', 'in', self.ids), ('product_id', '!=', False), ('picking_id', '=', self.env.context['picking_id'])], + ['id:count'], + ['result_package_id', 'product_id', 'product_uom_id', 'qty_done'], + lazy=False, orderby='qty_done asc' + ) + for res_group in res_groups: + product_id = self.env['product.product'].browse(res_group['product_id'][0]) + product_uom_id = self.env['uom.uom'].browse(res_group['product_uom_id'][0]) + package_weights[res_group['result_package_id'][0]] += ( + res_group['__count'] + * product_uom_id._compute_quantity(res_group['qty_done'], product_id.uom_id) + * product_id.weight + ) + for package in self: + weight = package.package_type_id.base_weight or 0.0 + if self.env.context.get('picking_id'): + package.weight = weight + package_weights[package.id] + else: + for quant in package.quant_ids: + weight += quant.quantity * quant.product_id.weight + package.weight = weight + + def _get_default_weight_uom(self): + return self.env['product.template']._get_weight_uom_name_from_ir_config_parameter() + + def _compute_weight_uom_name(self): + for package in self: + package.weight_uom_name = self.env['product.template']._get_weight_uom_name_from_ir_config_parameter() + + def _compute_weight_is_kg(self): + self.weight_is_kg = False + uom_id = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter() + if uom_id == self.env.ref('uom.product_uom_kgm'): + self.weight_is_kg = True + self.weight_uom_rounding = uom_id.rounding + + weight = fields.Float(compute='_compute_weight', digits='Stock Weight', help="Total weight of all the products contained in the package.") + weight_uom_name = fields.Char(string='Weight unit of measure label', compute='_compute_weight_uom_name', readonly=True, default=_get_default_weight_uom) + weight_is_kg = fields.Boolean("Technical field indicating whether weight uom is kg or not (i.e. lb)", compute="_compute_weight_is_kg") + weight_uom_rounding = fields.Float("Technical field indicating weight's number of decimal places", compute="_compute_weight_is_kg") + shipping_weight = fields.Float(string='Shipping Weight', help="Total weight of the package.") diff --git a/addons/delivery/tests/__init__.py b/addons/delivery/tests/__init__.py index 96d25ad3301..6a4394ff854 100644 --- a/addons/delivery/tests/__init__.py +++ b/addons/delivery/tests/__init__.py @@ -1,6 +1,6 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing detailsself. +# Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import test_delivery_cost, test_delivery_stock_move -from . import test_packing_delivery from . import test_carrier_propagation +from . import test_delivery_cost +from . import test_delivery_stock_move +from . import test_packing_delivery diff --git a/addons/delivery/tests/test_carrier_propagation.py b/addons/delivery/tests/test_carrier_propagation.py index 6c6b2d3e565..233161140ee 100644 --- a/addons/delivery/tests/test_carrier_propagation.py +++ b/addons/delivery/tests/test_carrier_propagation.py @@ -1,3 +1,5 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + from odoo.tests import Form from odoo.tests.common import TransactionCase diff --git a/addons/delivery/tests/test_delivery_cost.py b/addons/delivery/tests/test_delivery_cost.py index 236d004524e..76b377e6207 100644 --- a/addons/delivery/tests/test_delivery_cost.py +++ b/addons/delivery/tests/test_delivery_cost.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo.tests import common, Form from odoo.tools import float_compare diff --git a/addons/delivery/tests/test_delivery_stock_move.py b/addons/delivery/tests/test_delivery_stock_move.py index db9c3ea9357..61843172be5 100644 --- a/addons/delivery/tests/test_delivery_stock_move.py +++ b/addons/delivery/tests/test_delivery_stock_move.py @@ -1,7 +1,7 @@ -# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo.addons.account.tests.common import AccountTestInvoicingCommon -from odoo.tests import tagged, Form +from odoo.tests import Form, tagged @tagged('post_install', '-at_install') diff --git a/addons/delivery/tests/test_packing_delivery.py b/addons/delivery/tests/test_packing_delivery.py index e4710c705e8..12a4bafda86 100644 --- a/addons/delivery/tests/test_packing_delivery.py +++ b/addons/delivery/tests/test_packing_delivery.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo.addons.stock.tests.test_packing import TestPackingCommon @@ -48,7 +47,7 @@ class TestPacking(TestPackingCommon): 'location_dest_id': self.customer_location.id, 'carrier_id': self.test_carrier.id }) - move_line_paw = self.env['stock.move.line'].create({ + self.env['stock.move.line'].create({ 'product_id': self.product_aw.id, 'product_uom_id': self.uom_kg.id, 'picking_id': picking_ship.id, @@ -56,7 +55,7 @@ class TestPacking(TestPackingCommon): 'location_id': self.stock_location.id, 'location_dest_id': self.customer_location.id }) - move_line_pbw = self.env['stock.move.line'].create({ + self.env['stock.move.line'].create({ 'product_id': self.product_bw.id, 'product_uom_id': self.uom_kg.id, 'picking_id': picking_ship.id, diff --git a/addons/delivery/views/delivery_portal_template.xml b/addons/delivery/views/delivery_portal_template.xml index 6816f1aa44d..4a92df89085 100644 --- a/addons/delivery/views/delivery_portal_template.xml +++ b/addons/delivery/views/delivery_portal_template.xml @@ -1,3 +1,4 @@ +