trytond-carrier_load/load.py

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