trytond-carrier_load/load.py

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