3170 lines
115 KiB
Python
3170 lines
115 KiB
Python
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
# this repository contains the full copyright notices and license terms.
|
|
import datetime
|
|
from collections import defaultdict
|
|
from functools import partial
|
|
from itertools import groupby
|
|
|
|
from sql import Null
|
|
from sql.conditionals import Coalesce
|
|
from sql.functions import CharLength
|
|
|
|
from trytond.i18n import gettext, lazy_gettext
|
|
from trytond.model import (
|
|
ChatMixin, Index, ModelSQL, ModelView, Workflow, dualmethod, fields, sort)
|
|
from trytond.model.exceptions import AccessError
|
|
from trytond.modules.company import CompanyReport
|
|
from trytond.modules.company.model import employee_field, set_employee
|
|
from trytond.pool import Pool
|
|
from trytond.pyson import Bool, Eval, Id, If, TimeDelta
|
|
from trytond.transaction import Transaction
|
|
from trytond.wizard import Button, StateTransition, StateView, Wizard
|
|
|
|
from .exceptions import ShipmentCheckQuantityWarning
|
|
|
|
|
|
class ShipmentMixin:
|
|
__slots__ = ()
|
|
|
|
_rec_name = 'number'
|
|
number = fields.Char(
|
|
"Number", readonly=True,
|
|
help="The main identifier for the shipment.")
|
|
reference = fields.Char(
|
|
"Reference",
|
|
help="The external identifier for the shipment.")
|
|
planned_date = fields.Date(
|
|
lazy_gettext('stock.msg_shipment_planned_date'),
|
|
states={
|
|
'readonly': ~Eval('state').in_(['request', 'draft']),
|
|
},
|
|
help=lazy_gettext('stock.msg_shipment_planned_date_help'))
|
|
origin_planned_date = fields.Date(
|
|
lazy_gettext('stock.msg_shipment_origin_planned_date'),
|
|
readonly=True,
|
|
help=lazy_gettext('stock.msg_shipment_origin_planned_date_help'))
|
|
effective_date = fields.Date(
|
|
lazy_gettext('stock.msg_shipment_effective_date'),
|
|
states={
|
|
'readonly': Eval('state').in_(['cancelled', 'done']),
|
|
},
|
|
help=lazy_gettext('stock.msg_shipment_effective_date_help'))
|
|
delay = fields.Function(
|
|
fields.TimeDelta(
|
|
lazy_gettext('stock.msg_shipment_delay'),
|
|
states={
|
|
'invisible': (
|
|
~Eval('origin_planned_date')
|
|
& ~Eval('planned_date')),
|
|
}),
|
|
'get_delay')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
cls.number.search_unaccented = False
|
|
cls.reference.search_unaccented = False
|
|
super().__setup__()
|
|
t = cls.__table__()
|
|
cls._sql_indexes.update({
|
|
Index(t, (t.reference, Index.Similarity())),
|
|
})
|
|
cls._order = [
|
|
('effective_date', 'ASC NULLS LAST'),
|
|
('id', 'ASC'),
|
|
]
|
|
|
|
@classmethod
|
|
def order_number(cls, tables):
|
|
table, _ = tables[None]
|
|
return [
|
|
~((table.state == 'cancelled') & (table.number == Null)),
|
|
CharLength(table.number), table.number]
|
|
|
|
@classmethod
|
|
def order_effective_date(cls, tables):
|
|
table, _ = tables[None]
|
|
return [Coalesce(table.effective_date, table.planned_date)]
|
|
|
|
@classmethod
|
|
def set_number(cls, shipments):
|
|
'''
|
|
Fill the number field from sequence
|
|
'''
|
|
pool = Pool()
|
|
Config = pool.get('stock.configuration')
|
|
|
|
config = Config(1)
|
|
sequence_name = cls.__name__[len('stock.'):].replace('.', '_')
|
|
for company, c_shipments in groupby(
|
|
shipments, key=lambda s: s.company):
|
|
c_shipments = [s for s in c_shipments if not s.number]
|
|
if c_shipments:
|
|
sequence = config.get_multivalue(
|
|
sequence_name + '_sequence', company=company.id)
|
|
for shipment, number in zip(
|
|
c_shipments, sequence.get_many(len(c_shipments))):
|
|
shipment.number = number
|
|
cls.save(shipments)
|
|
|
|
def get_delay(self, name):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
planned_date = self.origin_planned_date or self.planned_date
|
|
if planned_date is not None:
|
|
if self.effective_date:
|
|
return self.effective_date - planned_date
|
|
elif self.planned_date:
|
|
return self.planned_date - planned_date
|
|
else:
|
|
with Transaction().set_context(company=self.company.id):
|
|
today = Date.today()
|
|
return today - planned_date
|
|
|
|
def get_rec_name(self, name):
|
|
items = []
|
|
if self.number:
|
|
items.append(self.number)
|
|
if self.reference:
|
|
items.append(f'[{self.reference}]')
|
|
if not items:
|
|
items.append(f'({self.id})')
|
|
return ' '.join(items)
|
|
|
|
@classmethod
|
|
def search_rec_name(cls, name, clause):
|
|
_, operator, value = clause
|
|
if operator.startswith('!') or operator.startswith('not '):
|
|
bool_op = 'AND'
|
|
else:
|
|
bool_op = 'OR'
|
|
domain = [bool_op,
|
|
('number', operator, value),
|
|
('reference', operator, value),
|
|
]
|
|
return domain
|
|
|
|
@classmethod
|
|
def view_attributes(cls):
|
|
return super().view_attributes() + [
|
|
('/tree', 'visual', If(Eval('state') == 'cancelled', 'muted', '')),
|
|
('/tree/field[@name="delay"]', 'visual',
|
|
If(Eval('delay', datetime.timedelta()) > TimeDelta(),
|
|
'danger', '')),
|
|
]
|
|
|
|
@classmethod
|
|
def copy(cls, shipments, default=None):
|
|
default = default.copy() if default is not None else {}
|
|
default.setdefault('number')
|
|
default.setdefault('reference')
|
|
return super().copy(shipments, default=default)
|
|
|
|
@classmethod
|
|
def on_modification(cls, mode, shipments, field_names=None):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
super().on_modification(mode, shipments, field_names=field_names)
|
|
if (mode in {'create', 'write'}
|
|
and (field_names is None or 'planned_date' in field_names)):
|
|
cls._set_move_planned_date(shipments)
|
|
elif mode == 'delete':
|
|
if hasattr(cls, 'moves'):
|
|
moves = [m for s in shipments for m in s.moves]
|
|
Move.delete(moves)
|
|
|
|
@classmethod
|
|
def check_modification(cls, mode, shipments, values=None, external=False):
|
|
super().check_modification(
|
|
mode, shipments, values=values, external=external)
|
|
if mode == 'delete':
|
|
for shipment in shipments:
|
|
if shipment.state not in {'cancelled', 'request', 'draft'}:
|
|
raise AccessError(gettext(
|
|
'stock.msg_shipment_delete_cancel',
|
|
shipment=shipment.rec_name))
|
|
|
|
@classmethod
|
|
def _set_move_planned_date(cls, shipments):
|
|
raise NotImplementedError
|
|
|
|
|
|
class ShipmentAssignMixin(ShipmentMixin):
|
|
__slots__ = ()
|
|
_assign_moves_field = None
|
|
|
|
partially_assigned = fields.Function(
|
|
fields.Boolean("Partially Assigned"),
|
|
'get_partially_assigned',
|
|
searcher='search_partially_assigned')
|
|
|
|
@classmethod
|
|
def assign_wizard(cls, shipments):
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def assign_moves(self):
|
|
return getattr(self, self._assign_moves_field)
|
|
|
|
@dualmethod
|
|
@ModelView.button
|
|
def assign_try(cls, shipments):
|
|
raise NotImplementedError
|
|
|
|
@dualmethod
|
|
def assign_reset(cls, shipments):
|
|
cls.wait(shipments)
|
|
|
|
@dualmethod
|
|
@ModelView.button
|
|
def assign_force(cls, shipments):
|
|
cls.assign(shipments)
|
|
|
|
@dualmethod
|
|
def assign_ignore(cls, shipments, moves=None):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
assign_moves = {
|
|
m for s in shipments for m in s.assign_moves
|
|
if m.assignation_required and m.state in {'staging', 'draft'}}
|
|
if moves is None:
|
|
moves = list(assign_moves)
|
|
else:
|
|
moves = [m for m in moves if m in assign_moves]
|
|
Move.write(moves, {
|
|
'quantity': 0,
|
|
})
|
|
to_assign = [
|
|
s for s in shipments
|
|
if all(
|
|
m.state not in {'staging', 'draft'}
|
|
for m in s.assign_moves if m.assignation_required)]
|
|
if to_assign:
|
|
cls.assign(to_assign)
|
|
|
|
@classmethod
|
|
def _get_assign_domain(cls):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
context = Transaction().context
|
|
return [
|
|
('company', '=', context.get('company')),
|
|
('state', '=', 'waiting'),
|
|
('planned_date', '<=', Date.today()),
|
|
]
|
|
|
|
@classmethod
|
|
def to_assign(cls):
|
|
return cls.search(cls._get_assign_domain())
|
|
|
|
@property
|
|
def assign_order_key(self):
|
|
return (self.planned_date, self.create_date)
|
|
|
|
def get_partially_assigned(self, name):
|
|
return (self.state != 'assigned'
|
|
and any(m.state == 'assigned' for m in self.assign_moves
|
|
if m.assignation_required))
|
|
|
|
@classmethod
|
|
def search_partially_assigned(cls, name, clause):
|
|
operators = {
|
|
'=': 'where',
|
|
'!=': 'not where',
|
|
}
|
|
reverse = {
|
|
'=': '!=',
|
|
'!=': '=',
|
|
}
|
|
if clause[1] in operators:
|
|
if not clause[2]:
|
|
operator = reverse[clause[1]]
|
|
else:
|
|
operator = clause[1]
|
|
return [
|
|
(cls._assign_moves_field, operators[operator], [
|
|
('state', '=', 'assigned'),
|
|
('assignation_required', '=', True),
|
|
]),
|
|
('state', '=', 'waiting'),
|
|
]
|
|
else:
|
|
return []
|
|
|
|
|
|
class ShipmentCheckQuantity:
|
|
"Check quantities per product between source and target moves"
|
|
__slots__ = ()
|
|
|
|
@property
|
|
def _check_quantity_source_moves(self):
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def _check_quantity_target_moves(self):
|
|
raise NotImplementedError
|
|
|
|
def check_quantity(self):
|
|
pool = Pool()
|
|
Warning = pool.get('res.user.warning')
|
|
Lang = pool.get('ir.lang')
|
|
lang = Lang.get()
|
|
|
|
source_qties = defaultdict(float)
|
|
for move in self._check_quantity_source_moves:
|
|
source_qties[move.product] += move.internal_quantity
|
|
|
|
target_qties = defaultdict(float)
|
|
for move in self._check_quantity_target_moves:
|
|
target_qties[move.product] += move.internal_quantity
|
|
|
|
diffs = {}
|
|
for product, incoming_qty in source_qties.items():
|
|
unit = product.default_uom
|
|
incoming_qty = unit.round(incoming_qty)
|
|
inventory_qty = unit.round(target_qties.pop(product, 0))
|
|
diff = inventory_qty - incoming_qty
|
|
if diff:
|
|
diffs[product] = diff
|
|
diffs.update((k, v) for k, v in target_qties.items() if v)
|
|
|
|
if diffs:
|
|
warning_name = Warning.format(
|
|
'check_quantity_product', [self])
|
|
if Warning.check(warning_name):
|
|
quantities = []
|
|
for product, quantity in diffs.items():
|
|
quantity = lang.format_number_symbol(
|
|
quantity, product.default_uom)
|
|
quantities.append(f"{product.rec_name}: {quantity}")
|
|
raise ShipmentCheckQuantityWarning(warning_name,
|
|
gettext(
|
|
'stock.msg_shipment_check_quantity',
|
|
shipment=self.rec_name,
|
|
quantities=', '.join(quantities)))
|
|
|
|
|
|
class ShipmentIn(
|
|
ShipmentCheckQuantity, ShipmentMixin, Workflow, ModelSQL, ModelView,
|
|
ChatMixin):
|
|
__name__ = 'stock.shipment.in'
|
|
|
|
company = fields.Many2One(
|
|
'company.company', "Company", required=True,
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
},
|
|
context={
|
|
'party_contact_mechanism_usage': 'delivery',
|
|
},
|
|
help="The company the shipment is associated with.")
|
|
supplier = fields.Many2One('party.party', 'Supplier',
|
|
states={
|
|
'readonly': (((Eval('state') != 'draft')
|
|
| Eval('incoming_moves', [0]))
|
|
& Eval('supplier')),
|
|
}, required=True,
|
|
context={
|
|
'company': Eval('company', -1),
|
|
'party_contact_mechanism_usage': 'delivery',
|
|
},
|
|
depends={'company'},
|
|
help="The party that supplied the stock.")
|
|
supplier_location = fields.Function(fields.Many2One('stock.location',
|
|
'Supplier Location'),
|
|
'on_change_with_supplier_location')
|
|
contact_address = fields.Many2One('party.address', 'Contact Address',
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
},
|
|
domain=[('party', '=', Eval('supplier', -1))],
|
|
help="The address at which the supplier can be contacted.")
|
|
warehouse = fields.Many2One('stock.location', "Warehouse",
|
|
required=True, domain=[('type', '=', 'warehouse')],
|
|
states={
|
|
'readonly': (Eval('state').in_(['cancelled', 'done'])
|
|
| Eval('incoming_moves', [0]) | Eval('inventory_moves', [0])),
|
|
},
|
|
help="Where the stock is received.")
|
|
warehouse_input = fields.Many2One(
|
|
'stock.location', "Warehouse Input", required=True,
|
|
domain=[
|
|
['OR',
|
|
('type', '=', 'storage'),
|
|
('id', '=', Eval('warehouse_storage', -1)),
|
|
],
|
|
If(Eval('state') == 'draft',
|
|
('parent', 'child_of', [Eval('warehouse', -1)]),
|
|
()),
|
|
],
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
})
|
|
warehouse_storage = fields.Many2One(
|
|
'stock.location', "Warehouse Storage", required=True,
|
|
domain=[
|
|
('type', 'in', ['storage', 'view']),
|
|
If(Eval('state') == 'draft',
|
|
('parent', 'child_of', [Eval('warehouse', -1)]),
|
|
()),
|
|
],
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
})
|
|
incoming_moves = fields.Function(fields.One2Many('stock.move', 'shipment',
|
|
'Incoming Moves',
|
|
add_remove=[
|
|
('shipment', '=', None),
|
|
('state', '=', 'draft'),
|
|
If(Eval('warehouse_input') == Eval('warehouse_storage'),
|
|
('to_location', 'child_of',
|
|
[Eval('warehouse_input', -1)], 'parent'),
|
|
('to_location', '=', Eval('warehouse_input'))),
|
|
],
|
|
order=[
|
|
('product', 'ASC'),
|
|
('id', 'ASC'),
|
|
],
|
|
search_order=[
|
|
('planned_date', 'ASC NULLS LAST'),
|
|
('id', 'ASC')
|
|
],
|
|
domain=[
|
|
If(Eval('state') == 'draft',
|
|
('from_location', '=', Eval('supplier_location')),
|
|
()),
|
|
If(Eval('warehouse_input') == Eval('warehouse_storage'),
|
|
('to_location', 'child_of',
|
|
[Eval('warehouse_input', -1)], 'parent'),
|
|
('to_location', '=', Eval('warehouse_input'))),
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
states={
|
|
'readonly': (
|
|
(Eval('state') != 'draft')
|
|
| ~Eval('warehouse') | ~Eval('supplier')),
|
|
},
|
|
help="The moves that bring the stock into the warehouse."),
|
|
'get_incoming_moves', setter='set_incoming_moves')
|
|
inventory_moves = fields.Function(fields.One2Many('stock.move', 'shipment',
|
|
'Inventory Moves',
|
|
domain=[
|
|
('from_location', '=', Eval('warehouse_input', -1)),
|
|
If(~Eval('state').in_(['done', 'cancelled']),
|
|
['OR',
|
|
('to_location', 'child_of',
|
|
[Eval('warehouse_storage', -1)], 'parent'),
|
|
('to_location.waste_warehouses', '=',
|
|
Eval('warehouse', -1)),
|
|
],
|
|
[],),
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
order=[
|
|
('to_location', 'ASC'),
|
|
('product', 'ASC'),
|
|
('id', 'ASC'),
|
|
],
|
|
states={
|
|
'readonly': Eval('state').in_(['draft', 'done', 'cancelled']),
|
|
'invisible': (
|
|
Eval('warehouse_input') == Eval('warehouse_storage')),
|
|
},
|
|
help="The moves that put the stock away into the storage area."),
|
|
'get_inventory_moves', setter='set_inventory_moves')
|
|
moves = fields.One2Many(
|
|
'stock.move', 'shipment', "Moves",
|
|
domain=[('company', '=', Eval('company', -1))],
|
|
states={
|
|
'readonly': True,
|
|
})
|
|
origins = fields.Function(fields.Char('Origins'), 'get_origins')
|
|
received_by = employee_field("Received By")
|
|
done_by = employee_field("Done By")
|
|
state = fields.Selection([
|
|
('draft', 'Draft'),
|
|
('received', 'Received'),
|
|
('done', 'Done'),
|
|
('cancelled', 'Cancelled'),
|
|
], "State", readonly=True, sort=False,
|
|
help="The current state of the shipment.")
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
t = cls.__table__()
|
|
cls._sql_indexes.update({
|
|
Index(
|
|
t,
|
|
(t.state, Index.Equality(cardinality='low')),
|
|
where=t.state.in_(['draft', 'received'])),
|
|
})
|
|
cls._order = [
|
|
('id', 'DESC'),
|
|
]
|
|
cls._transitions |= set((
|
|
('draft', 'received'),
|
|
('received', 'done'),
|
|
('draft', 'cancelled'),
|
|
('received', 'cancelled'),
|
|
('cancelled', 'draft'),
|
|
('done', 'cancelled'),
|
|
))
|
|
cls._buttons.update({
|
|
'cancel': {
|
|
'invisible': Eval('state').in_(['cancelled', 'done']),
|
|
'depends': ['state'],
|
|
},
|
|
'draft': {
|
|
'invisible': Eval('state') != 'cancelled',
|
|
'depends': ['state'],
|
|
},
|
|
'receive': {
|
|
'invisible': Eval('state') != 'draft',
|
|
'depends': ['state'],
|
|
},
|
|
'do': {
|
|
'invisible': Eval('state') != 'received',
|
|
'depends': ['state'],
|
|
},
|
|
})
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
pool = Pool()
|
|
Location = pool.get('stock.location')
|
|
cursor = Transaction().connection.cursor()
|
|
sql_table = cls.__table__()
|
|
location = Location.__table__()
|
|
|
|
super().__register__(module_name)
|
|
|
|
# Migration from 6.6: fill warehouse locations
|
|
cursor.execute(*sql_table.update(
|
|
[sql_table.warehouse_input],
|
|
location.select(
|
|
location.input_location,
|
|
where=location.id == sql_table.warehouse),
|
|
where=sql_table.warehouse_input == Null))
|
|
cursor.execute(*sql_table.update(
|
|
[sql_table.warehouse_storage],
|
|
location.select(
|
|
location.storage_location,
|
|
where=location.id == sql_table.warehouse),
|
|
where=sql_table.warehouse_storage == Null))
|
|
|
|
@classmethod
|
|
def order_effective_date(cls, tables):
|
|
table, _ = tables[None]
|
|
return [Coalesce(table.effective_date, table.planned_date)]
|
|
|
|
@staticmethod
|
|
def default_state():
|
|
return 'draft'
|
|
|
|
@classmethod
|
|
def default_warehouse(cls):
|
|
Location = Pool().get('stock.location')
|
|
return Location.get_default_warehouse()
|
|
|
|
@fields.depends('warehouse')
|
|
def on_change_warehouse(self):
|
|
if self.warehouse:
|
|
self.warehouse_input = self.warehouse.input_location
|
|
self.warehouse_storage = self.warehouse.storage_location
|
|
else:
|
|
self.warehouse_input = self.warehouse_storage = None
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@fields.depends('supplier')
|
|
def on_change_supplier(self):
|
|
self.contact_address = None
|
|
if self.supplier:
|
|
self.contact_address = self.supplier.address_get()
|
|
|
|
@fields.depends('supplier')
|
|
def on_change_with_supplier_location(self, name=None):
|
|
return self.supplier.supplier_location if self.supplier else None
|
|
|
|
def get_incoming_moves(self, name):
|
|
if self.warehouse_input == self.warehouse_storage:
|
|
moves = self.moves
|
|
else:
|
|
moves = filter(
|
|
lambda m: m.to_location == self.warehouse_input, self.moves)
|
|
return sort(moves, self.__class__.incoming_moves.order)
|
|
|
|
@classmethod
|
|
def set_incoming_moves(cls, shipments, name, value):
|
|
if not value:
|
|
return
|
|
cls.write(shipments, {
|
|
'moves': value,
|
|
})
|
|
|
|
def get_inventory_moves(self, name):
|
|
moves = filter(
|
|
lambda m: m.from_location == self.warehouse_input, self.moves)
|
|
return sort(moves, self.__class__.inventory_moves.order)
|
|
|
|
@classmethod
|
|
def set_inventory_moves(cls, shipments, name, value):
|
|
if not value:
|
|
return
|
|
cls.write(shipments, {
|
|
'moves': value,
|
|
})
|
|
|
|
@property
|
|
def _move_planned_date(self):
|
|
'''
|
|
Return the planned date for incoming moves and inventory_moves
|
|
'''
|
|
return self.planned_date, self.planned_date
|
|
|
|
@classmethod
|
|
def _set_move_planned_date(cls, shipments):
|
|
'''
|
|
Set planned date of moves for the shipments
|
|
'''
|
|
Move = Pool().get('stock.move')
|
|
to_write = []
|
|
for shipment in shipments:
|
|
dates = shipment._move_planned_date
|
|
incoming_date, inventory_date = dates
|
|
if incoming_date:
|
|
incoming_moves_to_write = [
|
|
m for m in shipment.incoming_moves
|
|
if (m.state not in {'done', 'cancelled'})]
|
|
if incoming_moves_to_write:
|
|
to_write.extend(
|
|
(incoming_moves_to_write, {
|
|
'planned_date': incoming_date,
|
|
}))
|
|
if inventory_date:
|
|
inventory_moves_to_write = [
|
|
m for m in shipment.inventory_moves
|
|
if (m.state not in {'done', 'cancelled'})]
|
|
if inventory_moves_to_write:
|
|
to_write.extend(
|
|
(inventory_moves_to_write, {
|
|
'planned_date': inventory_date,
|
|
}))
|
|
if to_write:
|
|
Move.write(*to_write)
|
|
|
|
def get_origins(self, name):
|
|
return ', '.join(set(filter(None,
|
|
(m.origin_name for m in self.incoming_moves))))
|
|
|
|
def chat_language(self, audience='internal'):
|
|
language = super().chat_language(audience=audience)
|
|
if audience == 'public':
|
|
language = self.supplier.lang.code if self.supplier.lang else None
|
|
return language
|
|
|
|
@classmethod
|
|
def preprocess_values(cls, mode, values):
|
|
pool = Pool()
|
|
Configuration = pool.get('stock.configuration')
|
|
values = super().preprocess_values(mode, values)
|
|
if mode == 'create' and not values.get('number'):
|
|
company_id = values.get('company', cls.default_company())
|
|
if company_id is not None:
|
|
configuration = Configuration(1)
|
|
if sequence := configuration.get_multivalue(
|
|
'shipment_in_sequence', company=company_id):
|
|
values['number'] = sequence.get()
|
|
return values
|
|
|
|
@classmethod
|
|
def copy(cls, shipments, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
default.setdefault('moves.origin', None)
|
|
default.setdefault('received_by', None)
|
|
default.setdefault('done_by', None)
|
|
return super().copy(shipments, default=default)
|
|
|
|
def _get_inventory_move(self, incoming_move):
|
|
'Return inventory move for the incoming move'
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Date = pool.get('ir.date')
|
|
if incoming_move.quantity <= 0.0:
|
|
return None
|
|
with Transaction().set_context(company=self.company.id):
|
|
today = Date.today()
|
|
move = Move()
|
|
move.product = incoming_move.product
|
|
move.unit = incoming_move.unit
|
|
move.quantity = incoming_move.quantity
|
|
move.from_location = incoming_move.to_location
|
|
move.to_location = self.warehouse_storage
|
|
move.state = (
|
|
'staging' if incoming_move.state == 'staging' else 'draft')
|
|
move.planned_date = max(
|
|
filter(None, [self._move_planned_date[1], today]))
|
|
move.company = incoming_move.company
|
|
move.origin = incoming_move
|
|
return move
|
|
|
|
@classmethod
|
|
def create_inventory_moves(cls, shipments):
|
|
for shipment in shipments:
|
|
if shipment.warehouse_storage == shipment.warehouse_input:
|
|
# Do not create inventory moves
|
|
continue
|
|
# Use moves instead of inventory_moves because save reset before
|
|
# adding new records and as set_inventory_moves is just a proxy to
|
|
# moves, it will reset also the incoming_moves
|
|
moves = list(shipment.moves)
|
|
for incoming_move in shipment.incoming_moves:
|
|
move = shipment._get_inventory_move(incoming_move)
|
|
if move:
|
|
moves.append(move)
|
|
shipment.moves = moves
|
|
cls.save(shipments)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('cancelled')
|
|
def cancel(cls, shipments):
|
|
Move = Pool().get('stock.move')
|
|
Move.cancel([m for s in shipments
|
|
for m in s.incoming_moves + s.inventory_moves])
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('draft')
|
|
def draft(cls, shipments):
|
|
Move = Pool().get('stock.move')
|
|
Move.draft([m for s in shipments for m in s.incoming_moves
|
|
if m.state != 'staging'])
|
|
Move.delete([m for s in shipments for m in s.inventory_moves
|
|
if m.state in {'staging', 'draft', 'cancelled'}])
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('received')
|
|
@set_employee('received_by')
|
|
def receive(cls, shipments):
|
|
Move = Pool().get('stock.move')
|
|
Move.do([m for s in shipments for m in s.incoming_moves])
|
|
Move.delete([m for s in shipments for m in s.inventory_moves
|
|
if m.state in ('draft', 'cancelled')])
|
|
cls.create_inventory_moves(shipments)
|
|
# Set received state to allow done transition
|
|
cls.write(shipments, {'state': 'received'})
|
|
to_do = [s for s in shipments
|
|
if s.warehouse_storage == s.warehouse_input]
|
|
if to_do:
|
|
cls.do(to_do)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('done')
|
|
@set_employee('done_by')
|
|
def do(cls, shipments):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Date = pool.get('ir.date')
|
|
inventory_moves = []
|
|
for shipment in shipments:
|
|
if shipment.warehouse_storage != shipment.warehouse_input:
|
|
shipment.check_quantity()
|
|
inventory_moves.extend(shipment.inventory_moves)
|
|
Move.do(inventory_moves)
|
|
for company, c_shipments in groupby(
|
|
shipments, key=lambda s: s.company):
|
|
with Transaction().set_context(company=company.id):
|
|
today = Date.today()
|
|
cls.write([s for s in c_shipments if not s.effective_date], {
|
|
'effective_date': today,
|
|
})
|
|
|
|
@property
|
|
def _check_quantity_source_moves(self):
|
|
return self.incoming_moves
|
|
|
|
@property
|
|
def _check_quantity_target_moves(self):
|
|
return self.inventory_moves
|
|
|
|
|
|
class ShipmentInReturn(
|
|
ShipmentAssignMixin, Workflow, ModelSQL, ModelView, ChatMixin):
|
|
__name__ = 'stock.shipment.in.return'
|
|
_assign_moves_field = 'moves'
|
|
|
|
company = fields.Many2One(
|
|
'company.company', "Company", required=True,
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
},
|
|
context={
|
|
'party_contact_mechanism_usage': 'delivery',
|
|
},
|
|
help="The company the shipment is associated with.")
|
|
supplier = fields.Many2One('party.party', 'Supplier',
|
|
states={
|
|
'readonly': (((Eval('state') != 'draft')
|
|
| Eval('moves', [0]))
|
|
& Eval('supplier', 0)),
|
|
}, required=True,
|
|
context={
|
|
'company': Eval('company', -1),
|
|
'party_contact_mechanism_usage': 'delivery',
|
|
},
|
|
depends={'company'},
|
|
help="The party that supplied the stock.")
|
|
delivery_address = fields.Many2One('party.address', 'Delivery Address',
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
},
|
|
domain=['OR',
|
|
('party', '=', Eval('supplier', -1)),
|
|
('warehouses', 'where', [
|
|
('id', '=', Eval('warehouse', -1)),
|
|
If(Eval('state') == 'draft',
|
|
('allow_pickup', '=', True),
|
|
()),
|
|
]),
|
|
],
|
|
help="Where the stock is sent to.")
|
|
from_location = fields.Many2One('stock.location', "From Location",
|
|
required=True, states={
|
|
'readonly': (Eval('state') != 'draft') | Eval('moves', [0]),
|
|
}, domain=[('type', 'in', ['storage', 'view'])],
|
|
help="Where the stock is moved from.")
|
|
to_location = fields.Many2One('stock.location', "To Location",
|
|
required=True, states={
|
|
'readonly': (Eval('state') != 'draft') | Eval('moves', [0]),
|
|
}, domain=[('type', '=', 'supplier')],
|
|
help="Where the stock is moved to.")
|
|
warehouse = fields.Function(
|
|
fields.Many2One('stock.location', "Warehouse"),
|
|
'on_change_with_warehouse')
|
|
moves = fields.One2Many('stock.move', 'shipment', 'Moves',
|
|
states={
|
|
'readonly': (((Eval('state') != 'draft') | ~Eval('from_location'))
|
|
& Eval('to_location')),
|
|
},
|
|
domain=[
|
|
If(Eval('state') == 'draft', [
|
|
('from_location', '=', Eval('from_location')),
|
|
('to_location', '=', Eval('to_location')),
|
|
],
|
|
If(~Eval('state').in_(['done', 'cancelled']), [
|
|
('from_location', 'child_of',
|
|
[Eval('from_location', -1)], 'parent'),
|
|
('to_location', 'child_of',
|
|
[Eval('to_location', -1)], 'parent'),
|
|
],
|
|
[])),
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
order=[
|
|
('from_location', 'ASC'),
|
|
('product', 'ASC'),
|
|
('id', 'ASC'),
|
|
],
|
|
help="The moves that return the stock to the supplier.")
|
|
origins = fields.Function(fields.Char('Origins'), 'get_origins')
|
|
assigned_by = employee_field("Assigned By")
|
|
done_by = employee_field("Done By")
|
|
state = fields.Selection([
|
|
('draft', 'Draft'),
|
|
('waiting', 'Waiting'),
|
|
('assigned', 'Assigned'),
|
|
('done', 'Done'),
|
|
('cancelled', 'Cancelled'),
|
|
], 'State', readonly=True, sort=False,
|
|
help="The current state of the shipment.")
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
t = cls.__table__()
|
|
cls._sql_indexes.update({
|
|
Index(
|
|
t,
|
|
(t.state, Index.Equality(cardinality='low')),
|
|
where=t.state.in_(['draft', 'waiting', 'assigned'])),
|
|
})
|
|
cls._order = [
|
|
('effective_date', 'ASC NULLS LAST'),
|
|
('id', 'ASC'),
|
|
]
|
|
cls._transitions |= set((
|
|
('draft', 'waiting'),
|
|
('waiting', 'assigned'),
|
|
('waiting', 'draft'),
|
|
('assigned', 'done'),
|
|
('assigned', 'waiting'),
|
|
('draft', 'cancelled'),
|
|
('waiting', 'cancelled'),
|
|
('assigned', 'cancelled'),
|
|
('cancelled', 'draft'),
|
|
('done', 'cancelled'),
|
|
))
|
|
cls._buttons.update({
|
|
'cancel': {
|
|
'invisible': Eval('state').in_(['cancelled', 'done']),
|
|
'depends': ['state'],
|
|
},
|
|
'draft': {
|
|
'invisible': ~Eval('state').in_(['waiting', 'cancelled']),
|
|
'icon': If(Eval('state') == 'cancelled',
|
|
'tryton-undo',
|
|
If(Eval('state') == 'waiting',
|
|
'tryton-back',
|
|
'tryton-forward')),
|
|
'depends': ['state'],
|
|
},
|
|
'wait': {
|
|
'invisible': ~Eval('state').in_(['assigned', 'draft']),
|
|
'icon': If(Eval('state') == 'assigned',
|
|
'tryton-back', 'tryton-forward'),
|
|
'depends': ['state'],
|
|
},
|
|
'do': {
|
|
'invisible': Eval('state') != 'assigned',
|
|
'depends': ['state'],
|
|
},
|
|
'assign_wizard': {
|
|
'invisible': Eval('state') != 'waiting',
|
|
'depends': ['state'],
|
|
},
|
|
'assign_try': {},
|
|
'assign_force': {},
|
|
})
|
|
|
|
@staticmethod
|
|
def default_state():
|
|
return 'draft'
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@fields.depends('supplier')
|
|
def on_change_supplier(self):
|
|
if self.supplier:
|
|
self.delivery_address = self.supplier.address_get('delivery')
|
|
self.to_location = self.supplier.supplier_location
|
|
|
|
@fields.depends('from_location')
|
|
def on_change_with_warehouse(self, name=None):
|
|
return self.from_location.warehouse if self.from_location else None
|
|
|
|
@property
|
|
def _move_planned_date(self):
|
|
'''
|
|
Return the planned date for the moves
|
|
'''
|
|
return self.planned_date
|
|
|
|
@classmethod
|
|
def _set_move_planned_date(cls, shipments):
|
|
'''
|
|
Set planned date of moves for the shipments
|
|
'''
|
|
Move = Pool().get('stock.move')
|
|
to_write = []
|
|
for shipment in shipments:
|
|
moves = [m for m in shipment.moves
|
|
if (m.state not in {'done', 'cancelled'}
|
|
and m.planned_date != shipment._move_planned_date)]
|
|
if moves:
|
|
to_write.extend((moves, {
|
|
'planned_date': shipment._move_planned_date,
|
|
}))
|
|
if to_write:
|
|
Move.write(*to_write)
|
|
|
|
def get_origins(self, name):
|
|
return ', '.join(set(filter(None,
|
|
(m.origin_name for m in self.moves))))
|
|
|
|
def chat_language(self, audience='internal'):
|
|
language = super().chat_language(audience=audience)
|
|
if audience == 'public':
|
|
language = self.supplier.lang.code if self.supplier.lang else None
|
|
return language
|
|
|
|
@classmethod
|
|
def copy(cls, shipments, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
default.setdefault('moves.origin', None)
|
|
default.setdefault('assigned_by', None)
|
|
default.setdefault('done_by', None)
|
|
return super().copy(shipments, default=default)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('draft')
|
|
def draft(cls, shipments):
|
|
Move = Pool().get('stock.move')
|
|
Move.draft([m for s in shipments for m in s.moves
|
|
if m.state != 'staging'])
|
|
for shipment in shipments:
|
|
Move.write([m for m in shipment.moves
|
|
if m.state != 'done'], {
|
|
'from_location': shipment.from_location.id,
|
|
'to_location': shipment.to_location.id,
|
|
'planned_date': shipment.planned_date,
|
|
})
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('waiting')
|
|
def wait(cls, shipments, moves=None):
|
|
"""
|
|
If moves is set, only this subset is set to draft.
|
|
"""
|
|
Move = Pool().get('stock.move')
|
|
if moves is None:
|
|
moves = sum((s.moves for s in shipments), ())
|
|
else:
|
|
assert all(m.shipment in shipments for m in moves)
|
|
Move.draft(moves)
|
|
cls.set_number(shipments)
|
|
cls._set_move_planned_date(shipments)
|
|
|
|
@classmethod
|
|
@Workflow.transition('assigned')
|
|
@set_employee('assigned_by')
|
|
def assign(cls, shipments):
|
|
Move = Pool().get('stock.move')
|
|
Move.assign([m for s in shipments for m in s.assign_moves])
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('done')
|
|
@set_employee('done_by')
|
|
def do(cls, shipments):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Date = pool.get('ir.date')
|
|
|
|
Move.do([m for s in shipments for m in s.moves])
|
|
for company, c_shipments in groupby(
|
|
shipments, key=lambda s: s.company):
|
|
with Transaction().set_context(company=company.id):
|
|
today = Date.today()
|
|
cls.write([s for s in c_shipments if not s.effective_date], {
|
|
'effective_date': today,
|
|
})
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('cancelled')
|
|
def cancel(cls, shipments):
|
|
Move = Pool().get('stock.move')
|
|
Move.cancel([m for s in shipments for m in s.moves])
|
|
|
|
@classmethod
|
|
@ModelView.button_action('stock.wizard_shipment_in_return_assign')
|
|
def assign_wizard(cls, shipments):
|
|
pass
|
|
|
|
@dualmethod
|
|
@ModelView.button
|
|
def assign_try(cls, shipments, with_childs=None):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
shipments = [s for s in shipments if s.state == 'waiting']
|
|
to_assign = defaultdict(list)
|
|
for shipment in shipments:
|
|
location_type = shipment.from_location.type
|
|
for move in shipment.assign_moves:
|
|
if move.assignation_required:
|
|
to_assign[location_type].append(move)
|
|
success = True
|
|
for location_type, moves in to_assign.items():
|
|
if with_childs is None:
|
|
_with_childs = location_type == 'view'
|
|
elif not with_childs and location_type == 'view':
|
|
_with_childs = True
|
|
else:
|
|
_with_childs = with_childs
|
|
if not Move.assign_try(moves, with_childs=_with_childs):
|
|
success = False
|
|
if success:
|
|
cls.assign(shipments)
|
|
else:
|
|
to_assign = []
|
|
for shipment in shipments:
|
|
if any(
|
|
m.state in {'staging', 'draft'}
|
|
for m in shipment.assign_moves
|
|
if m.assignation_required):
|
|
continue
|
|
to_assign.append(shipment)
|
|
if to_assign:
|
|
cls.assign(to_assign)
|
|
|
|
@classmethod
|
|
def _get_reschedule_domain(cls, date):
|
|
return [
|
|
('state', 'in', ['waiting', 'assigned']),
|
|
('planned_date', '<', date),
|
|
]
|
|
|
|
@classmethod
|
|
def reschedule(cls, date=None):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
if date is None:
|
|
date = Date.today()
|
|
shipments = cls.search(cls._get_reschedule_domain(date))
|
|
cls.write(shipments, {'planned_date': date})
|
|
|
|
|
|
class ShipmentOut(
|
|
ShipmentCheckQuantity, ShipmentAssignMixin, Workflow, ModelSQL,
|
|
ModelView, ChatMixin):
|
|
__name__ = 'stock.shipment.out'
|
|
_assign_moves_field = 'moves'
|
|
company = fields.Many2One(
|
|
'company.company', "Company", required=True,
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
},
|
|
context={
|
|
'party_contact_mechanism_usage': 'delivery',
|
|
},
|
|
help="The company the shipment is associated with.")
|
|
customer = fields.Many2One('party.party', 'Customer', required=True,
|
|
states={
|
|
'readonly': ((Eval('state') != 'draft')
|
|
| Eval('outgoing_moves', [0])),
|
|
},
|
|
context={
|
|
'company': Eval('company', -1),
|
|
'party_contact_mechanism_usage': 'delivery',
|
|
},
|
|
depends={'company'},
|
|
help="The party that purchased the stock.")
|
|
customer_location = fields.Function(fields.Many2One('stock.location',
|
|
'Customer Location'), 'on_change_with_customer_location')
|
|
delivery_address = fields.Many2One('party.address',
|
|
'Delivery Address', required=True,
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
},
|
|
domain=['OR',
|
|
('party', '=', Eval('customer', -1)),
|
|
('warehouses', 'where', [
|
|
('id', '=', Eval('warehouse', -1)),
|
|
If(Eval('state') == 'draft',
|
|
('allow_pickup', '=', True),
|
|
()),
|
|
]),
|
|
],
|
|
help="Where the stock is sent to.")
|
|
warehouse = fields.Many2One('stock.location', "Warehouse", required=True,
|
|
states={
|
|
'readonly': ((Eval('state') != 'draft')
|
|
| Eval('outgoing_moves', [0]) | Eval('inventory_moves', [0])),
|
|
}, domain=[('type', '=', 'warehouse')],
|
|
help="Where the stock is sent from.")
|
|
warehouse_storage = fields.Many2One(
|
|
'stock.location', "Warehouse Storage", required=True,
|
|
domain=[
|
|
('type', 'in', ['storage', 'view']),
|
|
If(Eval('state') == 'draft',
|
|
('parent', 'child_of', [Eval('warehouse', -1)]),
|
|
()),
|
|
],
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
})
|
|
warehouse_output = fields.Many2One(
|
|
'stock.location', "Warehouse Output", required=True,
|
|
domain=[
|
|
['OR',
|
|
('type', '=', 'storage'),
|
|
('id', '=', Eval('warehouse_output', -1)),
|
|
],
|
|
If(Eval('state') == 'draft',
|
|
('parent', 'child_of', [Eval('warehouse', -1)]),
|
|
()),
|
|
],
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
})
|
|
outgoing_moves = fields.Function(fields.One2Many('stock.move', 'shipment',
|
|
'Outgoing Moves',
|
|
domain=[
|
|
If(Eval('warehouse_output') == Eval('warehouse_storage'),
|
|
('from_location', 'child_of',
|
|
[Eval('warehouse_output', -1)], 'parent'),
|
|
('from_location', '=', Eval('warehouse_output', -1))),
|
|
If(~Eval('state').in_(['done', 'cancelled']),
|
|
('to_location', '=', Eval('customer_location', -1)),
|
|
()),
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
order=[
|
|
('product', 'ASC'),
|
|
('id', 'ASC'),
|
|
],
|
|
states={
|
|
'readonly': (Eval('state').in_(
|
|
If(Eval('warehouse_storage')
|
|
== Eval('warehouse_output'),
|
|
['done', 'cancelled'],
|
|
['waiting', 'packed', 'done', 'cancelled'],
|
|
))
|
|
| ~Eval('warehouse') | ~Eval('customer')),
|
|
},
|
|
help="The moves that send the stock to the customer."),
|
|
'get_outgoing_moves', setter='set_outgoing_moves')
|
|
inventory_moves = fields.Function(fields.One2Many('stock.move', 'shipment',
|
|
'Inventory Moves',
|
|
domain=[
|
|
If(Eval('state').in_(['waiting']),
|
|
('from_location', 'child_of',
|
|
[Eval('warehouse_storage', -1)], 'parent'),
|
|
()),
|
|
('to_location', '=', Eval('warehouse_output', -1)),
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
order=[
|
|
('from_location', 'ASC'),
|
|
('product', 'ASC'),
|
|
('id', 'ASC'),
|
|
],
|
|
states={
|
|
'readonly': Eval('state').in_(
|
|
['draft', 'assigned', 'picked', 'packed', 'done',
|
|
'cancelled']),
|
|
'invisible': (
|
|
Eval('warehouse_storage') == Eval('warehouse_output')),
|
|
},
|
|
help="The moves that pick the stock from the storage area."),
|
|
'get_inventory_moves', setter='set_inventory_moves')
|
|
moves = fields.One2Many(
|
|
'stock.move', 'shipment', "Moves",
|
|
domain=[('company', '=', Eval('company', -1))],
|
|
states={
|
|
'readonly': True,
|
|
})
|
|
origins = fields.Function(fields.Char('Origins'), 'get_origins')
|
|
picked_by = employee_field("Picked By")
|
|
packed_by = employee_field("Packed By")
|
|
shipped_by = employee_field("Shipped By")
|
|
done_by = employee_field("Done By")
|
|
state = fields.Selection([
|
|
('draft', 'Draft'),
|
|
('waiting', 'Waiting'),
|
|
('assigned', 'Assigned'),
|
|
('picked', 'Picked'),
|
|
('packed', 'Packed'),
|
|
('shipped', "Shipped"),
|
|
('done', 'Done'),
|
|
('cancelled', 'Cancelled'),
|
|
], "State", readonly=True, sort=False,
|
|
help="The current state of the shipment.")
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
t = cls.__table__()
|
|
cls._sql_indexes.update({
|
|
Index(
|
|
t,
|
|
(t.state, Index.Equality(cardinality='low')),
|
|
where=t.state.in_([
|
|
'draft', 'waiting', 'assigned',
|
|
'picked', 'packed', 'shipped'])),
|
|
})
|
|
cls._transitions |= set((
|
|
('draft', 'waiting'),
|
|
('waiting', 'assigned'),
|
|
('waiting', 'picked'),
|
|
('assigned', 'picked'),
|
|
('assigned', 'packed'),
|
|
('picked', 'packed'),
|
|
('packed', 'shipped'),
|
|
('packed', 'done'),
|
|
('packed', 'waiting'),
|
|
('packed', 'picked'),
|
|
('shipped', 'done'),
|
|
('assigned', 'waiting'),
|
|
('waiting', 'waiting'),
|
|
('waiting', 'draft'),
|
|
('draft', 'cancelled'),
|
|
('waiting', 'cancelled'),
|
|
('assigned', 'cancelled'),
|
|
('picked', 'cancelled'),
|
|
('packed', 'cancelled'),
|
|
('shipped', 'cancelled'),
|
|
('cancelled', 'draft'),
|
|
('done', 'cancelled'),
|
|
))
|
|
cls._buttons.update({
|
|
'cancel': {
|
|
'invisible': Eval('state').in_(['cancelled', 'done']),
|
|
'depends': ['state'],
|
|
},
|
|
'draft': {
|
|
'invisible': ~Eval('state').in_(['waiting', 'cancelled']),
|
|
'icon': If(Eval('state') == 'cancelled',
|
|
'tryton-undo',
|
|
If(Eval('state') == 'waiting',
|
|
'tryton-back',
|
|
'tryton-forward')),
|
|
'depends': ['state'],
|
|
},
|
|
'wait': {
|
|
'invisible': (
|
|
~(Eval('state').in_(['assigned', 'waiting', 'draft'])
|
|
| ((Eval('state') == 'packed')
|
|
& (Eval('warehouse_storage')
|
|
== Eval('warehouse_output'))))),
|
|
'icon': If(Eval('state').in_(['assigned', 'packed']),
|
|
'tryton-back',
|
|
If(Eval('state') == 'waiting',
|
|
'tryton-clear',
|
|
'tryton-forward')),
|
|
'depends': [
|
|
'state', 'warehouse_storage', 'warehouse_output'],
|
|
},
|
|
'pick': {
|
|
'invisible': If(
|
|
Eval('warehouse_storage') == Eval('warehouse_output'),
|
|
True,
|
|
~Eval('state').in_(['assigned', 'packed'])),
|
|
'icon': If(Eval('state') == 'packed',
|
|
'tryton-back',
|
|
'tryton-forward'),
|
|
'depends': [
|
|
'state', 'warehouse_storage', 'warehouse_output'],
|
|
},
|
|
'pack': {
|
|
'invisible': If(
|
|
Eval('warehouse_storage') == Eval('warehouse_output'),
|
|
Eval('state') != 'assigned',
|
|
Eval('state') != 'picked'),
|
|
'depends': [
|
|
'state', 'warehouse_storage', 'warehouse_output'],
|
|
},
|
|
'ship': {
|
|
'invisible': Eval('state') != 'packed',
|
|
'depends': ['state'],
|
|
},
|
|
'do': {
|
|
'invisible': ~Eval('state').in_(['packed', 'shipped']),
|
|
},
|
|
'assign_wizard': {
|
|
'invisible': Eval('state') != 'waiting',
|
|
},
|
|
'assign_try': {},
|
|
'assign_force': {},
|
|
})
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
pool = Pool()
|
|
Location = pool.get('stock.location')
|
|
cursor = Transaction().connection.cursor()
|
|
sql_table = cls.__table__()
|
|
location = Location.__table__()
|
|
|
|
super().__register__(module_name)
|
|
|
|
# Migration from 6.6: fill warehouse locations
|
|
cursor.execute(*sql_table.update(
|
|
[sql_table.warehouse_storage],
|
|
location.select(
|
|
location.storage_location,
|
|
where=location.id == sql_table.warehouse),
|
|
where=sql_table.warehouse_storage == Null))
|
|
cursor.execute(*sql_table.update(
|
|
[sql_table.warehouse_output],
|
|
location.select(
|
|
location.output_location,
|
|
where=location.id == sql_table.warehouse),
|
|
where=sql_table.warehouse_output == Null))
|
|
|
|
@staticmethod
|
|
def default_state():
|
|
return 'draft'
|
|
|
|
@classmethod
|
|
def default_warehouse(cls):
|
|
Location = Pool().get('stock.location')
|
|
return Location.get_default_warehouse()
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@fields.depends('warehouse')
|
|
def on_change_warehouse(self):
|
|
if self.warehouse:
|
|
if self.warehouse.picking_location:
|
|
self.warehouse_storage = self.warehouse.picking_location
|
|
else:
|
|
self.warehouse_storage = self.warehouse.storage_location
|
|
self.warehouse_output = self.warehouse.output_location
|
|
else:
|
|
self.warehouse_storage = self.warehouse_output = None
|
|
|
|
@fields.depends('customer', 'warehouse')
|
|
def on_change_customer(self):
|
|
self.delivery_address = None
|
|
if self.customer:
|
|
with Transaction().set_context(
|
|
warehouse=self.warehouse.id if self.warehouse else None):
|
|
self.delivery_address = self.customer.address_get(
|
|
type='delivery')
|
|
|
|
@fields.depends('customer')
|
|
def on_change_with_customer_location(self, name=None):
|
|
return self.customer.customer_location if self.customer else None
|
|
|
|
def get_outgoing_moves(self, name):
|
|
if self.warehouse_output == self.warehouse_storage:
|
|
moves = self.moves
|
|
else:
|
|
moves = filter(
|
|
lambda m: m.from_location == self.warehouse_output, self.moves)
|
|
return sort(moves, self.__class__.outgoing_moves.order)
|
|
|
|
@classmethod
|
|
def set_outgoing_moves(cls, shipments, name, value):
|
|
if not value:
|
|
return
|
|
cls.write(shipments, {
|
|
'moves': value,
|
|
})
|
|
|
|
def get_inventory_moves(self, name):
|
|
moves = filter(
|
|
lambda m: m.to_location == self.warehouse_output, self.moves)
|
|
return sort(moves, self.__class__.inventory_moves.order)
|
|
|
|
@classmethod
|
|
def set_inventory_moves(cls, shipments, name, value):
|
|
if not value:
|
|
return
|
|
cls.write(shipments, {
|
|
'moves': value,
|
|
})
|
|
|
|
def get_origins(self, name):
|
|
return ', '.join(set(filter(None,
|
|
(m.origin_name for m in self.outgoing_moves))))
|
|
|
|
def chat_language(self, audience='internal'):
|
|
language = super().chat_language(audience=audience)
|
|
if audience == 'public':
|
|
language = self.customer.lang.code if self.customer.lang else None
|
|
return language
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('draft')
|
|
def draft(cls, shipments):
|
|
Move = Pool().get('stock.move')
|
|
Move.draft([m for s in shipments for m in s.outgoing_moves
|
|
if m.state != 'staging'])
|
|
Move.delete([m for s in shipments for m in s.inventory_moves
|
|
if m.state in {'staging', 'draft', 'cancelled'}])
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('waiting')
|
|
def wait(cls, shipments, moves=None):
|
|
"""
|
|
Complete inventory moves to match the products and quantities
|
|
that are in the outgoing moves.
|
|
If moves is set, only this subset is set to draft.
|
|
"""
|
|
Move = Pool().get('stock.move')
|
|
|
|
if moves is None:
|
|
moves = sum((s.inventory_moves for s in shipments), ())
|
|
else:
|
|
assert all(m.shipment in shipments for m in moves)
|
|
Move.draft(moves)
|
|
Move.delete([m for s in shipments for m in s.inventory_moves
|
|
if m.state in ('draft', 'cancelled')])
|
|
Move.draft([
|
|
m for s in shipments for m in s.outgoing_moves
|
|
if m.state != 'staging'])
|
|
|
|
to_create = []
|
|
for shipment in shipments:
|
|
if shipment.warehouse_storage == shipment.warehouse_output:
|
|
# Do not create inventory moves
|
|
continue
|
|
for move in shipment.outgoing_moves:
|
|
if move.state in ('cancelled', 'done'):
|
|
continue
|
|
inventory_move = shipment._get_inventory_move(move)
|
|
if inventory_move:
|
|
to_create.append(inventory_move)
|
|
if to_create:
|
|
Move.save(to_create)
|
|
cls.set_number(shipments)
|
|
|
|
def _get_inventory_move(self, move):
|
|
'Return inventory move for the outgoing move if necessary'
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Uom = pool.get('product.uom')
|
|
quantity = move.quantity
|
|
for inventory_move in self.inventory_moves:
|
|
if (inventory_move.origin == move
|
|
and inventory_move.state != 'cancelled'):
|
|
quantity -= Uom.compute_qty(
|
|
inventory_move.unit, inventory_move.quantity, move.unit)
|
|
quantity = move.unit.round(quantity)
|
|
if quantity <= 0:
|
|
return
|
|
inventory_move = Move(
|
|
from_location=self.warehouse_storage,
|
|
to_location=move.from_location,
|
|
product=move.product,
|
|
unit=move.unit,
|
|
quantity=quantity,
|
|
shipment=self,
|
|
planned_date=move.planned_date,
|
|
company=move.company,
|
|
origin=move,
|
|
state='staging' if move.state == 'staging' else 'draft',
|
|
)
|
|
if inventory_move.on_change_with_unit_price_required():
|
|
inventory_move.unit_price = move.unit_price
|
|
inventory_move.currency = move.currency
|
|
else:
|
|
inventory_move.unit_price = None
|
|
inventory_move.currency = None
|
|
return inventory_move
|
|
|
|
@classmethod
|
|
@Workflow.transition('assigned')
|
|
def assign(cls, shipments):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Move.assign([m for s in shipments for m in s.assign_moves])
|
|
cls._sync_inventory_to_outgoing(shipments, quantity=False)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('picked')
|
|
@set_employee('picked_by')
|
|
def pick(cls, shipments):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Move.delete([
|
|
m for s in shipments for m in s.inventory_moves
|
|
if m.state == 'staging' or not m.quantity])
|
|
Move.do([m for s in shipments for m in s.inventory_moves])
|
|
Move.draft([m for s in shipments for m in s.outgoing_moves])
|
|
cls._sync_inventory_to_outgoing(shipments, quantity=True)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('packed')
|
|
@set_employee('packed_by')
|
|
def pack(cls, shipments):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
outgoing_moves, to_delete = [], []
|
|
for shipment in shipments:
|
|
for move in shipment.inventory_moves:
|
|
if move.state not in {'done', 'cancelled'}:
|
|
raise AccessError(
|
|
gettext('stock.msg_shipment_pack_inventory_done',
|
|
shipment=shipment.rec_name))
|
|
if shipment.warehouse_storage != shipment.warehouse_output:
|
|
shipment.check_quantity()
|
|
for move in shipment.outgoing_moves:
|
|
if move.quantity:
|
|
outgoing_moves.append(move)
|
|
else:
|
|
to_delete.append(move)
|
|
Move.delete(to_delete)
|
|
Move.assign(outgoing_moves)
|
|
|
|
@property
|
|
def _check_quantity_source_moves(self):
|
|
return self.inventory_moves
|
|
|
|
@property
|
|
def _check_quantity_target_moves(self):
|
|
return self.outgoing_moves
|
|
|
|
def _sync_move_key(self, move):
|
|
return (
|
|
('product', move.product),
|
|
('unit', move.unit),
|
|
)
|
|
|
|
def _sync_outgoing_move(self, template=None):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
move = Move(
|
|
from_location=self.warehouse_output,
|
|
to_location=self.customer_location,
|
|
quantity=0,
|
|
shipment=self,
|
|
planned_date=self.planned_date,
|
|
company=self.company,
|
|
)
|
|
if template:
|
|
move.origin = template.origin
|
|
if move.on_change_with_unit_price_required():
|
|
if template:
|
|
move.unit_price = template.unit_price
|
|
move.currency = template.currency
|
|
else:
|
|
move.unit_price = 0
|
|
move.currency = self.company.currency
|
|
else:
|
|
move.unit_price = None
|
|
move.currency = None
|
|
return move
|
|
|
|
@classmethod
|
|
def _sync_inventory_to_outgoing(cls, shipments, quantity=True):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Uom = pool.get('product.uom')
|
|
|
|
def active(move):
|
|
return move.state != 'cancelled'
|
|
|
|
moves, imoves = [], []
|
|
for shipment in shipments:
|
|
if shipment.warehouse_storage == shipment.warehouse_output:
|
|
# Do not have inventory moves
|
|
continue
|
|
|
|
outgoing_moves = {m: m for m in shipment.outgoing_moves}
|
|
inventory_qty = defaultdict(lambda: defaultdict(float))
|
|
inventory_moves = defaultdict(lambda: defaultdict(list))
|
|
for move in filter(active, shipment.outgoing_moves):
|
|
key = shipment._sync_move_key(move)
|
|
inventory_qty[move][key] = 0
|
|
for move in filter(active, shipment.inventory_moves):
|
|
key = shipment._sync_move_key(move)
|
|
outgoing_move = outgoing_moves.get(move.origin)
|
|
qty_default_uom = Uom.compute_qty(
|
|
move.unit, move.quantity,
|
|
move.product.default_uom, round=False)
|
|
inventory_qty[outgoing_move][key] += qty_default_uom
|
|
inventory_moves[outgoing_move][key].append(move)
|
|
|
|
for outgoing_move in inventory_qty:
|
|
if outgoing_move:
|
|
outgoing_key = shipment._sync_move_key(outgoing_move)
|
|
for key, qty in inventory_qty[outgoing_move].items():
|
|
if not quantity and outgoing_move:
|
|
# Do not create outgoing move with origin
|
|
# to allow to reset to draft
|
|
continue
|
|
if outgoing_move and key == outgoing_key:
|
|
move = outgoing_move
|
|
else:
|
|
move = shipment._sync_outgoing_move(outgoing_move)
|
|
for name, value in key:
|
|
setattr(move, name, value)
|
|
for imove in inventory_moves[outgoing_move][key]:
|
|
imove.origin = move
|
|
imoves.append(imove)
|
|
qty = Uom.compute_qty(
|
|
move.product.default_uom, qty, move.unit)
|
|
if quantity and move.quantity != qty:
|
|
move.quantity = qty
|
|
moves.append(move)
|
|
Move.save(moves)
|
|
Move.save(imoves)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('shipped')
|
|
@set_employee('shipped_by')
|
|
def ship(cls, shipments):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Move.delete([
|
|
m for s in shipments for m in s.outgoing_moves
|
|
if m.state == 'staging'])
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('done')
|
|
@set_employee('done_by')
|
|
def do(cls, shipments):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Date = pool.get('ir.date')
|
|
|
|
Move.delete([
|
|
m for s in shipments for m in s.outgoing_moves
|
|
if m.state == 'staging'])
|
|
Move.do([m for s in shipments for m in s.outgoing_moves])
|
|
for company, c_shipments in groupby(
|
|
shipments, key=lambda s: s.company):
|
|
with Transaction().set_context(company=company.id):
|
|
today = Date.today()
|
|
cls.write([s for s in c_shipments if not s.effective_date], {
|
|
'effective_date': today,
|
|
})
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('cancelled')
|
|
def cancel(cls, shipments):
|
|
Move = Pool().get('stock.move')
|
|
Move.cancel([m for s in shipments
|
|
for m in s.outgoing_moves + s.inventory_moves])
|
|
|
|
@property
|
|
def _move_planned_date(self):
|
|
'''
|
|
Return the planned date for outgoing moves and inventory moves
|
|
'''
|
|
return self.planned_date, self.planned_date
|
|
|
|
@classmethod
|
|
def _set_move_planned_date(cls, shipments):
|
|
'''
|
|
Set planned date of moves for the shipments
|
|
'''
|
|
Move = Pool().get('stock.move')
|
|
to_write = []
|
|
for shipment in shipments:
|
|
outgoing_date, inventory_date = shipment._move_planned_date
|
|
out_moves_to_write = [x for x in shipment.outgoing_moves
|
|
if (x.state not in {'done', 'cancelled'}
|
|
and x.planned_date != outgoing_date)]
|
|
if out_moves_to_write:
|
|
to_write.extend((out_moves_to_write, {
|
|
'planned_date': outgoing_date,
|
|
}))
|
|
|
|
inv_moves_to_write = [x for x in shipment.inventory_moves
|
|
if (x.state not in {'done', 'cancelled'}
|
|
and x.planned_date != inventory_date)]
|
|
if inv_moves_to_write:
|
|
to_write.extend((inv_moves_to_write, {
|
|
'planned_date': inventory_date,
|
|
}))
|
|
if to_write:
|
|
Move.write(*to_write)
|
|
|
|
@classmethod
|
|
def copy(cls, shipments, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
default.setdefault('moves.origin', None)
|
|
default.setdefault('picked_by', None)
|
|
default.setdefault('packed_by', None)
|
|
default.setdefault('done_by', None)
|
|
return super().copy(shipments, default=default)
|
|
|
|
@classmethod
|
|
@ModelView.button_action('stock.wizard_shipment_out_assign')
|
|
def assign_wizard(cls, shipments):
|
|
pass
|
|
|
|
@property
|
|
def assign_moves(self):
|
|
if self.warehouse_storage != self.warehouse_output:
|
|
return self.inventory_moves
|
|
else:
|
|
return self.outgoing_moves
|
|
|
|
@dualmethod
|
|
@ModelView.button
|
|
def assign_try(cls, shipments):
|
|
Move = Pool().get('stock.move')
|
|
shipments = [
|
|
s for s in shipments
|
|
if s.state == 'waiting']
|
|
to_assign = [
|
|
m for s in shipments for m in s.assign_moves
|
|
if m.assignation_required]
|
|
if Move.assign_try(to_assign):
|
|
cls.assign(shipments)
|
|
else:
|
|
to_assign = []
|
|
for shipment in shipments:
|
|
if any(
|
|
m.state in {'staging', 'draft'}
|
|
for m in shipment.assign_moves
|
|
if m.assignation_required):
|
|
continue
|
|
to_assign.append(shipment)
|
|
if to_assign:
|
|
cls.assign(to_assign)
|
|
|
|
@classmethod
|
|
def _get_reschedule_domain(cls, date):
|
|
return [
|
|
('state', 'in', ['waiting', 'assigned', 'picked', 'packed']),
|
|
('planned_date', '<', date),
|
|
]
|
|
|
|
@classmethod
|
|
def reschedule(cls, date=None):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
if date is None:
|
|
date = Date.today()
|
|
shipments = cls.search(cls._get_reschedule_domain(date))
|
|
cls.write(shipments, {'planned_date': date})
|
|
|
|
|
|
class ShipmentOutReturn(
|
|
ShipmentCheckQuantity, ShipmentMixin, Workflow, ModelSQL, ModelView,
|
|
ChatMixin):
|
|
__name__ = 'stock.shipment.out.return'
|
|
|
|
company = fields.Many2One(
|
|
'company.company', "Company", required=True,
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
},
|
|
context={
|
|
'party_contact_mechanism_usage': 'delivery',
|
|
},
|
|
help="The company the shipment is associated with.")
|
|
customer = fields.Many2One('party.party', 'Customer', required=True,
|
|
states={
|
|
'readonly': ((Eval('state') != 'draft')
|
|
| Eval('incoming_moves', [0])),
|
|
},
|
|
context={
|
|
'company': Eval('company', -1),
|
|
'party_contact_mechanism_usage': 'delivery',
|
|
},
|
|
depends={'company'},
|
|
help="The party that purchased the stock.")
|
|
customer_location = fields.Function(fields.Many2One('stock.location',
|
|
'Customer Location'), 'on_change_with_customer_location')
|
|
contact_address = fields.Many2One(
|
|
'party.address', "Contact Address",
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
},
|
|
domain=[('party', '=', Eval('customer', -1))],
|
|
help="The address the customer can be contacted at.")
|
|
warehouse = fields.Many2One('stock.location', "Warehouse", required=True,
|
|
states={
|
|
'readonly': ((Eval('state') != 'draft')
|
|
| Eval('incoming_moves', [0]) | Eval('inventory_moves', [0])),
|
|
}, domain=[('type', '=', 'warehouse')],
|
|
help="Where the stock is returned.")
|
|
warehouse_storage = fields.Many2One(
|
|
'stock.location', "Warehouse Storage", required=True,
|
|
domain=[
|
|
('type', 'in', ['storage', 'view']),
|
|
If(Eval('state') == 'draft',
|
|
('parent', 'child_of', [Eval('warehouse', -1)]),
|
|
()),
|
|
],
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
})
|
|
warehouse_input = fields.Many2One(
|
|
'stock.location', "Warehouse Input", required=True,
|
|
domain=[
|
|
['OR',
|
|
('type', '=', 'storage'),
|
|
('id', '=', Eval('warehouse_storage', -1)),
|
|
],
|
|
If(Eval('state') == 'draft',
|
|
('parent', 'child_of', [Eval('warehouse', -1)]),
|
|
()),
|
|
],
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
})
|
|
incoming_moves = fields.Function(fields.One2Many('stock.move', 'shipment',
|
|
'Incoming Moves',
|
|
domain=[
|
|
If(Eval('state') == 'draft',
|
|
('from_location', '=', Eval('customer_location')),
|
|
()),
|
|
If(Eval('warehouse_input') == Eval('warehouse_storage'),
|
|
('to_location', 'child_of',
|
|
[Eval('warehouse_input', -1)], 'parent'),
|
|
('to_location', '=', Eval('warehouse_input'))),
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
order=[
|
|
('product', 'ASC'),
|
|
('id', 'ASC'),
|
|
],
|
|
states={
|
|
'readonly': ((Eval('state') != 'draft')
|
|
| ~Eval('warehouse') | ~Eval('customer')),
|
|
},
|
|
help="The moves that bring the stock into the warehouse."),
|
|
'get_incoming_moves', setter='set_incoming_moves')
|
|
inventory_moves = fields.Function(fields.One2Many('stock.move', 'shipment',
|
|
'Inventory Moves',
|
|
domain=[
|
|
('from_location', '=', Eval('warehouse_input', -1)),
|
|
If(Eval('state').in_(['received']),
|
|
['OR',
|
|
('to_location', 'child_of',
|
|
[Eval('warehouse_storage', -1)], 'parent'),
|
|
('to_location.waste_warehouses', '=',
|
|
Eval('warehouse', -1)),
|
|
],
|
|
[]),
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
order=[
|
|
('to_location', 'ASC'),
|
|
('product', 'ASC'),
|
|
('id', 'ASC'),
|
|
],
|
|
states={
|
|
'readonly': Eval('state').in_(['draft', 'cancelled', 'done']),
|
|
'invisible': (
|
|
Eval('warehouse_input') == Eval('warehouse_storage')),
|
|
},
|
|
help="The moves that put the stock away into the storage area."),
|
|
'get_inventory_moves', setter='set_inventory_moves')
|
|
moves = fields.One2Many(
|
|
'stock.move', 'shipment', "Moves",
|
|
domain=[('company', '=', Eval('company', -1))],
|
|
states={
|
|
'readonly': True,
|
|
})
|
|
origins = fields.Function(fields.Char('Origins'), 'get_origins')
|
|
received_by = employee_field("Received By")
|
|
done_by = employee_field("Done By")
|
|
state = fields.Selection([
|
|
('draft', 'Draft'),
|
|
('received', 'Received'),
|
|
('done', 'Done'),
|
|
('cancelled', 'Cancelled'),
|
|
], "State", readonly=True, sort=False,
|
|
help="The current state of the shipment.")
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
t = cls.__table__()
|
|
cls._sql_indexes.update({
|
|
Index(
|
|
t,
|
|
(t.state, Index.Equality(cardinality='low')),
|
|
where=t.state.in_(['draft', 'received'])),
|
|
})
|
|
cls._transitions |= set((
|
|
('draft', 'received'),
|
|
('received', 'done'),
|
|
('received', 'draft'),
|
|
('draft', 'cancelled'),
|
|
('received', 'cancelled'),
|
|
('cancelled', 'draft'),
|
|
('done', 'cancelled'),
|
|
))
|
|
cls._buttons.update({
|
|
'cancel': {
|
|
'invisible': Eval('state').in_(['cancelled', 'done']),
|
|
'depends': ['state'],
|
|
},
|
|
'draft': {
|
|
'invisible': Eval('state') != 'cancelled',
|
|
'depends': ['state'],
|
|
},
|
|
'receive': {
|
|
'invisible': Eval('state') != 'draft',
|
|
'depends': ['state'],
|
|
},
|
|
'do': {
|
|
'invisible': Eval('state') != 'received',
|
|
'depends': ['state'],
|
|
},
|
|
})
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
pool = Pool()
|
|
Location = pool.get('stock.location')
|
|
cursor = Transaction().connection.cursor()
|
|
table = cls.__table_handler__(module_name)
|
|
sql_table = cls.__table__()
|
|
location = Location.__table__()
|
|
|
|
# Migration from 6.4: rename delivery_address to contact_address
|
|
table.column_rename('delivery_address', 'contact_address')
|
|
|
|
super().__register__(module_name)
|
|
|
|
# Migration from 6.4: remove required on contact_address
|
|
table.not_null_action('contact_address', 'remove')
|
|
|
|
# Migration from 6.6: fill warehouse locations
|
|
cursor.execute(*sql_table.update(
|
|
[sql_table.warehouse_input],
|
|
location.select(
|
|
location.input_location,
|
|
where=location.id == sql_table.warehouse),
|
|
where=sql_table.warehouse_input == Null))
|
|
cursor.execute(*sql_table.update(
|
|
[sql_table.warehouse_storage],
|
|
location.select(
|
|
location.storage_location,
|
|
where=location.id == sql_table.warehouse),
|
|
where=sql_table.warehouse_storage == Null))
|
|
|
|
@staticmethod
|
|
def default_state():
|
|
return 'draft'
|
|
|
|
@classmethod
|
|
def default_warehouse(cls):
|
|
Location = Pool().get('stock.location')
|
|
return Location.get_default_warehouse()
|
|
|
|
@fields.depends('warehouse')
|
|
def on_change_warehouse(self):
|
|
if self.warehouse:
|
|
self.warehouse_input = self.warehouse.input_location
|
|
self.warehouse_storage = self.warehouse.storage_location
|
|
else:
|
|
self.warehouse_input = self.warehouse_storage = None
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@fields.depends('customer')
|
|
def on_change_customer(self):
|
|
self.contact_address = None
|
|
if self.customer:
|
|
self.contact_address = self.customer.address_get()
|
|
|
|
@fields.depends('customer')
|
|
def on_change_with_customer_location(self, name=None):
|
|
return self.customer.customer_location if self.customer else None
|
|
|
|
def get_incoming_moves(self, name):
|
|
if self.warehouse_input == self.warehouse_storage:
|
|
moves = self.moves
|
|
else:
|
|
moves = filter(
|
|
lambda m: m.to_location == self.warehouse_input, self.moves)
|
|
return sort(moves, self.__class__.incoming_moves.order)
|
|
|
|
@classmethod
|
|
def set_incoming_moves(cls, shipments, name, value):
|
|
if not value:
|
|
return
|
|
cls.write(shipments, {
|
|
'moves': value,
|
|
})
|
|
|
|
def get_inventory_moves(self, name):
|
|
moves = filter(
|
|
lambda m: m.from_location == self.warehouse_input, self.moves)
|
|
return sort(moves, self.__class__.inventory_moves.order)
|
|
|
|
@classmethod
|
|
def set_inventory_moves(cls, shipments, name, value):
|
|
if not value:
|
|
return
|
|
cls.write(shipments, {
|
|
'moves': value,
|
|
})
|
|
|
|
def _get_move_planned_date(self):
|
|
'''
|
|
Return the planned date for incoming moves and inventory moves
|
|
'''
|
|
return self.planned_date, self.planned_date
|
|
|
|
@classmethod
|
|
def _set_move_planned_date(cls, shipments):
|
|
'''
|
|
Set planned date of moves for the shipments
|
|
'''
|
|
Move = Pool().get('stock.move')
|
|
to_write = []
|
|
for shipment in shipments:
|
|
dates = shipment._get_move_planned_date()
|
|
incoming_date, inventory_date = dates
|
|
incoming_moves_to_write = [x for x in shipment.incoming_moves
|
|
if (x.state not in {'done', 'cancelled'}
|
|
and x.planned_date != incoming_date)]
|
|
if incoming_moves_to_write:
|
|
to_write.extend((incoming_moves_to_write, {
|
|
'planned_date': incoming_date,
|
|
}))
|
|
inventory_moves_to_write = [x for x in shipment.inventory_moves
|
|
if (x.state not in {'done', 'cancelled'}
|
|
and x.planned_date != inventory_date)]
|
|
if inventory_moves_to_write:
|
|
to_write.extend((inventory_moves_to_write, {
|
|
'planned_date': inventory_date,
|
|
}))
|
|
if to_write:
|
|
Move.write(*to_write)
|
|
|
|
def get_origins(self, name):
|
|
return ', '.join(set(filter(None,
|
|
(m.origin_name for m in self.incoming_moves))))
|
|
|
|
def chat_language(self, audience='internal'):
|
|
language = super().chat_language(audience=audience)
|
|
if audience == 'public':
|
|
language = self.customer.lang.code if self.customer.lang else None
|
|
return language
|
|
|
|
@classmethod
|
|
def preprocess_values(cls, mode, values):
|
|
pool = Pool()
|
|
Configuration = pool.get('stock.configuration')
|
|
values = super().preprocess_values(mode, values)
|
|
if mode == 'create' and not values.get('number'):
|
|
company_id = values.get('company', cls.default_company())
|
|
if company_id is not None:
|
|
configuration = Configuration(1)
|
|
if sequence := configuration.get_multivalue(
|
|
'shipment_out_return_sequence', company=company_id):
|
|
values['number'] = sequence.get()
|
|
return values
|
|
|
|
@classmethod
|
|
def copy(cls, shipments, default=None):
|
|
if default is None:
|
|
default = {}
|
|
default = default.copy()
|
|
default.setdefault('moves.origin', None)
|
|
default.setdefault('received_by', None)
|
|
default.setdefault('done_by', None)
|
|
return super().copy(shipments, default=default)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('draft')
|
|
def draft(cls, shipments):
|
|
Move = Pool().get('stock.move')
|
|
Move.draft([m for s in shipments for m in s.incoming_moves
|
|
if m.state != 'staging'])
|
|
Move.delete([m for s in shipments for m in s.inventory_moves
|
|
if m.state in {'staging', 'draft', 'cancelled'}])
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('received')
|
|
@set_employee('received_by')
|
|
def receive(cls, shipments):
|
|
Move = Pool().get('stock.move')
|
|
Move.do([m for s in shipments for m in s.incoming_moves])
|
|
cls.create_inventory_moves(shipments)
|
|
# Set received state to allow done transition
|
|
cls.write(shipments, {'state': 'received'})
|
|
to_do = [s for s in shipments
|
|
if s.warehouse_storage == s.warehouse_input]
|
|
if to_do:
|
|
cls.do(to_do)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('done')
|
|
@set_employee('done_by')
|
|
def do(cls, shipments):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Date = pool.get('ir.date')
|
|
inventory_moves = []
|
|
for shipment in shipments:
|
|
if shipment.warehouse_storage != shipment.warehouse_input:
|
|
shipment.check_quantity()
|
|
inventory_moves.extend(shipment.inventory_moves)
|
|
Move.do(inventory_moves)
|
|
for company, c_shipments in groupby(
|
|
shipments, key=lambda s: s.company):
|
|
with Transaction().set_context(company=company.id):
|
|
today = Date.today()
|
|
cls.write([s for s in c_shipments if not s.effective_date], {
|
|
'effective_date': today,
|
|
})
|
|
|
|
@property
|
|
def _check_quantity_source_moves(self):
|
|
return self.incoming_moves
|
|
|
|
@property
|
|
def _check_quantity_target_moves(self):
|
|
return self.inventory_moves
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('cancelled')
|
|
def cancel(cls, shipments):
|
|
Move = Pool().get('stock.move')
|
|
Move.cancel([m for s in shipments
|
|
for m in s.incoming_moves + s.inventory_moves])
|
|
|
|
def _get_inventory_move(self, incoming_move):
|
|
'Return inventory move for the incoming move'
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Date = pool.get('ir.date')
|
|
if incoming_move.quantity <= 0.0:
|
|
return
|
|
with Transaction().set_context(company=self.company.id):
|
|
today = Date.today()
|
|
move = Move()
|
|
move.product = incoming_move.product
|
|
move.unit = incoming_move.unit
|
|
move.quantity = incoming_move.quantity
|
|
move.from_location = incoming_move.to_location
|
|
move.to_location = self.warehouse_storage
|
|
move.state = (
|
|
'staging' if incoming_move.state == 'staging' else 'draft')
|
|
move.planned_date = max(
|
|
filter(None, [self._get_move_planned_date()[1], today]))
|
|
move.company = incoming_move.company
|
|
move.origin = incoming_move
|
|
return move
|
|
|
|
@classmethod
|
|
def create_inventory_moves(cls, shipments):
|
|
for shipment in shipments:
|
|
if shipment.warehouse_storage == shipment.warehouse_input:
|
|
# Do not create inventory moves
|
|
continue
|
|
# Use moves instead of inventory_moves because save reset before
|
|
# adding new records and as set_inventory_moves is just a proxy to
|
|
# moves, it will reset also the incoming_moves
|
|
moves = list(shipment.moves)
|
|
for incoming_move in shipment.incoming_moves:
|
|
move = shipment._get_inventory_move(incoming_move)
|
|
if move:
|
|
moves.append(move)
|
|
shipment.moves = moves
|
|
cls.save(shipments)
|
|
|
|
|
|
class ShipmentInternal(
|
|
ShipmentCheckQuantity, ShipmentAssignMixin, Workflow, ModelSQL,
|
|
ModelView, ChatMixin):
|
|
__name__ = 'stock.shipment.internal'
|
|
_assign_moves_field = 'moves'
|
|
effective_start_date = fields.Date('Effective Start Date',
|
|
domain=[
|
|
If(Eval('effective_start_date') & Eval('effective_date'),
|
|
('effective_start_date', '<=', Eval('effective_date')),
|
|
()),
|
|
],
|
|
states={
|
|
'readonly': Eval('state').in_(['cancelled', 'shipped', 'done']),
|
|
},
|
|
help="When the stock was actually sent.")
|
|
planned_start_date = fields.Date('Planned Start Date',
|
|
domain=[
|
|
If(Eval('planned_start_date') & Eval('planned_date'),
|
|
('planned_start_date', '<=', Eval('planned_date')),
|
|
()),
|
|
],
|
|
states={
|
|
'readonly': ~Eval('state').in_(['request', 'draft']),
|
|
'required': Bool(Eval('planned_date')),
|
|
},
|
|
help="When the stock is expected to be sent.")
|
|
company = fields.Many2One(
|
|
'company.company', "Company", required=True,
|
|
states={
|
|
'readonly': ~Eval('state').in_(['request', 'draft']),
|
|
},
|
|
context={
|
|
'party_contact_mechanism_usage': 'delivery',
|
|
},
|
|
help="The company the shipment is associated with.")
|
|
from_location = fields.Many2One('stock.location', "From Location",
|
|
required=True, states={
|
|
'readonly': (~Eval('state').in_(['request', 'draft'])
|
|
| Eval('moves', [0])),
|
|
},
|
|
domain=[
|
|
('type', 'in', ['view', 'storage', 'lost_found']),
|
|
],
|
|
help="Where the stock is moved from.")
|
|
to_location = fields.Many2One('stock.location', "To Location",
|
|
required=True, states={
|
|
'readonly': (~Eval('state').in_(['request', 'draft'])
|
|
| Eval('moves', [0])),
|
|
}, domain=[
|
|
('type', 'in', ['view', 'storage', 'lost_found']),
|
|
],
|
|
help="Where the stock is moved to.")
|
|
transit_location = fields.Function(fields.Many2One('stock.location',
|
|
'Transit Location',
|
|
help="Where the stock is located while it is in transit between "
|
|
"the warehouses."),
|
|
'on_change_with_transit_location')
|
|
internal_transit_location = fields.Many2One(
|
|
'stock.location', "Internal Transit Location",
|
|
readonly=True,
|
|
domain=[
|
|
('type', '=', 'storage'),
|
|
('parent', '=', None),
|
|
('id', 'not in', [
|
|
Eval('from_location', -1), Eval('to_location', -1)]),
|
|
])
|
|
warehouse = fields.Function(
|
|
fields.Many2One(
|
|
'stock.location', "Warehouse",
|
|
domain=[
|
|
('type', '=', 'warehouse'),
|
|
],
|
|
help="Where the stock is sent from."),
|
|
'on_change_with_warehouse')
|
|
to_warehouse = fields.Function(
|
|
fields.Many2One(
|
|
'stock.location', "To Warehouse",
|
|
domain=[
|
|
('type', '=', 'warehouse'),
|
|
],
|
|
help="Where the stock is sent to."),
|
|
'on_change_with_to_warehouse')
|
|
moves = fields.One2Many('stock.move', 'shipment', 'Moves',
|
|
states={
|
|
'readonly': (Eval('state').in_(['cancelled', 'assigned', 'done'])
|
|
| ~Eval('from_location') | ~Eval('to_location')),
|
|
'invisible': (Bool(Eval('transit_location'))
|
|
& ~Eval('state').in_(['request', 'draft'])),
|
|
},
|
|
domain=[
|
|
If(Eval('state').in_(['request', 'draft']), [
|
|
('from_location', '=', Eval('from_location')),
|
|
('to_location', '=', Eval('to_location')),
|
|
],
|
|
If(~Eval('state').in_(['done', 'cancelled']),
|
|
If(~Eval('transit_location'),
|
|
[
|
|
('from_location', 'child_of',
|
|
[Eval('from_location', -1)], 'parent'),
|
|
('to_location', 'child_of',
|
|
[Eval('to_location', -1)], 'parent'),
|
|
],
|
|
['OR',
|
|
[
|
|
('from_location', 'child_of',
|
|
[Eval('from_location', -1)], 'parent'),
|
|
('to_location', '=', Eval('transit_location')),
|
|
],
|
|
[
|
|
('from_location', '=',
|
|
Eval('transit_location')),
|
|
('to_location', 'child_of',
|
|
[Eval('to_location', -1)], 'parent'),
|
|
],
|
|
]),
|
|
[])),
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
order=[
|
|
('from_location', 'ASC'),
|
|
('product', 'ASC'),
|
|
('id', 'ASC'),
|
|
],
|
|
help="The moves that perform the shipment.")
|
|
outgoing_moves = fields.Function(fields.One2Many('stock.move', 'shipment',
|
|
'Outgoing Moves',
|
|
domain=[
|
|
If(Eval('state').in_(['request', 'draft']), [
|
|
('from_location', 'child_of',
|
|
[Eval('from_location', -1)], 'parent'),
|
|
If(~Eval('transit_location'),
|
|
('to_location', 'child_of',
|
|
[Eval('to_location', -1)], 'parent'),
|
|
('to_location', '=', Eval('transit_location'))),
|
|
],
|
|
[]),
|
|
],
|
|
order=[
|
|
('from_location', 'ASC'),
|
|
('product', 'ASC'),
|
|
('id', 'ASC'),
|
|
],
|
|
states={
|
|
'readonly': Eval('state').in_(
|
|
['assigned', 'shipped', 'done', 'cancelled']),
|
|
'invisible': (~Eval('transit_location')
|
|
| Eval('state').in_(['request', 'draft'])),
|
|
},
|
|
help="The moves that send the stock out."),
|
|
'get_outgoing_moves', setter='set_moves')
|
|
incoming_moves = fields.Function(fields.One2Many('stock.move', 'shipment',
|
|
'Incoming Moves',
|
|
domain=[
|
|
If(~Eval('state').in_(['done', 'cancelled']), [
|
|
If(~Eval('transit_location'),
|
|
('from_location', 'child_of',
|
|
[Eval('from_location', -1)], 'parent'),
|
|
('from_location', '=', Eval('transit_location'))),
|
|
('to_location', 'child_of',
|
|
[Eval('to_location', -1)], 'parent'),
|
|
],
|
|
[]),
|
|
],
|
|
order=[
|
|
('to_location', 'ASC'),
|
|
('product', 'ASC'),
|
|
('id', 'ASC'),
|
|
],
|
|
states={
|
|
'readonly': Eval('state').in_(['done', 'cancelled']),
|
|
'invisible': (~Eval('transit_location')
|
|
| Eval('state').in_(['request', 'draft'])),
|
|
},
|
|
help="The moves that receive the stock in."),
|
|
'get_incoming_moves', setter='set_moves')
|
|
assigned_by = employee_field("Received By")
|
|
packed_by = employee_field("Packed By")
|
|
shipped_by = employee_field("Shipped By")
|
|
done_by = employee_field("Done By")
|
|
state = fields.Selection([
|
|
('request', 'Request'),
|
|
('draft', 'Draft'),
|
|
('waiting', 'Waiting'),
|
|
('assigned', 'Assigned'),
|
|
('packed', "Packed"),
|
|
('shipped', 'Shipped'),
|
|
('done', 'Done'),
|
|
('cancelled', 'Cancelled'),
|
|
], "State", readonly=True, sort=False,
|
|
help="The current state of the shipment.")
|
|
state_string = state.translated('state')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
t = cls.__table__()
|
|
cls._sql_indexes.update({
|
|
Index(
|
|
t,
|
|
(t.state, Index.Equality(cardinality='low')),
|
|
where=t.state.in_([
|
|
'request', 'draft', 'waiting',
|
|
'assigned', 'shipped'])),
|
|
})
|
|
cls._transitions |= set((
|
|
('request', 'draft'),
|
|
('draft', 'waiting'),
|
|
('waiting', 'waiting'),
|
|
('waiting', 'assigned'),
|
|
('assigned', 'packed'),
|
|
('assigned', 'done'),
|
|
('packed', 'shipped'),
|
|
('packed', 'waiting'),
|
|
('shipped', 'done'),
|
|
('waiting', 'draft'),
|
|
('assigned', 'waiting'),
|
|
('request', 'cancelled'),
|
|
('draft', 'cancelled'),
|
|
('waiting', 'cancelled'),
|
|
('assigned', 'cancelled'),
|
|
('packed', 'cancelled'),
|
|
('cancelled', 'draft'),
|
|
('done', 'cancelled'),
|
|
))
|
|
cls._buttons.update({
|
|
'cancel': {
|
|
'invisible': Eval('state').in_(
|
|
['cancelled', 'shipped', 'done']),
|
|
'depends': ['state'],
|
|
},
|
|
'draft': {
|
|
'invisible': ~Eval('state').in_(
|
|
['cancelled', 'request', 'waiting']),
|
|
'icon': If(Eval('state') == 'cancelled',
|
|
'tryton-undo',
|
|
If(Eval('state') == 'request',
|
|
'tryton-forward',
|
|
'tryton-back')),
|
|
'depends': ['state'],
|
|
},
|
|
'wait': {
|
|
'invisible': ~Eval('state').in_(['assigned', 'waiting',
|
|
'draft']),
|
|
'icon': If(Eval('state') == 'assigned',
|
|
'tryton-back',
|
|
If(Eval('state') == 'waiting',
|
|
'tryton-clear',
|
|
'tryton-forward')),
|
|
'depends': ['state'],
|
|
},
|
|
'pack': {
|
|
'invisible': (
|
|
(Eval('state') != 'assigned')
|
|
| ~Eval('transit_location')),
|
|
'depends': ['state', 'transit_location'],
|
|
},
|
|
'ship': {
|
|
'invisible': ((Eval('state') != 'packed')
|
|
| ~Eval('transit_location')),
|
|
'depends': ['state', 'transit_location'],
|
|
},
|
|
'do': {
|
|
'invisible': If(
|
|
~Eval('transit_location'),
|
|
Eval('state') != 'assigned',
|
|
Eval('state') != 'shipped'),
|
|
'depends': ['state', 'transit_location'],
|
|
},
|
|
'assign_wizard': {
|
|
'invisible': Eval('state') != 'waiting',
|
|
'depends': ['state'],
|
|
},
|
|
'assign_try': {},
|
|
'assign_force': {},
|
|
})
|
|
|
|
@classmethod
|
|
def order_effective_date(cls, tables):
|
|
table, _ = tables[None]
|
|
return [Coalesce(
|
|
table.effective_start_date, table.effective_date,
|
|
table.planned_start_date, table.planned_date)]
|
|
|
|
@staticmethod
|
|
def default_state():
|
|
return 'draft'
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@fields.depends(
|
|
'state', 'from_location', 'to_location', 'company',
|
|
'internal_transit_location')
|
|
def on_change_with_transit_location(self, name=None):
|
|
pool = Pool()
|
|
Config = pool.get('stock.configuration')
|
|
if self.state in {'request', 'draft'}:
|
|
from_ = self.from_location
|
|
to = self.to_location
|
|
if (from_
|
|
and to
|
|
and from_.warehouse != to.warehouse
|
|
and from_.warehouse
|
|
and to.warehouse):
|
|
return Config(1).get_multivalue(
|
|
'shipment_internal_transit',
|
|
company=self.company.id if self.company else None)
|
|
else:
|
|
return self.internal_transit_location
|
|
|
|
@fields.depends('from_location')
|
|
def on_change_with_warehouse(self, name=None):
|
|
return self.from_location.warehouse if self.from_location else None
|
|
|
|
@fields.depends('to_location')
|
|
def on_change_with_to_warehouse(self, name=None):
|
|
return self.to_location.warehouse if self.to_location else None
|
|
|
|
@fields.depends(
|
|
'planned_date', 'from_location', 'to_location',
|
|
methods=['on_change_with_transit_location'])
|
|
def on_change_with_planned_start_date(self, pattern=None):
|
|
pool = Pool()
|
|
LocationLeadTime = pool.get('stock.location.lead_time')
|
|
transit_location = self.on_change_with_transit_location()
|
|
if self.planned_date and transit_location:
|
|
if pattern is None:
|
|
pattern = {}
|
|
pattern.setdefault('warehouse_from',
|
|
self.from_location.warehouse.id
|
|
if self.from_location and self.from_location.warehouse
|
|
else None)
|
|
pattern.setdefault('warehouse_to',
|
|
self.to_location.warehouse.id
|
|
if self.to_location and self.to_location.warehouse
|
|
else None)
|
|
lead_time = LocationLeadTime.get_lead_time(pattern)
|
|
if lead_time:
|
|
return self.planned_date - lead_time
|
|
return self.planned_date
|
|
|
|
def get_outgoing_moves(self, name):
|
|
if not self.transit_location:
|
|
moves = self.moves
|
|
else:
|
|
moves = filter(
|
|
lambda m: m.to_location == self.transit_location, self.moves)
|
|
return sort(moves, self.__class__.outgoing_moves.order)
|
|
|
|
def get_incoming_moves(self, name):
|
|
if not self.transit_location:
|
|
moves = self.moves
|
|
else:
|
|
moves = filter(
|
|
lambda m: m.from_location == self.transit_location, self.moves)
|
|
return sort(moves, self.__class__.incoming_moves.order)
|
|
|
|
@classmethod
|
|
def set_moves(cls, shipments, name, value):
|
|
if not value:
|
|
return
|
|
cls.write(shipments, {
|
|
'moves': value,
|
|
})
|
|
|
|
@classmethod
|
|
def copy(cls, shipments, default=None):
|
|
def shipment_field(data, name):
|
|
model, shipment_id = data['shipment'].split(',', 1)
|
|
assert model == cls.__name__
|
|
shipment_id = int(shipment_id)
|
|
shipment = id2shipments[shipment_id]
|
|
return getattr(shipment, name)
|
|
|
|
def outgoing_moves(data):
|
|
shipment = id2shipments[data['id']]
|
|
return shipment.outgoing_moves
|
|
id2shipments = {s.id: s for s in shipments}
|
|
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
default.setdefault('moves', outgoing_moves)
|
|
default.setdefault('moves.origin', None)
|
|
default.setdefault('moves.from_location', partial(
|
|
shipment_field, name='from_location'))
|
|
default.setdefault('moves.to_location', partial(
|
|
shipment_field, name='to_location'))
|
|
default.setdefault('moves.planned_date', partial(
|
|
shipment_field, name='planned_date'))
|
|
default.setdefault('assigned_by', None)
|
|
default.setdefault('packed_by', None)
|
|
default.setdefault('shipped_by', None)
|
|
default.setdefault('done_by', None)
|
|
return super().copy(shipments, default=default)
|
|
|
|
def _sync_move_key(self, move):
|
|
return (
|
|
('product', move.product),
|
|
('unit', move.unit),
|
|
)
|
|
|
|
def _sync_incoming_move(self, template=None):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
move = Move(
|
|
from_location=self.transit_location,
|
|
to_location=self.to_location,
|
|
quantity=0,
|
|
shipment=self,
|
|
planned_date=self.planned_date,
|
|
company=self.company,
|
|
)
|
|
if template:
|
|
move.origin = template.origin
|
|
move.state = (
|
|
'staging' if template.state == 'staging' else 'draft')
|
|
if move.on_change_with_unit_price_required():
|
|
if template:
|
|
move.unit_price = template.unit_price
|
|
move.currency = template.currency
|
|
else:
|
|
move.unit_price = 0
|
|
move.currency = self.company.currency
|
|
else:
|
|
move.unit_price = None
|
|
move.currency = None
|
|
return move
|
|
|
|
@classmethod
|
|
def _sync_moves(cls, shipments):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Uom = pool.get('product.uom')
|
|
|
|
def active(move):
|
|
return move.state != 'cancelled'
|
|
|
|
moves, omoves = [], []
|
|
for shipment in shipments:
|
|
if not shipment.transit_location:
|
|
continue
|
|
|
|
incoming_moves = {m: m for m in shipment.incoming_moves}
|
|
outgoing_qty = defaultdict(lambda: defaultdict(float))
|
|
outgoing_moves = defaultdict(lambda: defaultdict(list))
|
|
for move in filter(active, shipment.incoming_moves):
|
|
key = shipment._sync_move_key(move)
|
|
outgoing_qty[move][key] = 0
|
|
for move in filter(active, shipment.outgoing_moves):
|
|
key = shipment._sync_move_key(move)
|
|
incoming_move = incoming_moves.get(move.origin)
|
|
qty_default_uom = Uom.compute_qty(
|
|
move.unit, move.quantity,
|
|
move.product.default_uom, round=False)
|
|
outgoing_qty[incoming_move][key] += qty_default_uom
|
|
outgoing_moves[incoming_move][key].append(move)
|
|
|
|
for incoming_move in outgoing_qty:
|
|
if incoming_move:
|
|
incoming_key = shipment._sync_move_key(incoming_move)
|
|
for key, qty in outgoing_qty[incoming_move].items():
|
|
if incoming_move and key == incoming_key:
|
|
move = incoming_move
|
|
else:
|
|
move = shipment._sync_incoming_move(incoming_move)
|
|
for name, value in key:
|
|
setattr(move, name, value)
|
|
for omove in outgoing_moves[incoming_move][key]:
|
|
omove.origin = move
|
|
omoves.append(omove)
|
|
qty = Uom.compute_qty(
|
|
move.product.default_uom, qty, move.unit)
|
|
if move.quantity != qty:
|
|
move.quantity = qty
|
|
moves.append(move)
|
|
# Save incoming moves first to get id for outgoing moves
|
|
Move.save(moves)
|
|
Move.save(omoves)
|
|
|
|
@classmethod
|
|
def _set_transit(cls, shipments):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
|
|
to_write = []
|
|
for shipment in shipments:
|
|
if not shipment.transit_location:
|
|
continue
|
|
moves = [m for m in shipment.moves
|
|
if m.state != 'done'
|
|
and m.from_location != shipment.transit_location
|
|
and m.to_location != shipment.transit_location]
|
|
if not moves:
|
|
continue
|
|
Move.copy(moves, default={
|
|
'to_location': shipment.transit_location.id,
|
|
'planned_date': shipment.planned_start_date,
|
|
'origin': lambda data: '%s,%s' % (
|
|
Move.__name__, data['id']),
|
|
})
|
|
to_write.append(moves)
|
|
to_write.append({
|
|
'from_location': shipment.transit_location.id,
|
|
'planned_date': shipment.planned_date,
|
|
})
|
|
if to_write:
|
|
Move.write(*to_write)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('draft')
|
|
def draft(cls, shipments):
|
|
Move = Pool().get('stock.move')
|
|
|
|
# First reset state to draft to allow update from and to location
|
|
Move.draft([m for s in shipments for m in s.moves
|
|
if m.state != 'staging'])
|
|
Move.delete([m for s in shipments for m in s.moves
|
|
if m.from_location == s.transit_location])
|
|
for shipment in shipments:
|
|
Move.write([m for m in shipment.moves
|
|
if m.state != 'done'], {
|
|
'from_location': shipment.from_location.id,
|
|
'to_location': shipment.to_location.id,
|
|
'planned_date': shipment.planned_date,
|
|
})
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('waiting')
|
|
def wait(cls, shipments, moves=None):
|
|
"""
|
|
If moves is set, only this subset is set to draft.
|
|
"""
|
|
Move = Pool().get('stock.move')
|
|
|
|
if moves is None:
|
|
moves = sum((s.moves for s in shipments), ())
|
|
else:
|
|
assert all(m.shipment in shipments for m in moves)
|
|
Move.draft(moves)
|
|
|
|
moves = []
|
|
for shipment in shipments:
|
|
if shipment.transit_location:
|
|
continue
|
|
for move in shipment.moves:
|
|
if move.state != 'done':
|
|
move.planned_date = shipment.planned_date
|
|
moves.append(move)
|
|
Move.save(moves)
|
|
|
|
cls.set_number(shipments)
|
|
cls._set_transit(shipments)
|
|
cls._sync_moves(shipments)
|
|
for shipment in shipments:
|
|
shipment.internal_transit_location = shipment.transit_location
|
|
shipment.state = 'waiting'
|
|
cls.save(shipments)
|
|
|
|
@classmethod
|
|
@Workflow.transition('assigned')
|
|
@set_employee('assigned_by')
|
|
def assign(cls, shipments):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Move.assign([m for s in shipments for m in s.assign_moves])
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('packed')
|
|
@set_employee('packed_by')
|
|
def pack(cls, shipments):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Move.delete([
|
|
m for s in shipments for m in s.outgoing_moves
|
|
if m.state == 'staging' or not m.quantity])
|
|
cls._sync_moves(shipments)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('shipped')
|
|
@set_employee('shipped_by')
|
|
def ship(cls, shipments):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Date = pool.get('ir.date')
|
|
Move.do([m for s in shipments for m in s.outgoing_moves])
|
|
cls._sync_moves(shipments)
|
|
for company, c_shipments in groupby(
|
|
shipments, key=lambda s: s.company):
|
|
with Transaction().set_context(company=company.id):
|
|
today = Date.today()
|
|
cls.write([s for s in c_shipments if not s.effective_start_date], {
|
|
'effective_start_date': today,
|
|
})
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('done')
|
|
@set_employee('done_by')
|
|
def do(cls, shipments):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Date = pool.get('ir.date')
|
|
incoming_moves = []
|
|
for shipment in shipments:
|
|
if shipment.transit_location:
|
|
shipment.check_quantity()
|
|
incoming_moves.extend(shipment.incoming_moves)
|
|
Move.do(incoming_moves)
|
|
for company, c_shipments in groupby(
|
|
shipments, key=lambda s: s.company):
|
|
with Transaction().set_context(company=company.id):
|
|
today = Date.today()
|
|
cls.write([s for s in c_shipments if not s.effective_date], {
|
|
'effective_date': today,
|
|
})
|
|
|
|
@property
|
|
def _check_quantity_source_moves(self):
|
|
return self.incoming_moves
|
|
|
|
@property
|
|
def _check_quantity_target_moves(self):
|
|
return self.outgoing_moves
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('cancelled')
|
|
def cancel(cls, shipments):
|
|
Move = Pool().get('stock.move')
|
|
Move.cancel([m for s in shipments for m in s.moves])
|
|
|
|
@classmethod
|
|
@ModelView.button_action('stock.wizard_shipment_internal_assign')
|
|
def assign_wizard(cls, shipments):
|
|
pass
|
|
|
|
@property
|
|
def assign_moves(self):
|
|
return self.outgoing_moves
|
|
|
|
@dualmethod
|
|
@ModelView.button
|
|
def assign_try(cls, shipments):
|
|
Move = Pool().get('stock.move')
|
|
to_assign = [
|
|
m for s in shipments for m in s.assign_moves
|
|
if m.assignation_required]
|
|
if Move.assign_try(to_assign):
|
|
cls.assign(shipments)
|
|
else:
|
|
to_assign = []
|
|
for shipment in shipments:
|
|
if any(
|
|
m.state in {'staging', 'draft'}
|
|
for m in shipment.assign_moves
|
|
if m.assignation_required):
|
|
continue
|
|
to_assign.append(shipment)
|
|
if to_assign:
|
|
cls.assign(to_assign)
|
|
|
|
@property
|
|
def _move_planned_date(self):
|
|
'''
|
|
Return the planned date for incoming moves and inventory_moves
|
|
'''
|
|
return self.planned_start_date, self.planned_date
|
|
|
|
@classmethod
|
|
def _set_move_planned_date(cls, shipments):
|
|
'''
|
|
Set planned date of moves for the shipments
|
|
'''
|
|
Move = Pool().get('stock.move')
|
|
to_write = []
|
|
for shipment in shipments:
|
|
dates = shipment._move_planned_date
|
|
if (shipment.transit_location
|
|
and shipment.state not in {'request', 'draft'}):
|
|
outgoing_date, incoming_date = dates
|
|
outgoing_moves = [m for m in shipment.outgoing_moves
|
|
if (m.state not in {'done', 'cancelled'}
|
|
and m.planned_date != outgoing_date)]
|
|
if outgoing_moves:
|
|
to_write.append(outgoing_moves)
|
|
to_write.append({
|
|
'planned_date': outgoing_date,
|
|
})
|
|
incoming_moves = [m for m in shipment.incoming_moves
|
|
if (m.state not in {'done', 'cancelled'}
|
|
and m.planned_date != incoming_date)]
|
|
if incoming_moves:
|
|
to_write.append(incoming_moves)
|
|
to_write.append({
|
|
'planned_date': incoming_date,
|
|
})
|
|
else:
|
|
planned_start_date = shipment.planned_start_date
|
|
moves = [m for m in shipment.moves
|
|
if (m.state not in {'done', 'cancelled'}
|
|
and m.planned_date != planned_start_date)]
|
|
if moves:
|
|
to_write.append(moves)
|
|
to_write.append({
|
|
'planned_date': planned_start_date,
|
|
})
|
|
if to_write:
|
|
Move.write(*to_write)
|
|
|
|
@classmethod
|
|
def _get_reschedule_domain(cls, date):
|
|
return [
|
|
('state', 'in', ['waiting', 'assigned']),
|
|
('planned_date', '<', date),
|
|
]
|
|
|
|
@classmethod
|
|
def reschedule(cls, date=None):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
if date is None:
|
|
date = Date.today()
|
|
shipments = cls.search(cls._get_reschedule_domain(date))
|
|
for shipment in shipments:
|
|
shipment.planned_date = date
|
|
shipment.planned_start_date = (
|
|
shipment.on_change_with_planned_start_date())
|
|
cls.save(shipments)
|
|
|
|
|
|
class Assign(Wizard):
|
|
__name__ = 'stock.shipment.assign'
|
|
start = StateTransition()
|
|
partial = StateView(
|
|
'stock.shipment.assign.partial',
|
|
'stock.shipment_assign_partial_view_form', [
|
|
Button("Cancel", 'cancel', 'tryton-cancel'),
|
|
Button("Wait", 'end', 'tryton-ok', True),
|
|
Button("Ignore", 'ignore', 'tryton-forward'),
|
|
Button("Force", 'force', 'tryton-forward',
|
|
states={
|
|
'invisible': ~Id('stock',
|
|
'group_stock_force_assignment').in_(
|
|
Eval('context', {}).get('groups', [])),
|
|
}),
|
|
])
|
|
cancel = StateTransition()
|
|
force = StateTransition()
|
|
ignore = StateTransition()
|
|
|
|
def transition_start(self):
|
|
self.record.assign_try()
|
|
if self.record.state == 'assigned':
|
|
return 'end'
|
|
else:
|
|
return 'partial'
|
|
|
|
def default_partial(self, fields):
|
|
values = {}
|
|
if 'moves' in fields:
|
|
values['moves'] = [
|
|
m.id for m in self.record.assign_moves
|
|
if m.state in {'staging', 'draft'}]
|
|
return values
|
|
|
|
def transition_cancel(self):
|
|
self.record.assign_reset()
|
|
return 'end'
|
|
|
|
def transition_force(self):
|
|
self.record.assign_force()
|
|
return 'end'
|
|
|
|
def transition_ignore(self):
|
|
self.record.assign_ignore(self.partial.moves)
|
|
return 'end'
|
|
|
|
|
|
class AssignPartial(ModelView):
|
|
__name__ = 'stock.shipment.assign.partial'
|
|
moves = fields.Many2Many(
|
|
'stock.move', None, None, "Moves", readonly=True,
|
|
help="The moves that were not assigned.")
|
|
|
|
|
|
class ShipmentReport(CompanyReport):
|
|
|
|
@classmethod
|
|
def moves(cls, shipment):
|
|
raise NotImplementedError
|
|
|
|
@classmethod
|
|
def moves_order(cls, shipment):
|
|
return []
|
|
|
|
@classmethod
|
|
def get_context(cls, shipments, header, data):
|
|
report_context = super().get_context(shipments, header, data)
|
|
report_context['moves'] = cls.moves
|
|
return report_context
|
|
|
|
|
|
class DeliveryNote(ShipmentReport):
|
|
__name__ = 'stock.shipment.out.delivery_note'
|
|
|
|
@classmethod
|
|
def execute(cls, ids, data):
|
|
with Transaction().set_context(address_with_party=False):
|
|
return super().execute(ids, data)
|
|
|
|
@classmethod
|
|
def moves(cls, shipment):
|
|
moves = [m for m in shipment.outgoing_moves if m.state != 'cancelled']
|
|
return sort(moves, cls.moves_order(shipment))
|
|
|
|
@classmethod
|
|
def moves_order(cls, shipment):
|
|
return shipment.__class__.outgoing_moves.order
|
|
|
|
|
|
class PickingList(ShipmentReport):
|
|
__name__ = 'stock.shipment.out.picking_list'
|
|
|
|
@classmethod
|
|
def moves(cls, shipment):
|
|
if shipment.warehouse_storage == shipment.warehouse_output:
|
|
moves = shipment.outgoing_moves
|
|
else:
|
|
moves = shipment.inventory_moves
|
|
moves = [m for m in moves if m.state != 'cancelled']
|
|
return sort(moves, cls.moves_order(shipment))
|
|
|
|
@classmethod
|
|
def moves_order(cls, shipment):
|
|
return shipment.__class__.inventory_moves.order
|
|
|
|
|
|
class SupplierRestockingList(ShipmentReport):
|
|
__name__ = 'stock.shipment.in.restocking_list'
|
|
|
|
@classmethod
|
|
def moves(cls, shipment):
|
|
if shipment.warehouse_input == shipment.warehouse_storage:
|
|
moves = shipment.incoming_moves
|
|
else:
|
|
moves = shipment.inventory_moves
|
|
moves = [m for m in moves if m.state != 'cancelled']
|
|
return sort(moves, cls.moves_order(shipment))
|
|
|
|
@classmethod
|
|
def moves_order(cls, shipment):
|
|
return shipment.__class__.inventory_moves.order
|
|
|
|
|
|
class CustomerReturnRestockingList(ShipmentReport):
|
|
__name__ = 'stock.shipment.out.return.restocking_list'
|
|
|
|
@classmethod
|
|
def moves(cls, shipment):
|
|
if shipment.warehouse_input == shipment.warehouse_storage:
|
|
moves = shipment.incoming_moves
|
|
else:
|
|
moves = shipment.inventory_moves
|
|
moves = [m for m in moves if m.state != 'cancelled']
|
|
return sort(moves, cls.moves_order(shipment))
|
|
|
|
@classmethod
|
|
def moves_order(cls, shipment):
|
|
return shipment.__class__.inventory_moves.order
|
|
|
|
|
|
class InteralShipmentReport(ShipmentReport):
|
|
__name__ = 'stock.shipment.internal.report'
|
|
|
|
@classmethod
|
|
def execute(cls, ids, data):
|
|
with Transaction().set_context(address_with_party=True):
|
|
return super().execute(ids, data)
|
|
|
|
@classmethod
|
|
def moves(cls, shipment):
|
|
if shipment.transit_location:
|
|
if shipment.state == 'shipped':
|
|
moves = shipment.incoming_moves
|
|
else:
|
|
moves = shipment.outgoing_moves
|
|
else:
|
|
moves = shipment.moves
|
|
moves = [m for m in moves if m.state != 'cancelled']
|
|
return sort(moves, cls.moves_order(shipment))
|
|
|
|
@classmethod
|
|
def moves_order(cls, shipment):
|
|
if shipment.state == 'shipped':
|
|
return shipment.__class__.incoming_moves.order
|
|
else:
|
|
return shipment.__class__.outgoing_moves.order
|