# The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import datetime from functools import partial, wraps from itertools import groupby from dateutil.relativedelta import relativedelta from sql import Literal, Null, Column from sql.aggregate import Count from sql.functions import CharLength from sql.operators import Concat from trytond.model import ModelSQL, ModelView, fields, Workflow, Model from trytond.model import Unique from trytond.modules.company import CompanyReport from trytond.pool import Pool, PoolMeta from trytond.pyson import Eval, If, Bool, Not, Or from trytond.transaction import Transaction from trytond.modules.incoterm.incoterm import ( IncotermDocumentMixin, IncotermMixin) from trytond.modules.stock_location_dock.stock import DockMixin from trytond.modules.product import price_digits from trytond.wizard import StateReport, Wizard, StateView, \ StateTransition, Button from decimal import Decimal try: import phonenumbers from phonenumbers import PhoneNumberFormat, NumberParseException except ImportError: phonenumbers = None from itertools import groupby try: from \ trytond.modules.analytic_account_root_mandatory_bypass.analytic_account \ import suppress_root_mandatory except ModuleNotFoundError: def suppress_root_mandatory(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper __all__ = ['Load', 'LoadOrder', 'LoadOrderLine', 'LoadOrderIncoterm', 'LoadSheet', 'RoadTransportNote', 'PrintLoadOrderShipment', 'Load2', 'Load3', 'LoadOrder3', 'CarrierLoadPurchase', 'PrintCarrierLoadPurchase'] # XXX fix: https://genshi.edgewall.org/ticket/582 from genshi.template.astutil import ASTCodeGenerator, ASTTransformer if not hasattr(ASTCodeGenerator, 'visit_NameConstant'): def visit_NameConstant(self, node): if node.value is None: self._write('None') elif node.value is True: self._write('True') elif node.value is False: self._write('False') else: raise Exception("Unknown NameConstant %r" % (node.value,)) ASTCodeGenerator.visit_NameConstant = visit_NameConstant if not hasattr(ASTTransformer, 'visit_NameConstant'): # Re-use visit_Name because _clone is deleted ASTTransformer.visit_NameConstant = ASTTransformer.visit_Name class CMRInstructionsMixin(object): edit_cmr_instructions = fields.Boolean('Edit CMR instructions', states={'readonly': Eval('state') == 'cancel'}, depends=['state']) cmr_instructions = fields.Function( fields.Text('CMR instructions', translate=True, states={ 'readonly': (Eval('state') == 'cancel') | Not( Bool(Eval('edit_cmr_instructions'))) }, depends=['state', 'edit_cmr_instructions']), 'on_change_with_cmr_instructions', setter='set_cmr_instructions') cmr_instructions_store = fields.Text('CMR instructions', translate=True, states={'readonly': Eval('state') == 'cancel'}, depends=['state']) cmr_template = fields.Function( fields.Many2One('carrier.load.cmr.template', 'CMR Template'), 'get_cmr_template') def get_cmr_template(self, name=None): Conf = Pool().get('carrier.configuration') conf = Conf(1) return conf.cmr_template and conf.cmr_template.id or None @fields.depends('edit_cmr_instructions', 'cmr_instructions_store', 'cmr_template') def on_change_with_cmr_instructions(self, name=None): if self.edit_cmr_instructions: return self.cmr_instructions_store Conf = Pool().get('carrier.configuration') conf = Conf(1) if conf.cmr_template: return conf.cmr_template.get_section_text('13', self) @classmethod def set_cmr_instructions(cls, records, name, value): cls.write(records, { 'cmr_instructions_store': value }) class Load(Workflow, ModelView, ModelSQL, DockMixin, CMRInstructionsMixin): """Carrier load""" __name__ = 'carrier.load' _rec_name = 'code' code = fields.Char('Code', required=True, select=True, states={'readonly': Eval('code_readonly', True)}, depends=['code_readonly']) code_readonly = fields.Function(fields.Boolean('Code Readonly'), 'get_code_readonly') company = fields.Many2One('company.company', 'Company', required=True, states={'readonly': Eval('state') != 'draft'}, domain=[('id', If(Eval('context', {}).contains('company'), '=', '!='), Eval('context', {}).get('company', -1))], depends=['state'], select=True) carrier = fields.Many2One('carrier', 'Carrier', select=True, ondelete='RESTRICT', states={ 'readonly': Eval('state') != 'draft', 'required': Bool(Eval('purchasable')) }, depends=['state', 'purchasable']) carrier_info = fields.Text('Carrier information', states={ 'readonly': Eval('state') != 'draft', 'invisible': Bool(Eval('purchasable')) | Bool(Eval('carrier')) }, depends=['state', 'purchasable', 'carrier']) vehicle_number = fields.Char('Vehicle reg. number', states={ 'readonly': Eval('state') != 'draft', 'required': Eval('state').in_(['confirmed', 'done']) & Bool( Eval('vehicle_required'))}, depends=['state', 'vehicle_required']) vehicle_required = fields.Function(fields.Boolean('Vehicle required'), 'get_number_required') trailer_number = fields.Char('Trailer reg. number', states={ 'readonly': Eval('state') != 'draft', 'required': Eval('state').in_(['confirmed', 'done']) & Bool( Eval('trailer_required'))}, depends=['state']) trailer_required = fields.Function(fields.Boolean('Trailer required'), 'get_number_required') date = fields.Date('Effective date', required=True, states={'readonly': Eval('state') != 'draft'}, depends=['state']) warehouse = fields.Many2One('stock.location', 'Warehouse', required=True, domain=[('type', '=', 'warehouse')], states={'readonly': Eval('state') != 'draft'}, depends=['state']) warehouse_output = fields.Function( fields.Many2One('stock.location', 'Warehouse output'), 'on_change_with_warehouse_output') orders = fields.One2Many('carrier.load.order', 'load', 'Orders', states={'readonly': (Eval('state') != 'draft') | ( Not(Bool(Eval('carrier'))) & Not(Bool('carrier_info'))) | Not(Bool(Eval('warehouse'))) }, depends=['state', 'carrier', 'carrier_info', 'warehouse']) state = fields.Selection([ ('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancel')], 'State', readonly=True, required=True) purchasable = fields.Boolean('Purchasable', states={ 'readonly': ((~Eval('state').in_(['draft', 'confirmed'])) | ( Bool(Eval('purchase'))))}, depends=['state', 'purchase']) unit_price = fields.Numeric('Unit Price', digits=price_digits, states={ 'readonly': ((~Eval('state').in_( ['draft', 'confirmed', 'done'])) | (Bool(Eval('purchase')))), 'invisible': ~Eval('purchasable')}, depends=['state', 'purchase', 'purchasable']) currency = fields.Many2One('currency.currency', 'Currency', states={ 'readonly': ((~Eval('state').in_(['draft', 'confirmed'])) | ( Bool(Eval('purchase')))), 'invisible': ~Eval('purchasable')}, depends=['state', 'purchase', 'purchasable']) currency_digits = fields.Function(fields.Integer('Currency Digits'), 'on_change_with_currency_digits') purchase = fields.Many2One('purchase.purchase', 'Purchase', readonly=True, states={'invisible': ~Eval('purchasable')}, depends=['purchasable']) purchase_state = fields.Function( fields.Selection([(None, '')], 'Purchase state', states={'invisible': ~Eval('purchasable')}, depends=['purchasable']), 'get_purchase_state', searcher='search_purchase_state') parties = fields.Function( fields.Char('Parties'), 'get_parties', searcher='search_parties') driver = fields.Char('Driver', states={'readonly': Eval('state') != 'draft'}, depends=['state']) driver_identifier = fields.Char('Driver identifier', states={ 'required': Bool(Eval('driver')), 'readonly': Eval('state') != 'draft'}, depends=['driver', 'state']) driver_phone = fields.Char('Driver Phone', states={ 'readonly': Eval('state') != 'draft'}, depends=['state']) comments = fields.Text('Comments', translate=True, states={ 'readonly': Eval('state') != 'draft'}, depends=['state']) @classmethod def __setup__(cls): super(Load, cls).__setup__() t = cls.__table__() cls._sql_constraints = [ ('code_uk1', Unique(t, t.code), 'Code must be unique.') ] cls._order = [ ('date', 'DESC'), ('id', 'DESC'), ] cls._transitions |= set((('draft', 'confirmed'), ('confirmed', 'draft'), ('confirmed', 'done'), ('done', 'confirmed'), ('draft', 'cancel'), ('cancel', 'draft'))) cls._error_messages.update({ 'delete_cancel': 'Carrier load "%s" must be cancelled before deletion.', 'purchase_price': 'Unit price in Load "%s" must be defined.', 'purchase_confirm': 'Carrier load "%s" must have no purchase to be confirmed.', 'invalid_phonenumber': ('The phone number "%(phone)s" ' 'is not valid.'), 'missing_carrier_info': 'Must define either Carrier or Carrier information on Load "%s".' }) cls._buttons.update({ 'cancel': { 'invisible': Eval('state').in_(['cancel', 'done']), 'depends': ['state']}, 'draft': { 'invisible': ~Eval('state').in_(['cancel', 'confirmed']), 'icon': If(Eval('state') == 'confirmed', 'tryton-back', 'tryton-forward'), 'depends': ['state']}, 'confirm': { 'invisible': ~Eval('state').in_(['draft', 'done']), 'icon': If(Eval('state') == 'draft', 'tryton-forward', 'tryton-back'), 'depends': ['state']}, 'do': { 'invisible': Eval('state') != 'confirmed', 'depends': ['state']}, 'create_purchase': { 'invisible': ( Not(Bool(Eval('purchasable'))) | (Eval('unit_price', None) == None) | Bool(Eval('purchase'))), 'depends': ['purchasable', 'unit_price', 'purchase']} }) if len(cls.purchase_state._field.selection) == 1: pool = Pool() Purchase = pool.get('purchase.purchase') cls.purchase_state._field.selection.extend(Purchase.state.selection) @classmethod def __register__(cls, module_name): sql_table = cls.__table__() cursor = Transaction().connection.cursor() super().__register__(module_name) table = cls.__table_handler__(module_name) # Migration from 4.8: refactor vehicle if table.column_exist('vehicle'): # get model here for avoid errors when removing dependency Vehicle = Pool().get('carrier.vehicle') vehicle = Vehicle.__table__() cursor.execute(*sql_table.join( vehicle, condition=(sql_table.vehicle == vehicle.id) ).select( sql_table.id, vehicle.registration_number, vehicle.registration_number2)) for id_, regnumber, regnumber2 in cursor.fetchall(): cursor.execute(*sql_table.update([ sql_table.vehicle_number, sql_table.trailer_number, ], [ regnumber, regnumber2 ], where=sql_table.id == id_)) table.drop_column('vehicle') table.not_null_action('carrier', action='remove') table.not_null_action('carrier_vehicle', action='remove') table.not_null_action('dock', action='remove') @classmethod def view_attributes(cls): if Transaction().context.get('loading_shipment', False): return [('//group[@id="state_buttons"]', 'states', {'invisible': True})] return [] @staticmethod def default_code_readonly(): Configuration = Pool().get('carrier.configuration') config = Configuration(1) return bool(config.load_sequence) def get_code_readonly(self, name): return True @classmethod def validate(cls, records): super().validate(records) for record in records: if (record.state not in ('cancel', 'draft') and not record.carrier and not record.carrier_info): cls.raise_user_error('missing_carrier_info', record.rec_name) record.check_valid_phonenumber() @classmethod def create(cls, vlist): Sequence = Pool().get('ir.sequence') Configuration = Pool().get('carrier.configuration') vlist = [x.copy() for x in vlist] config = Configuration(1) for values in vlist: if not values.get('code'): values['code'] = Sequence.get_id(config.load_sequence.id) return super(Load, cls).create(vlist) @classmethod def copy(cls, items, default=None): if default is None: default = {} default = default.copy() default['code'] = None default['orders'] = None return super(Load, cls).copy(items, default=default) @staticmethod def default_company(): return Transaction().context.get('company') @classmethod def default_state(cls): return 'draft' @staticmethod def default_date(): Date_ = Pool().get('ir.date') return Date_.today() @classmethod def default_warehouse(cls): Location = Pool().get('stock.location') locations = Location.search(cls.warehouse.domain) if len(locations) == 1: return locations[0].id @staticmethod def default_currency(): Company = Pool().get('company.company') company = Transaction().context.get('company') if company: company = Company(company) return company.currency.id @staticmethod def default_currency_digits(): Company = Pool().get('company.company') company = Transaction().context.get('company') if company: company = Company(company) return company.currency.digits return 2 @classmethod def default_purchasable(cls): pool = Pool() Configuration = pool.get('carrier.configuration') conf = Configuration(1) return conf.load_purchasable @classmethod def default_purchase_state(cls): return None @fields.depends('carrier', 'purchase') def on_change_carrier(self): pool = Pool() Currency = pool.get('currency.currency') cursor = Transaction().connection.cursor() table = self.__table__() if self.carrier: if not self.purchase: subquery = table.select(table.currency, where=(table.carrier == self.carrier.id) & (table.currency != None), order_by=table.id, limit=10) cursor.execute(*subquery.select(subquery.currency, group_by=subquery.currency, order_by=Count(Literal(1)).desc)) row = cursor.fetchone() if row: currency_id, = row self.currency = Currency(currency_id) self.currency_digits = self.currency.digits @fields.depends('driver_phone') def on_change_driver_phone(self): self.driver_phone = self.format_phone(self.driver_phone) def check_valid_phonenumber(self): if not phonenumbers or not self.driver_phone: return try: phonenumber = phonenumbers.parse(self.driver_phone) except NumberParseException: phonenumber = None if not (phonenumber and phonenumbers.is_valid_number(phonenumber)): self.raise_user_error( 'invalid_phonenumber', { 'phone': self.driver_phone }) @classmethod def format_phone(cls, value=None): if phonenumbers: try: phonenumber = phonenumbers.parse(value) except NumberParseException: pass else: value = phonenumbers.format_number( phonenumber, PhoneNumberFormat.INTERNATIONAL) return value def get_registration_numbers(self): if self.trailer_number: return '%s / %s' % (self.vehicle_number, self.trailer_number) return self.vehicle_number _autocomplete_limit = 100 @fields.depends('carrier', 'vehicle_number') def autocomplete_vehicle_number(self): return self._autocomplete_registration_numbers(self.carrier, 'vehicle_number', self.vehicle_number) @fields.depends('carrier', 'trailer_number') def autocomplete_trailer_number(self): return self._autocomplete_registration_numbers(self.carrier, 'trailer_number', self.trailer_number) @fields.depends('carrier', 'driver') def autocomplete_driver(self): return self._autocomplete_registration_numbers(self.carrier, 'driver', self.driver) @fields.depends('carrier', 'driver_identifier') def autocomplete_driver_identifier(self): return self._autocomplete_registration_numbers(self.carrier, 'driver_identifier', self.driver_identifier) @classmethod def _autocomplete_registration_numbers(cls, carrier, field_name, field_value): if not carrier: return [] cursor = Transaction().connection.cursor() sql_table = cls.__table__() number_column = Column(sql_table, field_name) where = ( (sql_table.carrier == carrier.id) & (number_column != Null) ) if field_value: where &= number_column.like('%%%s%%' % field_value) cursor.execute(*sql_table.select(number_column, where=where, group_by=number_column, limit=cls._autocomplete_limit) ) values = cursor.fetchall() if len(values) < cls._autocomplete_limit: return sorted({v[0] for v in values}) return [] def get_carrier_information(self): info = [] if self.carrier: info.append(self.carrier.party.full_name) if self.carrier.party.tax_identifier: info.append(self.carrier.party.tax_identifier.code) if self.carrier.party.addresses: info.extend(self.carrier.party.addresses[0].full_address.split( '\n')) else: info.extend((self.carrier_info or '').split('\n')) return info def get_carrier_name(self): return self.carrier and self.carrier.rec_name or ( self.carrier_info and self.carrier_info.split('\n')[0]) or '' @classmethod def get_number_required(cls, records, names): Conf = Pool().get('carrier.configuration') conf = Conf(1) res = {} for name in names: for record in records: res.setdefault(name, {})[record.id] = getattr(conf, name) return res @staticmethod def default_vehicle_required(): Conf = Pool().get('carrier.configuration') conf = Conf(1) return conf.vehicle_required @staticmethod def default_trailer_required(): Conf = Pool().get('carrier.configuration') conf = Conf(1) return conf.trailer_required @fields.depends('currency') def on_change_with_currency_digits(self, name=None): if self.currency: return self.currency.digits return 2 def get_purchase_state(self, name=None): if not self.purchase: return self.default_purchase_state() return self.purchase.state @classmethod def search_purchase_state(cls, name, clause): return [('purchase.state', ) + tuple(clause[1:])] @fields.depends('warehouse') def on_change_with_warehouse_output(self, name=None): if self.warehouse: return self.warehouse.output_location.id return None @classmethod def delete(cls, records): cls.cancel(records) for record in records: if record.state != 'cancel': cls.raise_user_error('delete_cancel', record.rec_name) super(Load, cls).delete(records) @classmethod @ModelView.button @Workflow.transition('cancel') def cancel(cls, records): Order = Pool().get('carrier.load.order') orders = [o for r in records for o in r.orders] Order.cancel(orders) @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, records): Order = Pool().get('carrier.load.order') orders = [o for r in records for o in r.orders if o.state == 'cancel'] Order.draft(orders) @classmethod @ModelView.button @Workflow.transition('confirmed') def confirm(cls, records): done_records = [r for r in records if r.state == 'done'] for record in done_records: if record.purchase: cls.raise_user_error('purchase_confirm', record.rec_name) @classmethod @ModelView.button @Workflow.transition('done') def do(cls, records): cls.create_purchase(records) @classmethod @ModelView.button def create_purchase(cls, records): pool = Pool() Purchase = pool.get('purchase.purchase') to_save = [] for record in records: if record.purchase: continue if not record.purchasable or record.unit_price is None: continue purchase = record.get_purchase() if purchase: purchase.save() Purchase.quote([purchase]) record.purchase = purchase to_save.append(record) if to_save: cls.save(to_save) def get_purchase(self): pool = Pool() Purchase = pool.get('purchase.purchase') PurchaseLine = pool.get('purchase.line') if not self.unit_price: self.raise_user_error('purchase_price', self.rec_name) _party = (getattr(self.carrier.party, 'supplier_to_invoice', None) or self.carrier.party) purchase = Purchase(company=self.company, party=_party, purchase_date=self.date) purchase.on_change_party() purchase.warehouse = self.warehouse purchase.currency = self.currency line = PurchaseLine(purchase=purchase, type='line', quantity=1, product=self.carrier.carrier_product, unit=self.carrier.carrier_product.purchase_uom) line.on_change_product() line.unit_price = line.amount = self.unit_price purchase.lines = [line] return purchase def get_parties(self, name=None): if not self.orders: return None _parties = set(o.party for o in self.orders if o.party) return ';'.join(p.rec_name for p in _parties) @classmethod def search_parties(cls, name, clause): return [('orders.party', ) + tuple(clause[1:])] # TODO: check party matches with party of origin in lines class LoadOrder(Workflow, ModelView, ModelSQL, IncotermDocumentMixin, CMRInstructionsMixin): """Carrier load order""" __name__ = 'carrier.load.order' _rec_name = 'code' load = fields.Many2One('carrier.load', 'Load', required=True, select=True, ondelete='CASCADE', states={'readonly': Eval('state') != 'draft'}, depends=['state']) code = fields.Char('Code', required=True, select=True, states={'readonly': Eval('code_readonly', True)}, depends=['code_readonly']) code_readonly = fields.Function(fields.Boolean('Code Readonly'), 'get_code_readonly') company = fields.Many2One('company.company', 'Company', required=True, states={'readonly': Eval('state') != 'draft'}, domain=[('id', If(Eval('context', {}).contains('company'), '=', '!='), Eval('context', {}).get('company', -1))], depends=['state'], select=True) date = fields.Function(fields.Date('Effective date'), 'on_change_with_date') start_date = fields.DateTime('Start date', states={'readonly': ~Eval('state').in_(['draft', 'waiting'])}, depends=['state']) end_date = fields.DateTime('End date', domain=[If(Eval('end_date') & Eval('start_date'), ('end_date', '>=', Eval('start_date')), ())], states={'readonly': ~Eval('state').in_(['draft', 'waiting'])}, depends=['state', 'start_date', 'end_date']) arrival_date = fields.Date('Arrival date', states={'readonly': Eval('state') != 'draft'}, depends=['state']) lines = fields.One2Many('carrier.load.order.line', 'order', 'Lines', states={'readonly': Eval('state') != 'draft'}, depends=['state']) party = fields.Many2One('party.party', 'Party', select=True, states={ 'readonly': (Eval('state') != 'draft') | (Eval('lines', [0])), 'required': (Eval('state') == 'done') & (Eval('type') != 'internal'), 'invisible': Eval('type') == 'internal'}, depends=['state', 'lines', 'type']) incoterms = fields.One2Many('carrier.load.order.incoterm', 'order', 'Incoterms', states={'readonly': ~Eval('state').in_(['draft', 'waiting']), 'invisible': ~Eval('party')}, depends=['state', 'party']) sale_edit = fields.Boolean('Edit sale', states={ 'readonly': Eval('state').in_(['done', 'cancel']) | Bool(Eval('sale')) }, depends=['state', 'sale']) sale = fields.Many2One('sale.sale', 'Sale', domain=[ If((Eval('state') != 'done') & Bool(Eval('sale_edit')), [ ('state', 'in', ['processing', 'quotation', 'confirmed']), ('shipment_method', '=', 'manual'), ['OR', ('lines.moves', '=', None), # needed to pass domain on running->done transition ('lines.moves.shipment', '=', Eval('shipment')) ] ], []) ], states={ 'invisible': ~Eval('party') | (Eval('type') != 'out'), 'readonly': (Eval('state') == 'done') | (Eval('type') != 'out') | Bool(Eval('shipment')) | Not(Bool(Eval('sale_edit'))) }, depends=['party', 'type', 'state', 'shipment', 'id', 'sale_edit']) shipment = fields.Reference('Shipment', selection='get_shipments', readonly=True, select=True) state = fields.Selection([ ('draft', 'Draft'), ('waiting', 'Waiting'), ('running', 'Running'), ('done', 'Done'), ('cancel', 'Cancel')], 'State', readonly=True, required=True) inventory_moves = fields.Function( fields.One2Many('stock.move', None, 'Inventory moves'), 'get_inventory_moves') outgoing_moves = fields.Function( fields.One2Many('stock.move', None, 'Outgoing moves'), 'get_outgoing_moves') carrier_amount = fields.Function( fields.Numeric('Carrier amount', digits=(16, Eval('_parent_order', {}).get('currency_digits', 2))), 'get_carrier_amount') type = fields.Selection([ ('out', 'Out'), ('internal', 'Internal'), ('in_return', 'In return')], 'Type', required=True, select=True, states={'readonly': Bool(Eval('lines', [])) | Bool(Eval('shipment', None)) | (Eval('state') != 'draft')}, depends=['lines', 'shipment', 'state']) warehouse = fields.Function(fields.Many2One('stock.location', 'Warehouse'), 'get_warehouse') to_location = fields.Many2One('stock.location', 'To location', domain=[('type', '=', 'storage')], states={'required': (Eval('type') == 'internal') & ~Eval('shipment', None), 'readonly': Eval('state') != 'draft', 'invisible': Eval('type') != 'internal'}, depends=['type', 'state']) @classmethod def __setup__(cls): super(LoadOrder, cls).__setup__() t = cls.__table__() cls._sql_constraints = [ ('code_uk1', Unique(t, t.code), 'Code must be unique.') ] cls._order = [ ('start_date', 'DESC'), ('id', 'DESC'), ] cls._transitions |= set((('draft', 'waiting'), ('waiting', 'draft'), ('draft', 'running'), ('waiting', 'running'), ('running', 'waiting'), ('running', 'done'), ('draft', 'cancel'), ('cancel', 'draft'))) cls._error_messages.update({ 'delete_cancel': 'Carrier load order "%s" must be ' + 'cancelled before deletion.', 'non_salable_product': 'Product "%s" is not salable.', 'no_sale_line_found': 'Cannot find a line on Sale "%s" with following data:\n%s', 'missing_customer_location': 'Missing Customer location on Party "%s".' }) cls._buttons.update({ 'cancel': { 'invisible': Eval('state').in_(['cancel', 'done']), 'depends': ['state']}, 'draft': { 'invisible': ~Eval('state').in_(['cancel', 'waiting']), 'icon': If(Eval('state') == 'cancel', 'tryton-undo', 'tryton-back'), 'depends': ['state']}, 'wait': { 'invisible': ~Eval('state').in_(['draft', 'running']), 'icon': If(Eval('state') == 'draft', 'tryton-forward', 'tryton-back')}, 'do': { 'invisible': Eval('state') != 'running', 'icon': 'tryton-ok', 'depends': ['state']}, 'do_wizard': { 'invisible': Eval('state') != 'running', 'icon': 'tryton-ok', 'depends': ['state'] } }) if cls.incoterm_version.states.get('invisible'): cls.incoterm_version.states['invisible'] |= (~Eval('party')) else: cls.incoterm_version.states['invisible'] = (~Eval('party')) if 'party' not in cls.incoterm_version.depends: cls.incoterm_version.depends.append('party') @classmethod def __register__(cls, module_name): pool = Pool() Sale = pool.get('sale.sale') Saleline = pool.get('sale.line') Move = pool.get('stock.move') sale = Sale.__table__() sale_line = Saleline.__table__() move = Move.__table__() sql_table = cls.__table__() super(LoadOrder, cls).__register__(module_name) cursor = Transaction().connection.cursor() # Migration from 3.6: type is required cursor.execute(*sql_table.update([sql_table.type], ['out'], where=sql_table.type == Null)) # Migration from 3.6: set shipment cursor.execute(*sql_table.join(sale, condition=Concat( cls.__name__ + ',', sql_table.id) == sale.origin ).join(sale_line, condition=sale_line.sale == sale.id ).join(move, condition=move.origin == Concat( Saleline.__name__ + ',', sale_line.id) ).select(sql_table.id, move.shipment, where=(sql_table.shipment == Null) & (sql_table.state == 'done') & (sql_table.type == 'out'), group_by=[sql_table.id, move.shipment]) ) for order_id, shipment_id in cursor.fetchall(): cursor.execute(*sql_table.update([sql_table.shipment], [shipment_id], where=sql_table.id == order_id)) @staticmethod def order_code(tables): table, _ = tables[None] return [CharLength(table.code), table.code] @staticmethod def default_type(): return 'out' @staticmethod def default_code_readonly(): Configuration = Pool().get('carrier.configuration') config = Configuration(1) return bool(config.load_order_sequence) def get_code_readonly(self, name): return True @classmethod def default_state(cls): return 'draft' @staticmethod def default_company(): return Transaction().context.get('company') def get_cmr_template(self, name=None): if self.party and self.party.cmr_template: return self.party.cmr_template return super().get_cmr_template(name) @fields.depends('edit_cmr_instructions', 'cmr_template', 'load', '_parent_load.edit_cmr_instructions', '_parent_load.cmr_instructions_store') def on_change_with_cmr_instructions(self, name=None): if self.edit_cmr_instructions: return self.cmr_instructions_store if self.load and self.load.edit_cmr_instructions: return self.load.cmr_instructions_store if self.cmr_template: return self.cmr_template.get_section_text('13', self) def get_warehouse(self, name=None): if self.load: return self.load.warehouse.id return None def get_inventory_moves(self, name=None): if self.lines: return [m.id for l in self.lines for m in l.inventory_moves] return [] def get_outgoing_moves(self, name=None): if self.lines: return [m.id for l in self.lines for m in l.outgoing_moves] return [] def get_carrier_amount(self, name=None): if not self.load.unit_price: return 0 return self.load.currency.round( Decimal(1 / len(self.load.orders)) * self.load.unit_price) @classmethod def create(cls, vlist): Sequence = Pool().get('ir.sequence') Configuration = Pool().get('carrier.configuration') vlist = [x.copy() for x in vlist] config = Configuration(1) for values in vlist: if not values.get('code'): values['code'] = Sequence.get_id(config.load_order_sequence.id) return super(LoadOrder, cls).create(vlist) @classmethod def copy(cls, items, default=None): if default is None: default = {} default = default.copy() default['code'] = None default['lines'] = None default['shipment'] = None return super(LoadOrder, cls).copy(items, default=default) @classmethod def get_models(cls, models): Model = Pool().get('ir.model') models = Model.search([ ('model', 'in', models), ]) return [('', '')] + [(m.model, m.name) for m in models] @classmethod def get_shipments(cls): return cls.get_models(cls._get_shipments()) @classmethod def _get_shipments(cls): return ['stock.shipment.out', 'stock.shipment.out.return', 'stock.shipment.internal', 'stock.shipment.in.return'] @fields.depends('load') def on_change_with_date(self, name=None): if self.load: return self.load.date return None @classmethod def delete(cls, records): cls.cancel(records) for record in records: if record.state != 'cancel': cls.raise_user_error('delete_cancel', record.rec_name) super(LoadOrder, cls).delete(records) @classmethod @ModelView.button @Workflow.transition('cancel') def cancel(cls, records): pass @classmethod @ModelView.button @Workflow.transition('draft') def draft(cls, records): pass @classmethod @ModelView.button @Workflow.transition('waiting') def wait(cls, records): pass @classmethod @ModelView.button @Workflow.transition('running') def run(cls, records): to_update = [r for r in records if not r.start_date] if to_update: cls.write(to_update, {'start_date': datetime.datetime.now()}) @classmethod @ModelView.button_action('carrier_load.wizard_load_order_do') def do_wizard(cls, records): return 'reload' @classmethod @ModelView.button @Workflow.transition('done') def do(cls, records): _end_date = datetime.datetime.now() for record in records: if not record.party.customer_location: cls.raise_user_error('missing_customer_location', record.party.rec_name) if not record.end_date: record.end_date = _end_date record.save() sale = record.create_sale() record.create_shipment() if sale: # delay quote for notification trigger sale.quote([sale]) def create_sale(self): pool = Pool() Sale = pool.get('sale.sale') if self.type != 'out': return if not self.party: return if self.sale: if self.sale_edit: # set origin if sale setted manually sale = self.sale sale.origin = self sale.save() return if self.sale.lines: return _sale = self.sale _sale = self._get_load_sale(Sale) items = self._get_items() keyfunc = partial(self._group_line_key, items) items = sorted(items, key=keyfunc) lines = [] for key, grouped_items in groupby(items, key=keyfunc): _groupitems = list(grouped_items) line = self._get_load_sale_line(_sale, key, _groupitems) lines.append(line) _sale.lines = lines _sale.save() self.sale = _sale self.save() return _sale def create_shipment(self): pool = Pool() Shipment = pool.get('stock.shipment.%s' % self.type) if self.shipment and self.shipment.moves: return if self.type == 'out': if not self.party: return if self.sale.shipment_method != 'manual': return shipment = self._get_shipment_out(self.sale) elif self.type == 'internal': if not self.to_location: return shipment = self._get_shipment_internal() else: raise NotImplementedError() items = self._get_items() keyfunc = partial(self._group_line_key, items) items = sorted(items, key=keyfunc) for key, grouped_items in groupby(items, key=keyfunc): _groupitems = list(grouped_items) key_dict = dict(key) _fields = list(key_dict.keys()) def get_line_values(line): line_values = [] for _field in _fields: value = getattr(line, _field, None) if isinstance(value, Model): value = int(value) line_values.append(value) return line_values if self.type == 'out': sale_line = [l for l in self.sale.lines if get_line_values(l) == list(key_dict.values())] if not sale_line: self.raise_user_error('no_sale_line_found', (self.sale.rec_name, '\n'.join([ ' - %s: %s' % (key, value) for key, value in key_dict.items()])) ) sale_line, = sale_line else: sale_line = None shipment.moves = (list(getattr(shipment, 'moves', [])) + self._get_shipment_moves(sale_line, _groupitems)) shipment.save() self.shipment = shipment self.save() Shipment.wait([shipment]) if not Shipment.assign_try([shipment]): Shipment.assign_force([shipment]) if self.type == 'out': Shipment.pack([shipment]) def _get_load_sale(self, Sale): pool = Pool() SaleIncoterm = pool.get('sale.incoterm') _date = self.end_date.date() try: Conf = pool.get('production.configuration') conf = Conf(1) if conf.daily_end_time and \ self.end_date.time() < conf.daily_end_time: _date -= relativedelta(days=1) except (KeyError, AttributeError): pass incoterms = [ SaleIncoterm(rule=incoterm.rule, value=incoterm.value, currency=incoterm.currency, place=incoterm.place) for incoterm in self.incoterms] sale = Sale( company=self.company, currency=Sale.default_currency(), warehouse=self.load.warehouse, sale_date=_date, incoterm_version=self.incoterm_version ) sale.party = self.party sale.on_change_party() sale.incoterms = incoterms sale.origin = self return sale def _get_shipment_out(self, sale): pool = Pool() Shipment = pool.get('stock.shipment.out') ShipmentIncoterm = pool.get('stock.shipment.out.incoterm') shipment = sale._get_shipment_sale( Shipment, key=(('planned_date', self.end_date.date()), ('warehouse', self.load.warehouse.id),)) shipment.reference = sale.reference shipment.dock = self.load.dock shipment.incoterm_version = sale.incoterm_version shipment.incoterms = [ ShipmentIncoterm(rule=incoterm.rule, value=incoterm.value, currency=incoterm.currency, place=incoterm.place) for incoterm in self.incoterms] return shipment def _get_shipment_internal(self): pool = Pool() Shipment = pool.get('stock.shipment.internal') shipment = Shipment( company=self.company, planned_date=self.end_date.date(), planned_start_date=self.end_date.date(), effective_date=self.end_date.date(), from_location=self.warehouse.storage_location, to_location=self.to_location) shipment.dock = self.load.dock return shipment def _get_shipment_moves(self, origin, grouped_items): if self.type == 'out': return [origin.get_move(shipment_type='out')] elif self.type == 'internal': return [] return [] def _get_load_sale_line(self, sale, key, grouped_items): pool = Pool() Saleline = pool.get('sale.line') Product = pool.get('product.product') values = { 'sale': sale, 'quantity': self._get_load_sale_line_quantity(grouped_items) } dictkey = dict(key) values.update(dictkey) line = Saleline(**values) product = Product(line.product) if not product.salable: self.raise_user_error('non_salable_product', product.rec_name) line.on_change_product() if 'unit_price' in values: line.unit_price = values['unit_price'] line.from_location = self.load.warehouse_output line.to_location = self.party.customer_location line.shipping_date = line.on_change_with_shipping_date(None) return line def _get_load_sale_line_quantity(self, grouped_items): Uom = Pool().get('product.uom') qty = 0 if grouped_items: to_uom = grouped_items[0].product.default_uom qty = to_uom.round(sum(Uom.compute_qty(m.uom, m.quantity, to_uom) for m in grouped_items)) return qty @classmethod def _group_line_key(cls, items, item): return ( ('product', item.product.id), ('unit', item.product.default_uom.id)) def _get_items(self): return self.lines @classmethod def view_attributes(cls): return super(LoadOrder, cls).view_attributes() + [ ('//page[@id="incoterms"]', 'states', { 'invisible': ~Eval('party')})] class LoadOrderIncoterm(ModelView, ModelSQL, IncotermMixin): """Load order Incoterm""" __name__ = 'carrier.load.order.incoterm' order = fields.Many2One('carrier.load.order', 'Order', required=True, ondelete='CASCADE') def get_rec_name(self, name): return '%s %s' % (self.rule.rec_name, self.place) def _get_relation_version(self): return self.order @fields.depends('order') def on_change_with_version(self, name=None): return super(LoadOrderIncoterm, self).on_change_with_version(name) class LoadOrderLine(ModelView, ModelSQL): """Carrier load order line""" __name__ = 'carrier.load.order.line' order = fields.Many2One('carrier.load.order', 'Load order', required=True, select=True, readonly=True, ondelete='CASCADE') origin = fields.Reference('Origin', selection='get_origin', readonly=True) product = fields.Function( fields.Many2One('product.product', 'Product'), 'on_change_with_product') uom = fields.Function( fields.Many2One('product.uom', 'UOM'), 'on_change_with_uom') unit_digits = fields.Function(fields.Integer('Unit Digits'), 'on_change_with_unit_digits') quantity = fields.Float('Quantity', digits=(16, Eval('unit_digits', 2)), depends=['unit_digits']) moves = fields.One2Many('stock.move', 'origin', 'Moves', readonly=True) inventory_moves = fields.Function( fields.One2Many('stock.move', None, 'Inventory moves'), 'get_inventory_moves') outgoing_moves = fields.Function( fields.One2Many('stock.move', None, 'Outgoing moves'), 'get_outgoing_moves') order_state = fields.Function( fields.Selection('get_order_states', 'Order state'), 'on_change_with_order_state') @classmethod def __setup__(cls): super(LoadOrderLine, cls).__setup__() cls._error_messages.update({ 'quantity_exceeded': 'Cannot exceed quantity of "%s".', }) @classmethod def _get_origin(cls): return [''] @classmethod def get_origin(cls): return cls.get_models(cls._get_origin()) @classmethod def get_models(cls, models): Model = Pool().get('ir.model') models = Model.search([ ('model', 'in', models), ]) return [('', '')] + [(m.model, m.name) for m in models] @fields.depends('origin') def on_change_with_product(self, name=None): if self.origin and getattr(self.origin, 'product', None): return self.origin.product.id return None @fields.depends('origin') def on_change_with_uom(self, name=None): if self.origin and getattr(self.origin, 'uom', None): return self.origin.uom.id return None @fields.depends('origin') def on_change_with_unit_digits(self, name=None): if self.origin and getattr(self.origin, 'uom', None): return self.origin.uom.digits return 2 @classmethod def validate(cls, records): cls.check_origin_quantity(records) super(LoadOrderLine, cls).validate(records) @classmethod def check_origin_quantity(cls, records): values = {} _field = cls._get_quantity_field() for record in records: if not record.origin: continue values.setdefault(record.origin, 0) values[record.origin] += getattr(record, _field, 0) record_ids = list(map(int, records)) for key, value in values.items(): others = cls.search([ ('origin', '=', '%s,%s' % (key.__name__, key.id)), ('id', 'not in', record_ids)]) if others: value += sum(getattr(o, _field, 0) for o in others) cls._raise_check_origin_quantity(key, _field, value) @classmethod def _raise_check_origin_quantity(cls, origin, fieldname, value): if origin and hasattr(origin, fieldname) and getattr(origin, fieldname, 0) < value: cls.raise_user_error('quantity_exceeded', origin.rec_name) @classmethod def _get_quantity_field(cls): return 'quantity' def get_inventory_moves(self, name=None): if not self.moves: return [] return [m.id for m in self.moves if m.to_location == self.order.load.warehouse_output] def get_outgoing_moves(self, name=None): if not self.moves: return [] return [m.id for m in self.moves if m.from_location == self.order.load.warehouse_output] @classmethod def get_order_states(cls): return LoadOrder.state.selection @fields.depends('order', '_parent_order.state') def on_change_with_order_state(self, name=None): if self.order: return self.order.state class DoLoadOrder(Wizard): """Do Carrier Load Order""" __name__ = 'carrier.load.order.do' start = StateTransition() do_ = StateTransition() @classmethod def next_states(cls): return ['start', 'do_'] @classmethod def next_action(cls, name): states = cls.next_states() try: return states[states.index(name) + 1] except IndexError: return 'end' def transition_start(self): return self.next_action('start') def transition_do_(self): pool = Pool() Order = pool.get('carrier.load.order') order = Order(Transaction().context.get('active_id')) Order.do([order]) return self.next_action('do_') class LoadSheet(CompanyReport): """Carrier load report""" __name__ = 'carrier.load.sheet' @classmethod def get_context(cls, records, data): report_context = super(LoadSheet, cls).get_context(records, data) report_context['product_quantity'] = lambda order, product: \ cls.product_quantity(order, product) products = {} for record in list(set(records)): for order in record.orders: products.update({order.id: cls._get_lines(order)}) report_context['order_products'] = products return report_context @classmethod def _get_lines(cls, order): Uom = Pool().get('product.uom') res = {} for line in order.lines: if not line.product: continue res.setdefault(line.product.id, cls.get_line_dict(line.product)) res[line.product.id]['quantity'] += Uom.compute_qty( line.uom, line.quantity, line.product.default_uom) return res @classmethod def get_line_dict(cls, item): return { 'record': item, 'quantity': 0, } class NoteMixin(object): @classmethod def get_context(cls, records, data): report_context = super(NoteMixin, cls).get_context(records, data) report_context['delivery_address'] = (lambda order: cls.delivery_address(order)) report_context['consignee_address'] = (lambda order: cls.consignee_address(order)) report_context['load_address'] = lambda order: cls.load_address(order) report_context['product_name'] = (lambda order, product_key, origins, language: cls.product_name(order, product_key, origins, language)) report_context['product_brand'] = ( lambda product_key, origins, language: cls.product_brand( product_key, origins, language)) report_context['product_packages'] = (lambda product_key, origins, language: cls.product_packages(product_key, origins, language)) report_context['product_packing'] = (lambda product_key, origins, language: cls.product_packing(product_key, origins, language)) report_context['product_weight'] = (lambda product_key, origins, language: cls.product_weight(product_key, origins, language)) report_context['product_volume'] = (lambda product_key, origins, language: cls.product_volume(product_key, origins, language)) report_context['instructions'] = ( lambda order, language: cls.instructions(order, language)) report_context['sender'] = lambda order: cls.sender(order) report_context['consignee'] = lambda order: cls.consignee(order) report_context['sender_address'] = (lambda order, sender_party: cls.sender_address(order, sender_party)) products = {} for record in list(set(records)): products.update({record.id: cls._get_products(record)}) report_context['order_products'] = products return report_context @classmethod def _get_products(cls, order): records = cls._get_product_origins(order) products = [] keyfunc = partial(cls._get_products_key, records) records = sorted(records, key=keyfunc) for key, grouped_records in groupby(records, key=keyfunc): grouped_records = list(grouped_records) products.append((key, grouped_records)) return products @classmethod def _get_products_key(cls, origins, origin): return (('product', origin.product), ) @classmethod def _get_product_origins(cls, order): if order.lines: return order.lines if order.shipment: return order.shipment.moves @classmethod def consignee_address(cls, order): if order.type == 'out': party = order.sale and order.sale.shipment_party if not party: party = order.party return party.address_get(type='invoice') return order.company.party.address_get(type='invoice') @classmethod def delivery_address(cls, order): if order.type == 'internal': return order.to_location.warehouse.address elif order.type == 'out': address = order.shipment and order.shipment.delivery_address or ( order.sale and order.sale.shipment_address) or None return address return None @classmethod def load_address(cls, order): return order.load.warehouse.address @classmethod def product_name(cls, order, product_key, origins, language): Product = Pool().get('product.product') product = product_key[0][1] with Transaction().set_context(language=language): return Product(product.id).rec_name if product else '' @classmethod def product_brand(cls, product_key, origins, language): return None @classmethod def product_packages(cls, product_key, origins, language): return 0 @classmethod def product_packing(cls, product_key, origins, language): return None @classmethod def product_weight(cls, product_key, origins, language): return None @classmethod def product_volume(cls, product_key, origins, language): return None @classmethod def sender(cls, order): if order.type == 'out' and order.sale and order.sale.shipment_party: return order.sale.party return order.company.party @classmethod def consignee(cls, order): if order.type == 'out': if order.sale and order.sale.shipment_party: return order.sale.shipment_party return order.sale and order.sale.party or order.party return order.company.party @classmethod def sender_address(cls, order, sender_party): return sender_party.address_get(type='invoice') class RoadTransportNote(NoteMixin, CompanyReport): """Road transport note""" __name__ = 'carrier.load.order.road_note' @classmethod def get_context(cls, records, data): Configuration = Pool().get('carrier.configuration') report_context = super(RoadTransportNote, cls).get_context( records, data) report_context['law_header'] = lambda language: \ cls.law_header(language) report_context['law_footer'] = lambda language: \ cls.law_footer(language) report_context['copies'] = Configuration(1).road_note_copies or 3 return report_context @classmethod def law_header(cls, language): Configuration = Pool().get('carrier.configuration') with Transaction().set_context(language=language): return Configuration(1).road_note_header @classmethod def law_footer(cls, language): Configuration = Pool().get('carrier.configuration') with Transaction().set_context(language=language): return Configuration(1).road_note_footer @classmethod def instructions(cls, order, language): Configuration = Pool().get('carrier.configuration') with Transaction().set_context(language=language): value = Configuration(1).road_note_instructions if value: value = value.splitlines() return value or [] class PrintLoadOrderShipment(Wizard): """Print load order shipment""" __name__ = 'carrier.load.order.print_shipment' start = StateTransition() internal_shipment = StateReport('stock.shipment.internal.report') delivery_note = StateReport('stock.shipment.out.delivery_note') def transition_start(self): Order = Pool().get('carrier.load.order') order = Order(Transaction().context['active_id']) if not order.shipment: return 'end' return self._get_shipment_report_state()[order.shipment.__name__] @classmethod def _get_shipment_report_state(cls): return { 'stock.shipment.internal': 'internal_shipment', 'stock.shipment.out': 'delivery_note' } def do_internal_shipment(self, action): return self._print_shipment(action) def do_delivery_note(self, action): Order = Pool().get('carrier.load.order') order = Order(Transaction().context['active_id']) action, data = self._print_shipment(action) if 'pyson_email' in action: _ = action.pop('pyson_email') action['email'] = {'to': order.party.email} return action, data def _print_shipment(self, action): Order = Pool().get('carrier.load.order') order = Order(Transaction().context['active_id']) return action, {'ids': [order.shipment.id]} class Load2(metaclass=PoolMeta): __name__ = 'carrier.load' @classmethod def write(cls, *args): actions = iter(args) args = [] to_update = [] for records, values in zip(actions, actions): if 'unit_price' in values: to_update.extend(records) args.extend((records, values)) super().write(*args) if to_update: cls.update_sale_carrier_amount(to_update) @classmethod def update_sale_carrier_amount(cls, records): pool = Pool() Sale = pool.get('sale.sale') SaleCost = pool.get('sale.cost') sales = [o.sale for r in records for o in r.orders if o.sale] costs = SaleCost.search([ ('document', 'in', [s.id for s in sales]), ('formula', 'like', '%%carrier_amount%'), ('document.state', '!=', 'cancel')]) to_distribute = set() to_save = [] for cost in costs: if cost.sale.state not in ('draft', 'quotation'): to_distribute.add(cost.sale) elif not cost._must_update_carrier_amount(): # not recompute costs with apply method continue cost.on_change_formula() to_save.append(cost) if to_save: SaleCost.save(to_save) if to_distribute: Sale.distribute_costs(list(to_distribute)) class Load3(metaclass=PoolMeta): __name__ = 'carrier.load' @classmethod def write(cls, *args): actions = iter(args) args = [] to_update = [] for records, values in zip(actions, actions): if 'carrier' in values: to_update.extend(records) args.extend((records, values)) super().write(*args) if to_update: cls.update_sale_carrier(to_update) @classmethod def update_sale_carrier(cls, records): pool = Pool() SaleCost = pool.get('sale.cost') for record in records: carrier_party = record.carrier and record.carrier.party or None costs = [c for o in record.orders if o.sale for c in o.sale.costs if 'carrier_amount' in c.formula and c.apply_method == 'invoice_in' and not c.invoice_lines and c.invoice_party != carrier_party] if costs: SaleCost.write(costs, {'invoice_party': carrier_party}) class LoadOrder3(metaclass=PoolMeta): __name__ = 'carrier.load.order' @classmethod def write(cls, *args): Load = Pool().get('carrier.load') actions = iter(args) args = [] to_update = [] for records, values in zip(actions, actions): if 'sale' in values: to_update.extend([r.load for r in records]) args.extend((records, values)) super().write(*args) if to_update: Load.update_sale_carrier(list(set(to_update))) class CarrierDefine(Wizard): """Define Carrier""" __name__ = 'carrier.load.define' start = StateTransition() carrier = StateView('carrier.load', 'carrier_load.load_view_simple_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Add', 'add', 'tryton-ok', default=True)]) add = StateTransition() @classmethod def __setup__(cls): super().__setup__() cls._error_messages.update({ 'many_warehouses': 'Warehouse must match.' }) def transition_start(self): Model = Pool().get(Transaction().context.get('active_model')) if hasattr(Model, 'warehouse'): records = Model.browse(Transaction().context['active_ids']) whs = set(r.warehouse for r in records) if len(whs) > 1: self.raise_user_error('many_warehouses') return 'carrier' def default_carrier(self, fields): Model = Pool().get(Transaction().context.get('active_model')) res = {} if hasattr(Model, 'warehouse'): records = Model.browse(Transaction().context['active_ids']) whs = set(r.warehouse and r.warehouse.id or None for r in records) res['warehouse'] = whs.pop() return res def transition_add(self): pool = Pool() Model = pool.get(Transaction().context.get('active_model')) self.carrier.save() records = Model.browse(Transaction().context['active_ids']) Model.write(records, { 'planned_carrier_loads': [('add', [self.carrier.id])] }) return 'end' class LoadOrder4(metaclass=PoolMeta): __name__ = 'carrier.load.order' @suppress_root_mandatory def create_sale(self): return super().create_sale() class CarrierLoadPurchase(CompanyReport): '''Carrier Load Purchase''' __name__ = 'carrier.load.purchase' @classmethod def get_context(cls, records, data): report_context = super().get_context(records, data) report_context['get_info_lines'] = (lambda purchase, customer: cls.get_info_lines(purchase, customer)) report_context['get_carrier'] = (lambda purchase: cls.get_carrier(purchase)) report_context['get_cmr_instructions'] = (lambda purchase: cls.get_cmr_instructions(purchase)) report_context['get_customers'] = (lambda purchase: cls.get_customers(purchase)) return report_context @classmethod def _get_line_keygroup(cls, line): return ( ('load_date', line.order.start_date.date()), ('load_place', line.order and line.order.load and line.order.load.warehouse and line.order.load.warehouse.address or None), ('unload_date', None), ('customer_ref', line.order.sale and line.order.sale.reference), ('product', line.product), ) @classmethod def get_info_lines(cls, purchase, customer): lines = cls._get_lines_to_group(purchase, customer) info_lines = {} for line in lines: key = cls._get_line_keygroup(line) info_lines.setdefault(key, 0) info_lines[key] += cls._get_line_quantity(line) return [(dict(k), v) for k, v in info_lines.items()] @classmethod def _get_lines_to_group(cls, purchase, customer): if purchase.loads: load = purchase.loads[0] return [order_line for order in load.orders if order.party == customer for order_line in order.lines ] @classmethod def _get_line_quantity(cls, line): return line.quantity @classmethod def get_carrier(cls, purchase): if purchase.loads: return purchase.loads[0].carrier @classmethod def get_cmr_instructions(cls, purchase): if purchase.loads: return purchase.loads[0].cmr_instructions @classmethod def get_customers(cls, purchase): if purchase.loads: return list(set(order.party for order in purchase.loads[0].orders)) class PrintCarrierLoadPurchase(Wizard): '''Print carrier load purchase''' __name__ = 'carrier.load.print_purchase' start = StateTransition() print_ = StateReport('carrier.load.purchase') def transition_start(self): return 'print_' def do_print_(self, action): pool = Pool() CarrierLoad = pool.get('carrier.load') carrier_loads = CarrierLoad.browse( Transaction().context.get('active_ids', [])) return action, {'ids': [cl.purchase.id for cl in carrier_loads if cl.purchase]}