260 lines
8.4 KiB
Python
260 lines
8.4 KiB
Python
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
# this repository contains the full copyright notices and license terms.
|
|
from decimal import Decimal
|
|
|
|
from sql import Null
|
|
|
|
from trytond.model import ModelView, Workflow, fields
|
|
from trytond.modules.product import price_digits, round_price
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.pyson import Bool, Eval
|
|
from trytond.transaction import Transaction
|
|
|
|
|
|
class Move(metaclass=PoolMeta):
|
|
__name__ = 'stock.move'
|
|
|
|
shipment_out_cost_price = fields.Numeric(
|
|
"Shipment Cost Price", digits=price_digits, readonly=True,
|
|
states={
|
|
'invisible': ~Eval('shipment_out_cost_price'),
|
|
},
|
|
)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._allow_modify_closed_period.add('shipment_out_cost_price')
|
|
|
|
|
|
class ShipmentCostMixin:
|
|
__slots__ = ()
|
|
|
|
carrier = fields.Many2One(
|
|
'carrier', "Carrier",
|
|
states={
|
|
'readonly': Eval('shipment_cost_readonly', True),
|
|
},
|
|
context={
|
|
'company': Eval('company', -1),
|
|
},
|
|
depends=['company'])
|
|
|
|
cost_currency_used = fields.Function(fields.Many2One(
|
|
'currency.currency', "Cost Currency",
|
|
states={
|
|
'readonly': (
|
|
Eval('shipment_cost_readonly', True)
|
|
| ~Eval('cost_edit', False)),
|
|
}),
|
|
'on_change_with_cost_currency_used', setter='set_cost')
|
|
cost_currency = fields.Many2One(
|
|
'currency.currency', "Cost Currency",
|
|
states={
|
|
'required': Bool(Eval('cost')),
|
|
'readonly': Eval('shipment_cost_readonly', True),
|
|
})
|
|
|
|
cost_used = fields.Function(fields.Numeric(
|
|
"Cost", digits=price_digits,
|
|
states={
|
|
'readonly': (
|
|
Eval('shipment_cost_readonly', True)
|
|
| ~Eval('cost_edit', False)),
|
|
}),
|
|
'on_change_with_cost_used', setter='set_cost')
|
|
cost = fields.Numeric("Cost", digits=price_digits, readonly=True)
|
|
cost_edit = fields.Boolean(
|
|
"Edit Cost",
|
|
states={
|
|
'readonly': Eval('shipment_cost_readonly', True),
|
|
'invisible': Eval('shipment_cost_readonly', True),
|
|
},
|
|
help="Check to edit the cost.")
|
|
|
|
shipment_cost_readonly = fields.Function(
|
|
fields.Boolean("Shipment Cost Read Only"),
|
|
'on_change_with_shipment_cost_readonly')
|
|
|
|
def _get_carrier_context(self):
|
|
return {}
|
|
|
|
def get_carrier_context(self):
|
|
return self._get_carrier_context()
|
|
|
|
@fields.depends('carrier', 'company', methods=['_get_carrier_context'])
|
|
def _compute_costs(self):
|
|
costs = {
|
|
'cost': None,
|
|
'cost_currency': None,
|
|
}
|
|
if self.carrier:
|
|
with Transaction().set_context(self._get_carrier_context()):
|
|
cost, currency_id = self.carrier.get_purchase_price()
|
|
if cost is not None:
|
|
costs['cost'] = round_price(cost)
|
|
costs['cost_currency'] = currency_id
|
|
return costs
|
|
|
|
@fields.depends(
|
|
'cost_currency', 'cost_edit',
|
|
methods=['_compute_costs', 'on_change_with_shipment_cost_readonly'])
|
|
def on_change_with_cost_currency_used(self, name=None):
|
|
readonly = self.on_change_with_shipment_cost_readonly()
|
|
if not self.cost_edit and not readonly:
|
|
return self._compute_costs()['cost_currency']
|
|
elif self.cost_currency:
|
|
return self.cost_currency
|
|
|
|
@fields.depends(
|
|
'cost', 'cost_edit',
|
|
methods=['_compute_costs', 'on_change_with_shipment_cost_readonly'])
|
|
def on_change_with_cost_used(self, name=None):
|
|
cost = self.cost
|
|
readonly = self.on_change_with_shipment_cost_readonly()
|
|
if not self.cost_edit and not readonly:
|
|
cost = self._compute_costs()['cost']
|
|
return cost
|
|
|
|
@classmethod
|
|
def set_cost(cls, lines, name, value):
|
|
if name.endswith('_used'):
|
|
name = name[:-len('_used')]
|
|
cls.write([l for l in lines if l.cost_edit], {
|
|
name: value,
|
|
})
|
|
|
|
def on_change_with_shipment_cost_readonly(self, name=None):
|
|
raise NotImplementedError
|
|
|
|
@classmethod
|
|
def index_set_field(cls, name):
|
|
index = super().index_set_field(name)
|
|
if name == 'cost_used':
|
|
index = cls.index_set_field('cost_currency_used') + 1
|
|
return index
|
|
|
|
@property
|
|
def shipment_cost_moves(self):
|
|
raise NotImplementedError
|
|
|
|
def _get_shipment_cost(self):
|
|
"Return the cost for the shipment in the company currency"
|
|
pool = Pool()
|
|
Currency = pool.get('currency.currency')
|
|
if self.cost_used:
|
|
return Currency.compute(
|
|
self.cost_currency_used, self.cost_used,
|
|
self.company.currency, round=False)
|
|
else:
|
|
return Decimal(0)
|
|
|
|
|
|
class ShipmentOutCostMixin(ShipmentCostMixin):
|
|
|
|
@classmethod
|
|
def __register__(cls, module):
|
|
pool = Pool()
|
|
Company = pool.get('company.company')
|
|
table = cls.__table__()
|
|
table_h = cls.__table_handler__(module)
|
|
company = Company.__table__()
|
|
cursor = Transaction().connection.cursor()
|
|
|
|
cost_currency_exists = table_h.column_exist('cost_currency')
|
|
|
|
super().__register__(module)
|
|
|
|
# Migration from 6.4: fill cost_currency
|
|
if not cost_currency_exists:
|
|
cursor.execute(*table.update(
|
|
columns=[table.cost_currency],
|
|
values=[company.select(
|
|
company.currency,
|
|
where=company.id == table.company)],
|
|
where=table.cost != Null))
|
|
|
|
@fields.depends('state')
|
|
def on_change_with_shipment_cost_readonly(self, name=None):
|
|
return self.state in {'done', 'cancelled'}
|
|
|
|
@classmethod
|
|
def set_shipment_cost(cls, shipments):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
moves = []
|
|
for shipment in shipments:
|
|
cost = shipment._get_shipment_cost()
|
|
if cost is None:
|
|
continue
|
|
factors = getattr(shipment,
|
|
'_get_allocation_shipment_cost_factors_by_%s' %
|
|
shipment.allocation_shipment_cost_method)()
|
|
moves.extend(shipment._allocate_shipment_cost(cost, factors))
|
|
Move.save(moves)
|
|
|
|
@property
|
|
def allocation_shipment_cost_method(self):
|
|
if self.carrier:
|
|
return self.carrier.shipment_cost_allocation_method
|
|
return 'cost'
|
|
|
|
def _get_allocation_shipment_cost_factors_by_cost(self):
|
|
sum_ = Decimal(0)
|
|
costs = {}
|
|
for move in self.shipment_cost_moves:
|
|
move_cost = (
|
|
(move.cost_price or 0) * Decimal(str(move.internal_quantity)))
|
|
costs[move] = move_cost
|
|
sum_ += move_cost
|
|
|
|
factors = {}
|
|
for move in self.shipment_cost_moves:
|
|
if sum_:
|
|
ratio = costs[move] / sum_
|
|
else:
|
|
ratio = Decimal(1) / len(self.shipment_cost_moves)
|
|
factors[move.id] = ratio
|
|
return factors
|
|
|
|
def _allocate_shipment_cost(self, cost, factors):
|
|
moves = []
|
|
for move in self.shipment_cost_moves:
|
|
ratio = factors[move.id]
|
|
quantity = Decimal(str(move.internal_quantity))
|
|
if quantity and ratio:
|
|
cost_price = round_price(cost * ratio / quantity)
|
|
else:
|
|
cost_price = Decimal(0)
|
|
if move.shipment_out_cost_price != cost_price:
|
|
move.shipment_out_cost_price = cost_price
|
|
moves.append(move)
|
|
return moves
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('done')
|
|
def do(cls, shipments):
|
|
for shipment in shipments:
|
|
shipment.cost = shipment.cost_used
|
|
shipment.cost_currency = shipment.cost_currency_used
|
|
cls.save(shipments)
|
|
super().do(shipments)
|
|
cls.set_shipment_cost(shipments)
|
|
|
|
|
|
class ShipmentOut(ShipmentOutCostMixin, metaclass=PoolMeta):
|
|
__name__ = 'stock.shipment.out'
|
|
|
|
@property
|
|
def shipment_cost_moves(self):
|
|
return self.outgoing_moves
|
|
|
|
|
|
class ShipmentOutReturn(ShipmentOutCostMixin, metaclass=PoolMeta):
|
|
__name__ = 'stock.shipment.out.return'
|
|
|
|
@property
|
|
def shipment_cost_moves(self):
|
|
return self.incoming_moves
|