# 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 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 from trytond.pyson import Eval, If, Bool 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, StateTransition, Wizard __all__ = ['Load', 'LoadOrder', 'LoadOrderLine', 'LoadOrderIncoterm', 'LoadSheet', 'CMR', 'RoadTransportNote', 'PrintLoadOrderShipment'] class Load(Workflow, ModelView, ModelSQL, DockMixin): """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', required=True, select=True, ondelete='RESTRICT', states={'readonly': Eval('state') != 'draft'}, depends=['state']) vehicle = fields.Many2One('carrier.vehicle', 'Vehicle', required=True, ondelete='RESTRICT', domain=[('carrier', '=', Eval('carrier'))], states={'readonly': Eval('state') != 'draft'}, depends=['state', 'carrier']) 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'}, depends=['state']) 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') @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._transitions |= set((('draft', 'confirmed'), ('confirmed', 'draft'), ('confirmed', 'done'), ('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.'}) cls._buttons.update({ 'cancel': {'invisible': Eval('state').in_(['cancel', 'done'])}, 'draft': {'invisible': ~Eval('state').in_(['cancel', 'confirmed']), 'icon': If(Eval('state') == 'confirmed', 'tryton-go-previous', 'tryton-go-next')}, 'confirm': {'invisible': Eval('state') != 'draft', 'icon': If(Eval('state') == 'draft', 'tryton-go-next', 'tryton-go-previous')}, 'do': {'invisible': Eval('state') != 'confirmed'}, 'create_purchase': {'invisible': (Eval('state') != 'done') | (Bool(Eval('purchase')))} }) if not cls.dock.required: cls.dock.required = True 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 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 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 len(self.carrier.vehicles) == 1: self.vehicle = self.carrier.vehicles[0] 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('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): pass @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.state not in ('confirmed', 'done'): continue if record.purchase: continue if not record.purchasable: 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): """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', states={'readonly': ~Eval('state').in_(['draft', 'waiting'])}, depends=['state']) 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 = fields.Many2One('sale.sale', 'Sale', readonly=True, states={'invisible': ~Eval('party') | (Eval('type') != 'out')}, depends=['party', 'type']) 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._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.' }) cls._buttons.update({ 'cancel': {'invisible': Eval('state').in_(['cancel', 'done'])}, 'draft': {'invisible': ~Eval('state').in_(['cancel', 'waiting']), 'icon': If(Eval('state') == 'cancel', 'tryton-clear', 'tryton-go-previous')}, 'wait': {'invisible': ~Eval('state').in_(['draft', 'running']), 'icon': If(Eval('state') == 'draft', 'tryton-go-next', 'tryton-go-previous')}, 'do': {'invisible': Eval('state') != 'running', 'icon': 'tryton-ok'}, }) 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_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( (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 @Workflow.transition('done') def do(cls, records): _end_date = datetime.datetime.now() for record in records: if not record.end_date: record.end_date = _end_date record.save() record.create_sale() record.create_shipment() def create_sale(self): pool = Pool() Sale = pool.get('sale.sale') if self.type != 'out': return if not self.party: return if self.sale: 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(key, _groupitems) lines.append(line) _sale.lines = lines _sale.save() self.sale = _sale self.save() Sale.quote([_sale]) def create_shipment(self): pool = Pool() Shipment = pool.get('stock.shipment.%s' % self.type) if self.shipment: return if self.type == 'out': if not self.party: return if self.shipment: 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 = 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) == key_dict.values()] 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') Conf = pool.get('carrier.configuration') conf = Conf(1) _date = self.end_date.date() if conf.work_end_time and self.end_date.time() < conf.work_end_time: _date -= relativedelta(days=1) incoterms = [ SaleIncoterm(rule=incoterm.rule, value=incoterm.value, currency=incoterm.currency, place=incoterm.place) for incoterm in self.incoterms] sale = Sale(company=self.company, 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, key, grouped_items): pool = Pool() Uom = pool.get('product.uom') Saleline = pool.get('sale.line') Product = pool.get('product.product') values = { 'quantity': sum(Uom.compute_qty(m.uom, m.quantity, m.product.default_uom) for m in grouped_items) } values.update(dict(key)) 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() 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 @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') @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 = map(int, records) for key, value in values.iteritems(): 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) if getattr(key, _field, 0) < value: cls.raise_user_error('quantity_exceeded', key.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] 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_products(order)}) report_context['order_products'] = products return report_context @classmethod def _get_products(cls, order): return list(set([l.product for l in order.lines])) @classmethod def product_quantity(cls, order, product): """Returns product quantity in load order""" Uom = Pool().get('product.uom') value = 0 for line in order.lines: if line.product and line.product.id == product.id: value += Uom.compute_qty(line.uom, line.quantity, product.default_uom) return value 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['product_name'] = (lambda product_id, order, language: cls.product_name(product_id, order, language)) report_context['product_brand'] = ( lambda product, language: cls.product_brand(product, language)) report_context['product_packages'] = (lambda order, product, language: cls.product_packages(order, product, language)) report_context['product_packing'] = (lambda product, language: cls.product_packing(product, language)) report_context['product_weight'] = (lambda order, product, language: cls.product_weight(order, product, language)) report_context['product_volume'] = (lambda order, product, language: cls.product_volume(order, product, 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) 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): if order.lines: return list(set([l.product for l in order.lines])) if order.shipment: return list(set([l.product for l in order.shipment.moves])) return [] @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': if not order.shipment: return None return order.shipment.delivery_address return None @classmethod def product_name(cls, product_id, order, language): Product = Pool().get('product.product') with Transaction().set_context(language=language): return Product(product_id).rec_name @classmethod def product_brand(cls, product, language): return None @classmethod def product_packages(cls, order, product, language): return None @classmethod def product_packing(cls, product, language): return None @classmethod def product_weight(cls, order, product, language): return None @classmethod def product_volume(cls, order, product, 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 class CMR(NoteMixin, CompanyReport): """CMR report""" __name__ = 'carrier.load.order.cmr' @classmethod def get_context(cls, records, data): Configuration = Pool().get('carrier.configuration') report_context = super(CMR, cls).get_context(records, data) report_context['copies'] = Configuration(1).cmr_copies or 3 return report_context @classmethod def instructions(cls, order, language): Configuration = Pool().get('carrier.configuration') with Transaction().set_context(language=language): value = Configuration(1).cmr_instructions if value: value = value.splitlines() return value or [] 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): return self._print_shipment(action) def _print_shipment(self, action): Order = Pool().get('carrier.load.order') order = Order(Transaction().context['active_id']) return action, {'ids': [order.shipment.id]}