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