trytond-carrier_load/load.py

1930 lines
67 KiB
Python
Raw Normal View History

# 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
2015-10-20 19:27:58 +02:00
from itertools import groupby
2016-03-25 09:25:24 +01:00
from dateutil.relativedelta import relativedelta
from sql import Literal, Null, Column
from sql.aggregate import Count
2016-02-18 10:37:52 +01:00
from sql.functions import CharLength
from sql.operators import Concat
2015-10-23 18:55:29 +02:00
from trytond.model import ModelSQL, ModelView, fields, Workflow, Model
2016-08-22 13:58:00 +02:00
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
2015-10-15 17:15:27 +02:00
from trytond.modules.incoterm.incoterm import (
IncotermDocumentMixin, IncotermMixin)
2015-10-20 19:27:58 +02:00
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
2019-10-01 12:51:38 +02:00
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
2015-10-15 17:15:27 +02:00
__all__ = ['Load', 'LoadOrder', 'LoadOrderLine',
'LoadOrderIncoterm', 'LoadSheet', 'RoadTransportNote',
'PrintLoadOrderShipment', 'Load2', 'Load3',
'LoadOrder3', 'CarrierLoadPurchase', 'PrintCarrierLoadPurchase']
2018-07-09 18:13:23 +02:00
# 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'
2016-01-21 00:38:08 +01:00
_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__()
2016-08-22 13:58:00 +02:00
t = cls.__table__()
cls._sql_constraints = [
2016-08-22 13:58:00 +02:00
('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.',
2019-10-17 16:13:28 +02:00
'invalid_phonenumber': ('The phone number "%(phone)s" '
2019-10-01 12:51:38 +02:00
'is not valid.'),
'missing_carrier_info':
'Must define either Carrier or Carrier information on Load "%s".'
})
cls._buttons.update({
2018-07-17 09:41:29 +02:00
'cancel': {
'invisible': Eval('state').in_(['cancel', 'done']),
'depends': ['state']},
'draft': {
'invisible': ~Eval('state').in_(['cancel', 'confirmed']),
'icon': If(Eval('state') == 'confirmed',
2019-02-26 11:03:17 +01:00
'tryton-back', 'tryton-forward'),
2018-07-17 09:41:29 +02:00
'depends': ['state']},
'confirm': {
'invisible': ~Eval('state').in_(['draft', 'done']),
2018-07-17 09:41:29 +02:00
'icon': If(Eval('state') == 'draft',
2019-02-26 11:03:17 +01:00
'tryton-forward', 'tryton-back'),
2018-07-17 09:41:29 +02:00
'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)
2019-10-01 12:51:38 +02:00
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)
2020-07-01 16:43:32 +02:00
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')
2016-08-22 13:58:00 +02:00
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
2019-10-01 12:51:38 +02:00
@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(
2019-12-20 12:39:17 +01:00
'\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':
2016-09-08 09:57:54 +02:00
cls.raise_user_error('delete_cancel', record.rec_name)
super(Load, cls).delete(records)
2016-09-08 09:57:54 +02:00
@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)
2016-09-08 09:57:54 +02:00
@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):
2016-02-01 12:53:26 +01:00
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()
2016-02-01 12:53:26 +01:00
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)
2016-11-21 13:07:26 +01:00
_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
2016-09-08 13:58:48 +02:00
_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:])]
2015-10-20 19:27:58 +02:00
# TODO: check party matches with party of origin in lines
class LoadOrder(Workflow, ModelView, ModelSQL, IncotermDocumentMixin,
CMRInstructionsMixin):
"""Carrier load order"""
__name__ = 'carrier.load.order'
2016-01-21 00:38:08 +01:00
_rec_name = 'code'
2021-03-15 11:49:41 +01:00
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,
2021-03-15 11:49:41 +01:00
states={'readonly': Eval('code_readonly', True)},
depends=['code_readonly'])
code_readonly = fields.Function(fields.Boolean('Code Readonly'),
2021-03-15 11:49:41 +01:00
'get_code_readonly')
2015-10-20 19:27:58 +02:00
company = fields.Many2One('company.company', 'Company', required=True,
2021-03-15 11:49:41 +01:00
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'),
2021-03-15 11:49:41 +01:00
'on_change_with_date')
start_date = fields.DateTime('Start date',
2021-03-15 11:49:41 +01:00
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')),
())],
2021-03-15 11:49:41 +01:00
states={'readonly': ~Eval('state').in_(['draft', 'waiting'])},
2021-03-15 12:01:41 +01:00
depends=['state', 'start_date', 'end_date'])
arrival_date = fields.Date('Arrival date',
2021-03-15 11:49:41 +01:00
states={'readonly': Eval('state') != 'draft'},
depends=['state'])
lines = fields.One2Many('carrier.load.order.line', 'order', 'Lines',
2021-03-15 11:49:41 +01:00
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'])
2021-03-15 11:49:41 +01:00
incoterms = fields.One2Many('carrier.load.order.incoterm', 'order',
'Incoterms',
states={'readonly': ~Eval('state').in_(['draft', 'waiting']),
'invisible': ~Eval('party')},
depends=['state', 'party'])
2019-10-11 13:58:36 +02:00
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=[
2019-10-11 13:58:36 +02:00
If((Eval('state') != 'done') & Bool(Eval('sale_edit')),
2019-10-11 12:54:09 +02:00
[
('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'))
]
2019-10-11 13:58:36 +02:00
],
2019-10-11 12:54:09 +02:00
[])
],
states={
'invisible': ~Eval('party') | (Eval('type') != 'out'),
2019-10-11 13:58:36 +02:00
'readonly': (Eval('state') == 'done') |
(Eval('type') != 'out') | Bool(Eval('shipment')) |
Not(Bool(Eval('sale_edit')))
},
2019-10-11 13:58:36 +02:00
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)
2015-10-26 20:39:12 +01:00
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__()
2016-08-22 13:58:00 +02:00
t = cls.__table__()
cls._sql_constraints = [
2016-08-22 13:58:00 +02:00
('code_uk1', Unique(t, t.code),
'Code must be unique.')
]
cls._order = [
('start_date', 'DESC'),
('id', 'DESC'),
]
cls._transitions |= set((('draft', 'waiting'),
('waiting', 'draft'),
2016-04-02 11:28:02 +02:00
('draft', 'running'),
('waiting', 'running'),
2016-01-24 21:05:14 +01:00
('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({
2018-07-17 09:41:29 +02:00
'cancel': {
'invisible': Eval('state').in_(['cancel', 'done']),
'depends': ['state']},
'draft': {
'invisible': ~Eval('state').in_(['cancel', 'waiting']),
'icon': If(Eval('state') == 'cancel',
2019-02-26 11:03:17 +01:00
'tryton-undo', 'tryton-back'),
2018-07-17 09:41:29 +02:00
'depends': ['state']},
'wait': {
'invisible': ~Eval('state').in_(['draft', 'running']),
'icon': If(Eval('state') == 'draft',
2019-02-26 11:03:17 +01:00
'tryton-forward', 'tryton-back')},
2018-07-17 09:41:29 +02:00
'do': {
'invisible': Eval('state') != 'running',
'icon': 'tryton-ok',
'depends': ['state']},
2020-12-24 08:13:28 +01:00
'do_wizard': {
'invisible': Eval('state') != 'running',
'icon': 'tryton-ok',
'depends': ['state']
}
})
2015-10-20 19:27:58 +02:00
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))
2016-02-18 10:37:52 +01:00
@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'
2015-10-20 19:27:58 +02:00
@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
2015-10-26 20:39:12 +01:00
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)
2016-09-08 09:57:54 +02:00
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',
2018-02-19 09:07:54 +01:00
'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)
2015-10-15 17:15:27 +02:00
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
2020-12-24 08:13:28 +01:00
@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])
2015-10-20 19:27:58 +02:00
def create_sale(self):
2015-10-20 19:27:58 +02:00
pool = Pool()
Sale = pool.get('sale.sale')
if self.type != 'out':
return
if not self.party:
return
if self.sale:
2019-10-11 13:58:36 +02:00
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):
2015-10-23 18:55:29 +02:00
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)
2019-02-26 11:03:17 +01:00
_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',
2019-10-11 17:24:05 +02:00
(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))
2018-09-27 14:16:18 +02:00
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])
2015-10-20 19:27:58 +02:00
def _get_load_sale(self, Sale):
pool = Pool()
SaleIncoterm = pool.get('sale.incoterm')
2016-03-25 09:25:24 +01:00
_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
2015-10-20 19:27:58 +02:00
incoterms = [
SaleIncoterm(rule=incoterm.rule,
value=incoterm.value,
currency=incoterm.currency,
place=incoterm.place)
for incoterm in self.incoterms]
2019-09-19 12:19:03 +02:00
sale = Sale(
company=self.company,
currency=Sale.default_currency(),
warehouse=self.load.warehouse,
sale_date=_date,
incoterm_version=self.incoterm_version
)
2015-10-23 18:55:29 +02:00
sale.party = self.party
sale.on_change_party()
2015-10-20 19:27:58 +02:00
sale.incoterms = incoterms
sale.origin = self
2015-10-20 19:27:58 +02:00
return sale
def _get_shipment_out(self, sale):
2015-10-20 19:27:58 +02:00
pool = Pool()
Shipment = pool.get('stock.shipment.out')
ShipmentIncoterm = pool.get('stock.shipment.out.incoterm')
shipment = sale._get_shipment_sale(
2016-02-22 09:03:14 +01:00
Shipment, key=(('planned_date', self.end_date.date()),
2015-10-20 19:27:58 +02:00
('warehouse', self.load.warehouse.id),))
2017-10-24 09:41:04 +02:00
shipment.reference = sale.reference
2015-10-20 19:27:58 +02:00
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')
2017-07-28 18:55:55 +02:00
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 []
2015-10-20 19:27:58 +02:00
def _get_load_sale_line(self, sale, key, grouped_items):
2015-10-20 19:27:58 +02:00
pool = Pool()
Saleline = pool.get('sale.line')
Product = pool.get('product.product')
2015-10-20 19:27:58 +02:00
values = {
'sale': sale,
'quantity': self._get_load_sale_line_quantity(grouped_items)
2015-10-20 19:27:58 +02:00
}
dictkey = dict(key)
values.update(dictkey)
2015-10-20 19:27:58 +02:00
line = Saleline(**values)
product = Product(line.product)
if not product.salable:
self.raise_user_error('non_salable_product', product.rec_name)
2015-10-20 19:27:58 +02:00
line.on_change_product()
if 'unit_price' in values:
line.unit_price = values['unit_price']
2015-10-20 19:27:58 +02:00
line.from_location = self.load.warehouse_output
line.to_location = self.party.customer_location
2016-08-23 09:17:44 +02:00
line.shipping_date = line.on_change_with_shipping_date(None)
2015-10-20 19:27:58 +02:00
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
2015-10-20 19:27:58 +02:00
@classmethod
def _group_line_key(cls, items, item):
2015-10-20 19:27:58 +02:00
return (
2015-10-23 18:55:29 +02:00
('product', item.product.id),
('unit', item.product.default_uom.id))
2015-10-20 19:27:58 +02:00
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')})]
2015-10-15 17:15:27 +02:00
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)
2015-10-15 17:15:27 +02:00
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',
2016-04-05 12:10:40 +02:00
required=True, select=True, readonly=True,
2015-10-15 17:15:27 +02:00
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'])
2015-10-26 20:39:12 +01:00
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)
2019-02-26 11:03:17 +01:00
record_ids = list(map(int, records))
2019-02-26 11:03:17 +01:00
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'
2015-10-26 20:39:12 +01:00
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):
2017-10-06 14:27:07 +02:00
@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))
2017-10-06 14:27:07 +02:00
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))
2017-10-06 14:27:07 +02:00
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))
2017-10-06 14:27:07 +02:00
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):
2017-10-06 14:27:07 +02:00
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
2017-10-06 14:27:07 +02:00
@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')
2017-10-06 14:27:07 +02:00
report_context = super(RoadTransportNote, cls).get_context(
records, data)
2017-10-06 14:27:07 +02:00
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):
2019-11-21 13:53:52 +01:00
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))
2020-10-08 00:22:48 +02:00
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]}