# The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import datetime from functools import partial 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 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 trytond.exceptions import UserError from trytond.i18n import gettext from decimal import Decimal try: import phonenumbers from phonenumbers import PhoneNumberFormat, NumberParseException except ImportError: phonenumbers = None # 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): __slots__ = () edit_cmr_instructions = fields.Boolean('Edit CMR instructions', states={'readonly': Eval('state') == 'cancelled'}) cmr_instructions = fields.Function( fields.Text('CMR instructions', translate=True, states={ 'readonly': (Eval('state') == 'cancelled') | Not( Bool(Eval('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') == 'cancelled'}) 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)}) code_readonly = fields.Function(fields.Boolean('Code Readonly'), 'get_code_readonly') company = fields.Many2One('company.company', 'Company', required=True, states={ 'readonly': (Eval('state') != 'draft') | Eval('orders', []) | Eval('purchase') }, select=True) carrier = fields.Many2One('carrier', 'Carrier', select=True, ondelete='RESTRICT', states={ 'readonly': Eval('state') != 'draft', 'required': Bool(Eval('purchasable')) }) carrier_info = fields.Text('Carrier information', states={ 'readonly': Eval('state') != 'draft', 'invisible': Bool(Eval('purchasable')) | Bool(Eval('carrier')) }) vehicle_number = fields.Char('Vehicle reg. number', states={ 'readonly': Eval('state') != 'draft', 'required': Eval('state').in_(['confirmed', 'done']) & Bool( Eval('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'))}) trailer_required = fields.Function(fields.Boolean('Trailer required'), 'get_number_required') date = fields.Date('Effective date', required=True, states={'readonly': Eval('state') != 'draft'}) warehouse = fields.Many2One('stock.location', 'Warehouse', required=True, domain=[('type', '=', 'warehouse')], states={ 'readonly': ((Eval('state') != 'draft') | Bool(Eval('orders'))) }) warehouse_output = fields.Function( fields.Many2One('stock.location', 'Warehouse output'), 'on_change_with_warehouse_output') orders = fields.One2Many('carrier.load.order', 'load', 'Orders', domain=[ ('company', '=', Eval('company')) ], states={ 'readonly': (Eval('state') != 'draft') | (Not(Bool(Eval('carrier'))) & Not(Bool('carrier_info'))) | Not(Bool(Eval('warehouse'))) }) state = fields.Selection([ ('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State', readonly=True, required=True) purchasable = fields.Boolean('Purchasable', states={ 'readonly': ((~Eval('state').in_(['draft', 'confirmed'])) | ( Bool(Eval('purchase'))))}) unit_price = fields.Numeric('Unit Price', digits=price_digits, states={ 'readonly': ((~Eval('state').in_( ['draft', 'confirmed', 'done'])) | (Bool(Eval('purchase')))), 'invisible': ~Eval('purchasable')}) currency = fields.Many2One('currency.currency', 'Currency', states={ 'readonly': ((~Eval('state').in_(['draft', 'confirmed'])) | ( Bool(Eval('purchase')))), 'invisible': ~Eval('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')}) purchase_state = fields.Function( fields.Selection([(None, '')], 'Purchase state', states={'invisible': ~Eval('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'}) driver_identifier = fields.Char('Driver identifier', states={ 'required': Bool(Eval('driver')), 'readonly': Eval('state') != 'draft'}) driver_phone = fields.Char('Driver Phone', states={ 'readonly': Eval('state') != 'draft'}) comments = fields.Text('Comments', translate=True, states={ 'readonly': Eval('state') != 'draft'}, depends=['state']) origins = fields.Function(fields.Char('Origins'), 'get_origins', searcher='search_origins') @classmethod def __setup__(cls): super(Load, cls).__setup__() t = cls.__table__() cls._sql_constraints = [ ('code_uk1', Unique(t, t.code), 'carrier_load.msg_carrier_load_code_uk1') ] cls._order = [ ('date', 'DESC'), ('id', 'DESC'), ] cls._transitions |= set((('draft', 'confirmed'), ('confirmed', 'draft'), ('confirmed', 'done'), ('done', 'confirmed'), ('draft', 'cancelled'), ('cancelled', 'draft'))) cls._buttons.update({ 'cancel': { 'invisible': Eval('state').in_(['cancelled', 'done']), 'depends': ['state']}, 'draft': { 'invisible': ~Eval('state').in_(['cancelled', '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) table.not_null_action('carrier', action='remove') table.not_null_action('carrier_vehicle', action='remove') table.not_null_action('dock', action='remove') # Migration from 5.6: rename state cancel to cancelled cursor.execute(*sql_table.update( [sql_table.state], ['cancelled'], where=sql_table.state == 'cancel')) @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 ('cancelled', 'draft') and not record.carrier and not record.carrier_info): raise UserError(gettext( 'carrier_load.msg_carrier_load_missing_carrier_info', load=record.rec_name)) record.check_valid_phonenumber() @classmethod def create(cls, vlist): Configuration = Pool().get('carrier.configuration') vlist = [x.copy() for x in vlist] config = Configuration(1) default_company = cls.default_company() for values in vlist: if not values.get('code'): values['code'] = config.get_multivalue( 'load_sequence', company=values.get('company', default_company)).get() 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(): if not Transaction().context.get('define_carrier', False): 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', 'currency', '_parent_carrier.id', '_parent_currency.digits', '_parent_purchase.id') 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)): raise UserError(gettext( 'carrier_load.msg_carrier_load_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')) if self.driver: info.append(self.driver) if self.driver_identifier: info.append(self.driver_identifier) if self.driver_phone: info.append(self.driver_phone) 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 != 'cancelled': raise UserError(gettext( 'carrier_load.msg_carrier_load_delete_cancel', load=record.rec_name)) super(Load, cls).delete(records) @classmethod @ModelView.button @Workflow.transition('cancelled') 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 == 'cancelled'] 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: raise UserError(gettext( 'carrier_load.msg_carrier_load_purchase_confirm', load=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: raise UserError(gettext( 'carrier_load.msg_carrier_load_purchase_price', load=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:])] @classmethod def get_origins(cls, records, name): res = {r.id: None for r in records} for record in records: origins = ', '.join(o.origins for o in record.orders if o.origins) if origins: res[record.id] = origins return res @classmethod def search_origins(cls, name, clause): return [ ('orders.origins', ) + 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'}) code = fields.Char('Code', required=True, select=True, states={'readonly': Eval('code_readonly', True)}) code_readonly = fields.Function(fields.Boolean('Code Readonly'), 'get_code_readonly') company = fields.Many2One('company.company', 'Company', required=True, states={ 'readonly': (Eval('state') != 'draft') | Eval('shipment') | Eval('sale') | Eval('lines', []) }, 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'])}) 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'])}) arrival_date = fields.Date('Arrival date', states={'readonly': Eval('state') != 'draft'}) lines = fields.One2Many('carrier.load.order.line', 'order', 'Lines', states={'readonly': Eval('state') != 'draft'}) 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'}) incoterms = fields.One2Many('carrier.load.order.incoterm', 'order', 'Incoterms', states={'readonly': ~Eval('state').in_(['draft', 'waiting']), 'invisible': ~Eval('party')}) sale_edit = fields.Boolean('Edit sale', states={ 'readonly': Eval('state').in_(['done', 'cancelled']) | Bool(Eval('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'))) }) shipment = fields.Reference('Shipment', selection='get_shipments', readonly=True, select=True) state = fields.Selection([ ('draft', 'Draft'), ('waiting', 'Waiting'), ('running', 'Running'), ('done', 'Done'), ('cancelled', 'Cancelled')], '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')}) warehouse = fields.Function(fields.Many2One('stock.location', 'Warehouse'), 'get_warehouse') to_location = fields.Many2One('stock.location', 'To location', domain=[ If((Eval('state'), 'in', ['draft', 'waiting', 'running']), ('id', 'in', Eval('storage_locations', [])), ()) ], states={'required': (Eval('type') == 'internal') & ~Eval('shipment', None), 'readonly': Eval('state') != 'draft', 'invisible': Eval('type') != 'internal' }, depends=['type', 'state', 'storage_locations']) storage_locations = fields.Function( fields.Many2Many('stock.location', None, None, "Storage Locations"), 'on_change_with_storage_locations') origins = fields.Function(fields.Char('Origins'), 'get_origins', searcher='search_origins') sales = fields.One2Many('sale.sale', 'origin', "Sales") @classmethod def __setup__(cls): super(LoadOrder, cls).__setup__() t = cls.__table__() cls._sql_constraints = [ ('code_uk1', Unique(t, t.code), 'carrier_load.msg_carrier_load_order_code_uk1') ] cls._order = [ ('start_date', 'DESC'), ('id', 'DESC'), ] cls._transitions |= set((('draft', 'waiting'), ('waiting', 'draft'), ('draft', 'running'), ('waiting', 'running'), ('running', 'waiting'), ('running', 'done'), ('draft', 'cancelled'), ('cancelled', 'draft'))) cls._buttons.update({ 'cancel': { 'invisible': Eval('state').in_(['cancelled', 'done']), 'depends': ['state']}, 'draft': { 'invisible': ~Eval('state').in_(['cancelled', 'waiting']), 'icon': If(Eval('state') == 'cancelled', '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')) @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)) # Migration from 5.6: rename state cancel to cancelled cursor.execute(*sql_table.update( [sql_table.state], ['cancelled'], where=sql_table.state == 'cancel')) @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 get_origins(cls, records, name): res = {r.id: None for r in records} for record in records: origins = ', '.join(l.origin.rec_name for l in record.lines if l.origin) if origins: res[record.id] = origins return res @classmethod def search_origins(cls, name, clause): Line = Pool().get('carrier.load.order.line') domains = [] for model in Line._get_origin(): if not model: continue domains.append( ('lines.origin.rec_name', ) + tuple(clause[1:]) + (model, )) return ['OR', *domains ] @fields.depends('type') def on_change_with_storage_locations(self, name=None): pool = Pool() Location = pool.get('stock.location') if self.type == 'internal': locations = set([location.storage_location for location in Location.search([('type', '=', 'warehouse')])]) locations = Location.search([ ('parent', 'child_of', list(map(int, locations))), ('type', '=', 'storage')]) return list(map(int, locations)) return [] @fields.depends('type') def on_change_type(self, name=None): if self.type != 'internal': self.to_location = None @classmethod def create(cls, vlist): Configuration = Pool().get('carrier.configuration') vlist = [x.copy() for x in vlist] config = Configuration(1) default_company = cls.default_company() for values in vlist: if not values.get('code'): values['code'] = config.get_multivalue( 'load_order_sequence', company=values.get('company', default_company)).get() 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', '_parent_load.date') 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 != 'cancelled': raise UserError(gettext( 'carrier_load.msg_carrier_load_order_delete_cancel', order=record.rec_name)) super(LoadOrder, cls).delete(records) @classmethod @ModelView.button @Workflow.transition('cancelled') 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): pass @classmethod @ModelView.button @Workflow.transition('done') def do(cls, records): pool = Pool() Sale = pool.get('sale.sale') Configuration = pool.get('carrier.configuration') configuration = Configuration(1) _end_date = datetime.datetime.now() to_save = [] for record in records: if (record.type == 'out' and record.party and not record.party.customer_location): raise UserError(gettext( 'carrier_load.' 'msg_carrier_load_order_missing_customer_location', party=record.party.rec_name)) if not record.end_date: record.end_date = _end_date to_save.append(record) if to_save: cls.save(to_save) sales = cls.create_sale(records) cls.create_shipment(records) # set to draft sales to_draft = [] for sale in sales: if sale.state in ('cancelled', 'quotation'): to_draft.append(sale) if to_draft: Sale.draft(to_draft) if configuration.sale_state != 'draft' and sales: for state, method in Sale.get_carrier_states_methods(): func = getattr(Sale, method) func(sales) if state == configuration.sale_state: break 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 self.sale.origin = self return if self.sale.lines: return _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 return _sale @classmethod def create_sale(cls, orders): Sale = Pool().get('sale.sale') orders2sales = {} for order in orders: sale = order._create_sale() if sale: orders2sales[order] = sale if orders2sales: Sale.save(list(orders2sales.values())) for order, sale in orders2sales.items(): order.sale = sale cls.save(list(orders2sales.keys())) return list(orders2sales.values()) def _create_shipment(self): 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: raise UserError(gettext( 'carrier_load.' 'msg_carrier_load_order_no_sale_line_found', sale=self.sale.rec_name, data='\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)) return shipment @classmethod def create_shipment(cls, orders): pool = Pool() Configuration = pool.get('carrier.configuration') ShipmentInternal = pool.get('stock.shipment.internal') ShipmentOut = pool.get('stock.shipment.out') configuration = Configuration(1) orders2shipments = {} for order in orders: shipment = order._create_shipment() if shipment: orders2shipments.setdefault(order.type, {})[order] = shipment for type_, order2shipment in orders2shipments.items(): Shipment = pool.get('stock.shipment.%s' % type_) shipments = list(order2shipment.values()) Shipment.save(shipments) for order, shipment in order2shipment.items(): order.shipment = shipment cls.save(list(order2shipment.keys())) if type_ == 'out': shipment_state = configuration.shipment_out_state if type_ == 'internal': shipment_state = configuration.shipment_internal_state # set to draft shipments to_draft = [] for s in shipments: if s.state == 'cancelled': to_draft.append(s) if to_draft: if type_ == 'out': ShipmentOut.draft(to_draft) if type_ == 'internal': ShipmentInternal.draft(to_draft) if shipment_state != 'draft': for state, method in Shipment.get_carrier_states_methods(): func = getattr(Shipment, method) func(shipments) if state == shipment_state: break def _get_load_sale(self, Sale): pool = Pool() SaleIncoterm = pool.get('sale.incoterm') if self.sale: return self.sale _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(), date_time_=self.end_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: raise UserError(gettext( 'carrier_load.msg_carrier_load_order_non_salable_product', 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')})] @classmethod def get_carrier_report(cls, order=None, from_address=None, to_address=None): if order: from_country = (order.warehouse and order.warehouse.address and order.warehouse.address.country) if order.type == 'out': to_country = (order.sale and order.sale.shipment_address and order.sale.shipment_address.country) elif order.type == 'internal': to_country = (order.to_location and order.to_location.address and order.to_location.address.country) elif order.type == 'in_return': to_address = order.party.address_get(type='delivery') to_country = to_address and to_address.country else: from_country = from_address and from_address.country to_country = to_address and to_address.country if (not from_country or not to_country or from_country != to_country): return 'cmr' return 'road_note' 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', '_parent_order.incoterm_version') 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))) 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 _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: raise UserError(gettext( 'carrier_load.msg_carrier_load_order_line_quantity_exceeded', origin=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, header, data): report_context = super(LoadSheet, cls).get_context(records, header, 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): __slots__ = () @classmethod def get_context(cls, records, header, data): report_context = super(NoteMixin, cls).get_context(records, header, 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, header, data): Configuration = Pool().get('carrier.configuration') report_context = super().get_context(records, header, 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', '!=', 'cancelled')]) 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 CarrierStateView(StateView): def get_view(self, wizard, state_name): with Transaction().set_context(define_carrier=True): return super().get_view(wizard, state_name) class CarrierDefine(Wizard): """Define Carrier""" __name__ = 'carrier.load.define' start = StateTransition() carrier = CarrierStateView('carrier.load', 'carrier_load.load_view_simple_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Add', 'add', 'tryton-ok', default=True)]) add = StateTransition() 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: raise UserError(gettext( 'carrier_load.msg_carrier_load_define_many_warehouses')) if hasattr(Model, 'company'): records = Model.browse(Transaction().context['active_ids']) cpies = set(r.company for r in records) if len(cpies) > 1: raise UserError(gettext( 'carrier_load.msg_carrier_load_define_many_companies')) return 'carrier' def default_carrier(self, fields): Model = Pool().get(Transaction().context.get('active_model')) res = {} records = Model.browse(Transaction().context['active_ids']) if hasattr(Model, 'warehouse'): whs = set(r.warehouse.id for r in records if r.warehouse) res['warehouse'] = whs.pop() if hasattr(Model, 'company'): # this does not work as default set value again cpies = set(r.company.id for r in records if r.company) res['company'] = cpies.pop() return res def transition_add(self): pool = Pool() Model = pool.get(Transaction().context.get('active_model')) records = Model.browse(Transaction().context['active_ids']) if hasattr(Model, 'company'): company, = set(r.company for r in records if r.company) if self.carrier.company != company: self.carrier.company = company self.carrier.save() Model.write(records, { 'planned_carrier_loads': [('add', [self.carrier.id])] }) return 'end' class CarrierLoadPurchase(CompanyReport): '''Carrier Load Purchase''' __name__ = 'carrier.load.purchase' @classmethod def get_context(cls, records, header, data): report_context = super().get_context(records, header, data) report_context['get_info_lines'] = (lambda purchase, address: cls.get_info_lines(purchase, address)) 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_addresses'] = (lambda purchase: cls.get_addresses(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, address): lines = cls._get_lines_to_group(purchase, address) 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, address): if purchase.loads: load = purchase.loads[0] return [order_line for order in load.orders for order_line in order.lines if cls._get_line_address(order_line) == address ] @classmethod def _get_line_address(cls, line): if line.order.type == 'internal': return line.order.to_location.address elif line.order.type == 'out': if line.origin and hasattr(line.origin, 'shipment_address'): return line.origin.shipment_address return line.order.sale and line.order.sale.shipment_address or \ line.order.party.address_get('delivery') @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_addresses(cls, purchase): addresses = set() if purchase.loads: for order in purchase.loads[0].orders: if order.type == 'internal': addresses.add(order.to_location.address) elif order.type == 'out': addresses.add(order.sale and order.sale.shipment_address or order.party.address_get('delivery')) return list(addresses) 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', [])) emails = [cl.purchase.party.email for cl in carrier_loads if cl.purchase] if emails: action ['email'] = {'to': emails[0], 'subject': 'Compras'} return action, {'ids': [cl.purchase.id for cl in carrier_loads if cl.purchase]} class Load4(metaclass=PoolMeta): __name__ = 'carrier.load' vehicle = fields.Many2One('carrier.vehicle', "Vehicle", domain=[ ('active', '=', Bool(True)), ('carrier', '=', Eval('carrier'))], states={'readonly': Eval('state') != 'draft'}, depends=['state', 'carrier']) @fields.depends('vehicle', '_parent_vehicle.driver', '_parent_vehicle.driver_identifier', '_parent_vehicle.trailer_number', '_parent_vehicle.number') def on_change_vehicle(self): if self.vehicle: self.driver = self.vehicle.driver self.driver_identifier = self.vehicle.driver_identifier self.trailer_number = self.vehicle.trailer_number self.vehicle_number = self.vehicle.number class LoadOrder4(metaclass=PoolMeta): __name__ = 'carrier.load.order' def _get_load_sale_line(self, sale, key, grouped_items): line = super()._get_load_sale_line(sale, key, grouped_items) if hasattr(line.__class__, 'base_price'): line.base_price = line.unit_price line.discount = 0 return line