trytond-carrier_load/load.py

2116 lines
74 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
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
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
2021-10-11 19:02:56 +02:00
from trytond.exceptions import UserError
from trytond.i18n import gettext
from decimal import Decimal
2019-10-01 12:51:38 +02:00
try:
import phonenumbers
from phonenumbers import PhoneNumberFormat, NumberParseException
except ImportError:
phonenumbers = None
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):
2022-10-24 13:56:59 +02:00
__slots__ = ()
edit_cmr_instructions = fields.Boolean('Edit CMR instructions',
2022-10-24 13:56:59 +02:00
states={'readonly': Eval('state') == 'cancelled'})
cmr_instructions = fields.Function(
fields.Text('CMR instructions', translate=True,
states={
2021-06-04 00:11:54 +02:00
'readonly': (Eval('state') == 'cancelled') | Not(
Bool(Eval('edit_cmr_instructions')))
2022-10-24 13:56:59 +02:00
}),
'on_change_with_cmr_instructions', setter='set_cmr_instructions')
cmr_instructions_store = fields.Text('CMR instructions', translate=True,
2022-10-24 13:56:59 +02:00
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'
2016-01-21 00:38:08 +01:00
_rec_name = 'code'
code = fields.Char('Code', required=True, select=True,
2022-10-24 13:56:59 +02:00
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')
},
2022-10-24 13:56:59 +02:00
select=True)
carrier = fields.Many2One('carrier', 'Carrier', select=True,
ondelete='RESTRICT',
states={
'readonly': Eval('state') != 'draft',
'required': Bool(Eval('purchasable'))
2022-10-24 13:56:59 +02:00
})
carrier_info = fields.Text('Carrier information',
states={
'readonly': Eval('state') != 'draft',
'invisible': Bool(Eval('purchasable')) | Bool(Eval('carrier'))
2022-10-24 13:56:59 +02:00
})
vehicle_number = fields.Char('Vehicle reg. number',
states={
'readonly': Eval('state') != 'draft',
'required': Eval('state').in_(['confirmed', 'done']) & Bool(
2022-10-24 13:56:59 +02:00
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(
2022-10-24 13:56:59 +02:00
Eval('trailer_required'))})
trailer_required = fields.Function(fields.Boolean('Trailer required'),
'get_number_required')
date = fields.Date('Effective date', required=True,
2022-10-24 13:56:59 +02:00
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')))
2022-10-24 13:56:59 +02:00
})
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('done', 'Done'),
2021-06-04 00:11:54 +02:00
('cancelled', 'Cancelled')], 'State',
readonly=True, required=True)
purchasable = fields.Boolean('Purchasable',
states={
'readonly': ((~Eval('state').in_(['draft', 'confirmed'])) | (
2022-10-24 13:56:59 +02:00
Bool(Eval('purchase'))))})
unit_price = fields.Numeric('Unit Price', digits=price_digits,
states={
'readonly': ((~Eval('state').in_(
['draft', 'confirmed', 'done'])) | (Bool(Eval('purchase')))),
2022-10-24 13:56:59 +02:00
'invisible': ~Eval('purchasable')})
currency = fields.Many2One('currency.currency', 'Currency',
states={
'readonly': ((~Eval('state').in_(['draft', 'confirmed'])) | (
Bool(Eval('purchase')))),
2022-10-24 13:56:59 +02:00
'invisible': ~Eval('purchasable')})
currency_digits = fields.Function(fields.Integer('Currency Digits'),
'on_change_with_currency_digits')
purchase = fields.Many2One('purchase.purchase', 'Purchase', readonly=True,
2022-10-24 13:56:59 +02:00
states={'invisible': ~Eval('purchasable')})
purchase_state = fields.Function(
fields.Selection([(None, '')], 'Purchase state',
2022-10-24 13:56:59 +02:00
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',
2022-10-24 13:56:59 +02:00
states={'readonly': Eval('state') != 'draft'})
driver_identifier = fields.Char('Driver identifier',
states={
'required': Bool(Eval('driver')),
2022-10-24 13:56:59 +02:00
'readonly': Eval('state') != 'draft'})
driver_phone = fields.Char('Driver Phone',
states={
2022-10-24 13:56:59 +02:00
'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__()
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),
2021-10-11 19:02:56 +02:00
'carrier_load.msg_carrier_load_code_uk1')
]
cls._order = [
('date', 'DESC'),
('id', 'DESC'),
]
cls._transitions |= set((('draft', 'confirmed'),
('confirmed', 'draft'),
('confirmed', 'done'),
('done', 'confirmed'),
2021-06-04 00:11:54 +02:00
('draft', 'cancelled'),
('cancelled', 'draft')))
cls._buttons.update({
2018-07-17 09:41:29 +02:00
'cancel': {
2021-06-04 00:11:54 +02:00
'invisible': Eval('state').in_(['cancelled', 'done']),
2018-07-17 09:41:29 +02:00
'depends': ['state']},
'draft': {
2021-06-04 00:11:54 +02:00
'invisible': ~Eval('state').in_(['cancelled', 'confirmed']),
2018-07-17 09:41:29 +02:00
'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)
table.not_null_action('carrier', action='remove')
table.not_null_action('carrier_vehicle', action='remove')
table.not_null_action('dock', action='remove')
2021-06-04 00:11:54 +02:00
# 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:
2021-06-04 00:11:54 +02:00
if (record.state not in ('cancelled', 'draft') and
not record.carrier and
not record.carrier_info):
2021-10-11 19:02:56 +02:00
raise UserError(gettext(
'carrier_load.msg_carrier_load_missing_carrier_info',
load=record.rec_name))
2019-10-01 12:51:38 +02:00
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()
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():
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
2021-10-11 19:02:56 +02:00
@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')
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)):
2021-10-11 19:02:56 +02:00
raise UserError(gettext(
'carrier_load.msg_carrier_load_invalid_phonenumber',
phone=self.driver_phone))
2019-10-01 12:51:38 +02:00
@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'))
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:
2021-06-04 00:11:54 +02:00
if record.state != 'cancelled':
2021-10-11 19:02:56 +02:00
raise UserError(gettext(
'carrier_load.msg_carrier_load_delete_cancel',
load=record.rec_name))
super(Load, cls).delete(records)
2016-09-08 09:57:54 +02:00
@classmethod
@ModelView.button
2021-06-04 00:11:54 +02:00
@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)
2016-09-08 09:57:54 +02:00
@classmethod
@ModelView.button
@Workflow.transition('draft')
def draft(cls, records):
Order = Pool().get('carrier.load.order')
2021-06-04 00:11:54 +02:00
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:
2021-10-11 19:02:56 +02:00
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):
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:
2021-10-11 19:02:56 +02:00
raise UserError(gettext(
'carrier_load.msg_carrier_load_purchase_price',
load=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:])]
@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:])
]
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',
2022-10-24 13:56:59 +02:00
states={'readonly': Eval('state') != 'draft'})
code = fields.Char('Code', required=True, select=True,
2022-10-24 13:56:59 +02:00
states={'readonly': Eval('code_readonly', True)})
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,
states={
'readonly':
(Eval('state') != 'draft')
| Eval('shipment')
| Eval('sale')
| Eval('lines', [])
},
2022-10-24 13:56:59 +02:00
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',
2022-10-24 13:56:59 +02:00
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')),
())],
2022-10-24 13:56:59 +02:00
states={'readonly': ~Eval('state').in_(['draft', 'waiting'])})
arrival_date = fields.Date('Arrival date',
2022-10-24 13:56:59 +02:00
states={'readonly': Eval('state') != 'draft'})
lines = fields.One2Many('carrier.load.order.line', 'order', 'Lines',
2022-10-24 13:56:59 +02:00
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'),
2022-10-24 13:56:59 +02:00
'invisible': Eval('type') == 'internal'})
2021-03-15 11:49:41 +01:00
incoterms = fields.One2Many('carrier.load.order.incoterm', 'order',
'Incoterms',
states={'readonly': ~Eval('state').in_(['draft', 'waiting']),
2022-10-24 13:56:59 +02:00
'invisible': ~Eval('party')})
2019-10-11 13:58:36 +02:00
sale_edit = fields.Boolean('Edit sale',
states={
2021-06-04 00:11:54 +02:00
'readonly': Eval('state').in_(['done', 'cancelled']) |
2019-10-11 13:58:36 +02:00
Bool(Eval('sale'))
2022-10-24 13:56:59 +02:00
})
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')))
2022-10-24 13:56:59 +02:00
})
shipment = fields.Reference('Shipment', selection='get_shipments',
readonly=True, select=True)
state = fields.Selection([
('draft', 'Draft'),
('waiting', 'Waiting'),
('running', 'Running'),
('done', 'Done'),
2021-06-04 00:11:54 +02:00
('cancelled', 'Cancelled')], '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)) |
2022-10-24 13:56:59 +02:00
(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__()
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),
2021-10-11 19:02:56 +02:00
'carrier_load.msg_carrier_load_order_code_uk1')
]
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'),
2021-06-04 00:11:54 +02:00
('draft', 'cancelled'),
('cancelled', 'draft')))
cls._buttons.update({
2018-07-17 09:41:29 +02:00
'cancel': {
2021-06-04 00:11:54 +02:00
'invisible': Eval('state').in_(['cancelled', 'done']),
2018-07-17 09:41:29 +02:00
'depends': ['state']},
'draft': {
2021-06-04 00:11:54 +02:00
'invisible': ~Eval('state').in_(['cancelled', 'waiting']),
'icon': If(Eval('state') == 'cancelled',
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'))
@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))
2021-06-04 00:11:54 +02:00
# Migration from 5.6: rename state cancel to cancelled
cursor.execute(*sql_table.update(
[sql_table.state], ['cancelled'],
where=sql_table.state == 'cancel'))
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 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()
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']
2021-10-11 19:02:56 +02:00
@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:
2021-06-04 00:11:54 +02:00
if record.state != 'cancelled':
2021-10-11 19:02:56 +02:00
raise UserError(gettext(
'carrier_load.msg_carrier_load_order_delete_cancel',
order=record.rec_name))
2015-10-15 17:15:27 +02:00
super(LoadOrder, cls).delete(records)
@classmethod
@ModelView.button
2021-06-04 00:11:54 +02:00
@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
2020-12-24 08:13:28 +01:00
@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):
2021-10-11 19:02:56 +02:00
raise UserError(gettext(
'carrier_load.'
'msg_carrier_load_order_missing_customer_location',
2021-10-11 19:02:56 +02:00
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
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
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)
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:
2021-10-11 19:02:56 +02:00
raise UserError(gettext(
'carrier_load.'
'msg_carrier_load_order_no_sale_line_found',
2021-10-11 19:02:56 +02:00
sale=self.sale.rec_name,
data='\n'.join([
2019-10-11 17:24:05 +02:00
' - %s: %s' % (key, value)
2021-10-11 19:02:56 +02:00
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
2015-10-20 19:27:58 +02:00
def _get_load_sale(self, Sale):
pool = Pool()
SaleIncoterm = pool.get('sale.incoterm')
if self.sale:
return self.sale
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(),
date_time_=self.end_date,
2017-07-28 18:55:55 +02:00
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:
2021-10-11 19:02:56 +02:00
raise UserError(gettext(
'carrier_load.msg_carrier_load_order_non_salable_product',
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')})]
@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'
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
2021-10-11 19:02:56 +02:00
@fields.depends('order', '_parent_order.incoterm_version')
2015-10-15 17:15:27 +02:00
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'),
2022-10-24 13:56:59 +02:00
'on_change_with_unit_digits')
quantity = fields.Float('Quantity', digits=(16, Eval('unit_digits', 2)))
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 _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:
2021-10-11 19:02:56 +02:00
raise UserError(gettext(
'carrier_load.msg_carrier_load_order_line_quantity_exceeded',
origin=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
2021-06-04 00:11:54 +02:00
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):
2022-10-24 13:56:59 +02:00
__slots__ = ()
2017-10-06 14:27:07 +02:00
@classmethod
2021-06-04 00:11:54 +02:00
def get_context(cls, records, header, data):
report_context = super(NoteMixin, cls).get_context(records, header, data)
2017-10-06 14:27:07 +02:00
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
2021-06-04 00:11:54 +02:00
def get_context(cls, records, header, data):
Configuration = Pool().get('carrier.configuration')
2021-10-11 19:02:56 +02:00
report_context = super().get_context(records, header, 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%'),
2021-06-04 00:11:54 +02:00
('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:
2021-10-11 19:02:56 +02:00
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
2021-06-04 00:11:54 +02:00
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', []))
2021-06-02 08:54:01 +02:00
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