424 lines
15 KiB
Python
424 lines
15 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 json
|
|
|
|
from trytond.exceptions import UserError
|
|
from trytond.i18n import gettext
|
|
from trytond.model import Model, ModelStorage, ModelView, dualmethod, fields
|
|
from trytond.model.exceptions import ValidationError
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.pyson import Eval
|
|
from trytond.transaction import Transaction
|
|
from trytond.wizard import Button, StateTransition, StateView, Wizard
|
|
|
|
|
|
class ShipmentUnassignMixin:
|
|
'''Mixin to unassign quantity from assigned shipment moves'''
|
|
__slots__ = ()
|
|
|
|
@dualmethod
|
|
def unassign(cls, shipments, moves, quantities):
|
|
'''
|
|
Unassign the quantity from the corresponding move of the shipments.
|
|
'''
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
to_unassign = []
|
|
if not all(m.state == 'assigned' for m in moves):
|
|
raise ValueError("Not assigned move")
|
|
Move.draft(moves)
|
|
for move, unassign_quantity in zip(moves, quantities):
|
|
if not unassign_quantity:
|
|
continue
|
|
if unassign_quantity > move.quantity:
|
|
raise ValueError(
|
|
"Unassigned quantity greater than move quantity")
|
|
if unassign_quantity == move.quantity:
|
|
to_unassign.append(move)
|
|
else:
|
|
with Transaction().set_context(_stock_move_split=True):
|
|
to_unassign.extend(Move.copy(
|
|
[move],
|
|
{'quantity': unassign_quantity}))
|
|
move.quantity -= unassign_quantity
|
|
Move.save(moves)
|
|
Move.assign(moves)
|
|
if to_unassign:
|
|
cls.wait(shipments, to_unassign)
|
|
|
|
|
|
class ShipmentInReturn(ShipmentUnassignMixin, metaclass=PoolMeta):
|
|
__name__ = 'stock.shipment.in.return'
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._buttons.update({
|
|
'assign_manual_wizard': {
|
|
'invisible': Eval('state') != 'waiting',
|
|
'depends': ['state'],
|
|
},
|
|
})
|
|
|
|
@classmethod
|
|
@ModelView.button_action(
|
|
'stock_assign_manual.wizard_shipment_in_return_assign_manual')
|
|
def assign_manual_wizard(cls, shipments):
|
|
pass
|
|
|
|
|
|
class ShipmentOut(ShipmentUnassignMixin, metaclass=PoolMeta):
|
|
__name__ = 'stock.shipment.out'
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._buttons.update({
|
|
'assign_manual_wizard': {
|
|
'invisible': ((Eval('state') != 'waiting')
|
|
| (Eval('warehouse_storage')
|
|
== Eval('warehouse_output'))),
|
|
'depends': [
|
|
'state', 'warehouse_storage', 'warehouse_output'],
|
|
},
|
|
})
|
|
|
|
@classmethod
|
|
@ModelView.button_action(
|
|
'stock_assign_manual.wizard_shipment_out_assign_manual')
|
|
def assign_manual_wizard(cls, shipments):
|
|
pass
|
|
|
|
|
|
class ShipmentInternal(ShipmentUnassignMixin, metaclass=PoolMeta):
|
|
__name__ = 'stock.shipment.internal'
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._buttons.update({
|
|
'assign_manual_wizard': {
|
|
'invisible': Eval('state') != 'waiting',
|
|
'depends': ['state'],
|
|
},
|
|
})
|
|
|
|
@classmethod
|
|
@ModelView.button_action(
|
|
'stock_assign_manual.wizard_shipment_internal_assign_manual')
|
|
def assign_manual_wizard(cls, shipments):
|
|
pass
|
|
|
|
|
|
class ShipmentAssignManual(Wizard):
|
|
__name__ = 'stock.shipment.assign.manual'
|
|
start_state = 'next_'
|
|
next_ = StateTransition()
|
|
show = StateView('stock.shipment.assign.manual.show',
|
|
'stock_assign_manual.shipment_assign_manual_show_view_form', [
|
|
Button("Cancel", 'end', 'tryton-cancel'),
|
|
Button("Skip", 'skip', 'tryton-forward', validate=False),
|
|
Button("Assign", 'assign', 'tryton-ok', default=True),
|
|
])
|
|
skip = StateTransition()
|
|
assign = StateTransition()
|
|
|
|
def transition_next_(self):
|
|
|
|
def next_move():
|
|
for move in self.record.assign_moves:
|
|
if move.state == 'draft' and move not in self.show.skipped:
|
|
self.show.move = move
|
|
return move
|
|
|
|
if self.show.skipped is None:
|
|
self.show.skipped = []
|
|
|
|
if not next_move():
|
|
if all(m.state == 'assigned' for m in self.record.assign_moves):
|
|
self.model.assign([self.record])
|
|
return 'end'
|
|
return 'show'
|
|
|
|
def default_show(self, fields):
|
|
defaults = {}
|
|
if 'skipped' in fields:
|
|
defaults['skipped'] = [m.id for m in self.show.skipped]
|
|
if 'move' in fields:
|
|
defaults['move'] = self.show.move.id
|
|
if 'unit' in fields:
|
|
defaults['unit'] = self.show.move.unit.id
|
|
if 'move_quantity' in fields:
|
|
defaults['move_quantity'] = self.show.move.quantity
|
|
return defaults
|
|
|
|
def transition_skip(self):
|
|
moves = list(self.show.skipped)
|
|
moves.append(self.show.move)
|
|
self.show.skipped = moves
|
|
return 'next_'
|
|
|
|
def transition_assign(self):
|
|
self.show.assign()
|
|
return 'next_'
|
|
|
|
|
|
class ShipmentAssignManualShow(ModelView):
|
|
__name__ = 'stock.shipment.assign.manual.show'
|
|
|
|
skipped = fields.Many2Many(
|
|
'stock.move', None, None, "Skipped", readonly=True)
|
|
move = fields.Many2One('stock.move', "Move", readonly=True)
|
|
quantity = fields.Float(
|
|
"Quantity", digits='unit',
|
|
domain=['OR',
|
|
('quantity', '=', None),
|
|
[
|
|
('quantity', '>', 0),
|
|
('quantity', '<=', Eval('move_quantity', 0)),
|
|
],
|
|
],
|
|
help="The maximum quantity to assign from the place.\n"
|
|
"Leave empty for the full quantity of the move.")
|
|
unit = fields.Many2One('product.uom', "Unit", readonly=True)
|
|
move_quantity = fields.Float("Move Quantity", readonly=True)
|
|
place = fields.Selection('get_places', "Place", required=True, sort=False)
|
|
place_string = place.translated('place')
|
|
|
|
@fields.depends('move')
|
|
def get_places(self, with_childs=True, grouping=('product',)):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
Location = pool.get('stock.location')
|
|
Move = pool.get('stock.move')
|
|
Product = pool.get('product.product')
|
|
Lang = pool.get('ir.lang')
|
|
lang = Lang.get()
|
|
|
|
if not self.move:
|
|
return []
|
|
|
|
if with_childs:
|
|
locations = Location.search([
|
|
('parent', 'child_of', [self.move.from_location.id]),
|
|
])
|
|
else:
|
|
locations = [self.move.from_location]
|
|
|
|
location_ids = [loc.id for loc in locations]
|
|
product_ids = [self.move.product.id]
|
|
with Transaction().set_context(company=self.move.company.id):
|
|
stock_date_end = Date.today()
|
|
with Transaction().set_context(
|
|
stock_date_end=stock_date_end,
|
|
stock_assign=True,
|
|
forecast=False,
|
|
company=self.move.company.id):
|
|
pbl = Product.products_by_location(
|
|
location_ids, with_childs=False,
|
|
grouping=grouping, grouping_filter=(product_ids,))
|
|
|
|
def get_key(move, location_id):
|
|
key = (location_id,)
|
|
for field in grouping:
|
|
value = getattr(move, field)
|
|
if isinstance(value, Model):
|
|
value = value.id
|
|
key += (value,)
|
|
return key
|
|
|
|
def match(key, pattern):
|
|
for k, p in zip(key, pattern):
|
|
if p is None or k == p:
|
|
continue
|
|
else:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
def get_name(key):
|
|
move = Move()
|
|
parts = [Location(key[0]).rec_name]
|
|
for field, value in zip(grouping, key[1:]):
|
|
setattr(move, field, value)
|
|
value = getattr(move, field)
|
|
if isinstance(value, ModelStorage):
|
|
parts.append(value.rec_name)
|
|
elif value:
|
|
parts.append(str(value))
|
|
return ' - '.join(parts)
|
|
|
|
# Prevent picking from the destination location
|
|
try:
|
|
locations.remove(self.move.to_location)
|
|
except ValueError:
|
|
pass
|
|
# Try first to pick from source location
|
|
locations.remove(self.move.from_location)
|
|
locations.insert(0, self.move.from_location)
|
|
places = [(None, '')]
|
|
quantities = self.move.sort_quantities(
|
|
pbl.items(), locations, grouping)
|
|
for key, qty in quantities:
|
|
move_key = get_key(self.move, key[0])
|
|
if qty > 0 and match(key, move_key):
|
|
uom = self.move.product.default_uom
|
|
quantity = lang.format_number_symbol(
|
|
pbl[key], uom, digits=uom.digits)
|
|
name = '%(name)s (%(quantity)s)' % {
|
|
'name': get_name(key),
|
|
'quantity': quantity
|
|
}
|
|
places.append((json.dumps(key), name))
|
|
return places
|
|
|
|
def assign(self, grouping=('product',)):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Lang = pool.get('ir.lang')
|
|
lang = Lang.get()
|
|
if self.quantity is not None:
|
|
if not (0 <= self.quantity <= self.move.quantity):
|
|
uom = self.move.product.default_uom
|
|
raise ValidationError(gettext(
|
|
'stock_assign_manual.msg_invalid_quantity',
|
|
quantity=lang.format_number(
|
|
self.move_quantity, uom.digits)))
|
|
quantity = self.move.unit.round(self.quantity)
|
|
remainder = self.move.unit.round(self.move.quantity - quantity)
|
|
self.move.quantity = quantity
|
|
self.move.save()
|
|
if remainder:
|
|
Move.copy([self.move], {'quantity': remainder})
|
|
key = json.loads(self.place)
|
|
values = self._apply(key, grouping)
|
|
quantity = self.move.quantity
|
|
Move.assign_try([self.move], with_childs=False, grouping=grouping)
|
|
if self.move.state != 'assigned':
|
|
# Restore initial values as assign_try may have saved the move
|
|
for field, value in values.items():
|
|
setattr(self.move, field, value)
|
|
self.move.save()
|
|
if self.move.quantity == quantity:
|
|
raise UserError(gettext(
|
|
'stock_assign_manual.msg_assign_failed',
|
|
move=self.move.rec_name,
|
|
place=self.place_string))
|
|
|
|
def _apply(self, key, grouping):
|
|
"""Update the move according to the key
|
|
and return a dictionary with the initial values."""
|
|
values = {'from_location': self.move.from_location.id}
|
|
location_id = key[0]
|
|
self.move.from_location = location_id
|
|
for field, value in zip(grouping, key[1:]):
|
|
if value is not None and '.' not in field:
|
|
values[field] = getattr(self.move, field)
|
|
setattr(self.move, field, value)
|
|
return values
|
|
|
|
|
|
class ShipmentUnassignManual(Wizard):
|
|
__name__ = 'stock.shipment.unassign.manual'
|
|
start = StateTransition()
|
|
show = StateView('stock.shipment.unassign.manual.show',
|
|
'stock_assign_manual.shipment_unassign_manual_show_view_form', [
|
|
Button("Cancel", 'end', 'tryton-cancel'),
|
|
Button("Unassign", 'unassign', 'tryton-ok', default=True),
|
|
])
|
|
unassign = StateTransition()
|
|
|
|
def transition_start(self):
|
|
moves = self.record.assign_moves
|
|
if any(m.state == 'assigned' for m in moves):
|
|
return 'show'
|
|
return 'end'
|
|
|
|
def default_show(self, fields):
|
|
moves = self.record.assign_moves
|
|
move_ids = [m.id for m in moves if m.state == 'assigned']
|
|
return {
|
|
'assigned_moves': move_ids,
|
|
}
|
|
|
|
def transition_unassign(self):
|
|
moves = []
|
|
quantities = []
|
|
for m in self.show.moves:
|
|
moves.append(m.move)
|
|
quantities.append(m.unassigned_quantity)
|
|
self.record.unassign(moves, quantities)
|
|
return 'end'
|
|
|
|
|
|
class ShipmentAssignedMove(ModelView):
|
|
__name__ = 'stock.shipment.assigned.move'
|
|
|
|
move = fields.Many2One('stock.move', "Move", required=True)
|
|
unassigned_quantity = fields.Float(
|
|
"Unassigned Quantity", digits='unit',
|
|
domain=['OR',
|
|
('unassigned_quantity', '=', None),
|
|
[
|
|
('unassigned_quantity', '>=', 0),
|
|
('unassigned_quantity', '<=', Eval('move_quantity', 0)),
|
|
],
|
|
],
|
|
help="The quantity to unassign")
|
|
assigned_quantity = fields.Float(
|
|
"Assigned Quantity", digits='unit',
|
|
domain=['OR',
|
|
('assigned_quantity', '=', None),
|
|
[
|
|
('assigned_quantity', '>=', 0),
|
|
('assigned_quantity', '<=', Eval('move_quantity', 0)),
|
|
],
|
|
],
|
|
help="The quantity left assigned")
|
|
unit = fields.Function(
|
|
fields.Many2One('product.uom', "Unit"), 'on_change_with_unit')
|
|
move_quantity = fields.Function(
|
|
fields.Float("Move Quantity"), 'on_change_with_move_quantity')
|
|
|
|
@staticmethod
|
|
def default_unassigned_quantity():
|
|
return 0.0
|
|
|
|
@fields.depends('move', 'unassigned_quantity', 'assigned_quantity')
|
|
def on_change_move(self, name=None):
|
|
if self.move:
|
|
self.assigned_quantity = self.move.quantity
|
|
self.unassigned_quantity = 0.0
|
|
|
|
@fields.depends('assigned_quantity', 'move', 'unassigned_quantity', 'unit')
|
|
def on_change_unassigned_quantity(self, name=None):
|
|
if self.move and self.unassigned_quantity:
|
|
self.assigned_quantity = self.unit.round(
|
|
self.move.quantity - self.unassigned_quantity)
|
|
|
|
@fields.depends('unassigned_quantity', 'move', 'assigned_quantity', 'unit')
|
|
def on_change_assigned_quantity(self, name=None):
|
|
if self.move and self.assigned_quantity:
|
|
self.unassigned_quantity = self.unit.round(
|
|
self.move.quantity - self.assigned_quantity)
|
|
|
|
@fields.depends('move')
|
|
def on_change_with_unit(self, name=None):
|
|
return self.move.unit if self.move else None
|
|
|
|
@fields.depends('move')
|
|
def on_change_with_move_quantity(self, name=None):
|
|
if self.move:
|
|
return self.move.quantity
|
|
|
|
|
|
class ShipmentUnassignManualShow(ModelView):
|
|
__name__ = 'stock.shipment.unassign.manual.show'
|
|
|
|
moves = fields.One2Many(
|
|
'stock.shipment.assigned.move', None, "Moves",
|
|
domain=[('move.id', 'in', Eval('assigned_moves'))],
|
|
help="The moves to unassign.")
|
|
assigned_moves = fields.Many2Many(
|
|
'stock.move', None, None, "Assigned Moves")
|