1191 lines
44 KiB
Python
1191 lines
44 KiB
Python
# 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]}
|