diff --git a/addons/delivery/__manifest__.py b/addons/delivery/__manifest__.py index ae142082a46..7ee5795749e 100644 --- a/addons/delivery/__manifest__.py +++ b/addons/delivery/__manifest__.py @@ -4,33 +4,22 @@ { 'name': 'Delivery Costs', 'version': '1.0', - 'category': 'Inventory/Delivery', + 'category': 'Sales/Delivery', 'description': """ -Allows you to add delivery methods in sale orders and picking. -============================================================== - -You can define your own carrier for prices. When creating -invoices from picking, the system is able to add and compute the shipping line. +Allows you to add delivery methods in sale orders. +================================================== +You can define your own carrier for prices. +The system is able to add and compute the shipping line. """, - 'depends': ['sale_stock', 'sale_management'], + 'depends': ['sale'], 'data': [ 'security/ir.model.access.csv', 'security/delivery_carrier_security.xml', - 'views/product_template_view.xml', 'views/delivery_view.xml', 'views/partner_view.xml', - 'views/delivery_portal_template.xml', 'data/delivery_data.xml', - 'views/report_shipping.xml', - 'views/report_deliveryslip.xml', - 'views/report_package_barcode.xml', 'views/res_config_settings_views.xml', - 'wizard/choose_delivery_package_views.xml', 'wizard/choose_delivery_carrier_views.xml', - 'views/stock_package_type_views.xml', - 'views/stock_picking_type_views.xml', - 'views/stock_rule_views.xml', - 'views/stock_move_line_views.xml', 'report/ir_actions_report_templates.xml', ], 'demo': ['data/delivery_demo.xml'], diff --git a/addons/delivery/data/delivery_data.xml b/addons/delivery/data/delivery_data.xml index b363d8bda92..593f26a8064 100644 --- a/addons/delivery/data/delivery_data.xml +++ b/addons/delivery/data/delivery_data.xml @@ -6,7 +6,7 @@ Deliveries - Free delivery charges + Standard delivery Delivery_007 service @@ -16,10 +16,8 @@ order - Free delivery charges + Standard delivery 0.0 - - 1000 1 fixed diff --git a/addons/delivery/data/delivery_demo.xml b/addons/delivery/data/delivery_demo.xml index 7b3f9b3985f..61a742f2405 100644 --- a/addons/delivery/data/delivery_demo.xml +++ b/addons/delivery/data/delivery_demo.xml @@ -71,88 +71,5 @@ - - - - - outgoing shipment - - - - - - - - - - - - outgoing shipment - - - - - - - - - - - - outgoing shipment - - - - - - - - - - - - - - - - - - - - - diff --git a/addons/delivery/models/__init__.py b/addons/delivery/models/__init__.py index 0a096042e15..dea7dd944c0 100644 --- a/addons/delivery/models/__init__.py +++ b/addons/delivery/models/__init__.py @@ -3,11 +3,6 @@ from . import delivery_carrier from . import delivery_price_rule from . import delivery_zip_prefix -from . import product_template from . import res_partner from . import sale_order from . import sale_order_line -from . import stock_move -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 6305e301582..4084f9a2b65 100644 --- a/addons/delivery/models/delivery_carrier.py +++ b/addons/delivery/models/delivery_carrier.py @@ -3,13 +3,10 @@ import psycopg2 import re -from odoo import _, api, fields, models, registry, SUPERUSER_ID -from odoo.exceptions import UserError, ValidationError -from odoo.tools import float_round +from odoo import _, api, fields, models, registry, Command, SUPERUSER_ID +from odoo.exceptions import UserError from odoo.tools.safe_eval import safe_eval -from .delivery_request_objects import DeliveryCommodity, DeliveryPackage - class DeliveryCarrier(models.Model): _name = 'delivery.carrier' @@ -51,12 +48,15 @@ class DeliveryCarrier(models.Model): debug_logging = fields.Boolean('Debug logging', help="Log requests in order to ease debugging") company_id = fields.Many2one('res.company', string='Company', related='product_id.company_id', store=True, readonly=False) product_id = fields.Many2one('product.product', string='Delivery Product', required=True, ondelete='restrict') + currency_id = fields.Many2one(related='product_id.currency_id') - invoice_policy = fields.Selection([ - ('estimated', 'Estimated cost'), - ('real', 'Real cost') - ], string='Invoicing Policy', default='estimated', required=True, - help="Estimated Cost: the customer will be invoiced the estimated cost of the shipping.\nReal Cost: the customer will be invoiced the real cost of the shipping, the cost of the shipping will be updated on the SO after the delivery.") + invoice_policy = fields.Selection( + selection=[('estimated', "Estimated cost")], + string="Invoicing Policy", + default='estimated', + required=True, + help="Estimated Cost: the customer will be invoiced the estimated cost of the shipping.", + ) country_ids = fields.Many2many('res.country', 'delivery_carrier_country_rel', 'carrier_id', 'country_id', 'Countries') state_ids = fields.Many2many('res.country.state', 'delivery_carrier_state_rel', 'carrier_id', 'state_id', 'States') @@ -71,7 +71,11 @@ class DeliveryCarrier(models.Model): margin = fields.Float(help='This percentage will be added to the shipping price.') fixed_margin = fields.Float(help='This fixed amount will be added to the shipping price.') free_over = fields.Boolean('Free if order amount is above', help="If the order total amount (shipping excluded) is above or equal to this value, the customer benefits from a free shipping", default=False) - amount = fields.Float(string='Amount', help="Amount of the order to benefit from a free shipping, expressed in the company currency") + amount = fields.Float( + string="Amount", + default=1000, + help="Amount of the order to benefit from a free shipping, expressed in the company currency", + ) can_generate_return = fields.Boolean(compute="_compute_can_generate_return") return_label_on_delivery = fields.Boolean(string="Generate Return Label", help="The return label is automatically generated at the delivery.") @@ -89,7 +93,7 @@ class DeliveryCarrier(models.Model): ) _sql_constraints = [ - ('margin_not_under_100_percent', 'CHECK (margin >= -100)', 'Margin cannot be lower than -100%'), + ('margin_not_under_100_percent', 'CHECK (margin >= -1)', '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."), ] @@ -154,13 +158,13 @@ class DeliveryCarrier(models.Model): if not self.return_label_on_delivery: self.get_return_label_from_portal = False - @api.onchange('state_ids') - def onchange_states(self): - self.country_ids = [(6, 0, self.country_ids.ids + self.state_ids.mapped('country_id.id'))] - @api.onchange('country_ids') - def onchange_countries(self): - self.state_ids = [(6, 0, self.state_ids.filtered(lambda state: state.id in self.country_ids.mapped('state_ids').ids).ids)] + def _onchange_country_ids(self): + self.state_ids -= self.state_ids.filtered( + lambda state: state._origin.id not in self.country_ids.state_ids.ids + ) + if not self.country_ids: + self.zip_prefix_ids = [Command.clear()] def _get_delivery_type(self): """Return the delivery type. @@ -200,7 +204,7 @@ class DeliveryCarrier(models.Model): product_currency=company.currency_id ) # apply margin on computed price - res['price'] = float(res['price']) * (1.0 + (self.margin / 100.0)) + self.fixed_margin + res['price'] = float(res['price']) * (1.0 + self.margin) + self.fixed_margin # save the real price in case a free_over rule overide it to 0 res['carrier_price'] = res['price'] # free when order is large enough @@ -209,48 +213,6 @@ class DeliveryCarrier(models.Model): res['price'] = 0.0 return res - def send_shipping(self, pickings): - ''' Send the package to the service provider - - :param pickings: A recordset of pickings - :return list: A list of dictionaries (one per picking) containing of the form:: - { 'exact_price': price, - 'tracking_number': number } - # TODO missing labels per package - # TODO missing currency - # TODO missing success, error, warnings - ''' - self.ensure_one() - if hasattr(self, '%s_send_shipping' % self.delivery_type): - return getattr(self, '%s_send_shipping' % self.delivery_type)(pickings) - - def get_return_label(self,pickings, tracking_number=None, origin_date=None): - self.ensure_one() - if self.can_generate_return: - return getattr(self, '%s_get_return_label' % self.delivery_type)(pickings, tracking_number, origin_date) - - def get_return_label_prefix(self): - return 'ReturnLabel-%s' % self.delivery_type - - def get_tracking_link(self, picking): - ''' Ask the tracking link to the service provider - - :param picking: record of stock.picking - :return str: an URL containing the tracking link or False - ''' - self.ensure_one() - if hasattr(self, '%s_get_tracking_link' % self.delivery_type): - return getattr(self, '%s_get_tracking_link' % self.delivery_type)(picking) - - def cancel_shipment(self, pickings): - ''' Cancel a shipment - - :param pickings: A recordset of pickings - ''' - self.ensure_one() - if hasattr(self, '%s_cancel_shipment' % self.delivery_type): - return getattr(self, '%s_cancel_shipment' % self.delivery_type)(pickings) - def log_xml(self, xml_string, func): self.ensure_one() @@ -275,16 +237,6 @@ class DeliveryCarrier(models.Model): except psycopg2.Error: pass - def _get_default_custom_package_code(self): - """ Some delivery carriers require a prefix to be sent in order to use custom - packages (ie not official ones). This optional method will return it as a string. - """ - self.ensure_one() - if hasattr(self, '_%s_get_default_custom_package_code' % self.delivery_type): - return getattr(self, '_%s_get_default_custom_package_code' % self.delivery_type)() - else: - return False - # ------------------------------------------------ # # Fixed price shipping, aka a very simple provider # # ------------------------------------------------ # @@ -313,19 +265,6 @@ class DeliveryCarrier(models.Model): 'error_message': False, 'warning_message': False} - def fixed_send_shipping(self, pickings): - res = [] - for p in pickings: - res = res + [{'exact_price': p.carrier_id.fixed_price, - 'tracking_number': False}] - return res - - def fixed_get_tracking_link(self, picking): - return False - - def fixed_cancel_shipment(self, pickings): - raise NotImplementedError() - # ----------------------------------- # # Based on rule delivery type methods # # ----------------------------------- # @@ -355,7 +294,7 @@ class DeliveryCarrier(models.Model): 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 + from_currency, to_currency = order.company_id.currency_id, order.currency_id elif conversion == 'pricelist_to_company': from_currency, to_currency = order.currency_id, order.company_id.currency_id @@ -423,112 +362,3 @@ class DeliveryCarrier(models.Model): raise UserError(_("Not available for current order")) 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 # - # -------------------------------- # - - def _get_packages_from_order(self, order, default_package_type): - packages = [] - - total_cost = 0 - for line in order.order_line.filtered(lambda line: not line.is_delivery and not line.display_type): - total_cost += self._product_price_to_company_currency(line.product_qty, line.product_id, order.company_id) - - total_weight = order._get_estimated_weight() + default_package_type.base_weight - if total_weight == 0.0: - weight_uom_name = self.env['product.template']._get_weight_uom_name_from_ir_config_parameter() - raise UserError(_("The package cannot be created because the total weight of the products in the picking is 0.0 %s") % (weight_uom_name)) - # If max weight == 0 => division by 0. If this happens, we want to have - # more in the max weight than in the total weight, so that it only - # creates ONE package with everything. - max_weight = default_package_type.max_weight or total_weight + 1 - total_full_packages = int(total_weight / max_weight) - last_package_weight = total_weight % max_weight - - package_weights = [max_weight] * total_full_packages + ([last_package_weight] if last_package_weight else []) - partial_cost = total_cost / len(package_weights) # separate the cost uniformly - order_commodities = self._get_commodities_from_order(order) - - # Split the commodities value uniformly as well - for commodity in order_commodities: - commodity.monetary_value /= len(package_weights) - commodity.qty = max(1, commodity.qty // len(package_weights)) - - for weight in package_weights: - packages.append(DeliveryPackage(order_commodities, weight, default_package_type, total_cost=partial_cost, currency=order.company_id.currency_id, order=order)) - return packages - - def _get_packages_from_picking(self, picking, default_package_type): - packages = [] - - if picking.is_return_picking: - commodities = self._get_commodities_from_stock_move_lines(picking.move_line_ids) - weight = picking._get_estimated_weight() + default_package_type.base_weight - packages.append(DeliveryPackage(commodities, weight, default_package_type, currency=picking.company_id.currency_id, picking=picking)) - return packages - - # Create all packages. - for package in picking.package_ids: - move_lines = picking.move_line_ids.filtered(lambda ml: ml.result_package_id == package) - commodities = self._get_commodities_from_stock_move_lines(move_lines) - package_total_cost = 0.0 - for quant in package.quant_ids: - package_total_cost += self._product_price_to_company_currency(quant.quantity, quant.product_id, picking.company_id) - packages.append(DeliveryPackage(commodities, package.shipping_weight or package.weight, package.package_type_id, name=package.name, total_cost=package_total_cost, currency=picking.company_id.currency_id, picking=picking)) - - # Create one package: either everything is in pack or nothing is. - if picking.weight_bulk: - commodities = self._get_commodities_from_stock_move_lines(picking.move_line_ids) - package_total_cost = 0.0 - for move_line in picking.move_line_ids: - package_total_cost += self._product_price_to_company_currency(move_line.qty_done, move_line.product_id, picking.company_id) - packages.append(DeliveryPackage(commodities, picking.weight_bulk, default_package_type, name='Bulk Content', total_cost=package_total_cost, currency=picking.company_id.currency_id, picking=picking)) - elif not packages: - raise UserError(_("The package cannot be created because the total weight of the products in the picking is 0.0 %s") % (picking.weight_uom_name)) - - return packages - - def _get_commodities_from_order(self, order): - commodities = [] - - for line in order.order_line.filtered(lambda line: not line.is_delivery and not line.display_type): - unit_quantity = line.product_uom._compute_quantity(line.product_uom_qty, line.product_id.uom_id) - rounded_qty = max(1, float_round(unit_quantity, precision_digits=0)) - country_of_origin = line.product_id.country_of_origin.code or order.warehouse_id.partner_id.country_id.code - commodities.append(DeliveryCommodity(line.product_id, amount=rounded_qty, monetary_value=line.price_reduce_taxinc, country_of_origin=country_of_origin)) - - return commodities - - def _get_commodities_from_stock_move_lines(self, move_lines): - commodities = [] - - for line in move_lines.filtered(lambda line: line.product_id.type in ['product', 'consu']): - if line.state == 'done': - unit_quantity = line.product_uom_id._compute_quantity(line.qty_done, line.product_id.uom_id) - else: - unit_quantity = line.product_uom_id._compute_quantity(line.product_uom_qty, line.product_id.uom_id) - rounded_qty = max(1, float_round(unit_quantity, precision_digits=0)) - country_of_origin = line.product_id.country_of_origin.code or line.picking_id.picking_type_id.warehouse_id.partner_id.country_id.code - commodities.append(DeliveryCommodity(line.product_id, amount=rounded_qty, monetary_value=line.sale_price, country_of_origin=country_of_origin)) - - return commodities - - def _product_price_to_company_currency(self, quantity, product, company): - return company.currency_id._convert(quantity * product.standard_price, product.currency_id, company, fields.Date.today()) diff --git a/addons/delivery/models/delivery_price_rule.py b/addons/delivery/models/delivery_price_rule.py index 1583e9d99ae..90a10a944c3 100644 --- a/addons/delivery/models/delivery_price_rule.py +++ b/addons/delivery/models/delivery_price_rule.py @@ -2,31 +2,53 @@ from odoo import api, fields, models +from odoo.tools import format_amount + +VARIABLE_SELECTION = [ + ('weight', "Weight"), + ('volume', "Volume"), + ('wv', "Weight * Volume"), + ('price', "Price"), + ('quantity', "Quantity"), +] + 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') + @api.depends('variable', 'operator', 'max_value', 'list_base_price', 'list_price', 'variable_factor', 'currency_id') 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) + name = '%s fixed price %s' % ( + name, format_amount(self.env, rule.list_base_price, rule.currency_id) + ) elif rule.list_price and not rule.list_base_price: - name = '%s %.02f times %s' % (name, rule.list_price, rule.variable_factor) + name = '%s %s times %s' % (name, format_amount( + self.env, rule.list_price, rule.currency_id + ), rule.variable_factor) else: - name = '%s fixed price %.02f plus %.02f times %s' % (name, rule.list_base_price, rule.list_price, rule.variable_factor) + name = '%s fixed price %s plus %s times %s' % ( + name, + format_amount(self.env, rule.list_base_price, rule.currency_id), + format_amount(self.env, rule.list_price, rule.currency_id), + 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') + currency_id = fields.Many2one(related='carrier_id.currency_id') - variable = fields.Selection([('weight', 'Weight'), ('volume', 'Volume'), ('wv', 'Weight * Volume'), ('price', 'Price'), ('quantity', 'Quantity')], required=True, default='weight') + variable = fields.Selection(selection=VARIABLE_SELECTION, required=True, default='quantity') 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') + variable_factor = fields.Selection( + selection=VARIABLE_SELECTION, string="Variable Factor", required=True, default='weight' + ) diff --git a/addons/delivery/models/sale_order.py b/addons/delivery/models/sale_order.py index 0a8d3909756..b3588f05dcd 100644 --- a/addons/delivery/models/sale_order.py +++ b/addons/delivery/models/sale_order.py @@ -112,17 +112,13 @@ class SaleOrder(models.Model): values = { 'order_id': self.id, 'name': so_description, + 'price_unit': price_unit, 'product_uom_qty': 1, 'product_uom': carrier.product_id.uom_id.id, 'product_id': carrier.product_id.id, 'tax_id': [(6, 0, taxes_ids)], 'is_delivery': True, } - if carrier.invoice_policy == 'real': - values['price_unit'] = 0 - values['name'] += _(' (Estimated Cost: %s )', self._format_currency_amount(price_unit)) - else: - values['price_unit'] = price_unit if carrier.free_over and self.currency_id.is_zero(price_unit) : values['name'] += '\n' + _('Free Shipping') if self.order_line: @@ -131,14 +127,6 @@ class SaleOrder(models.Model): del context return sol - def _format_currency_amount(self, amount): - pre = post = u'' - if self.currency_id.position == 'before': - pre = u'{symbol}\N{NO-BREAK SPACE}'.format(symbol=self.currency_id.symbol or '') - else: - post = u'\N{NO-BREAK SPACE}{symbol}'.format(symbol=self.currency_id.symbol or '') - return u' {pre}{0}{post}'.format(amount, pre=pre, post=post) - @api.depends('order_line.is_delivery', 'order_line.is_downpayment') def _compute_invoice_status(self): super()._compute_invoice_status() diff --git a/addons/delivery/security/ir.model.access.csv b/addons/delivery/security/ir.model.access.csv index 5eb1fcc9e13..49d024378cb 100644 --- a/addons/delivery/security/ir.model.access.csv +++ b/addons/delivery/security/ir.model.access.csv @@ -6,9 +6,6 @@ access_delivery_price_rule,delivery.price.rule,model_delivery_price_rule,sales_t access_delivery_carrier_manager,delivery.carrier,model_delivery_carrier,sales_team.group_sale_manager,1,1,1,1 access_delivery_price_rule_manager,delivery.price.rule,model_delivery_price_rule,sales_team.group_sale_manager,1,1,1,1 access_delivery_carrier_partner_manager,delivery.carrier partner_manager,model_delivery_carrier,base.group_partner_manager,1,0,0,0 -access_delivery_carrier_stock_user,delivery.carrier stock_user,model_delivery_carrier,stock.group_stock_user,1,0,0,0 -access_delivery_carrier_stock_manager,delivery.carrier,model_delivery_carrier,stock.group_stock_manager,1,1,1,1 -access_delivery_zip_prefix_stock_manager,delivery.zip.prefix,model_delivery_zip_prefix,stock.group_stock_manager,1,1,1,1 -access_delivery_price_rule_stock_manager,delivery.price.rule,model_delivery_price_rule,stock.group_stock_manager,1,1,1,1 -access_choose_delivery_package,access.choose.delivery.package,model_choose_delivery_package,stock.group_stock_user,1,1,1,0 -access_choose_delivery_carrier,access.choose.delivery.carrier,model_choose_delivery_carrier,stock.group_stock_user,1,1,1,0 +access_delivery_zip_prefix_sale_manager,delivery.zip.prefix,model_delivery_zip_prefix,base.group_partner_manager,1,1,1,1 +access_delivery_price_rule_sale_manager,delivery.price.rule,model_delivery_price_rule,sales_team.group_sale_manager,1,1,1,1 +access_choose_delivery_carrier,delivery.choose.delivery.carrier,model_choose_delivery_carrier,sales_team.group_sale_salesman,1,1,1,0 diff --git a/addons/delivery/tests/__init__.py b/addons/delivery/tests/__init__.py index 6a4394ff854..b2c1ec5743e 100644 --- a/addons/delivery/tests/__init__.py +++ b/addons/delivery/tests/__init__.py @@ -1,6 +1,3 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. -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/views/delivery_view.xml b/addons/delivery/views/delivery_view.xml index 460dad1da48..17b3e42a077 100644 --- a/addons/delivery/views/delivery_view.xml +++ b/addons/delivery/views/delivery_view.xml @@ -10,6 +10,7 @@ + @@ -95,15 +96,30 @@ + + + + +