diff --git a/CHANGELOG b/CHANGELOG index 2a3150fc3f..151276fc81 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +* Use purchase carrier price as cost for shipment +* Allow shipment costs to not be sold * Add carrier on shipment list view * Exclude by default shipment cost from promotion amount * Allow shipment cost price to be edited diff --git a/doc/index.rst b/doc/index.rst index b1f2451e54..ccda7361be 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -8,8 +8,10 @@ Two new fields are added to *Sale* and *Sale Configuration*: - *Carrier*: The carrier used for the sale. - *Shipment Cost Method*: The method used to compute the cost. - - *On Order*: The cost will be computed once for the sale. - - *On Shipment*: The cost will be computed for each shipments. + - *On Order*: The cost will be invoiced once for the sale. + - *On Shipment*: The cost will be invoiced for each shipments. + - *None*: The cost will not be invoiced to the customer but added to the + outgoing moves cost. At the quotation if a carrier is selected a new line is appended with the shipment cost but the added line can be modified when going back to draft. diff --git a/sale.py b/sale.py index b0e9dea87d..a8dad4f114 100644 --- a/sale.py +++ b/sale.py @@ -90,12 +90,20 @@ class Sale(metaclass=PoolMeta): fields.Many2Many('carrier', None, None, 'Available Carriers'), 'on_change_with_available_carriers') shipment_cost_method = fields.Selection([ + (None, "None"), ('order', 'On Order'), ('shipment', 'On Shipment'), - ], 'Shipment Cost Method', required=True, states={ + ], 'Shipment Cost Method', states={ 'readonly': Eval('state') != 'draft', }, depends=['state']) + @classmethod + def __register__(cls, module): + super().__register__(module) + table_h = cls.__table_handler__(module) + # Migration from 5.8: remove required on shipment_cost_method + table_h.not_null_action('shipment_cost_method', 'remove') + @staticmethod def default_shipment_cost_method(): Config = Pool().get('sale.configuration') @@ -209,7 +217,9 @@ class Sale(metaclass=PoolMeta): return 0 def set_shipment_cost(self): - cost = self.compute_shipment_cost() + cost = None + if self.shipment_cost_method: + cost = self.compute_shipment_cost() removed = [] unit_price = None lines = list(self.lines or []) @@ -256,7 +266,9 @@ class Sale(metaclass=PoolMeta): return cost_line def create_shipment(self, shipment_type): - Shipment = Pool().get('stock.shipment.out') + pool = Pool() + Shipment = pool.get('stock.shipment.out') + Currency = pool.get('currency.currency') shipments = super(Sale, self).create_shipment(shipment_type) if shipment_type == 'out' and shipments and self.carrier: @@ -264,11 +276,15 @@ class Sale(metaclass=PoolMeta): shipment.carrier = self.carrier with Transaction().set_context( shipment.get_carrier_context()): - cost, currency_id = self.carrier.get_sale_price() - cost = round_price(cost) - shipment.carrier = self.carrier - shipment.cost = cost - shipment.cost_currency = currency_id + cost, currency_id = self.carrier.get_purchase_price() + shipment.cost = round_price(Currency.compute( + Currency(currency_id), cost, shipment.company.currency, + round=False)) + with Transaction().set_context( + shipment.get_carrier_context()): + cost_sale, currency_id = self.carrier.get_sale_price() + shipment.cost_sale = round_price(cost_sale) + shipment.cost_sale_currency = currency_id Shipment.save(shipments) return shipments diff --git a/stock.py b/stock.py index 65d6411fba..a717d7ec3d 100644 --- a/stock.py +++ b/stock.py @@ -17,22 +17,42 @@ class ShipmentOut(metaclass=PoolMeta): 'picked', 'packed']), }, depends=['state']) - cost_currency = fields.Many2One('currency.currency', - 'Cost Currency', states={ - 'invisible': ~Eval('carrier'), - 'required': Bool(Eval('carrier')), - 'readonly': ~Eval('state').in_(['draft', 'waiting', 'assigned', - 'picked', 'packed']), - }, depends=['carrier', 'state']) - cost = fields.Numeric('Cost', + cost = fields.Numeric("Cost", digits=price_digits, states={ 'invisible': ~Eval('carrier'), - 'readonly': ~Eval('state').in_(['draft', 'waiting', 'assigned', - 'picked', 'packed']), + 'readonly': ~Eval('state').in_( + ['draft', 'waiting', 'assigned', 'packed']), + }, depends=['carrier', 'state']) + cost_sale_currency = fields.Many2One( + 'currency.currency', "Cost Sale Currency", + states={ + 'invisible': ~Eval('carrier'), + 'required': Bool(Eval('carrier')), + 'readonly': ~Eval('state').in_( + ['draft', 'waiting', 'assigned', 'picked', 'packed']), + }, depends=['carrier', 'state']) + cost_sale = fields.Numeric( + "Cost Sale", digits=price_digits, + states={ + 'invisible': ~Eval('carrier'), + 'readonly': ~Eval('state').in_( + ['draft', 'waiting', 'assigned', 'picked', 'packed']), }, depends=['carrier', 'state']) cost_invoice_line = fields.Many2One('account.invoice.line', 'Cost Invoice Line', readonly=True) + @classmethod + def __register__(cls, module): + table_h = cls.__table_handler__(module) + # Migration from 5.8: rename cost into cost_sale + if (table_h.column_exist('cost') + and not table_h.column_exist('cost_sale')): + table_h.column_rename('cost', 'cost_sale') + if (table_h.column_exist('cost_currency') + and not table_h.column_exist('cost_sale_currency')): + table_h.column_rename('cost_currency', 'cost_sale_currency') + super().__register__(module) + def _get_carrier_context(self): return {} @@ -43,18 +63,27 @@ class ShipmentOut(metaclass=PoolMeta): def on_change_carrier(self): self.on_change_inventory_moves() - @fields.depends('carrier', methods=['_get_carrier_context']) + @fields.depends('carrier', 'company', methods=['_get_carrier_context']) def on_change_inventory_moves(self): + pool = Pool() + Currency = pool.get('currency.currency') try: super(ShipmentOut, self).on_change_inventory_moves() except AttributeError: pass if not self.carrier: + self.cost = None + self.cost_sale = None + self.cost_sale_currency = None return with Transaction().set_context(self._get_carrier_context()): - cost, currency_id = self.carrier.get_sale_price() - self.cost = round_price(cost) - self.cost_currency = currency_id + cost, currency_id = self.carrier.get_purchase_price() + cost_sale, sale_currency_id = self.carrier.get_sale_price() + self.cost = round_price(Currency.compute( + Currency(currency_id), cost, self.company.currency, + round=False)) + self.cost_sale = round_price(cost_sale) + self.cost_sale_currency = sale_currency_id def _get_cost_tax_rule_pattern(self): 'Get tax rule pattern for invoice line' @@ -66,8 +95,8 @@ class ShipmentOut(metaclass=PoolMeta): Currency = pool.get('currency.currency') InvoiceLine = pool.get('account.invoice.line') - if not self.cost: - return {} + if not self.cost_sale: + return invoice_line = InvoiceLine() product = self.carrier.carrier_product invoice_line.type = 'line' @@ -82,10 +111,11 @@ class ShipmentOut(metaclass=PoolMeta): invoice_line.quantity = 1 # XXX invoice_line.unit = product.sale_uom.id - cost = self.cost - if invoice.currency != self.cost_currency: + cost = self.cost_sale + if invoice.currency != self.cost_sale_currency: with Transaction().set_context(date=invoice.currency_date): - cost = Currency.compute(self.cost_currency, cost, + cost = Currency.compute( + self.cost_sale_currency, cost, invoice.currency, round=False) invoice_line.unit_price = round_price(cost) invoice_line.currency = invoice.currency @@ -114,3 +144,9 @@ class ShipmentOut(metaclass=PoolMeta): shipment=self.rec_name, product=product.rec_name)) return invoice_line + + def _get_shipment_cost(self): + cost = super()._get_shipment_cost(self) + if all(not m.sale.shipment_cost_method for m in self.outgoing_moves): + cost += self.cost + return cost diff --git a/tests/scenario_sale_shipment_cost.rst b/tests/scenario_sale_shipment_cost.rst index 1ce1d068ec..8b23dda083 100644 --- a/tests/scenario_sale_shipment_cost.rst +++ b/tests/scenario_sale_shipment_cost.rst @@ -84,6 +84,8 @@ Create product:: >>> carrier_template.account_category = account_category >>> carrier_template.save() >>> carrier_product, = carrier_template.products + >>> carrier_product.cost_price = Decimal('2') + >>> carrier_product.save() Create carrier:: @@ -140,14 +142,16 @@ Send products:: >>> shipment.carrier == carrier True >>> shipment.cost + Decimal('2.0000') + >>> shipment.cost_sale Decimal('3.0000') - >>> shipment.cost_currency == company.currency + >>> shipment.cost_sale_currency == company.currency True >>> move, = shipment.inventory_moves >>> move.quantity = 4 - >>> shipment.cost + >>> shipment.cost_sale Decimal('3.0000') - >>> shipment.cost_currency == company.currency + >>> shipment.cost_sale_currency == company.currency True >>> shipment.state 'waiting' @@ -267,3 +271,48 @@ Check customer invoice:: >>> invoice, = sale.invoices >>> len(invoice.lines) 2 + +Sale products with no cost:: + + >>> sale = Sale() + >>> sale.party = customer + >>> sale.carrier = carrier + >>> sale.payment_term = payment_term + >>> sale.invoice_method = 'shipment' + >>> sale.shipment_cost_method = None + >>> sale_line = sale.lines.new() + >>> sale_line.product = product + >>> sale_line.quantity = 1.0 + >>> sale.click('quote') + >>> len(sale.lines) + 1 + >>> sale.click('confirm') + >>> sale.click('process') + >>> sale.state + 'processing' + +Check no customer invoice:: + + >>> len(sale.invoices) + 0 + +Send products:: + + >>> shipment, = sale.shipments + >>> shipment.cost + Decimal('2.0000') + >>> shipment.cost_sale + Decimal('3.0000') + >>> shipment.click('assign_force') + >>> shipment.click('pick') + >>> shipment.click('pack') + >>> shipment.click('done') + >>> shipment.state + 'done' + +Check customer invoice:: + + >>> sale.reload() + >>> invoice, = sale.invoices + >>> len(invoice.lines) + 1 diff --git a/tryton.cfg b/tryton.cfg index 984d3758a1..0b67609f3e 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -11,6 +11,7 @@ depends: stock extras_depend: sale_promotion + stock_shipment_cost xml: sale.xml stock.xml diff --git a/view/shipment_out_form.xml b/view/shipment_out_form.xml index 1f0a8a66d2..0d311c76e9 100644 --- a/view/shipment_out_form.xml +++ b/view/shipment_out_form.xml @@ -6,9 +6,11 @@ this repository contains the full copyright notices and license terms. -->