# 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 Index, ModelSQL, ModelView, Workflow, fields 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): "Account Configuration Shipment Cost Sequence" __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): "Shipment Cost" __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()), 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 create(cls, vlist): pool = Pool() Config = pool.get('account.configuration') vlist = [v.copy() for v in vlist] config = Config(1) default_company = cls.default_company() for values in vlist: if values.get('number') is None: values['number'] = config.get_multivalue( 'shipment_cost_sequence', company=values.get('company', default_company)).get() return super().create(vlist) class ShipmentCost_Shipment(ModelSQL): "Shipment Cost - Shipment" __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): "Shipment Cost - Shipment Return" __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): "Post Shipment Cost" __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): "Show Shipment Cost" __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): "Shipment Cost Show" __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): "Post Shipment Cost" __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)