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