469 lines
16 KiB
Python
469 lines
16 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 itertools import groupby
|
|
|
|
from sql.functions import CharLength
|
|
|
|
from trytond.i18n import gettext
|
|
from trytond.model import (
|
|
ChatMixin, Index, ModelSQL, ModelView, Workflow, fields)
|
|
from trytond.model.exceptions import AccessError
|
|
from trytond.modules.company.model import CompanyValueMixin
|
|
from trytond.modules.product import price_digits, round_price
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.pyson import Eval, Id
|
|
from trytond.transaction import Transaction
|
|
from trytond.wizard import Button, StateTransition, StateView, Wizard
|
|
|
|
from .exceptions import NoShipmentWarning, SamePartiesWarning
|
|
|
|
|
|
class Configuration(metaclass=PoolMeta):
|
|
__name__ = 'account.configuration'
|
|
shipment_cost_sequence = fields.MultiValue(fields.Many2One(
|
|
'ir.sequence', "Shipment Cost Sequence", required=True,
|
|
domain=[
|
|
('company', 'in',
|
|
[Eval('context', {}).get('company', -1), None]),
|
|
('sequence_type', '=',
|
|
Id('account_stock_shipment_cost',
|
|
'sequence_type_shipment_cost')),
|
|
]))
|
|
|
|
@classmethod
|
|
def default_shipment_cost_sequence(cls, **pattern):
|
|
return cls.multivalue_model(
|
|
'shipment_cost_sequence').default_shipment_cost_sequence()
|
|
|
|
|
|
class ConfigurationShipmentCostSequence(ModelSQL, CompanyValueMixin):
|
|
__name__ = 'account.configuration.shipment_cost_sequence'
|
|
shipment_cost_sequence = fields.Many2One(
|
|
'ir.sequence', "Shipment Cost Sequence", required=True,
|
|
domain=[
|
|
('company', 'in', [Eval('company', -1), None]),
|
|
('sequence_type', '=',
|
|
Id('account_stock_shipment_cost',
|
|
'sequence_type_shipment_cost')),
|
|
])
|
|
|
|
@classmethod
|
|
def default_shipment_cost_sequence(cls, **pattern):
|
|
pool = Pool()
|
|
ModelData = pool.get('ir.model.data')
|
|
try:
|
|
return ModelData.get_id(
|
|
'account_stock_shipment_cost', 'sequence_shipment_cost')
|
|
except KeyError:
|
|
return None
|
|
|
|
|
|
class ShipmentCost(Workflow, ModelSQL, ModelView, ChatMixin):
|
|
__name__ = 'account.shipment_cost'
|
|
_rec_name = 'number'
|
|
|
|
_states = {
|
|
'readonly': Eval('state') != 'draft',
|
|
}
|
|
|
|
number = fields.Char(
|
|
"Number", readonly=True,
|
|
help="The main identifier for the shipment cost.")
|
|
company = fields.Many2One(
|
|
'company.company', "Company", required=True,
|
|
states=_states,
|
|
help="The company the shipment cost is associated with.")
|
|
|
|
shipments = fields.Many2Many(
|
|
'account.shipment_cost-stock.shipment.out',
|
|
'shipment_cost', 'shipment', "Shipments",
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
('state', '=', 'done'),
|
|
],
|
|
states=_states)
|
|
shipment_returns = fields.Many2Many(
|
|
'account.shipment_cost-stock.shipment.out.return',
|
|
'shipment_cost', 'shipment', "Shipment Returns",
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
('state', 'in', ['received', 'done']),
|
|
],
|
|
states=_states)
|
|
|
|
invoice_lines = fields.One2Many(
|
|
'account.invoice.line', 'shipment_cost', 'Invoice Lines',
|
|
add_remove=[
|
|
('shipment_cost', '=', None),
|
|
],
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
('invoice.state', 'in', ['posted', 'paid']),
|
|
('invoice.type', '=', 'in'),
|
|
('product.shipment_cost', '=', True),
|
|
('type', '=', 'line'),
|
|
],
|
|
states=_states)
|
|
allocation_method = fields.Selection([
|
|
('shipment', "By Shipment"),
|
|
], "Allocation Method", required=True,
|
|
states=_states)
|
|
|
|
posted_date = fields.Date("Posted Date", readonly=True)
|
|
state = fields.Selection([
|
|
('draft', "Draft"),
|
|
('posted', "Posted"),
|
|
('cancelled', "Cancelled"),
|
|
], "State", readonly=True, sort=False)
|
|
|
|
factors = fields.Dict(None, "Factors", readonly=True)
|
|
|
|
del _states
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
cls.number.search_unaccented = False
|
|
super().__setup__()
|
|
t = cls.__table__()
|
|
cls._sql_indexes.add(
|
|
Index(
|
|
t,
|
|
(t.state, Index.Equality(cardinality='low')),
|
|
where=t.state == 'draft'))
|
|
cls._order.insert(0, ('number', 'DESC'))
|
|
cls._transitions |= set((
|
|
('draft', 'posted'),
|
|
('draft', 'cancelled'),
|
|
('posted', 'cancelled'),
|
|
('cancelled', 'draft'),
|
|
))
|
|
cls._buttons.update({
|
|
'cancel': {
|
|
'invisible': Eval('state') == 'cancelled',
|
|
'depends': ['state'],
|
|
},
|
|
'draft': {
|
|
'invisible': Eval('state') != 'cancelled',
|
|
'depends': ['state'],
|
|
},
|
|
'post_wizard': {
|
|
'invisible': Eval('state') != 'draft',
|
|
'depends': ['state'],
|
|
},
|
|
'show': {
|
|
'invisible': Eval('state').in_(['draft', 'cancelled']),
|
|
'depends': ['state']
|
|
},
|
|
})
|
|
|
|
@classmethod
|
|
def order_number(cls, tables):
|
|
table, _ = tables[None]
|
|
return [CharLength(table.number), table.number]
|
|
|
|
@classmethod
|
|
def default_company(cls):
|
|
return Transaction().context.get('company')
|
|
|
|
@classmethod
|
|
def default_allocation_method(cls):
|
|
return 'shipment'
|
|
|
|
@classmethod
|
|
def default_state(cls):
|
|
return 'draft'
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('cancelled')
|
|
def cancel(cls, shipment_costs):
|
|
for shipment_cost in shipment_costs:
|
|
if shipment_cost.state == 'posted':
|
|
getattr(shipment_cost, 'unallocate_cost_by_%s' %
|
|
shipment_cost.allocation_method)()
|
|
cls.write(shipment_costs, {
|
|
'posted_date': None,
|
|
'factors': None,
|
|
'state': 'cancelled',
|
|
})
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('draft')
|
|
def draft(cls, shipment_costs):
|
|
pass
|
|
|
|
@property
|
|
def cost(self):
|
|
pool = Pool()
|
|
Currency = pool.get('currency.currency')
|
|
|
|
currency = self.company.currency
|
|
cost = Decimal(0)
|
|
|
|
for line in self.invoice_lines:
|
|
with Transaction().set_context(date=line.invoice.currency_date):
|
|
cost += Currency.compute(
|
|
line.invoice.currency, line.amount, currency, round=False)
|
|
return cost
|
|
|
|
@property
|
|
def all_shipments(self):
|
|
return self.shipments + self.shipment_returns
|
|
|
|
@property
|
|
def parties(self):
|
|
return {l.invoice.party for l in self.invoice_lines}
|
|
|
|
def allocate_cost_by_shipment(self):
|
|
self.factors = self._get_factors('shipment')
|
|
self._allocate_cost(self.factors)
|
|
|
|
def unallocate_cost_by_shipment(self):
|
|
factors = self.factors or self._get_factors('shipment')
|
|
self._allocate_cost(factors, sign=-1)
|
|
|
|
def _get_factors(self, method=None):
|
|
if method is None:
|
|
method = self.allocation_method
|
|
return getattr(self, '_get_%s_factors' % method)()
|
|
|
|
def _get_shipment_factors(self):
|
|
shipments = self.all_shipments
|
|
length = Decimal(len(shipments))
|
|
factor = 1 / length
|
|
return {str(shipment): factor for shipment in shipments}
|
|
|
|
def _allocate_cost(self, factors, sign=1):
|
|
"Allocate cost on shipments using factors"
|
|
pool = Pool()
|
|
Shipment = pool.get('stock.shipment.out')
|
|
ShipmentReturn = pool.get('stock.shipment.out.return')
|
|
assert sign in {1, -1}
|
|
|
|
cost = self.cost * sign
|
|
for shipments, klass in [
|
|
(list(self.shipments), Shipment),
|
|
(list(self.shipment_returns), ShipmentReturn),
|
|
]:
|
|
for shipment in shipments:
|
|
try:
|
|
factor = factors[str(shipment)]
|
|
except KeyError:
|
|
# Try with just id for backward compatibility
|
|
factor = factors[str(shipment.id)]
|
|
if (any(c.state == 'posted' for c in shipment.shipment_costs)
|
|
and shipment.cost):
|
|
shipment.cost += round_price(cost * factor)
|
|
else:
|
|
shipment.cost = round_price(cost * factor)
|
|
if shipment.cost_currency != shipment.company.currency:
|
|
shipment.cost_currency = shipment.company.currency
|
|
klass.save(shipments)
|
|
klass.set_shipment_cost(shipments)
|
|
|
|
@classmethod
|
|
@ModelView.button_action(
|
|
'account_stock_shipment_cost.wizard_shipment_cost_post')
|
|
def post_wizard(cls, shipment_costs):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button_action(
|
|
'account_stock_shipment_cost.wizard_shipment_cost_show')
|
|
def show(cls, shipment_costs):
|
|
pass
|
|
|
|
@classmethod
|
|
@Workflow.transition('posted')
|
|
def post(cls, shipment_costs):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
Warning = pool.get('res.user.warning')
|
|
|
|
for shipment_cost in shipment_costs:
|
|
parties = shipment_cost.parties
|
|
all_shipments = shipment_cost.all_shipments
|
|
if not all_shipments:
|
|
key = Warning.format('post no shipment', [shipment_cost])
|
|
if Warning.check(key):
|
|
raise NoShipmentWarning(
|
|
key,
|
|
gettext('account_stock_shipment_cost'
|
|
'.msg_shipment_cost_post_no_shipment',
|
|
shipment_cost=shipment_cost.rec_name))
|
|
for shipment in all_shipments:
|
|
for other in shipment.shipment_costs:
|
|
if other == shipment_cost:
|
|
continue
|
|
if other.parties & parties:
|
|
key = Warning.format(
|
|
'post same parties', [shipment_cost])
|
|
if Warning.check(key):
|
|
raise SamePartiesWarning(
|
|
key,
|
|
gettext('account_stock_shipment_cost'
|
|
'.msg_shipment_cost_post_same_parties',
|
|
shipment_cost=shipment_cost.rec_name,
|
|
shipment=shipment.rec_name,
|
|
other=other.rec_name))
|
|
else:
|
|
break
|
|
getattr(shipment_cost, 'allocate_cost_by_%s' %
|
|
shipment_cost.allocation_method)()
|
|
for company, c_shipment_costs in groupby(
|
|
shipment_costs, key=lambda s: s.company):
|
|
with Transaction().set_context(company=company.id):
|
|
today = Date.today()
|
|
cls.write(list(c_shipment_costs), {
|
|
'posted_date': today,
|
|
'state': 'posted',
|
|
})
|
|
|
|
@classmethod
|
|
def preprocess_values(cls, mode, values):
|
|
pool = Pool()
|
|
Config = pool.get('account.configuration')
|
|
values = super().preprocess_values(mode, values)
|
|
if values.get('number') is None:
|
|
config = Config(1)
|
|
company_id = values.get('company', cls.default_company())
|
|
if company_id is not None:
|
|
if sequence := config.get_multivalue(
|
|
'shipment_cost_sequence', company=company_id):
|
|
values['number'] = sequence.get()
|
|
return values
|
|
|
|
@classmethod
|
|
def check_modification(
|
|
cls, mode, shipment_costs, values=None, external=False):
|
|
super().check_modification(
|
|
mode, shipment_costs, values=values, external=external)
|
|
if mode == 'delete':
|
|
for shipment_cost in shipment_costs:
|
|
if shipment_cost.state not in {'cancelled', 'draft'}:
|
|
raise AccessError(
|
|
gettext('account_stock_shipment_cost'
|
|
'.msg_shipment_cost_delete_cancel',
|
|
shipment_cost=shipment_cost.rec_name))
|
|
|
|
|
|
class ShipmentCost_Shipment(ModelSQL):
|
|
__name__ = 'account.shipment_cost-stock.shipment.out'
|
|
shipment_cost = fields.Many2One(
|
|
'account.shipment_cost', "Shipment Cost",
|
|
required=True, ondelete='CASCADE')
|
|
shipment = fields.Many2One(
|
|
'stock.shipment.out', "Shipment", required=True, ondelete='CASCADE')
|
|
|
|
|
|
class ShipmentCost_ShipmentReturn(ModelSQL):
|
|
__name__ = 'account.shipment_cost-stock.shipment.out.return'
|
|
shipment_cost = fields.Many2One(
|
|
'account.shipment_cost', "Shipment Cost",
|
|
required=True, ondelete='CASCADE')
|
|
shipment = fields.Many2One(
|
|
'stock.shipment.out.return', "Shipment",
|
|
required=True, ondelete='CASCADE')
|
|
|
|
|
|
class ShowShipmentCostMixin(Wizard):
|
|
start_state = 'show'
|
|
show = StateView('account.shipment_cost.show',
|
|
'account_stock_shipment_cost.shipment_cost_show_view_form', [])
|
|
|
|
@property
|
|
def factors(self):
|
|
return self.record._get_factors()
|
|
|
|
def default_show(self, fields):
|
|
shipments = []
|
|
cost = self.record.cost
|
|
default = {
|
|
'cost': round_price(cost),
|
|
'shipments': shipments,
|
|
}
|
|
factors = self.factors
|
|
for shipment in self.record.all_shipments:
|
|
shipments.append({
|
|
'shipment': str(shipment),
|
|
'cost': round_price(cost * factors[str(shipment)]),
|
|
})
|
|
return default
|
|
|
|
|
|
class PostShipmentCost(ShowShipmentCostMixin):
|
|
__name__ = 'account.shipment_cost.post'
|
|
post = StateTransition()
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.show.buttons.extend([
|
|
Button("Cancel", 'end', 'tryton-cancel'),
|
|
Button("Post", 'post', 'tryton-ok', default=True),
|
|
])
|
|
|
|
def transition_post(self):
|
|
self.model.post([self.record])
|
|
return 'end'
|
|
|
|
|
|
class ShowShipmentCost(ShowShipmentCostMixin):
|
|
__name__ = 'account.shipment_cost.show'
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.show.buttons.extend([
|
|
Button("Close", 'end', 'tryton-close', default=True),
|
|
])
|
|
|
|
@property
|
|
def factors(self):
|
|
return self.record.factors or super().factors
|
|
|
|
|
|
class ShipmentCostShow(ModelView):
|
|
__name__ = 'account.shipment_cost.show'
|
|
|
|
cost = fields.Numeric("Cost", digits=price_digits, readonly=True)
|
|
shipments = fields.One2Many(
|
|
'account.shipment_cost.show.shipment', None, "Shipments",
|
|
readonly=True)
|
|
|
|
|
|
class ShipmentCostShowShipment(ModelView):
|
|
__name__ = 'account.shipment_cost.show.shipment'
|
|
|
|
shipment = fields.Reference("Shipments", [
|
|
('stock.shipment.out', "Shipment"),
|
|
('stock.shipment.out.return', "Shipment Return"),
|
|
], readonly=True)
|
|
cost = fields.Numeric("Cost", digits=price_digits, readonly=True)
|
|
|
|
|
|
class InvoiceLine(metaclass=PoolMeta):
|
|
__name__ = 'account.invoice.line'
|
|
shipment_cost = fields.Many2One(
|
|
'account.shipment_cost', "Shipment Cost",
|
|
readonly=True,
|
|
states={
|
|
'invisible': ~Eval('shipment_cost'),
|
|
})
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._check_modify_exclude.add('shipment_cost')
|
|
|
|
@classmethod
|
|
def copy(cls, lines, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
default.setdefault('shipment_cost', None)
|
|
return super().copy(lines, default=default)
|