diff --git a/addons/stock/__manifest__.py b/addons/stock/__manifest__.py
index 58f16ed5757..46c9e4a8ce9 100644
--- a/addons/stock/__manifest__.py
+++ b/addons/stock/__manifest__.py
@@ -54,7 +54,6 @@
'views/procurement_views.xml',
'views/stock_warehouse_views.xml',
'views/stock_move_views.xml',
- 'views/stock_pack_operation_views.xml',
'views/product_views.xml',
'views/stock_config_settings_views.xml',
diff --git a/addons/stock/data/stock_demo.yml b/addons/stock/data/stock_demo.yml
index 13d82adf10b..0263730991a 100644
--- a/addons/stock/data/stock_demo.yml
+++ b/addons/stock/data/stock_demo.yml
@@ -91,12 +91,12 @@
picking_type_id: stock.picking_type_out
location_id: stock.stock_location_stock
location_dest_id: stock.stock_location_customers
- pack_operation_ids:
- - product_id: product.product_product_6
- product_qty: 100
- product_uom_id: product.product_uom_unit
- location_id: stock.stock_location_stock
- location_dest_id: stock.stock_location_customers
+ pack_operation_ids:
+ - product_id: product.product_product_6
+ qty_done: 100
+ product_uom_id: product.product_uom_unit
+ location_id: stock.stock_location_stock
+ location_dest_id: stock.stock_location_customers
-
!record {model: stock.picking, id: outgoing_shipment_main_warehouse2}:
picking_type_id: stock.picking_type_out
@@ -231,12 +231,12 @@
location_id: stock.stock_location_suppliers
location_dest_id: stock.stock_location_shop0
state: done
- pack_operation_ids:
- - product_id: product.product_delivery_01
- product_qty: 100
- product_uom_id: product.product_uom_unit
- location_id: stock.stock_location_suppliers
- location_dest_id: stock.stock_location_shop0
+ pack_operation_ids:
+ - product_id: product.product_delivery_01
+ qty_done: 100
+ product_uom_id: product.product_uom_unit
+ location_id: stock.stock_location_suppliers
+ location_dest_id: stock.stock_location_shop0
-
!record {model: stock.picking, id: incomming_chicago_warehouse2}:
picking_type_id: chi_picking_type_in
@@ -296,12 +296,12 @@
picking_type_id: chi_picking_type_out
location_id: stock.stock_location_shop0
location_dest_id: stock.stock_location_customers
- pack_operation_ids:
- - product_id: product.product_product_6
- product_qty: 100
- product_uom_id: product.product_uom_unit
- location_id: stock.stock_location_shop0
- location_dest_id: stock.stock_location_customers
+ pack_operation_ids:
+ - product_id: product.product_product_6
+ qty_done: 100
+ product_uom_id: product.product_uom_unit
+ location_id: stock.stock_location_shop0
+ location_dest_id: stock.stock_location_customers
-
!record {model: stock.picking, id: outgoing_chicago_warehouse2}:
picking_type_id: chi_picking_type_out
diff --git a/addons/stock/models/procurement.py b/addons/stock/models/procurement.py
index 011fe65ea8c..8ac7fc638d7 100644
--- a/addons/stock/models/procurement.py
+++ b/addons/stock/models/procurement.py
@@ -77,7 +77,7 @@ class ProcurementOrder(models.Model):
def propagate_cancels(self):
# set the context for the propagation of the procurement cancellation
# TDE FIXME: was in cancel, moved here for consistency
- cancel_moves = self.with_context(cancel_procurement=True).filtered(lambda order: order.rule_id.action == 'move').mapped('move_ids')
+ cancel_moves = self.filtered(lambda order: order.rule_id.action == 'move').mapped('move_ids').filtered(lambda m: m.state not in ('cancel', 'done'))
if cancel_moves:
cancel_moves.action_cancel()
return self.search([('move_dest_id', 'in', cancel_moves.filtered(lambda move: move.propagate).ids)])
@@ -161,7 +161,7 @@ class ProcurementOrder(models.Model):
'partner_id': self.rule_id.partner_address_id.id or (self.group_id and self.group_id.partner_id.id) or False,
'location_id': self.rule_id.location_src_id.id,
'location_dest_id': self.location_id.id,
- 'move_dest_id': self.move_dest_id and self.move_dest_id.id or False,
+ 'move_dest_ids': self.move_dest_id and [(4, self.move_dest_id.id)] or False,
'procurement_id': self.id,
'rule_id': self.rule_id.id,
'procure_method': self.rule_id.procure_method,
diff --git a/addons/stock/models/stock_location.py b/addons/stock/models/stock_location.py
index 62c447360f4..b96421dfd34 100644
--- a/addons/stock/models/stock_location.py
+++ b/addons/stock/models/stock_location.py
@@ -117,6 +117,10 @@ class Location(models.Model):
('view_location_id.parent_left', '<=', self.parent_left),
('view_location_id.parent_right', '>=', self.parent_left)], limit=1)
+ def should_impact_quants(self):
+ self.ensure_one()
+ return False if self.usage in ('supplier', 'inventory', 'production', 'customer') else True
+
class Route(models.Model):
_name = 'stock.location.route'
@@ -223,7 +227,7 @@ class PushedFlow(models.Model):
else:
new_move_vals = self._prepare_move_copy_values(move, new_date)
new_move = move.copy(new_move_vals)
- move.write({'move_dest_id': new_move.id})
+ move.write({'move_dest_ids': [(4, new_move.id)]})
new_move.action_confirm()
def _prepare_move_copy_values(self, move_to_copy, new_date):
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 46d68d62f3a..7d785e00358 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -3,6 +3,8 @@
from datetime import datetime
from dateutil import relativedelta
+from itertools import groupby
+from operator import itemgetter
from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
@@ -80,31 +82,34 @@ class StockMove(models.Model):
'res.partner', 'Destination Address ',
states={'done': [('readonly', True)]},
help="Optional address where goods are to be delivered, specifically used for allotment")
- move_dest_id = fields.Many2one(
- 'stock.move', 'Destination Move',
- copy=False, index=True,
+ move_dest_ids = fields.Many2many(
+ 'stock.move', 'stock_move_move_rel', 'move_orig_id', 'move_dest_id', 'Destination Moves',
+ copy=False,
help="Optional: next stock move when chaining them")
- move_orig_ids = fields.One2many(
- 'stock.move', 'move_dest_id', 'Original Move',
+ move_orig_ids = fields.Many2many(
+ 'stock.move', 'stock_move_move_rel', 'move_dest_id', 'move_orig_id', 'Original Move',
+ copy=False,
help="Optional: previous stock move when chaining them")
picking_id = fields.Many2one('stock.picking', 'Transfer Reference', index=True, states={'done': [('readonly', True)]})
picking_partner_id = fields.Many2one('res.partner', 'Transfer Destination Address', related='picking_id.partner_id')
note = fields.Text('Notes')
+ # TODO: state should be computed according to the move lines
state = fields.Selection([
('draft', 'New'), ('cancel', 'Cancelled'),
- ('waiting', 'Waiting Another Move'), ('confirmed', 'Waiting Availability'),
- ('assigned', 'Available'), ('done', 'Done')], string='Status',
+ ('waiting', 'Waiting Another Move'),
+ ('confirmed', 'Waiting Availability'),
+ ('partially_available', 'Partially Available'),
+ ('assigned', 'Available'),
+ ('done', 'Done')], string='Status',
copy=False, default='draft', index=True, readonly=True,
help="* New: When the stock move is created and not yet confirmed.\n"
"* Waiting Another Move: This state can be seen when a move is waiting for another one, for example in a chained flow.\n"
"* Waiting Availability: This state is reached when the procurement resolution is not straight forward. It may need the scheduler to run, a component to be manufactured...\n"
"* Available: When products are reserved, it is set to \'Available\'.\n"
"* Done: When the shipment is processed, the state is \'Done\'.")
- partially_available = fields.Boolean('Partially Available', copy=False, readonly=True, help="Checks if the move has some stock reserved")
price_unit = fields.Float(
'Unit Price', help="Technical field used to record the product cost set by the user during a picking confirmation (when costing "
"method used is 'average price' or 'real'). Value given in company currency and in product uom.") # as it's a technical field, we intentionally don't provide the digits attribute
- split_from = fields.Many2one('stock.move', "Move Split From", copy=False, help="Technical field used to track the origin of a split move, which can be useful in case of debug")
backorder_id = fields.Many2one('stock.picking', 'Back Order of', related='picking_id.backorder_id', index=True)
origin = fields.Char("Source Document")
procure_method = fields.Selection([
@@ -116,9 +121,6 @@ class StockMove(models.Model):
"its current stock) to gather products. If we want to chain moves and have this one to wait for the previous,"
"this second option should be chosen.")
scrapped = fields.Boolean('Scrapped', related='location_dest_id.scrap_location', readonly=True, store=True)
- linked_move_operation_ids = fields.One2many(
- 'stock.move.operation.link', 'move_id', 'Linked Operations', readonly=True,
- help='Operations that impact this move for the computation of the remaining quantities')
remaining_qty = fields.Float(
'Remaining Quantity', compute='_get_remaining_qty',
digits=0, states={'done': [('readonly', True)]},
@@ -132,6 +134,7 @@ class StockMove(models.Model):
help='If checked, when this move is cancelled, cancel the linked move too')
picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type')
inventory_id = fields.Many2one('stock.inventory', 'Inventory')
+ pack_operation_ids = fields.One2many('stock.pack.operation', 'move_id')
origin_returned_move_id = fields.Many2one('stock.move', 'Origin return move', copy=False, help='Move that created the return move')
returned_move_ids = fields.One2many('stock.move', 'origin_returned_move_id', 'All returned moves', help='Optional: all returned moves created from this move')
reserved_availability = fields.Float(
@@ -147,12 +150,42 @@ class StockMove(models.Model):
restrict_partner_id = fields.Many2one('res.partner', 'Owner ', help="Technical field used to depict a restriction on the ownership of quants to consider when marking this move as 'done'")
route_ids = fields.Many2many('stock.location.route', 'stock_location_route_move', 'move_id', 'route_id', 'Destination route', help="Preferred route to be followed by the procurement order")
warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', help="Technical field depicting the warehouse to consider for the route selection on the next procurement (if any).")
+ has_tracking = fields.Selection(related='product_id.tracking', string='Product with Tracking')
+ quantity_done = fields.Float('Quantity', compute='_qty_done_compute', digits=dp.get_precision('Product Unit of Measure'))
@api.one
@api.depends('product_id', 'product_uom', 'product_uom_qty')
def _compute_product_qty(self):
- if self.product_uom:
- self.product_qty = self.product_uom._compute_quantity(self.product_uom_qty, self.product_id.uom_id)
+ self.product_qty = self.product_uom._compute_quantity(self.product_uom_qty, self.product_id.uom_id)
+
+ def _get_move_lines(self):
+ """ This will return the move lines to consider when applying _quantity_done_compute on a stock.move.
+ In some context, such as MRP, it is necessary to compute quantity_done on filtered sock.move.line."""
+ self.ensure_one()
+ return self.pack_operation_ids
+
+ @api.multi
+ @api.depends('pack_operation_ids.qty_done', 'pack_operation_ids.product_uom_id')
+ def _quantity_done_compute(self):
+ for move in self:
+ for move_line in move._get_move_lines():
+ # Transform the move_line quantity_done into the move uom.
+ move.quantity_done += move_line.product_uom_id._compute_quantity(move_line.qty_done, move.product_uom)
+
+ @api.multi
+ def _quantity_done_set(self):
+ quantity_done = self[0].quantity_done # any call to create will invalidate `move.quantity_done`
+ for move in self:
+ move_lines = move._get_move_lines()
+ if not move_lines:
+ if quantity_done:
+ # do not impact reservation here
+ move_line = self.env['stock.pack.operation'].create(dict(move._prepare_move_line_vals(), qty_done=quantity_done))
+ move.write({'pack_operation_ids': [(4, move_line.id)]})
+ elif len(move_lines) == 1:
+ move_lines[0].qty_done = quantity_done
+ else:
+ raise UserError("Cannot set the done quantity from this stock move, work directly with the move lines.")
def _set_product_qty(self):
""" The meaning of product_qty field changed lately and is now a functional field computing the quantity
@@ -162,24 +195,32 @@ class StockMove(models.Model):
raise UserError(_('The requested operation cannot be processed because of a programming error setting the `product_qty` field instead of the `product_uom_qty`.'))
@api.one
- @api.depends('linked_move_operation_ids.qty')
def _get_remaining_qty(self):
- self.remaining_qty = float_round(self.product_qty - sum(self.mapped('linked_move_operation_ids').mapped('qty')), precision_rounding=self.product_id.uom_id.rounding)
+ # TODO: sle jco
+ pass
@api.one
- @api.depends('reserved_quant_ids.qty')
+ @api.depends('pack_operation_ids.product_qty')
def _compute_reserved_availability(self):
- self.reserved_availability = sum(self.mapped('reserved_quant_ids').mapped('qty'))
+ """ Fill the `availability` field on a stock move, which is the actual reserved quantity
+ and is represented by the aggregated `product_qty` on the linked move lines. If the move
+ is force assigned, the value will be 0.
+ """
+ self.reserved_availability = self.product_id.uom_id._compute_quantity(sum(self.pack_operation_ids.mapped('product_qty')), self.product_uom, rounding_method='HALF-UP')
@api.one
@api.depends('state', 'product_id', 'product_qty', 'location_id')
def _compute_product_availability(self):
+ """ Fill the `availability` field on a stock move, which is the quantity to potentially
+ reserve. When the move is done, `availability` is set to the quantity the move did actually
+ move.
+ """
if self.state == 'done':
self.availability = self.product_qty
else:
- quants = self.env['stock.quant'].search([('location_id', 'child_of', self.location_id.id), ('product_id', '=', self.product_id.id), ('reservation_id', '=', False)])
- self.availability = min(self.product_qty, sum(quants.mapped('qty')))
+ total_availability = self.env['stock.quant']._get_available_quantity(self.product_id, self.location_id)
+ self.availability = min(self.product_qty, total_availability)
@api.multi
def _compute_string_qty_information(self):
@@ -242,6 +283,10 @@ class StockMove(models.Model):
@api.multi
def write(self, vals):
+ # FIXME: pim fix your crap
+ if vals.get('product_uom_qty') and self.env.context.get('do_not_unreserve') is None:
+ self.filtered(lambda m: m.state not in ['draft', 'done', 'cancel']).do_unreserve()
+
# TDE CLEANME: it is a gros bordel + tracking
Picking = self.env['stock.picking']
# Check that we do not modify a stock.move which is done
@@ -262,7 +307,7 @@ class StockMove(models.Model):
if not self._context.get('do_not_propagate', False) and (propagated_date_field or propagated_changes_dict):
#any propagation is (maybe) needed
for move in self:
- if move.move_dest_id and move.propagate:
+ if move.move_dest_ids and move.propagate:
if 'date_expected' in propagated_changes_dict:
propagated_changes_dict.pop('date_expected')
if propagated_date_field:
@@ -290,6 +335,43 @@ class StockMove(models.Model):
pickings.message_track(pickings.fields_get(['state']), initial_values)
return res
+ @api.multi
+ def action_show_details(self):
+ """ Returns an action that will open a form view (in a popup) allowing to work on all the
+ move lines of a particular move. This form view is used when "show operations" is not
+ checked on the picking type.
+ """
+ self.ensure_one()
+
+ # If "show suggestions" is not checked on the picking type, we have to filter out the
+ # reserved move lines. We do this by displaying `pack_operation_nosuggest_ids`. We use
+ # different views to display one field or another so that the webclient doesn't have to
+ # fetch both.
+ if self.picking_id.picking_type_id.show_reserved:
+ view = self.env.ref('stock.view_stock_move_operations')
+ else:
+ view = self.env.ref('stock.view_stock_move_nosuggest_operations')
+
+ return {
+ 'name': _('Detailed Operations'),
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'stock.move',
+ 'views': [(view.id, 'form')],
+ 'view_id': view.id,
+ 'target': 'new',
+ 'res_id': self.id,
+ 'context': dict(
+ self.env.context,
+ show_lots_m2o=self.has_tracking != 'none' and (self.picking_type_id.use_existing_lots or self.state == 'done'), # able to create lots, whatever the value of ` use_create_lots`.
+ show_lots_text=self.has_tracking != 'none' and self.picking_type_id.use_create_lots and not self.picking_type_id.use_existing_lots and self.state != 'done',
+ show_source_location=self.location_id.child_ids,
+ show_destination_location=self.location_dest_id.child_ids,
+ show_package=not self.location_id.usage == 'supplier',
+ ),
+ }
+
# Misc tools
# ------------------------------------------------------------
@@ -297,18 +379,6 @@ class StockMove(models.Model):
""" Returns the unit price to store on the quant """
return self.price_unit or self.product_id.standard_price
- @api.returns('self')
- @api.multi # TDE: DECORATOR to remove
- def get_ancestors(self):
- '''Find the first level ancestors of given move '''
- ancestors = self.env['stock.move']
- move = self
- while move:
- ancestors |= move.move_orig_ids
- move = not move.move_orig_ids and move.split_from or False
- return ancestors
- find_move_ancestors = get_ancestors
-
def _filter_closed_moves(self):
""" Helper methods when having to avoid working on moves that are
already done or canceled. In a lot of cases you may handle a batch
@@ -323,14 +393,24 @@ class StockMove(models.Model):
@api.multi
def do_unreserve(self):
- pass
+ if any(move.state in ('done', 'cancel') for move in self):
+ raise UserError(_('Cannot unreserve a done move'))
+ for move in self:
+ move.move_line_ids.unlink()
+ if(move.procure_method == 'make_to_order' and not move.move_orig_ids):
+ move.state = 'waiting'
+ elif(move.move_orig_ids and not all(orig.state in ('done', 'cancel') for orig in move.move_orig_ids)):
+ move.state = 'waiting'
+ else:
+ move.state = 'confirmed'
+ return True
def _push_apply(self):
# TDE CLEANME: I am quite sure I already saw this code somewhere ... in routing ??
Push = self.env['stock.location.path']
for move in self:
# if the move is already chained, there is no need to check push rules
- if move.move_dest_id:
+ if move.move_dest_ids:
continue
# if the move is a returned move, we don't want to check push rules, as returning a returned move is the only decent way
# to receive goods without triggering the push rules again (which would duplicate chained operations)
@@ -348,7 +428,6 @@ class StockMove(models.Model):
# Make sure it is not returning the return
if rules and (not move.origin_returned_move_id or move.origin_returned_move_id.location_dest_id.id != rules.location_dest_id.id):
rules._apply(move)
- return True
@api.onchange('product_id', 'product_qty')
def onchange_quantity(self):
@@ -372,6 +451,19 @@ class StockMove(models.Model):
if self.date_expected:
self.date = self.date_expected
+ @api.onchange('product_uom')
+ def onchange_product_uom(self):
+ if self.product_uom.factor > self.product_id.uom_id.factor:
+ return {
+ 'warning': {
+ 'title': "Unsafe unit of measure",
+ 'message': _("You are using a unit of measure smaller than the one you are using in "
+ "order to stock your product. This can lead to rounding problem on reserved quantity! "
+ "You should use the smaller unit of measure possible in order to valuate your stock or "
+ "change its rounding precision to a smaller value (example: 0.00001)."),
+ }
+ }
+
# TDE DECORATOR: remove that api.multi when action_confirm is migrated
@api.multi
def assign_picking(self):
@@ -431,20 +523,11 @@ class StockMove(models.Model):
# if the move is preceeded, then it's waiting (if preceeding move is done, then action_assign has been called already and its state is already available)
if move.move_orig_ids:
move_waiting |= move
- # if the move is split and some of the ancestor was preceeded, then it's waiting as well
else:
- inner_move = move.split_from
- while inner_move:
- if inner_move.move_orig_ids:
- move_waiting |= move
- break
- inner_move = inner_move.split_from
+ if move.procure_method == 'make_to_order':
+ move_create_proc |= move
else:
- if move.procure_method == 'make_to_order':
- move_create_proc |= move
- else:
- move_to_confirm |= move
-
+ move_to_confirm |= move
if not move.picking_id and move.picking_type_id:
key = (move.group_id.id, move.location_id.id, move.location_dest_id.id)
if key not in to_assign:
@@ -477,6 +560,7 @@ class StockMove(models.Model):
attribute_price = set_default_price_unit_from_product
def _prepare_procurement_from_move(self):
+ self.ensure_one()
origin = (self.group_id and (self.group_id.name + ":") or "") + (self.rule_id and self.rule_id.name or self.origin or self.picking_id.name or "/")
group_id = self.group_id and self.group_id.id or False
if self.rule_id:
@@ -502,18 +586,11 @@ class StockMove(models.Model):
@api.multi
def force_assign(self):
- # TDE CLEANME: removed return value
- self.write({'state': 'assigned'})
- self.check_recompute_pack_op()
-
- # TDE DECORATOR: internal
- @api.multi
- def check_recompute_pack_op(self):
- pickings = self.mapped('picking_id').filtered(lambda picking: picking.state not in ('waiting', 'confirmed')) # In case of 'all at once' delivery method it should not prepare pack operations
- # Check if someone was treating the picking already
- pickings_partial = pickings.filtered(lambda picking: not any(operation.qty_done for operation in picking.pack_operation_ids))
- pickings_partial.do_prepare_partial()
- (pickings - pickings_partial).write({'recompute_pack_op': True})
+ """ Allow to work on stock move lines even if the reservationis not possible. We just mark
+ the move as assigned, so the view does not block the user.
+ """
+ for move in self.filtered(lambda m: m.state in ['confirmed', 'waiting', 'partially_available', 'assigned']):
+ move.write({'state': 'assigned'})
@api.multi
def check_tracking(self, pack_operation):
@@ -526,151 +603,263 @@ class StockMove(models.Model):
not (move.restrict_lot_id or (pack_operation and (pack_operation.product_id and pack_operation.pack_lot_ids)) or (pack_operation and not pack_operation.product_id)):
raise UserError(_('You need to provide a Lot/Serial Number for product %s') % move.product_id.name)
+ def _prepare_move_line_vals(self, quantity=None, reserved_quant=None):
+ self.ensure_one()
+ # apply putaway
+ location_dest_id = self.location_dest_id.get_putaway_strategy(self.product_id).id or self.location_dest_id.id
+ vals = {
+ 'move_id': self.id,
+ 'product_id': self.product_id.id,
+ 'product_uom_id': self.product_uom.id,
+ 'location_id': self.location_id.id,
+ 'location_dest_id': location_dest_id,
+ 'picking_id': self.picking_id.id,
+ }
+ if quantity:
+ uom_quantity = self.product_id.uom_id._compute_quantity(quantity, self.product_uom, rounding_method='HALF-UP')
+ vals = dict(vals, product_uom_qty=uom_quantity)
+ if reserved_quant:
+ vals = dict(
+ vals,
+ location_id=reserved_quant.location_id.id,
+ lot_id=reserved_quant.lot_id.id or False,
+ package_id=reserved_quant.package_id.id or False,
+ owner_id =reserved_quant.owner_id.id or False,
+ )
+ return vals
+
+ def _increase_reserved_quantity(self, need, available_quantity, location_id, lot_id=None, package_id=None, owner_id=None, strict=True):
+ """ Create or update move lines.
+ """
+ self.ensure_one()
+
+ if not lot_id:
+ lot_id = self.env['stock.production.lot']
+ if not package_id:
+ package_id = self.env['stock.quant.package']
+ if not owner_id:
+ owner_id = self.env['res.partner']
+
+ taken_quantity = min(available_quantity, need)
+
+ # Find a candidate move line to update or create a new one.
+ quants = self.env['stock.quant']._increase_reserved_quantity(
+ self.product_id, location_id, taken_quantity, lot_id=lot_id,
+ package_id=package_id, owner_id=owner_id, strict=strict
+ )
+ for reserved_quant, quantity in quants:
+ to_update = self.pack_operation_ids.filtered(lambda m: m.location_id.id == reserved_quant.location_id.id and m.lot_id.id == reserved_quant.lot_id.id and m.package_id.id == reserved_quant.package_id.id and m.owner_id.id == reserved_quant.owner_id.id)
+ if to_update:
+ to_update[0].with_context(bypass_reservation_update=True).product_uom_qty += self.product_id.uom_id._compute_quantity(taken_quantity, self.product_uom, rounding_method='HALF-UP')
+ else:
+ if self.product_id.tracking == 'serial':
+ for i in range(0, int(quantity)):
+ self.env['stock.pack.operation'].create(self._prepare_move_line_vals(quantity=1, reserved_quant=reserved_quant))
+ else:
+ self.env['stock.pack.operation'].create(self._prepare_move_line_vals(quantity=quantity, reserved_quant=reserved_quant))
+ return taken_quantity
+
@api.multi
def action_assign(self):
- return True
+ """ Reserve stock moves by creating their stock move lines. A stock move is
+ considered reserved once the sum of `product_qty` for all its move lines is
+ equal to its `product_qty`. If it is less, the stock move is considered
+ partially available.
+ """
+ for move in self.filtered(lambda m: m.state in ['confirmed', 'waiting', 'partially_available']):
+ if move.location_id.usage in ('supplier', 'inventory', 'production', 'customer')\
+ or move.product_id.type == 'consu':
+ # create the move line(s) but do not impact quants
+ if move.product_id.tracking == 'serial':
+ for i in range(0, int(move.product_qty)):
+ move_line_id = self.env['stock.pack.operation'].create(move._prepare_move_line_vals(quantity=1))
+ move.write({'pack_operation_ids': [(4, move_line_id.id, 0)]})
+ else:
+ move_line_id = self.env['stock.pack.operation'].create(move._prepare_move_line_vals(quantity=move.product_qty))
+ move.write({'pack_operation_ids': [(4, move_line_id.id, 0)]})
+ move.write({'state': 'assigned'})
+ else:
+ if not move.move_orig_ids:
+ if move.procure_method == 'make_to_order':
+ continue
+ # Reserve new quants and create move lines accordingly.
+ available_quantity = self.env['stock.quant']._get_available_quantity(move.product_id, move.location_id)
+ if available_quantity <= 0:
+ continue
+ need = move.product_qty - move.reserved_availability
+ taken_quantity = move._increase_reserved_quantity(need, available_quantity, move.location_id, strict=False)
+ if need == taken_quantity:
+ move.state = 'assigned'
+ else:
+ move.state = 'partially_available'
+ else:
+ # Check what our parents brought and what our siblings took in order to
+ # determine what we can distribute.
+ move_lines_in = move.move_orig_ids.filtered(lambda m: m.state == 'done').mapped('pack_operation_ids')
+ keys_in = ['location_dest_id', 'lot_id', 'result_package_id', 'owner_id']
+ grouped_move_lines_in = {}
+ for k, g in groupby(sorted(move_lines_in, key=itemgetter(*keys_in)), key=itemgetter(*keys_in)):
+ grouped_move_lines_in[k] = sum(self.env['stock.pack.operation'].concat(*list(g)).mapped('qty_done'))
+
+ move_lines_out_done = (move.move_orig_ids.mapped('move_dest_ids') - move)\
+ .filtered(lambda m: m.state in ['done'])\
+ .mapped('pack_operation_ids')
+ move_lines_out_reserved = (move.move_orig_ids.mapped('move_dest_ids') - move)\
+ .filtered(lambda m: m.state in ['partially_available', 'assigned'])\
+ .mapped('pack_operation_ids')
+ keys_out = ['location_id', 'lot_id', 'package_id', 'owner_id']
+ grouped_move_lines_out = {}
+ for k, g in groupby(sorted(move_lines_out_done, key=itemgetter(*keys_out)), key=itemgetter(*keys_out)):
+ grouped_move_lines_out[k] = sum(self.env['stock.pack.operation'].concat(*list(g)).mapped('qty_done'))
+ for k, g in groupby(sorted(move_lines_out_reserved, key=itemgetter(*keys_out)), key=itemgetter(*keys_out)):
+ grouped_move_lines_out[k] = sum(self.env['stock.pack.operation'].concat(*list(g)).mapped('product_qty'))
+ available_move_lines = {key: grouped_move_lines_in[key] - grouped_move_lines_out.get(key, 0) for key in grouped_move_lines_in.keys()}
+ # pop key if the quantity available amount to 0
+ available_move_lines = dict((k, v) for k, v in available_move_lines.iteritems() if v)
+
+ if not available_move_lines:
+ continue
+ for move_line in move.move_line_ids.filtered(lambda m: m.product_qty):
+ if available_move_lines.get((move_line.location_id, move_line.lot_id, move_line.result_package_id, move_line.owner_id)):
+ available_move_lines[(move_line.location_id, move_line.lot_id, move_line.result_package_id, move_line.owner_id)] -= move_line.product_qty
+ for (location_id, lot_id, package_id, owner_id), quantity in available_move_lines.items():
+ need = move.product_qty - sum(move.pack_operation_ids.mapped('product_qty'))
+ taken_quantity = move._increase_reserved_quantity(need, quantity, location_id, lot_id, package_id, owner_id)
+ if need - taken_quantity == 0.0:
+ move.state = 'assigned'
+ break
+ if move.state != 'partially_available':
+ move.state = 'partially_available'
@api.multi
def action_cancel(self):
+ if any(move.state == 'done' for move in self):
+ raise UserError(_('You cannot cancel a stock move that has been set to \'Done\'.'))
+ for move in self:
+ move.do_unreserve()
+ siblings_states = (move.move_dest_ids.mapped('move_orig_ids') - move).mapped('state')
+ if move.propagate:
+ # only cancel the next move if all my siblings are also cancelled
+ if all(state == 'cancel' for state in siblings_states):
+ move.move_dest_ids.action_cancel()
+ else:
+ if all(state in ('done', 'cancel') for state in siblings_states):
+ move.move_dest_ids.write({'procure_method': 'make_to_stock'})
+ move.move_dest_ids.write({'move_orig_ids': [(3, move.id, 0)]})
+ self.write({'state': 'cancel', 'move_orig_ids': [(5, 0, 0)]})
+ self.mapped('procurement_id').check()
return True
@api.multi
- def action_done(self):
- """ Process completely the moves given and if all moves are done, it will finish the picking. """
- self.filtered(lambda move: move.state == 'draft').action_confirm()
+ def _create_extra_move(self):
+ """ If the quantity done on a move exceeds its quantity todo, this method will create an
+ extra move attached to a (potentially split) move line. If the previous condition is not
+ met, it'll return an empty recordset.
+
+ The rationale for the creation of an extra move is the application of a potential push
+ rule that will handle the extra quantities.
+ """
+ self.ensure_one()
+ extra_move = self.env['stock.move']
+ rounding = self.product_uom.rounding
+ # moves created after the picking is assigned do not have `product_uom_qty`, but we shouldn't create extra moves for them
+ if self.product_uom_qty and float_compare(self.quantity_done, self.product_uom_qty, precision_rounding=rounding) > 0:
+ # create the extra moves
+ extra_move_quantity = float_round(
+ self.quantity_done - self.product_uom_qty,
+ precision_rounding=self.product_uom.rounding,
+ rounding_method ='UP')
+ extra_move_vals = {
+ 'product_uom_qty': extra_move_quantity,
+ 'picking_id': self.picking_id.id}
+ extra_move = self.copy(default=extra_move_vals).action_confirm()
- Uom = self.env['product.uom']
- Quant = self.env['stock.quant']
-
- pickings = self.env['stock.picking']
- procurements = self.env['procurement.order']
- operations = self.env['stock.pack.operation']
-
- remaining_move_qty = {}
-
- for move in self:
- if move.picking_id:
- pickings |= move.picking_id
- remaining_move_qty[move.id] = move.product_qty
- for link in move.linked_move_operation_ids:
- operations |= link.operation_id
- pickings |= link.operation_id.picking_id
-
- # Sort operations according to entire packages first, then package + lot, package only, lot only
- operations = operations.sorted(key=lambda x: ((x.package_id and not x.product_id) and -4 or 0) + (x.package_id and -2 or 0) + (x.pack_lot_ids and -1 or 0))
-
- for operation in operations:
-
- # product given: result put immediately in the result package (if False: without package)
- # but if pack moved entirely, quants should not be written anything for the destination package
- quant_dest_package_id = operation.product_id and operation.result_package_id.id or False
- entire_pack = not operation.product_id and True or False
-
- # compute quantities for each lot + check quantities match
- lot_quantities = dict((pack_lot.lot_id.id, operation.product_uom_id._compute_quantity(pack_lot.qty, operation.product_id.uom_id)
- ) for pack_lot in operation.pack_lot_ids)
-
- qty = operation.product_qty
- if operation.product_uom_id and operation.product_uom_id != operation.product_id.uom_id:
- qty = operation.product_uom_id._compute_quantity(qty, operation.product_id.uom_id)
- if operation.pack_lot_ids and float_compare(sum(pycompat.values(lot_quantities)), qty, precision_rounding=operation.product_id.uom_id.rounding) != 0.0:
- raise UserError(_('You have a difference between the quantity on the operation and the quantities specified for the lots. '))
-
- quants_taken = []
- false_quants = []
- lot_move_qty = {}
-
- prout_move_qty = {}
- for link in operation.linked_move_operation_ids:
- prout_move_qty[link.move_id] = prout_move_qty.get(link.move_id, 0.0) + link.qty
-
- # Process every move only once for every pack operation
- for move in list(prout_move_qty):
- # TDE FIXME: do in batch ?
- move.check_tracking(operation)
-
- # TDE FIXME: I bet the message error is wrong
- if not remaining_move_qty.get(move.id):
- raise UserError(_("The roundings of your unit of measure %s on the move vs. %s on the product don't allow to do these operations or you are not transferring the picking at once. ") % (move.product_uom.name, move.product_id.uom_id.name))
-
- if not operation.pack_lot_ids:
- preferred_domain_list = [[('reservation_id', '=', move.id)], [('reservation_id', '=', False)], ['&', ('reservation_id', '!=', move.id), ('reservation_id', '!=', False)]]
- quants = Quant.quants_get_preferred_domain(
- prout_move_qty[move], move, ops=operation, domain=[('qty', '>', 0)],
- preferred_domain_list=preferred_domain_list)
- Quant.quants_move(quants, move, operation.location_dest_id, location_from=operation.location_id,
- lot_id=False, owner_id=operation.owner_id.id, src_package_id=operation.package_id.id,
- dest_package_id=quant_dest_package_id, entire_pack=entire_pack)
+ # link it to some move lines
+ for move_line in self.pack_operation_ids.filtered(lambda ml: ml.qty_done):
+ if float_compare(move_line.qty_done, extra_move_quantity, precision_rounding=rounding) <= 0:
+ # move this move line to our extra move
+ move_line.move_id = extra_move.id
+ extra_move_quantity -= move_line.qty_done
else:
- # Check what you can do with reserved quants already
- qty_on_link = prout_move_qty[move]
- rounding = operation.product_id.uom_id.rounding
- for reserved_quant in move.reserved_quant_ids:
- if (reserved_quant.owner_id.id != operation.owner_id.id) or (reserved_quant.location_id.id != operation.location_id.id) or \
- (reserved_quant.package_id.id != operation.package_id.id):
- continue
- if not reserved_quant.lot_id:
- false_quants += [reserved_quant]
- elif float_compare(lot_quantities.get(reserved_quant.lot_id.id, 0), 0, precision_rounding=rounding) > 0:
- if float_compare(lot_quantities[reserved_quant.lot_id.id], reserved_quant.qty, precision_rounding=rounding) >= 0:
- lot_quantities[reserved_quant.lot_id.id] -= reserved_quant.qty
- quants_taken += [(reserved_quant, reserved_quant.qty)]
- qty_on_link -= reserved_quant.qty
- else:
- quants_taken += [(reserved_quant, lot_quantities[reserved_quant.lot_id.id])]
- lot_quantities[reserved_quant.lot_id.id] = 0
- qty_on_link -= lot_quantities[reserved_quant.lot_id.id]
- lot_move_qty[move.id] = qty_on_link
+ # split this move line and assign the new part to our extra move
+ quantity_split = float_round(
+ move_line.qty_done - extra_move_quantity,
+ precision_rounding=self.product_uom.rounding,
+ rounding_method='UP')
+ move_line.qty_done = quantity_split
+ move_line.copy(default={'move_id': extra_move.id, 'qty_done': extra_move_quantity, 'product_uom_qty': 0})
+ extra_move_quantity -= extra_move_quantity
+ if extra_move_quantity == 0.0:
+ break
+ return extra_move
- remaining_move_qty[move.id] -= prout_move_qty[move]
+ @api.multi
+ def action_done(self):
+ self.filtered(lambda move: move.state == 'draft').action_confirm() # MRP allows scrapping draft moves
- # Handle lots separately
- if operation.pack_lot_ids:
- # TDE FIXME: fix call to move_quants_by_lot to ease understanding
- self._move_quants_by_lot(operation, lot_quantities, quants_taken, false_quants, lot_move_qty, quant_dest_package_id)
+ moves = self.filtered(lambda x: x.state not in ('done', 'cancel'))
+ moves_todo = self.env['stock.move']
+ # Create extra moves where necessary
+ for move in moves:
+ # Here, the `quantity_done` was already rounded to the product UOM by the `do_produce` wizard. However,
+ # it is possible that the user changed the value before posting the inventory by a value that should be
+ # rounded according to the move's UOM. In this specific case, we chose to round up the value, because it
+ # is what is expected by the user (if i consumed/produced a little more, the whole UOM unit should be
+ # consumed/produced and the moves are split correctly).
+ # FIXME: move rounding to move line
+ # rounding = move.product_uom.rounding
+ # move.quantity_done = float_round(move.quantity_done, precision_rounding=rounding, rounding_method ='UP')
+ if move.quantity_done <= 0:
+ continue
+ moves_todo |= move
+ moves_todo |= move._create_extra_move()
+ # Split moves where necessary and move quants
+ for move in moves_todo:
+ rounding = move.product_uom.rounding
+ if float_compare(move.quantity_done, move.product_uom_qty, precision_rounding=rounding) < 0:
+ # Need to do some kind of conversion here
+ qty_split = move.product_uom._compute_quantity(move.product_uom_qty - move.quantity_done, move.product_id.uom_id)
+ new_move = move.split(qty_split)
+ for move_line in move.pack_operation_ids:
+ if move_line.product_qty:
+ # FIXME: there will be an issue if the move was partially available
+ # By decreasing `product_qty`, we free the reservation.
+ # FIXME: if qty_done > product_qty, this could raise if nothing is in stock
+ try:
+ move_line.write({'product_uom_qty': move_line.qty_done})
+ except UserError:
+ pass
- # Handle pack in pack
- if not operation.product_id and operation.package_id and operation.result_package_id.id != operation.package_id.parent_id.id:
- operation.package_id.sudo().write({'parent_id': operation.result_package_id.id})
-
- # Check for remaining qtys and unreserve/check move_dest_id in
- move_dest_ids = set()
- for move in self:
- if float_compare(remaining_move_qty[move.id], 0, precision_rounding=move.product_id.uom_id.rounding) > 0: # In case no pack operations in picking
- move.check_tracking(False) # TDE: do in batch ? redone ? check this
-
- preferred_domain_list = [[('reservation_id', '=', move.id)], [('reservation_id', '=', False)], ['&', ('reservation_id', '!=', move.id), ('reservation_id', '!=', False)]]
- quants = Quant.quants_get_preferred_domain(
- remaining_move_qty[move.id], move, domain=[('qty', '>', 0)],
- preferred_domain_list=preferred_domain_list)
- Quant.quants_move(
- quants, move, move.location_dest_id,
- lot_id=move.restrict_lot_id.id, owner_id=move.restrict_partner_id.id)
-
- # If the move has a destination, add it to the list to reserve
- if move.move_dest_id and move.move_dest_id.state in ('waiting', 'confirmed'):
- move_dest_ids.add(move.move_dest_id.id)
-
- if move.procurement_id:
- procurements |= move.procurement_id
-
- # unreserve the quants and make them available for other operations/moves
- move.quants_unreserve()
-
- # Check the packages have been placed in the correct locations
- self.mapped('quant_ids').filtered(lambda quant: quant.package_id and quant.qty > 0).mapped('package_id')._check_location_constraint()
-
- # set the move as done
- self.write({'state': 'done', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
- procurements.check()
- # assign destination moves
- if move_dest_ids:
- # TDE FIXME: record setise me
- self.browse(list(move_dest_ids)).action_assign()
-
- pickings.filtered(lambda picking: picking.state == 'done' and not picking.date_done).write({'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
-
- return True
+ # If you were already putting stock.move.lots on the next one in the work order, transfer those to the new move
+ move.pack_operation_ids.filtered(lambda x: x.qty_done == 0.0).write({'move_id': new_move})
+ self.browse(new_move).quantity_done = 0.0
+ for packop in move.pack_operation_ids:
+ if float_compare(packop.qty_done, 0, precision_rounding=rounding) > 0:
+ if move.has_tracking != 'none' and (move.picking_type_id.use_create_lots or move.picking_type_id.use_existing_lots):
+ if packop.lot_name and not packop.lot_id:
+ lot = self.env['stock.production.lot'].create(
+ {'name': packop.lot_name, 'product_id': packop.product_id.id})
+ packop.write({'lot_id': lot.id})
+ if not packop.lot_id:
+ raise UserError(_('You need to supply a lot/serial number.'))
+ # execute `action_done` even if nothing was done, to free the reservation
+ packop.action_done()
+ picking = self and self[0].picking_id or False
+ moves_todo.write({'state': 'done', 'date': fields.Datetime.now()})
+ moves_todo.mapped('move_dest_ids').action_assign()
+ if picking:
+ moves_to_backorder = picking.move_lines.filtered(lambda x: x.state not in ('done', 'cancel'))
+ if moves_to_backorder:
+ backorder_picking = picking.copy({
+ 'name': '/',
+ 'move_lines': [],
+ 'pack_operation_ids': [],
+ 'backorder_id': picking.id
+ })
+ picking.message_post('Backorder Created') #message needs to be improved
+ moves_to_backorder.write({'picking_id': backorder_picking.id})
+ moves_to_backorder.action_assign()
+ return moves_todo
@api.multi
def unlink(self):
@@ -702,9 +891,9 @@ class StockMove(models.Model):
'product_uom_qty': uom_qty,
'procure_method': 'make_to_stock',
'restrict_lot_id': restrict_lot_id,
- 'split_from': self.id,
'procurement_id': self.procurement_id.id,
- 'move_dest_id': self.move_dest_id.id,
+ 'move_dest_ids': [(4, x.id) for x in self.move_dest_ids if x.state not in ('done', 'cancel')],
+ 'move_orig_ids': [(4, x.id) for x in self.move_orig_ids],
'origin_returned_move_id': self.origin_returned_move_id.id,
}
if restrict_partner_id:
@@ -717,17 +906,31 @@ class StockMove(models.Model):
# ctx = context.copy()
# TDE CLEANME: used only in write in this file, to clean
# ctx['do_not_propagate'] = True
- self.with_context(do_not_propagate=True).write({'product_uom_qty': self.product_uom_qty - uom_qty})
- if self.move_dest_id and self.propagate and self.move_dest_id.state not in ('done', 'cancel'):
- new_move_prop = self.move_dest_id.split(qty)
- new_move.write({'move_dest_id': new_move_prop})
+ # FIXME: pim fix your crap
+ self.with_context(do_not_propagate=True, do_not_unreserve=True).write({'product_uom_qty': self.product_uom_qty - uom_qty})
+
+ # if self.move_dest_id and self.propagate and self.move_dest_id.state not in ('done', 'cancel'):
+ # new_move_prop = self.move_dest_id.split(qty)
+ # new_move.write({'move_dest_id': new_move_prop})
# returning the first element of list returned by action_confirm is ok because we checked it wouldn't be exploded (and
# thus the result of action_confirm should always be a list of 1 element length)
new_move.action_confirm()
# TDE FIXME: due to action confirm change
return new_move.id
+ def _recompute_state(self):
+ for move in self:
+ if move.reserved_availability == move.product_uom_qty:
+ move.state = 'assigned'
+ elif move.reserved_availability and move.reserved_availability <= move.product_uom_qty:
+ move.state = 'partially_available'
+ else:
+ if move.move_orig_ids:
+ move.state = 'waiting'
+ else:
+ move.state = 'confirmed'
+
@api.multi
def action_show_picking(self):
view = self.env.ref('stock.view_picking_form')
diff --git a/addons/stock/models/stock_pack_operation.py b/addons/stock/models/stock_pack_operation.py
index 4de97bc9cc3..e5fd2a866e0 100644
--- a/addons/stock/models/stock_pack_operation.py
+++ b/addons/stock/models/stock_pack_operation.py
@@ -4,9 +4,9 @@
from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
-from odoo.exceptions import UserError, ValidationError
-from odoo.tools.float_utils import float_round, float_compare
+from odoo.exceptions import UserError
from odoo.tools.pycompat import izip
+from odoo.tools.float_utils import float_round, float_compare, float_is_zero
class PackOperation(models.Model):
@@ -14,104 +14,40 @@ class PackOperation(models.Model):
_description = "Packing Operation"
_order = "result_package_id desc, id"
- # TDE FIXME: strange, probably to remove
- def _get_default_from_loc(self):
- default_loc = self.env.context.get('default_location_id')
- if default_loc:
- return self.env['stock.location'].browse(default_loc).name
-
- # TDE FIXME: strange, probably to remove
- def _get_default_to_loc(self):
- default_loc = self.env.context.get('default_location_dest_id')
- if default_loc:
- return self.env['stock.location'].browse(default_loc).name
picking_id = fields.Many2one(
'stock.picking', 'Stock Picking',
- required=True,
help='The stock operation where the packing has been made')
- product_id = fields.Many2one('product.product', 'Product', ondelete="cascade")
- product_uom_id = fields.Many2one('product.uom', 'Unit of Measure')
- product_qty = fields.Float('To Do', default=0.0, digits=dp.get_precision('Product Unit of Measure'), required=True)
+ move_id = fields.Many2one(
+ 'stock.move', 'Stock Move',
+ help="Change to a better name")
+ product_id = fields.Many2one('product.product', 'Product', ondelete="cascade") #might be a related with the move also --> no, because you can put them next to each other
+ product_uom_id = fields.Many2one('product.uom', 'Unit of Measure', required=True)
+ product_qty = fields.Float(
+ 'Real Reserved Quantity', digits=0,
+ compute='_compute_product_qty', inverse='_set_product_qty', store=True)
+ product_uom_qty = fields.Float('Reserved', default=0.0, digits=dp.get_precision('Product Unit of Measure'), required=True)
ordered_qty = fields.Float('Ordered Quantity', digits=dp.get_precision('Product Unit of Measure'))
- qty_done = fields.Float('Done', default=0.0, digits=dp.get_precision('Product Unit of Measure'))
- is_done = fields.Boolean(compute='_compute_is_done', string='Done', readonly=False, oldname='processed_boolean')
+ qty_done = fields.Float('Done', default=0.0, digits=dp.get_precision('Product Unit of Measure'), copy=False)
package_id = fields.Many2one('stock.quant.package', 'Source Package')
- pack_lot_ids = fields.One2many('stock.pack.operation.lot', 'operation_id', 'Lots/Serial Numbers Used')
result_package_id = fields.Many2one(
'stock.quant.package', 'Destination Package',
ondelete='cascade', required=False,
help="If set, the operations are packed into this package")
- date = fields.Datetime('Date', default=fields.Date.context_today, required=True)
+ lot_id = fields.Many2one('stock.production.lot', 'Lot')
+ date = fields.Datetime('Date', default=fields.Datetime.now(), required=True)
owner_id = fields.Many2one('res.partner', 'Owner', help="Owner of the quants")
- linked_move_operation_ids = fields.One2many(
- 'stock.move.operation.link', 'operation_id', string='Linked Moves',
- readonly=True,
- help='Moves impacted by this operation for the computation of the remaining quantities')
- remaining_qty = fields.Float(
- compute='_get_remaining_qty', string="Remaining Qty", digits=0,
- help="Remaining quantity in default UoM according to moves matched with this operation.")
- location_id = fields.Many2one('stock.location', 'Source Location', required=True)
- location_dest_id = fields.Many2one('stock.location', 'Destination Location', required=True)
- picking_source_location_id = fields.Many2one('stock.location', related='picking_id.location_id')
- picking_destination_location_id = fields.Many2one('stock.location', related='picking_id.location_dest_id')
- # TDE FIXME: unnecessary fields IMO, to remove
- from_loc = fields.Char(compute='_compute_location_description', default=_get_default_from_loc, string='From')
- to_loc = fields.Char(compute='_compute_location_description', default=_get_default_to_loc, string='To')
- fresh_record = fields.Boolean('Newly created pack operation', default=True)
+ location_id = fields.Many2one('stock.location', 'From', required=True)
+ location_dest_id = fields.Many2one('stock.location', 'To', required=True)
+ from_loc = fields.Char(compute='_compute_location_description')
+ to_loc = fields.Char(compute='_compute_location_description')
lots_visible = fields.Boolean(compute='_compute_lots_visible')
- state = fields.Selection(selection=[
- ('draft', 'Draft'),
- ('cancel', 'Cancelled'),
- ('waiting', 'Waiting Another Operation'),
- ('confirmed', 'Waiting Availability'),
- ('partially_available', 'Partially Available'),
- ('assigned', 'Available'),
- ('done', 'Done')], related='picking_id.state')
+ state = fields.Selection(related='move_id.state')
+ consume_line_ids = fields.Many2many('stock.move.line', 'stock_move_line_consume_rel', 'consume_line_id', 'produce_line_id', help="Technical link to see who consumed what. ")
+ produce_line_ids = fields.Many2many('stock.move.line', 'stock_move_line_consume_rel', 'produce_line_id', 'consume_line_id', help="Technical link to see which line was produced with this. ")
+
@api.one
- def _compute_is_done(self):
- self.is_done = self.qty_done > 0.0
-
- @api.onchange('is_done')
- def on_change_is_done(self):
- if not self.product_id:
- if self.is_done and self.qty_done == 0:
- self.qty_done = 1.0
- if not self.is_done and self.qty_done != 0:
- self.qty_done = 0.0
-
- def _get_remaining_prod_quantities(self):
- '''Get the remaining quantities per product on an operation with a package. This function returns a dictionary'''
- # TDE CLEANME: merge with _get_all_products_quantities in quant to ease code understanding + clean code
- # if the operation doesn't concern a package, it's not relevant to call this function
- if not self.package_id or self.product_id:
- return {self.product_id: self.remaining_qty}
- # get the total of products the package contains
- res = self.package_id._get_all_products_quantities()
- # reduce by the quantities linked to a move
- for record in self.linked_move_operation_ids:
- if record.move_id.product_id.id not in res:
- res[record.move_id.product_id] = 0
- res[record.move_id.product_id] -= record.qty
- return res
-
- @api.one
- def _get_remaining_qty(self):
- if self.package_id and not self.product_id:
- # dont try to compute the remaining quantity for packages because it's not relevant (a package could include different products).
- # should use _get_remaining_prod_quantities instead
- # TDE FIXME: actually resolve the comment hereabove
- self.remaining_qty = 0
- else:
- qty = self.product_qty
- if self.product_uom_id:
- qty = self.product_uom_id._compute_quantity(self.product_qty, self.product_id.uom_id)
- for record in self.linked_move_operation_ids:
- qty -= record.qty
- self.remaining_qty = float_round(qty, precision_rounding=self.product_id.uom_id.rounding)
-
- @api.multi
def _compute_location_description(self):
for operation, operation_sudo in izip(self, self.sudo()):
operation.from_loc = '%s%s' % (operation_sudo.location_id.name, operation.product_id and operation_sudo.package_id.name or '')
@@ -119,17 +55,24 @@ class PackOperation(models.Model):
@api.one
def _compute_lots_visible(self):
- if self.pack_lot_ids:
- self.lots_visible = True
- elif self.picking_id.picking_type_id and self.product_id.tracking != 'none': # TDE FIXME: not sure correctly migrated
+ if self.picking_id.picking_type_id and self.product_id.tracking != 'none': # TDE FIXME: not sure correctly migrated
picking = self.picking_id
self.lots_visible = picking.picking_type_id.use_existing_lots or picking.picking_type_id.use_create_lots
else:
self.lots_visible = self.product_id.tracking != 'none'
- @api.onchange('pack_lot_ids')
- def _onchange_packlots(self):
- self.qty_done = sum([x.qty for x in self.pack_lot_ids])
+ @api.one
+ @api.depends('product_id', 'product_uom_id', 'product_uom_qty')
+ def _compute_product_qty(self):
+ self.product_qty = self.product_uom_id._compute_quantity(self.product_uom_qty, self.product_id.uom_id, rounding_method='HALF-UP')
+
+ @api.one
+ def _set_product_qty(self):
+ """ The meaning of product_qty field changed lately and is now a functional field computing the quantity
+ in the default product UoM. This code has been added to raise an error if a write is made given a value
+ for `product_qty`, where the same write should set the `product_uom_qty` field instead, in order to
+ detect errors. """
+ raise UserError(_('The requested operation cannot be processed because of a programming error setting the `product_qty` field instead of the `product_uom_qty`.'))
@api.multi
@api.onchange('product_id', 'product_uom_id')
@@ -137,7 +80,10 @@ class PackOperation(models.Model):
if self.product_id:
self.lots_visible = self.product_id.tracking != 'none'
if not self.product_uom_id or self.product_uom_id.category_id != self.product_id.uom_id.category_id:
- self.product_uom_id = self.product_id.uom_id.id
+ if self.move_id.product_uom:
+ self.product_uom_id = self.move_id.product_uom.id
+ else:
+ self.product_uom_id = self.product_id.uom_id.id
res = {'domain': {'product_uom_id': [('category_id', '=', self.product_uom_id.category_id.id)]}}
else:
res = {'domain': {'product_uom_id': []}}
@@ -145,173 +91,230 @@ class PackOperation(models.Model):
@api.model
def create(self, vals):
- vals['ordered_qty'] = vals.get('product_qty')
- return super(PackOperation, self).create(vals)
+ vals['ordered_qty'] = vals.get('product_uom_qty')
+ ml = super(PackOperation, self).create(vals)
+ if ml.state == 'done':
+ Quant = self.env['stock.quant']
+ quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id,rounding_method='HALF-UP')
+ if ml.location_id.should_impact_quants() and ml.product_id.type == 'product':
+ available_qty = Quant._decrease_available_quantity(ml.product_id, ml.location_id, quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
+ if available_qty < 0 and ml.lot_id:
+ # see if we can compensate the negative quants with some untracked quants
+ untracked_qty = Quant._get_available_quantity(ml.product_id, ml.location_id, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
+ if untracked_qty:
+ taken_from_untracked_qty = min(untracked_qty, abs(quantity))
+ Quant._decrease_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id)
+ Quant._increase_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
+ if ml.location_dest_id.should_impact_quants() and ml.product_id.type == 'product':
+ Quant._increase_available_quantity(ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id)
+ next_moves = ml.move_id.move_dest_ids.filtered(lambda move: move.state not in ('done', 'cancel'))
+ next_moves.do_unreserve()
+ next_moves.action_assign()
+ return ml
@api.multi
- def write(self, values):
- # TDE FIXME: weird stuff, protectin pack op ?
- values['fresh_record'] = False
- return super(PackOperation, self).write(values)
+ def write(self, vals):
+ """ Through the interface, we allow users to change the charateristics of a move line. If a
+ quantity has been reserved for this move line, we impact the reservation directly to free
+ the old quants and allocate the new ones.
+ """
+ if self.env.context.get('bypass_reservation_update'):
+ return super(PackOperation, self).write(vals)
+
+ Quant = self.env['stock.quant']
+ # We forbid to change the reserved quantity in the interace, but it is needed in the
+ # case of stock.move's split.
+ # TODO Move me in the update
+ if 'product_uom_qty' in vals:
+ for ml in self.filtered(lambda m: m.state in ('partially_available', 'assigned')):
+ if ml.location_id.should_impact_quants() and ml.product_id.type == 'product':
+ qty_to_decrease = ml.product_qty - ml.product_uom_id._compute_quantity(vals['product_uom_qty'], ml.product_id.uom_id, rounding_method='HALF-UP')
+ try:
+ Quant._decrease_reserved_quantity(ml.product_id, ml.location_id, qty_to_decrease, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
+ except UserError:
+ if ml.lot_id:
+ Quant._decrease_reserved_quantity(ml.product_id, ml.location_id, qty_to_decrease, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id)
+ else:
+ raise
+
+ triggers = [
+ ('location_id', 'stock.location'),
+ ('location_dest_id', 'stock.location'),
+ ('lot_id', 'stock.production.lot'),
+ ('package_id', 'stock.quant.package'),
+ ('result_package_id', 'stock.quant.package'),
+ ('owner_id', 'res.partner')
+ ]
+ updates = {}
+ for key, model in triggers:
+ if key in vals:
+ updates[key] = self.env[model].browse(vals[key])
+
+ if updates:
+ for ml in self.filtered(lambda ml: ml.state in ['partially_available', 'assigned']):
+ if ml.location_id.should_impact_quants() and ml.product_id.type == 'product':
+ try:
+ Quant._decrease_reserved_quantity(ml.product_id, ml.location_id, ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
+ except UserError:
+ if ml.lot_id:
+ Quant._decrease_reserved_quantity(ml.product_id, ml.location_id, ml.product_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id)
+ else:
+ raise
+
+ if updates.get('location_id', ml.location_id).should_impact_quants() and ml.product_id.type == 'product':
+ new_product_qty = 0
+ try:
+ q = Quant._increase_reserved_quantity(ml.product_id, updates.get('location_id', ml.location_id), ml.product_qty, lot_id=updates.get('lot_id', ml.lot_id),
+ package_id=updates.get('package_id', ml.package_id), owner_id=updates.get('owner_id', ml.owner_id), strict=True)
+ new_product_qty = sum([x[1] for x in q])
+ except UserError:
+ if updates.get('lot_id'):
+ # If we were not able to reserve on tracked quants, we can use untracked ones.
+ try:
+ q = Quant._increase_reserved_quantity(ml.product_id, updates.get('location_id', ml.location_id), ml.product_qty, lot_id=False,
+ package_id=updates.get('package_id', ml.package_id), owner_id=updates.get('owner_id', ml.owner_id), strict=True)
+ new_product_qty = sum([x[1] for x in q])
+ except UserError:
+ pass
+ if new_product_qty != ml.product_qty:
+ new_product_uom_qty = self.product_id.uom_id._compute_quantity(new_product_qty, self.product_uom_id, rounding_method='HALF-UP')
+ ml.with_context(bypass_reservation_update=True).product_uom_qty = new_product_uom_qty
+
+ # When editing a done move line, the reserved availability of a potential chained move is impacted. Take care of running again `action_assign` on the concerned moves.
+ next_moves = self.env['stock.move']
+ if updates or 'qty_done' in vals:
+ for ml in self.filtered(lambda ml: ml.move_id.state == 'done'):
+ # undo the original move line
+ if ml.location_dest_id.should_impact_quants() and ml.product_id.type == 'product':
+ Quant._decrease_available_quantity(ml.product_id, ml.location_dest_id, ml.qty_done, lot_id=ml.lot_id,
+ package_id=ml.package_id, owner_id=ml.owner_id)
+ if ml.location_id.should_impact_quants() and ml.product_id.type == 'product':
+ Quant._increase_available_quantity(ml.product_id, ml.location_id, ml.qty_done, lot_id=ml.lot_id,
+ package_id=ml.package_id, owner_id=ml.owner_id)
+
+ # move what's been actually done
+ product_id = ml.product_id
+ location_id = updates.get('location_id', ml.location_id)
+ location_dest_id = updates.get('location_dest_id', ml.location_dest_id)
+ qty_done = vals.get('qty_done', ml.qty_done)
+ lot_id = updates.get('lot_id', ml.lot_id)
+ package_id = updates.get('package_id', ml.package_id)
+ result_package_id = updates.get('result_package_id', ml.result_package_id)
+ owner_id = updates.get('owner_id', ml.owner_id)
+ quantity = ml.move_id.product_uom._compute_quantity(qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
+ if location_id.should_impact_quants() and ml.product_id.type == 'product':
+ ml._free_reservation(product_id, location_id, quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id)
+ available_qty = Quant._decrease_available_quantity(product_id, location_id, quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id)
+ if available_qty < 0 and lot_id:
+ # see if we can compensate the negative quants with some untracked quants
+ untracked_qty = Quant._get_available_quantity(product_id, location_id, lot_id=False, package_id=package_id, owner_id=owner_id, strict=True)
+ if untracked_qty:
+ taken_from_untracked_qty = min(untracked_qty, abs(available_qty))
+ Quant._decrease_available_quantity(product_id, location_id, taken_from_untracked_qty, lot_id=False, package_id=package_id, owner_id=owner_id)
+ Quant._increase_available_quantity(product_id, location_id, taken_from_untracked_qty, lot_id=lot_id, package_id=package_id, owner_id=owner_id)
+ ml._free_reservation(ml.product_id, location_id, untracked_qty, lot_id=False, package_id=package_id, owner_id=owner_id)
+ if location_dest_id.should_impact_quants() and ml.product_id.type == 'product' and qty_done:
+ Quant._increase_available_quantity(product_id, location_dest_id, quantity, lot_id=lot_id, package_id=result_package_id, owner_id=owner_id)
+ # Unreserve and reserve following move in order to have the real reserved quantity on move_line.
+ next_moves |= ml.move_id.move_dest_ids.filtered(lambda move: move.state not in ('done', 'cancel'))
+ res = super(PackOperation, self).write(vals)
+ next_moves.do_unreserve()
+ next_moves.action_assign()
+ return res
@api.multi
def unlink(self):
- if any([operation.state in ('done', 'cancel') for operation in self]):
- raise UserError(_('You can not delete pack operations of a done picking'))
+ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
+ for ml in self:
+ if ml.state in ('done', 'cancel'):
+ raise UserError(_('You can not delete pack operations of a done picking'))
+ # Unlinking a pack operation should unreserve.
+ if ml.location_id.should_impact_quants() and ml.product_id.type == 'product' and not float_is_zero(ml.product_qty, precision_digits=precision):
+ self.env['stock.quant']._decrease_reserved_quantity(ml.product_id, ml.location_id, ml.product_qty, lot_id=ml.lot_id,
+ package_id=ml.package_id, owner_id=ml.owner_id)
return super(PackOperation, self).unlink()
- @api.multi
- def split_quantities(self):
- for operation in self:
- if float_compare(operation.product_qty, operation.qty_done, precision_rounding=operation.product_uom_id.rounding) == 1:
- cpy = operation.copy(default={'qty_done': 0.0, 'product_qty': operation.product_qty - operation.qty_done})
- operation.write({'product_qty': operation.qty_done})
- operation._copy_remaining_pack_lot_ids(cpy)
- else:
- raise UserError(_('The quantity to split should be smaller than the quantity To Do. '))
- return True
+ def action_done(self):
+ """ This method will finalize the work with a move line by "moving" quants to the
+ destination location.
+ """
+ for ml in self:
+ if ml.product_id.type != 'consu':
+ Quant = self.env['stock.quant']
+ rounding = ml.product_uom_id.rounding
- @api.multi
- def save(self):
- # TDE FIXME: does not seem to be used -> actually, it does
- # TDE FIXME: move me somewhere else, because the return indicated a wizard, in pack op, it is quite strange
- # HINT: 4. How to manage lots of identical products?
- # Create a picking and click on the Mark as TODO button to display the Lot Split icon. A window will pop-up. Click on Add an item and fill in the serial numbers and click on save button
- for pack in self:
- if pack.product_id.tracking != 'none':
- pack.write({'qty_done': sum(pack.pack_lot_ids.mapped('qty'))})
- return {'type': 'ir.actions.act_window_close'}
+ # if this move line is force assigned, unreserve elsewhere if needed
+ if float_compare(ml.qty_done, ml.product_qty, precision_rounding=rounding) > 0:
+ extra_qty = ml.qty_done - ml.product_qty
+ ml._free_reservation(ml.product_id, ml.location_id, extra_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
+ # unreserve what's been reserved
+ if ml.location_id.should_impact_quants() and ml.product_id.type == 'product' and ml.product_qty:
+ try:
+ Quant._decrease_reserved_quantity(ml.product_id, ml.location_id, ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
+ except UserError:
+ Quant._decrease_reserved_quantity(ml.product_id, ml.location_id, ml.product_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id)
- @api.multi
- def action_split_lots(self):
- action_ctx = dict(self.env.context)
- # If it's a returned stock move, we do not want to create a lot
- returned_move = self.linked_move_operation_ids.mapped('move_id').mapped('origin_returned_move_id')
- picking_type = self.picking_id.picking_type_id
- action_ctx.update({
- 'serial': self.product_id.tracking == 'serial',
- 'only_create': picking_type.use_create_lots and not picking_type.use_existing_lots and not returned_move,
- 'create_lots': picking_type.use_create_lots,
- 'state_done': self.picking_id.state == 'done',
- 'show_reserved': any([lot for lot in self.pack_lot_ids if lot.qty_todo > 0.0])})
- view_id = self.env.ref('stock.view_pack_operation_lot_form').id
- return {
- 'name': _('Lot/Serial Number Details'),
- 'type': 'ir.actions.act_window',
- 'view_type': 'form',
- 'view_mode': 'form',
- 'res_model': 'stock.pack.operation',
- 'views': [(view_id, 'form')],
- 'view_id': view_id,
- 'target': 'new',
- 'res_id': self.ids[0],
- 'context': action_ctx}
+ # move what's been actually done
+ quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
+ if ml.location_id.should_impact_quants() and ml.product_id.type == 'product':
+ available_qty = Quant._decrease_available_quantity(ml.product_id, ml.location_id, quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
+ if available_qty < 0 and ml.lot_id:
+ # see if we can compensate the negative quants with some untracked quants
+ untracked_qty = Quant._get_available_quantity(ml.product_id, ml.location_id, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
+ if untracked_qty:
+ taken_from_untracked_qty = min(untracked_qty, abs(quantity))
+ Quant._decrease_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id)
+ Quant._increase_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
+ if ml.location_dest_id.should_impact_quants() and ml.product_id.type == 'product':
+ Quant._increase_available_quantity(ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id)
- @api.multi
- def show_details(self):
- # TDE FIXME: does not seem to be used
- view_id = self.env.ref('stock.view_pack_operation_details_form_save').id
- return {
- 'name': _('Operation Details'),
- 'type': 'ir.actions.act_window',
- 'view_type': 'form',
- 'view_mode': 'form',
- 'res_model': 'stock.pack.operation',
- 'views': [(view_id, 'form')],
- 'view_id': view_id,
- 'target': 'new',
- 'res_id': self.ids[0],
- 'context': self.env.context}
+ def _free_reservation(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None):
+ """ When editing a done move line or validating one with some forced quantities, it is
+ possible to impact quants that were not reserved. It is therefore necessary to edit or
+ unlink the move lines that reserved a quantity now unavailable.
+ """
+ self.ensure_one()
- @api.multi
- def _check_serial_number(self):
- for operation in self:
- if operation.picking_id and \
- (operation.picking_id.picking_type_id.use_existing_lots or operation.picking_id.picking_type_id.use_create_lots) and \
- operation.product_id and operation.product_id.tracking != 'none' and \
- operation.qty_done > 0.0:
- if not operation.pack_lot_ids:
- raise UserError(_('You need to provide a Lot/Serial Number for product %s') % operation.product_id.name)
- if operation.product_id.tracking == 'serial':
- for opslot in operation.pack_lot_ids:
- if opslot.qty not in (1.0, 0.0):
- raise UserError(_('You should provide a different serial number for each piece'))
- check_tracking = _check_serial_number
+ # Check the available quantity, with the `strict` kw set to `True`. If the available
+ # quantity is greather than the quantity now unavailable, there is nothing to do.
+ available_quantity = self.env['stock.quant']._get_available_quantity(
+ product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True
+ )
+ if quantity > available_quantity:
+ # We now have to find the move lines that reserved our now unavailable quantity. We
+ # take care to exclude ourselves and the move lines were work had already been done.
+ oudated_move_lines_domain = [
+ ('move_id.state', 'not in', ['done', 'cancel']),
+ ('product_id', '=', product_id.id),
+ ('lot_id', '=', lot_id.id if lot_id else False),
+ ('location_id', '=', location_id.id),
+ ('owner_id', '=', owner_id.id if owner_id else False),
+ ('package_id', '=', package_id.id if package_id else False),
+ ('product_qty', '>', 0.0),
+ ('qty_done', '=', 0.0),
+ ('id', '!=', self.id),
+ ]
+ oudated_candidates = self.env['stock.pack.operation'].search(oudated_move_lines_domain)
- @api.multi
- def _copy_remaining_pack_lot_ids(self, new_operation):
- for op in self:
- for lot in op.pack_lot_ids:
- new_qty_todo = lot.qty_todo - lot.qty
+ # As the move's state is not computed over the move lines, we'll have to manually
+ # recompute the moves which we adapted their lines.
+ move_to_recompute_state = self.env['stock.move']
- if float_compare(new_qty_todo, 0, precision_rounding=op.product_uom_id.rounding) > 0:
- lot.copy({
- 'operation_id': new_operation.id,
- 'qty_todo': new_qty_todo,
- 'qty': 0,
- })
-
-
-class PackOperationLot(models.Model):
- _name = "stock.pack.operation.lot"
- _description = "Lot/Serial number for pack ops"
-
- operation_id = fields.Many2one('stock.pack.operation')
- qty = fields.Float('Done', default=1.0)
- lot_id = fields.Many2one('stock.production.lot', 'Lot/Serial Number')
- lot_name = fields.Char('Lot/Serial Number')
- qty_todo = fields.Float('To Do', default=0.0)
- plus_visible = fields.Boolean(compute='_compute_plus_visible', default=True)
-
- _sql_constraints = [
- ('qty', 'CHECK(qty >= 0.0)', 'Quantity must be greater than or equal to 0.0!'),
- ('uniq_lot_id', 'unique(operation_id, lot_id)', 'You have already mentioned this lot in another line'),
- ('uniq_lot_name', 'unique(operation_id, lot_name)', 'You have already mentioned this lot name in another line')]
-
- @api.one
- def _compute_plus_visible(self):
- if self.operation_id.product_id.tracking == 'serial':
- self.plus_visible = (self.qty == 0.0)
- else:
- self.plus_visible = (self.qty_todo == 0.0) or (self.qty < self.qty_todo)
-
- @api.constrains('lot_id', 'lot_name')
- def _check_lot(self):
- if any(not lot.lot_name and not lot.lot_id for lot in self):
- raise ValidationError(_('Lot/Serial Number required'))
- return True
-
- def action_add_quantity(self, quantity):
- for lot in self:
- lot.write({'qty': lot.qty + quantity})
- lot.operation_id.write({'qty_done': sum(operation_lot.qty for operation_lot in lot.operation_id.pack_lot_ids)})
- return self.mapped('operation_id').action_split_lots()
-
- @api.multi
- def do_plus(self):
- return self.action_add_quantity(1)
-
- @api.multi
- def do_minus(self):
- return self.action_add_quantity(-1)
-
-
-class OperationLink(models.Model):
- """ Make link between stock.move and stock.pack.operation in order to compute
- the remaining quantities on each of those objects. """
- _name = "stock.move.operation.link"
- _description = "Pack Operation / Moves Link"
-
- qty = fields.Float(
- 'Quantity', help="Quantity of products to consider when talking about the contribution of this pack operation towards the "
- "remaining quantity of the move (and inverse). Given in the product main uom.")
- operation_id = fields.Many2one(
- 'stock.pack.operation', 'Operation',
- ondelete="cascade", required=True)
- move_id = fields.Many2one(
- 'stock.move', 'Move',
- ondelete="cascade", required=True)
- reserved_quant_id = fields.Many2one(
- 'stock.quant', 'Reserved Quant',
- help="Technical field containing the quant that created this link between an operation and a stock move. "
- "Used at the stock_move_obj.action_done() time to avoid seeking a matching quant again")
+ rounding = self.product_uom_id.rounding
+ for candidate in oudated_candidates:
+ if float_compare(candidate.product_qty, quantity, precision_rounding=rounding) <= 0:
+ quantity -= candidate.product_qty
+ move_to_recompute_state |= candidate.move_id
+ candidate.unlink()
+ else:
+ # split this move line and assign the new part to our extra move
+ quantity_split = float_round(
+ candidate.product_qty - quantity,
+ precision_rounding=self.product_uom_id.rounding,
+ rounding_method='UP')
+ candidate.product_uom_qty = self.product_id.uom_id._compute_quantity(quantity_split, self.product_uom_id, rounding_method='HALF-UP')
+ quantity -= quantity_split
+ move_to_recompute_state |= candidate.move_id
+ if quantity == 0.0:
+ break
+ move_to_recompute_state._recompute_state()
diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py
index c02b962ef81..de261ea45d6 100644
--- a/addons/stock/models/stock_picking.py
+++ b/addons/stock/models/stock_picking.py
@@ -261,18 +261,10 @@ class Picking(models.Model):
default=lambda self: self.env['res.company']._company_default_get('stock.picking'),
index=True, required=True,
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
- # TDE FIXME: separate those two kind of pack operations
+
pack_operation_ids = fields.One2many(
'stock.pack.operation', 'picking_id', 'Related Packing Operations',
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
- pack_operation_product_ids = fields.One2many(
- 'stock.pack.operation', 'picking_id', 'Non pack',
- domain=[('product_id', '!=', False)],
- states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
- pack_operation_pack_ids = fields.One2many(
- 'stock.pack.operation', 'picking_id', 'Pack',
- domain=[('product_id', '=', False)],
- states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
pack_operation_exist = fields.Boolean(
'Has Pack Operations', compute='_compute_pack_operation_exist',
@@ -285,16 +277,13 @@ class Picking(models.Model):
printed = fields.Boolean('Printed')
# Used to search on pickings
product_id = fields.Many2one('product.product', 'Product', related='move_lines.product_id')
- recompute_pack_op = fields.Boolean(
- 'Recompute pack operation?', copy=False,
- help='True if reserved quants changed, which mean we might need to recompute the package operations')
- launch_pack_operations = fields.Boolean("Launch Pack Operations", copy=False)
+ show_operations = fields.Boolean(related='picking_type_id.show_operations')
_sql_constraints = [
('name_uniq', 'unique(name, company_id)', 'Reference must be unique per company!'),
]
- @api.depends('move_type', 'launch_pack_operations', 'move_lines.state', 'move_lines.picking_id', 'move_lines.partially_available')
+ @api.depends('move_type', 'move_lines.state', 'move_lines.picking_id')
@api.one
def _compute_state(self):
''' State of a picking depends on the state of its related stock.move
@@ -307,9 +296,8 @@ class Picking(models.Model):
- one of the move is assigned or partially available: partially available
- otherwise in waiting or confirmed state
'''
- if not self.move_lines and self.launch_pack_operations:
- self.state = 'assigned'
- elif not self.move_lines:
+ # FIXME: goes in 3 times at each write?
+ if not self.move_lines:
self.state = 'draft'
elif any(move.state == 'draft' for move in self.move_lines): # TDE FIXME: should be all ?
self.state = 'draft'
@@ -318,16 +306,34 @@ class Picking(models.Model):
elif all(move.state in ['cancel', 'done'] for move in self.move_lines):
self.state = 'done'
else:
- # We sort our moves by importance of state: "confirmed" should be first, then we'll have
- # "waiting" and finally "assigned" at the end.
+ # We sort our moves by importance of state:
+ # ------------- 0
+ # | Confirmed |
+ # -------------
+ # | Partial |
+ # -------------
+ # | Waiting |
+ # -------------
+ # | Assigned |
+ # ------------- len - 1
+ sort_map = {
+ 'assigned': 4,
+ 'waiting': 3,
+ 'partially_available': 2,
+ 'confirmed': 1,
+ }
moves_todo = self.move_lines\
.filtered(lambda move: move.state not in ['cancel', 'done'])\
- .sorted(key=lambda move: (move.state == 'assigned' and 2) or (move.state == 'waiting' and 1) or 0)
+ .sorted(key=lambda move: sort_map.get(move.state, 0))
if self.move_type == 'one':
- self.state = moves_todo[0].state or 'draft'
- elif moves_todo[0].state != 'assigned' and any(x.partially_available or x.state == 'assigned' for x in moves_todo):
+ if moves_todo[0].state in ('partially_available', 'confirmed'):
+ self.state = 'confirmed'
+ else:
+ self.state = moves_todo[0].state or 'draft'
+ elif moves_todo[0].state != 'assigned' and any(x.state in ['assigned', 'partially_available'] for x in moves_todo):
self.state = 'partially_available'
else:
+ # take the less important state among all move_lines.
self.state = moves_todo[-1].state or 'draft'
@api.one
@@ -443,10 +449,14 @@ class Picking(models.Model):
@api.multi
def action_confirm(self):
- self.filtered(lambda picking: not picking.move_lines).write({'launch_pack_operations': True})
- # TDE CLEANME: use of launch pack operation, really useful ?
- self.mapped('move_lines').filtered(lambda move: move.state == 'draft').action_confirm()
- self.filtered(lambda picking: picking.location_id.usage in ('supplier', 'inventory', 'production')).force_assign()
+ # call `action_confirm` on every draft move
+ self.mapped('move_lines')\
+ .filtered(lambda move: move.state == 'draft')\
+ .action_confirm()
+ # call `action_assign` on every confirmed move which location_id bypasses the reservation
+ self.filtered(lambda picking: picking.location_id.usage in ('supplier', 'inventory', 'production'))\
+ .filtered(lambda move: move.state == 'confirmed')\
+ .mapped('move_lines').action_assign()
return True
@api.multi
@@ -468,7 +478,7 @@ class Picking(models.Model):
""" Changes state of picking to available if moves are confirmed or waiting.
@return: True
"""
- self.mapped('move_lines').filtered(lambda move: move.state in ['confirmed', 'waiting']).force_assign()
+ self.mapped('move_lines').filtered(lambda move: move.state in ['confirmed', 'waiting', 'partially_available']).force_assign()
return True
@api.multi
@@ -485,419 +495,157 @@ class Picking(models.Model):
"""
# TDE FIXME: remove decorator when migration the remaining
# TDE FIXME: draft -> automatically done, if waiting ?? CLEAR ME
- draft_moves = self.mapped('move_lines').filtered(lambda self: self.state == 'draft')
- todo_moves = self.mapped('move_lines').filtered(lambda self: self.state in ['draft', 'assigned', 'confirmed'])
- draft_moves.action_confirm()
+ todo_moves = self.mapped('move_lines').filtered(lambda self: self.state in ['draft', 'partially_available', 'assigned', 'confirmed'])
+ # Check if there are ops not linked to moves yet
+ for pick in self:
+ # # Explode manually added packages
+ # for ops in pick.pack_operation_ids.filtered(lambda x: not x.move_id and not x.product_id):
+ # for quant in ops.package_id.quant_ids: #Or use get_content for multiple levels
+ # self.pack_operation_ids.create({'product_id': quant.product_id.id,
+ # 'package_id': quant.package_id.id,
+ # 'result_package_id': ops.result_package_id,
+ # 'lot_id': quant.lot_id.id,
+ # 'owner_id': quant.owner_id.id,
+ # 'product_uom_id': quant.product_id.uom_id.id,
+ # 'product_qty': quant.qty,
+ # 'qty_done': quant.qty,
+ # 'location_id': quant.location_id.id, # Could be ops too
+ # 'location_dest_id': ops.location_dest_id.id,
+ # 'picking_id': pick.id
+ # }) # Might change first element
+ # # Link existing moves or add moves when no one is related
+
+ for ops in pick.pack_operation_ids.filtered(lambda x: not x.move_id):
+ # Search move with this product
+ moves = pick.move_lines.filtered(lambda x: x.product_id == ops.product_id)
+ if moves: #could search move that needs it the most (that has some quantities left)
+ ops.move_id = moves[0].id
+ else:
+ new_move = self.env['stock.move'].create({
+ 'name': _('New Move:') + ops.product_id.display_name,
+ 'product_id': ops.product_id.id,
+ 'product_uom_qty': ops.qty_done,
+ 'product_uom': ops.product_uom_id.id,
+ 'location_id': pick.location_id.id,
+ 'location_dest_id': pick.location_dest_id.id,
+ 'picking_id': pick.id,
+ })
+ ops.move_id = new_move.id
+ new_move.action_confirm()
+ todo_moves |= new_move
+ #'qty_done': ops.qty_done})
todo_moves.action_done()
return True
- def _prepare_pack_ops(self, quants, forced_qties):
- """ Prepare pack_operations, returns a list of dict to give at create """
- # TDE CLEANME: oh dear ...
- valid_quants = quants.filtered(lambda quant: quant.qty > 0)
- _Mapping = namedtuple('Mapping', ('product', 'package', 'owner', 'location', 'location_dst_id'))
-
- all_products = valid_quants.mapped('product_id') | self.env['product.product'].browse(p.id for p in forced_qties) | self.move_lines.mapped('product_id')
- computed_putaway_locations = dict(
- (product, self.location_dest_id.get_putaway_strategy(product).id or self.location_dest_id.id) for product in all_products)
-
- product_to_uom = dict((product.id, product.uom_id) for product in all_products)
- picking_moves = self.move_lines.filtered(lambda move: move.state not in ('done', 'cancel'))
- for move in picking_moves:
- # If we encounter an UoM that is smaller than the default UoM or the one already chosen, use the new one instead.
- if move.product_uom != product_to_uom[move.product_id.id] and move.product_uom.factor > product_to_uom[move.product_id.id].factor:
- product_to_uom[move.product_id.id] = move.product_uom
- if len(picking_moves.mapped('location_id')) > 1:
- raise UserError(_('The source location must be the same for all the moves of the picking.'))
- if len(picking_moves.mapped('location_dest_id')) > 1:
- raise UserError(_('The destination location must be the same for all the moves of the picking.'))
-
- pack_operation_values = []
- # find the packages we can move as a whole, create pack operations and mark related quants as done
- top_lvl_packages = valid_quants._get_top_level_packages(computed_putaway_locations)
- for pack in top_lvl_packages:
- pack_quants = pack.get_content()
- pack_operation_values.append({
- 'picking_id': self.id,
- 'package_id': pack.id,
- 'product_qty': 1.0,
- 'location_id': pack.location_id.id,
- 'location_dest_id': computed_putaway_locations[pack_quants[0].product_id],
- 'owner_id': pack.owner_id.id,
- })
- valid_quants -= pack_quants
-
- # Go through all remaining reserved quants and group by product, package, owner, source location and dest location
- # Lots will go into pack operation lot object
- qtys_grouped = {}
- lots_grouped = {}
- for quant in valid_quants:
- key = _Mapping(quant.product_id, quant.package_id, quant.owner_id, quant.location_id, computed_putaway_locations[quant.product_id])
- qtys_grouped.setdefault(key, 0.0)
- qtys_grouped[key] += quant.qty
- if quant.product_id.tracking != 'none' and quant.lot_id:
- lots_grouped.setdefault(key, dict()).setdefault(quant.lot_id.id, 0.0)
- lots_grouped[key][quant.lot_id.id] += quant.qty
- # Do the same for the forced quantities (in cases of force_assign or incomming shipment for example)
- for product, qty in pycompat.items(forced_qties):
- if qty <= 0.0:
- continue
- key = _Mapping(product, self.env['stock.quant.package'], self.owner_id, self.location_id, computed_putaway_locations[product])
- qtys_grouped.setdefault(key, 0.0)
- qtys_grouped[key] += qty
-
- # Create the necessary operations for the grouped quants and remaining qtys
- Uom = self.env['product.uom']
- product_id_to_vals = {} # use it to create operations using the same order as the picking stock moves
- for mapping, qty in pycompat.items(qtys_grouped):
- uom = product_to_uom[mapping.product.id]
- val_dict = {
- 'picking_id': self.id,
- 'product_qty': mapping.product.uom_id._compute_quantity(qty, uom),
- 'product_id': mapping.product.id,
- 'package_id': mapping.package.id,
- 'owner_id': mapping.owner.id,
- 'location_id': mapping.location.id,
- 'location_dest_id': mapping.location_dst_id,
- 'product_uom_id': uom.id,
- 'pack_lot_ids': [
- (0, 0, {'lot_id': lot, 'qty': 0.0, 'qty_todo': lots_grouped[mapping][lot]})
- for lot in lots_grouped.get(mapping, {})],
- }
- product_id_to_vals.setdefault(mapping.product.id, list()).append(val_dict)
-
- for move in self.move_lines.filtered(lambda move: move.state not in ('done', 'cancel')):
- values = product_id_to_vals.pop(move.product_id.id, [])
- pack_operation_values += values
- return pack_operation_values
+ do_transfer = action_done #TODO:replace later
@api.multi
- def do_prepare_partial(self):
- # TDE CLEANME: oh dear ...
- PackOperation = self.env['stock.pack.operation']
-
- # get list of existing operations and delete them
- existing_packages = PackOperation.search([('picking_id', 'in', self.ids)]) # TDE FIXME: o2m / m2o ?
- if existing_packages:
- existing_packages.unlink()
+ def _check_entire_pack(self):
+ """ This function check if entire packs are moved in the picking"""
for picking in self:
- forced_qties = {} # Quantity remaining after calculating reserved quants
- picking_quants = self.env['stock.quant']
- # Calculate packages, reserved quants, qtys of this picking's moves
- for move in picking.move_lines:
- if move.state not in ('assigned', 'confirmed', 'waiting'):
- continue
- move_quants = move.reserved_quant_ids
- picking_quants += move_quants
- forced_qty = 0.0
- if move.state == 'assigned':
- qty = move.product_uom._compute_quantity(move.product_uom_qty, move.product_id.uom_id, round=False)
- forced_qty = qty - sum([x.qty for x in move_quants])
- # if we used force_assign() on the move, or if the move is incoming, forced_qty > 0
- if float_compare(forced_qty, 0, precision_rounding=move.product_id.uom_id.rounding) > 0:
- if forced_qties.get(move.product_id):
- forced_qties[move.product_id] += forced_qty
- else:
- forced_qties[move.product_id] = forced_qty
- for vals in picking._prepare_pack_ops(picking_quants, forced_qties):
- vals['fresh_record'] = False
- PackOperation |= PackOperation.create(vals)
- # recompute the remaining quantities all at once
- self.do_recompute_remaining_quantities()
- for pack in PackOperation:
- pack.ordered_qty = sum(
- pack.mapped('linked_move_operation_ids').mapped('move_id').filtered(lambda r: r.state != 'cancel').mapped('ordered_qty')
- )
- self.write({'recompute_pack_op': False})
+ origin_packages = picking.pack_operation_ids.mapped("package_id")
+ for pack in origin_packages:
+ all_in = True
+ packops = picking.pack_operation_ids.filtered(lambda x: x.package_id == pack)
+ keys = ['product_id', 'lot_id']
+
+ grouped_quants = {}
+ for k, g in groupby(sorted(pack.quant_ids, key=itemgetter(*keys)), key=itemgetter(*keys)):
+ grouped_quants[k] = sum(self.env['stock.quant'].concat(*list(g)).mapped('quantity'))
+
+ grouped_ops = {}
+ for k, g in groupby(sorted(packops, key=itemgetter(*keys)), key=itemgetter(*keys)):
+ grouped_ops[k] = sum(self.env['stock.pack.operation'].concat(*list(g)).mapped('product_qty'))
+ if any(grouped_quants[key] - grouped_ops.get(key, 0) != 0 for key in grouped_quants)\
+ or any(grouped_ops[key] - grouped_quants[key] != 0 for key in grouped_ops):
+ all_in = False
+ if all_in and packops:
+ packops.write({'result_package_id': pack.id})
@api.multi
def do_unreserve(self):
- return True
-
- def recompute_remaining_qty(self, done_qtys=False):
-
- def _create_link_for_index(operation_id, index, product_id, qty_to_assign, quant_id=False):
- move_dict = prod2move_ids[product_id][index]
- qty_on_link = min(move_dict['remaining_qty'], qty_to_assign)
- self.env['stock.move.operation.link'].create({'move_id': move_dict['move'].id, 'operation_id': operation_id, 'qty': qty_on_link, 'reserved_quant_id': quant_id})
- if move_dict['remaining_qty'] == qty_on_link:
- prod2move_ids[product_id].pop(index)
- else:
- move_dict['remaining_qty'] -= qty_on_link
- return qty_on_link
-
- def _create_link_for_quant(operation_id, quant, qty):
- """create a link for given operation and reserved move of given quant, for the max quantity possible, and returns this quantity"""
- if not quant.reservation_id.id:
- return _create_link_for_product(operation_id, quant.product_id.id, qty)
- qty_on_link = 0
- for i in range(0, len(prod2move_ids[quant.product_id.id])):
- if prod2move_ids[quant.product_id.id][i]['move'].id != quant.reservation_id.id:
- continue
- qty_on_link = _create_link_for_index(operation_id, i, quant.product_id.id, qty, quant_id=quant.id)
- break
- return qty_on_link
-
- def _create_link_for_product(operation_id, product_id, qty):
- '''method that creates the link between a given operation and move(s) of given product, for the given quantity.
- Returns True if it was possible to create links for the requested quantity (False if there was not enough quantity on stock moves)'''
- qty_to_assign = qty
- Product = self.env["product.product"]
- product = Product.browse(product_id)
- rounding = product.uom_id.rounding
- qtyassign_cmp = float_compare(qty_to_assign, 0.0, precision_rounding=rounding)
- if prod2move_ids.get(product_id):
- while prod2move_ids[product_id] and qtyassign_cmp > 0:
- qty_on_link = _create_link_for_index(operation_id, 0, product_id, qty_to_assign, quant_id=False)
- qty_to_assign -= qty_on_link
- qtyassign_cmp = float_compare(qty_to_assign, 0.0, precision_rounding=rounding)
- return qtyassign_cmp == 0
-
- # TDE CLEANME: oh dear ...
- Uom = self.env['product.uom']
- QuantPackage = self.env['stock.quant.package']
- OperationLink = self.env['stock.move.operation.link']
-
- quants_in_package_done = set()
- prod2move_ids = {}
- still_to_do = []
- # make a dictionary giving for each product, the moves and related quantity that can be used in operation links
- moves = sorted([x for x in self.move_lines if x.state not in ('done', 'cancel')], key=lambda x: (((x.state == 'assigned') and -2 or 0) + (x.partially_available and -1 or 0)))
- for move in moves:
- if not prod2move_ids.get(move.product_id.id):
- prod2move_ids[move.product_id.id] = [{'move': move, 'remaining_qty': move.product_qty}]
- else:
- prod2move_ids[move.product_id.id].append({'move': move, 'remaining_qty': move.product_qty})
-
- need_rereserve = False
- # sort the operations in order to give higher priority to those with a package, then a lot/serial number
- operations = self.pack_operation_ids
- operations = sorted(operations, key=lambda x: ((x.package_id and not x.product_id) and -4 or 0) + (x.package_id and -2 or 0) + (x.pack_lot_ids and -1 or 0))
- # delete existing operations to start again from scratch
- links = OperationLink.search([('operation_id', 'in', [x.id for x in operations])])
- if links:
- links.unlink()
- # 1) first, try to create links when quants can be identified without any doubt
- for ops in operations:
- lot_qty = {}
- for packlot in ops.pack_lot_ids:
- lot_qty[packlot.lot_id.id] = ops.product_uom_id._compute_quantity(packlot.qty, ops.product_id.uom_id)
- # for each operation, create the links with the stock move by seeking on the matching reserved quants,
- # and deffer the operation if there is some ambiguity on the move to select
- if ops.package_id and not ops.product_id and (not done_qtys or ops.qty_done):
- # entire package
- for quant in ops.package_id.get_content():
- remaining_qty_on_quant = quant.qty
- if quant.reservation_id:
- # avoid quants being counted twice
- quants_in_package_done.add(quant.id)
- qty_on_link = _create_link_for_quant(ops.id, quant, quant.qty)
- remaining_qty_on_quant -= qty_on_link
- if remaining_qty_on_quant:
- still_to_do.append((ops, quant.product_id.id, remaining_qty_on_quant))
- need_rereserve = True
- elif ops.product_id.id:
- # Check moves with same product
- product_qty = ops.qty_done if done_qtys else ops.product_qty
- qty_to_assign = ops.product_uom_id._compute_quantity(product_qty, ops.product_id.uom_id)
- precision_rounding = ops.product_id.uom_id.rounding
- for move_dict in prod2move_ids.get(ops.product_id.id, []):
- move = move_dict['move']
- for quant in move.reserved_quant_ids:
- if float_compare(qty_to_assign, 0, precision_rounding=precision_rounding) != 1:
- break
- if quant.id in quants_in_package_done:
- continue
-
- # check if the quant is matching the operation details
- if ops.package_id:
- flag = quant.package_id == ops.package_id
- else:
- flag = not quant.package_id.id
- flag = flag and (ops.owner_id.id == quant.owner_id.id)
- if flag:
- if not lot_qty:
- max_qty_on_link = min(quant.qty, qty_to_assign)
- qty_on_link = _create_link_for_quant(ops.id, quant, max_qty_on_link)
- qty_to_assign -= qty_on_link
- else:
- if lot_qty.get(quant.lot_id.id): # if there is still some qty left
- max_qty_on_link = min(quant.qty, qty_to_assign, lot_qty[quant.lot_id.id])
- qty_on_link = _create_link_for_quant(ops.id, quant, max_qty_on_link)
- qty_to_assign -= qty_on_link
- lot_qty[quant.lot_id.id] -= qty_on_link
-
- qty_assign_cmp = float_compare(qty_to_assign, 0, precision_rounding=precision_rounding)
- if qty_assign_cmp > 0:
- # qty reserved is less than qty put in operations. We need to create a link but it's deferred after we processed
- # all the quants (because they leave no choice on their related move and needs to be processed with higher priority)
- still_to_do += [(ops, ops.product_id.id, qty_to_assign)]
- need_rereserve = True
-
- # 2) then, process the remaining part
- all_op_processed = True
- for ops, product_id, remaining_qty in still_to_do:
- all_op_processed = _create_link_for_product(ops.id, product_id, remaining_qty) and all_op_processed
- return (need_rereserve, all_op_processed)
-
- def picking_recompute_remaining_quantities(self, done_qtys=False):
- need_rereserve = False
- all_op_processed = True
- if self.pack_operation_ids:
- need_rereserve, all_op_processed = self.recompute_remaining_qty(done_qtys=done_qtys)
- return need_rereserve, all_op_processed
-
- def do_recompute_remaining_quantities(self, done_qtys=False):
- # TDE FIXME
- tmp = self.filtered(lambda picking: picking.pack_operation_ids)
- if tmp:
- for pick in tmp:
- pick.recompute_remaining_qty(done_qtys=done_qtys)
-
- def rereserve_quants(self, move_ids=[]):
- """ Unreserve quants then try to reassign quants."""
- if not move_ids:
- self.do_unreserve()
- self.action_assign()
- else:
- moves = self.env['stock.move'].browse(move_ids)
- if self.env.context.get('no_state_change'):
- moves = moves.filtered(lambda m: m.reserved_quant_ids)
- moves.do_unreserve()
- moves.action_assign(no_prepare=True)
+ for move in self:
+ for move_line in move.move_lines:
+ move_line.do_unreserve()
+ self.write({'state': 'confirmed'})
@api.multi
- def do_new_transfer(self):
- for pick in self:
- if pick.state == 'done':
- raise UserError(_('The pick is already validated'))
- pack_operations_delete = self.env['stock.pack.operation']
- if not pick.move_lines and not pick.pack_operation_ids:
- raise UserError(_('Please create some Initial Demand or Mark as Todo and create some Operations. '))
- # In draft or with no pack operations edited yet, ask if we can just do everything
- if pick.state == 'draft' or all([x.qty_done == 0.0 for x in pick.pack_operation_ids]):
- # If no lots when needed, raise error
- picking_type = pick.picking_type_id
- if (picking_type.use_create_lots or picking_type.use_existing_lots):
- for pack in pick.pack_operation_ids:
- if pack.product_id and pack.product_id.tracking != 'none':
- raise UserError(_('Some products require lots/serial numbers, so you need to specify those first!'))
- view = self.env.ref('stock.view_immediate_transfer')
- wiz = self.env['stock.immediate.transfer'].create({'pick_id': pick.id})
- # TDE FIXME: a return in a loop, what a good idea. Really.
- return {
- 'name': _('Immediate Transfer?'),
- 'type': 'ir.actions.act_window',
- 'view_type': 'form',
- 'view_mode': 'form',
- 'res_model': 'stock.immediate.transfer',
- 'views': [(view.id, 'form')],
- 'view_id': view.id,
- 'target': 'new',
- 'res_id': wiz.id,
- 'context': self.env.context,
- }
+ def button_validate(self):
+ self.ensure_one()
+ pack_operations_delete = self.env['stock.pack.operation']
+ if not self.move_lines and not self.pack_operation_ids:
+ raise UserError(_('Please add some lines to move'))
+ # In draft or with no pack operations edited yet, ask if we can just do everything
+ if self.state == 'draft' or all([x.qty_done == 0.0 for x in self.pack_operation_ids]):
+ # If no lots when needed, raise error
+ picking_type = self.picking_type_id
+ if (picking_type.use_create_lots or picking_type.use_existing_lots):
+ for pack in self.pack_operation_ids:
+ if pack.product_id and pack.product_id.tracking != 'none':
+ raise UserError(_('Some products require lots/serial numbers, so you need to specify those first!'))
+ view = self.env.ref('stock.view_immediate_transfer')
+ wiz = self.env['stock.immediate.transfer'].create({'pick_id': self.id})
+ return {
+ 'name': _('Immediate Transfer?'),
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'stock.immediate.transfer',
+ 'views': [(view.id, 'form')],
+ 'view_id': view.id,
+ 'target': 'new',
+ 'res_id': wiz.id,
+ 'context': self.env.context,
+ }
- # Check backorder should check for other barcodes
- if pick.check_backorder():
- view = self.env.ref('stock.view_backorder_confirmation')
- wiz = self.env['stock.backorder.confirmation'].create({'pick_id': pick.id})
- # TDE FIXME: same reamrk as above actually
- return {
- 'name': _('Create Backorder?'),
- 'type': 'ir.actions.act_window',
- 'view_type': 'form',
- 'view_mode': 'form',
- 'res_model': 'stock.backorder.confirmation',
- 'views': [(view.id, 'form')],
- 'view_id': view.id,
- 'target': 'new',
- 'res_id': wiz.id,
- 'context': self.env.context,
- }
- for operation in pick.pack_operation_ids:
- if operation.qty_done < 0:
- raise UserError(_('No negative quantities allowed'))
- if operation.qty_done > 0:
- operation.write({'product_qty': operation.qty_done})
- else:
- pack_operations_delete |= operation
- if pack_operations_delete:
- pack_operations_delete.unlink()
- self.do_transfer()
+ # Check backorder should check for other barcodes
+ if self.check_backorder():
+ view = self.env.ref('stock.view_backorder_confirmation')
+ wiz = self.env['stock.backorder.confirmation'].create({'pick_id': self.id})
+ return {
+ 'name': _('Create Backorder?'),
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'stock.backorder.confirmation',
+ 'views': [(view.id, 'form')],
+ 'view_id': view.id,
+ 'target': 'new',
+ 'res_id': wiz.id,
+ 'context': self.env.context,
+ }
+ for operation in self.pack_operation_ids:
+ if operation.qty_done < 0:
+ raise UserError(_('No negative quantities allowed'))
+ if operation.qty_done > 0:
+ pass
+ #operation.write({'product_qty': operation.qty_done})
+ else:
+ pack_operations_delete |= operation
+ if pack_operations_delete:
+ pack_operations_delete.unlink()
+ self.action_done()
return
+ do_new_transfer = button_validate #TODO: replace later
+
def check_backorder(self):
- need_rereserve, all_op_processed = self.picking_recompute_remaining_quantities(done_qtys=True)
+ self.ensure_one()
+ quantity_todo = {}
+ quantity_done = {}
for move in self.move_lines:
- if float_compare(move.remaining_qty, 0, precision_rounding=move.product_id.uom_id.rounding) != 0:
- return True
- return False
-
- @api.multi
- def do_transfer(self):
- """ If no pack operation, we do simple action_done of the picking.
- Otherwise, do the pack operations. """
- # TDE CLEAN ME: reclean me, please
- self._create_lots_for_picking()
-
- no_pack_op_pickings = self.filtered(lambda picking: not picking.pack_operation_ids)
- no_pack_op_pickings.action_done()
- other_pickings = self - no_pack_op_pickings
- for picking in other_pickings:
- need_rereserve, all_op_processed = picking.picking_recompute_remaining_quantities()
- todo_moves = self.env['stock.move']
- toassign_moves = self.env['stock.move']
-
- # create extra moves in the picking (unexpected product moves coming from pack operations)
- if not all_op_processed:
- todo_moves |= picking._create_extra_moves()
-
- if need_rereserve or not all_op_processed:
- moves_reassign = any(x.origin_returned_move_id or x.move_orig_ids for x in picking.move_lines if x.state not in ['done', 'cancel'])
- if moves_reassign and picking.location_id.usage not in ("supplier", "production", "inventory"):
- # unnecessary to assign other quants than those involved with pack operations as they will be unreserved anyways.
- picking.with_context(reserve_only_ops=True, no_state_change=True).rereserve_quants(move_ids=picking.move_lines.ids)
- picking.do_recompute_remaining_quantities()
-
- # split move lines if needed
- for move in picking.move_lines:
- rounding = move.product_id.uom_id.rounding
- remaining_qty = move.remaining_qty
- if move.state in ('done', 'cancel'):
- # ignore stock moves cancelled or already done
- continue
- elif move.state == 'draft':
- toassign_moves |= move
- if float_compare(remaining_qty, 0, precision_rounding=rounding) == 0:
- if move.state in ('draft', 'assigned', 'confirmed'):
- todo_moves |= move
- elif float_compare(remaining_qty, 0, precision_rounding=rounding) > 0 and float_compare(remaining_qty, move.product_qty, precision_rounding=rounding) < 0:
- # TDE FIXME: shoudl probably return a move - check for no track key, by the way
- new_move_id = move.split(remaining_qty)
- new_move = self.env['stock.move'].with_context(mail_notrack=True).browse(new_move_id)
- todo_moves |= move
- # Assign move as it was assigned before
- toassign_moves |= new_move
-
- # TDE FIXME: do_only_split does not seem used anymore
- if todo_moves and not self.env.context.get('do_only_split'):
- todo_moves.action_done()
- elif self.env.context.get('do_only_split'):
- picking = picking.with_context(split=todo_moves.ids)
-
- picking._create_backorder()
- return True
-
- def _create_lots_for_picking(self):
- Lot = self.env['stock.production.lot']
- for pack_op_lot in self.mapped('pack_operation_ids').mapped('pack_lot_ids'):
- if not pack_op_lot.lot_id:
- lot = Lot.create({'name': pack_op_lot.lot_name, 'product_id': pack_op_lot.operation_id.product_id.id})
- pack_op_lot.write({'lot_id': lot.id})
- # TDE FIXME: this should not be done here
- self.mapped('pack_operation_ids').mapped('pack_lot_ids').filtered(lambda op_lot: op_lot.qty == 0.0).unlink()
- create_lots_for_picking = _create_lots_for_picking
+ quantity_todo.setdefault(move.product_id.id, 0)
+ quantity_done.setdefault(move.product_id.id, 0)
+ quantity_todo[move.product_id.id] += move.product_qty
+ quantity_done[move.product_id.id] += move.quantity_done #TODO: convert to base units
+ for ops in self.pack_operation_ids.filtered(lambda x: x.package_id and not x.product_id and not x.move_id):
+ for quant in ops.package_id.quant_ids:
+ quantity_done.setdefault(quant.product_id.id, 0)
+ quantity_done[quant.product_id.id] += quant.qty
+ for pack in self.pack_operation_ids.filtered(lambda x: x.product_id and not x.move_id):
+ quantity_done.setdefault(pack.product_id.id, 0)
+ quantity_done[pack.product_id.id] += pack.qty_done
+ return any(quantity_done[x] < quantity_todo.get(x, 0) for x in quantity_done)
def _create_extra_moves(self):
'''This function creates move lines on a picking, at the time of do_transfer, based on
@@ -994,13 +742,6 @@ class Picking(models.Model):
new_operation = operation.copy({'product_qty': operation.qty_done,'qty_done': operation.qty_done})
operation.write({'product_qty': operation.product_qty - operation.qty_done,'qty_done': 0})
- if operation.pack_lot_ids:
- packlots_transfer = [(4, x.id) for x in operation.pack_lot_ids]
- new_operation.write({'pack_lot_ids': packlots_transfer})
-
- # the stock.pack.operation.lot records now belong to the new, packaged stock.pack.operation
- # we have to create new ones with new quantities for our original, unfinished stock.pack.operation
- new_operation._copy_remaining_pack_lot_ids(operation)
op = new_operation
pack_operation_ids |= op
@@ -1012,7 +753,6 @@ class Picking(models.Model):
raise UserError(_('Please process some quantities to put in the pack first!'))
return package
-
@api.multi
def put_in_pack(self):
return self._put_in_pack()
diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py
index ea6fb20b962..1f7e44f23ea 100644
--- a/addons/stock/models/stock_warehouse.py
+++ b/addons/stock/models/stock_warehouse.py
@@ -521,7 +521,9 @@ class Warehouse(models.Model):
'action': 'move',
'picking_type_id': routing.picking_type.id,
'procure_method': first_rule is True and 'make_to_stock' or 'make_to_order',
- 'warehouse_id': self.id}
+ 'warehouse_id': self.id,
+ 'propagate': routing.picking_type != self.pick_type_id,
+ }
route_pull_values.update(values or {})
route_pull_values.update(pull_values or {})
pull_rules_list.append(route_pull_values)
diff --git a/addons/stock/security/ir.model.access.csv b/addons/stock/security/ir.model.access.csv
index 6b13597199d..781a2b1156f 100644
--- a/addons/stock/security/ir.model.access.csv
+++ b/addons/stock/security/ir.model.access.csv
@@ -62,12 +62,6 @@ access_product_putaway_manager,product.putaway all managers,model_product_putawa
access_stock_removal_all,product.removal all users,model_product_removal,base.group_user,1,0,0,0
access_stock_fixed_putaway_strat,stock_fixed_putaway_strat managers,model_stock_fixed_putaway_strat,stock.group_stock_manager,1,1,1,1
access_stock_fixed_putaway_user,stock_fixed_putaway_strat user,model_stock_fixed_putaway_strat,stock.group_stock_user,1,0,0,0
-access_stock_move_operation_link_manager,stock.move.operation.link manager,model_stock_move_operation_link,stock.group_stock_manager,1,1,1,1
-access_stock_move_operation_link_user,stock.move.operation.link user,model_stock_move_operation_link,stock.group_stock_user,1,1,1,1
-access_stock_move_operation_link_all,stock.move.operation.link all users,model_stock_move_operation_link,base.group_user,1,1,1,1
-access_stock_pack_operation_lot_manager,stock.pack.operation.lot manager,model_stock_pack_operation_lot,stock.group_stock_manager,1,1,1,1
-access_stock_pack_operation_lot_user,stock.pack.operation.lot user,model_stock_pack_operation_lot,stock.group_stock_user,1,1,1,1
-access_stock_pack_operation_lot_all,stock.pack.operation.lot all users,model_stock_pack_operation_lot,base.group_user,1,1,1,1
access_stock_pack_operation_portal,stock.pack.operation portal,stock.model_stock_pack_operation,base.group_portal,1,0,0,0
access_product_price_history_stock_user,prices.history stock user,product.model_product_price_history,stock.group_stock_user,1,0,0,0
access_product_price_history_stock_manager,prices.history stock manager,product.model_product_price_history,stock.group_stock_manager,1,1,1,1
diff --git a/addons/stock/views/stock_move_views.xml b/addons/stock/views/stock_move_views.xml
index 041fbecbe0e..426cf829172 100644
--- a/addons/stock/views/stock_move_views.xml
+++ b/addons/stock/views/stock_move_views.xml
@@ -178,7 +178,7 @@
-
+
@@ -222,7 +222,7 @@
-
+
diff --git a/addons/stock/views/stock_pack_operation_views.xml b/addons/stock/views/stock_pack_operation_views.xml
deleted file mode 100644
index cd8469d4672..00000000000
--- a/addons/stock/views/stock_pack_operation_views.xml
+++ /dev/null
@@ -1,118 +0,0 @@
-
-
-
- stock.pack.operation.details.form
- stock.pack.operation
-
-
-
-
-
-
-
- stock.pack.operation.details.form.save
- stock.pack.operation
- primary
-
-
-
-
-
-
-
-
-
-
-
-
-
- Pack Details
- ir.actions.act_window
- stock.pack.operation
-
- form
- form
- new
- {}
-
-
-
- stock.pack.operation.lots.form
- stock.pack.operation
-
-
-
-
-
-
-
- stock.pack.operation.lots.form
- stock.pack.operation.lot
-
-
-
-
-
-
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index 4fcd6b2b76f..727927af368 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -209,9 +209,10 @@
-
-
+
+
+
+
@@ -247,48 +248,35 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
+ attrs="{'invisible': ['|', ('state', 'not in', ['confirmed', 'assigned', 'waiting', 'partially_available','done'])]}"/>
-
-
-
-
+
-