[REF] stock: new picking, move and pack.operation implementation
The big change is that we have now a many2many between the move and a one2many between a move and its pack.operation (note: this model is renamed as stock.move.line in a further commit). Due to these changes, `action_done` had to be rewritten intierely, as it depended and stock.pack.operation.lot and stock.move.operation.link, that are now removed. This method was laregly inspired by the one in the mrp module, and a further commit will make MRP uses all the core of stock. Due to the previous commit that refactored the stock.quant implementation, the new concept is that the `product_qty` that will always be in the uom of the quant (`product_qty` is a computed field in the same style as the one of stock.move). This commit also implements the logic that a manager is able to edit done stock.pack.operation. The stock.move has now a "partially_available" state (it was previously a flag). The "split_from" field is removed now that we have a many2many between moves (a sibling move is always a move that was splitted, or with the backorder_id field). The done quantity of a stock move is computed across the done quantity of the move lines. There is an inverse method that allows user to fill the done quantity directly on a stock move. The reserve quantity of stock move is computed accross the `product_qty` of the linked pack operations. `do_unreserve` will simply unlink the associated pack operation. Unlinking a pack operation will unreserve. `force_assign` will mark the move as available, in order to allow the user to create pack operations (none are created because nothing was reserved). `action_assign` is rewritten and splitted in two parts: if the move has a parent (mto) or not (mts). In the mto, we'll check the available move lines and only reserve accordingly. In the mts, we'll reserve directly on the quants. `action_cancel` has changd a lot: in case of a delivery, the first step does not anymore propagate cancellation to following moves and SO. As a result it does not propagate in the other side in case of MTO or production. It does not delete them if we delete the related sale order for example. In order to correct this next activities will be added in the future. Technically action cancel on move is based on procurment rule, this commit will set the propagate field to false on the rule if the picking_type is 'pick' or if we are in one step delivery. We use this commit to reimplement `action_assign` (wiped by the new quant implentation). A move is reserved if its move lines exist, so we introduce a one2many between stock.move and stock.pack.operation (note: a further commit renamed this model into stock.move.line) We use this commit to implement `action_done` (wiped by the removal of the models making the link between stock.move and stock.pack.operations). We can use the one2many to the pack operations. blabla manymany au lieu de one2many blabla mto mts Remove get_ancestors, as orig_ids will be used instead validation when we are creating some packe operation with lot name, the pack_operation are unlink and a new picking is craeted. This is caused the function _create_lot_for_picking that unlink pack operation having no quantity reserved instead of ones having no quantity done. To fix this we changed product_qty to qty_done and move the code from action_done of picking to action_done of move fixup! [IMP] Merge_moves functionality + many2many procurement and original moves instead of one2many fixup! [REF] stock: basics of new model and stock.move reservation edit reserved move line This is the 1st commit message: [REM] stock: stock.pack.operation.lot, stock.move.operation.link These models will become useless once a one2many will be made between stock.move and stock.pack.operation. The `action_done` method on stock move has been wiped, as it depended completely on the removed models. Joint work with Josse Colpaert <jco@odoo.com>, amoyaux <arm@odoo.com> and Pierre Masereel <pim@odoo.com>
This commit is contained in:
@@ -54,7 +54,6 @@
|
|||||||
'views/procurement_views.xml',
|
'views/procurement_views.xml',
|
||||||
'views/stock_warehouse_views.xml',
|
'views/stock_warehouse_views.xml',
|
||||||
'views/stock_move_views.xml',
|
'views/stock_move_views.xml',
|
||||||
'views/stock_pack_operation_views.xml',
|
|
||||||
'views/product_views.xml',
|
'views/product_views.xml',
|
||||||
'views/stock_config_settings_views.xml',
|
'views/stock_config_settings_views.xml',
|
||||||
|
|
||||||
|
|||||||
@@ -91,12 +91,12 @@
|
|||||||
picking_type_id: stock.picking_type_out
|
picking_type_id: stock.picking_type_out
|
||||||
location_id: stock.stock_location_stock
|
location_id: stock.stock_location_stock
|
||||||
location_dest_id: stock.stock_location_customers
|
location_dest_id: stock.stock_location_customers
|
||||||
pack_operation_ids:
|
pack_operation_ids:
|
||||||
- product_id: product.product_product_6
|
- product_id: product.product_product_6
|
||||||
product_qty: 100
|
qty_done: 100
|
||||||
product_uom_id: product.product_uom_unit
|
product_uom_id: product.product_uom_unit
|
||||||
location_id: stock.stock_location_stock
|
location_id: stock.stock_location_stock
|
||||||
location_dest_id: stock.stock_location_customers
|
location_dest_id: stock.stock_location_customers
|
||||||
-
|
-
|
||||||
!record {model: stock.picking, id: outgoing_shipment_main_warehouse2}:
|
!record {model: stock.picking, id: outgoing_shipment_main_warehouse2}:
|
||||||
picking_type_id: stock.picking_type_out
|
picking_type_id: stock.picking_type_out
|
||||||
@@ -231,12 +231,12 @@
|
|||||||
location_id: stock.stock_location_suppliers
|
location_id: stock.stock_location_suppliers
|
||||||
location_dest_id: stock.stock_location_shop0
|
location_dest_id: stock.stock_location_shop0
|
||||||
state: done
|
state: done
|
||||||
pack_operation_ids:
|
pack_operation_ids:
|
||||||
- product_id: product.product_delivery_01
|
- product_id: product.product_delivery_01
|
||||||
product_qty: 100
|
qty_done: 100
|
||||||
product_uom_id: product.product_uom_unit
|
product_uom_id: product.product_uom_unit
|
||||||
location_id: stock.stock_location_suppliers
|
location_id: stock.stock_location_suppliers
|
||||||
location_dest_id: stock.stock_location_shop0
|
location_dest_id: stock.stock_location_shop0
|
||||||
-
|
-
|
||||||
!record {model: stock.picking, id: incomming_chicago_warehouse2}:
|
!record {model: stock.picking, id: incomming_chicago_warehouse2}:
|
||||||
picking_type_id: chi_picking_type_in
|
picking_type_id: chi_picking_type_in
|
||||||
@@ -296,12 +296,12 @@
|
|||||||
picking_type_id: chi_picking_type_out
|
picking_type_id: chi_picking_type_out
|
||||||
location_id: stock.stock_location_shop0
|
location_id: stock.stock_location_shop0
|
||||||
location_dest_id: stock.stock_location_customers
|
location_dest_id: stock.stock_location_customers
|
||||||
pack_operation_ids:
|
pack_operation_ids:
|
||||||
- product_id: product.product_product_6
|
- product_id: product.product_product_6
|
||||||
product_qty: 100
|
qty_done: 100
|
||||||
product_uom_id: product.product_uom_unit
|
product_uom_id: product.product_uom_unit
|
||||||
location_id: stock.stock_location_shop0
|
location_id: stock.stock_location_shop0
|
||||||
location_dest_id: stock.stock_location_customers
|
location_dest_id: stock.stock_location_customers
|
||||||
-
|
-
|
||||||
!record {model: stock.picking, id: outgoing_chicago_warehouse2}:
|
!record {model: stock.picking, id: outgoing_chicago_warehouse2}:
|
||||||
picking_type_id: chi_picking_type_out
|
picking_type_id: chi_picking_type_out
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class ProcurementOrder(models.Model):
|
|||||||
def propagate_cancels(self):
|
def propagate_cancels(self):
|
||||||
# set the context for the propagation of the procurement cancellation
|
# set the context for the propagation of the procurement cancellation
|
||||||
# TDE FIXME: was in cancel, moved here for consistency
|
# 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:
|
if cancel_moves:
|
||||||
cancel_moves.action_cancel()
|
cancel_moves.action_cancel()
|
||||||
return self.search([('move_dest_id', 'in', cancel_moves.filtered(lambda move: move.propagate).ids)])
|
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,
|
'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_id': self.rule_id.location_src_id.id,
|
||||||
'location_dest_id': self.location_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,
|
'procurement_id': self.id,
|
||||||
'rule_id': self.rule_id.id,
|
'rule_id': self.rule_id.id,
|
||||||
'procure_method': self.rule_id.procure_method,
|
'procure_method': self.rule_id.procure_method,
|
||||||
|
|||||||
@@ -117,6 +117,10 @@ class Location(models.Model):
|
|||||||
('view_location_id.parent_left', '<=', self.parent_left),
|
('view_location_id.parent_left', '<=', self.parent_left),
|
||||||
('view_location_id.parent_right', '>=', self.parent_left)], limit=1)
|
('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):
|
class Route(models.Model):
|
||||||
_name = 'stock.location.route'
|
_name = 'stock.location.route'
|
||||||
@@ -223,7 +227,7 @@ class PushedFlow(models.Model):
|
|||||||
else:
|
else:
|
||||||
new_move_vals = self._prepare_move_copy_values(move, new_date)
|
new_move_vals = self._prepare_move_copy_values(move, new_date)
|
||||||
new_move = move.copy(new_move_vals)
|
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()
|
new_move.action_confirm()
|
||||||
|
|
||||||
def _prepare_move_copy_values(self, move_to_copy, new_date):
|
def _prepare_move_copy_values(self, move_to_copy, new_date):
|
||||||
|
|||||||
+400
-197
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dateutil import relativedelta
|
from dateutil import relativedelta
|
||||||
|
from itertools import groupby
|
||||||
|
from operator import itemgetter
|
||||||
|
|
||||||
from odoo import api, fields, models, _
|
from odoo import api, fields, models, _
|
||||||
from odoo.addons import decimal_precision as dp
|
from odoo.addons import decimal_precision as dp
|
||||||
@@ -80,31 +82,34 @@ class StockMove(models.Model):
|
|||||||
'res.partner', 'Destination Address ',
|
'res.partner', 'Destination Address ',
|
||||||
states={'done': [('readonly', True)]},
|
states={'done': [('readonly', True)]},
|
||||||
help="Optional address where goods are to be delivered, specifically used for allotment")
|
help="Optional address where goods are to be delivered, specifically used for allotment")
|
||||||
move_dest_id = fields.Many2one(
|
move_dest_ids = fields.Many2many(
|
||||||
'stock.move', 'Destination Move',
|
'stock.move', 'stock_move_move_rel', 'move_orig_id', 'move_dest_id', 'Destination Moves',
|
||||||
copy=False, index=True,
|
copy=False,
|
||||||
help="Optional: next stock move when chaining them")
|
help="Optional: next stock move when chaining them")
|
||||||
move_orig_ids = fields.One2many(
|
move_orig_ids = fields.Many2many(
|
||||||
'stock.move', 'move_dest_id', 'Original Move',
|
'stock.move', 'stock_move_move_rel', 'move_dest_id', 'move_orig_id', 'Original Move',
|
||||||
|
copy=False,
|
||||||
help="Optional: previous stock move when chaining them")
|
help="Optional: previous stock move when chaining them")
|
||||||
picking_id = fields.Many2one('stock.picking', 'Transfer Reference', index=True, states={'done': [('readonly', True)]})
|
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')
|
picking_partner_id = fields.Many2one('res.partner', 'Transfer Destination Address', related='picking_id.partner_id')
|
||||||
note = fields.Text('Notes')
|
note = fields.Text('Notes')
|
||||||
|
# TODO: state should be computed according to the move lines
|
||||||
state = fields.Selection([
|
state = fields.Selection([
|
||||||
('draft', 'New'), ('cancel', 'Cancelled'),
|
('draft', 'New'), ('cancel', 'Cancelled'),
|
||||||
('waiting', 'Waiting Another Move'), ('confirmed', 'Waiting Availability'),
|
('waiting', 'Waiting Another Move'),
|
||||||
('assigned', 'Available'), ('done', 'Done')], string='Status',
|
('confirmed', 'Waiting Availability'),
|
||||||
|
('partially_available', 'Partially Available'),
|
||||||
|
('assigned', 'Available'),
|
||||||
|
('done', 'Done')], string='Status',
|
||||||
copy=False, default='draft', index=True, readonly=True,
|
copy=False, default='draft', index=True, readonly=True,
|
||||||
help="* New: When the stock move is created and not yet confirmed.\n"
|
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 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"
|
"* 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"
|
"* Available: When products are reserved, it is set to \'Available\'.\n"
|
||||||
"* Done: When the shipment is processed, the state is \'Done\'.")
|
"* 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(
|
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 "
|
'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
|
"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)
|
backorder_id = fields.Many2one('stock.picking', 'Back Order of', related='picking_id.backorder_id', index=True)
|
||||||
origin = fields.Char("Source Document")
|
origin = fields.Char("Source Document")
|
||||||
procure_method = fields.Selection([
|
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,"
|
"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.")
|
"this second option should be chosen.")
|
||||||
scrapped = fields.Boolean('Scrapped', related='location_dest_id.scrap_location', readonly=True, store=True)
|
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_qty = fields.Float(
|
||||||
'Remaining Quantity', compute='_get_remaining_qty',
|
'Remaining Quantity', compute='_get_remaining_qty',
|
||||||
digits=0, states={'done': [('readonly', True)]},
|
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')
|
help='If checked, when this move is cancelled, cancel the linked move too')
|
||||||
picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type')
|
picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type')
|
||||||
inventory_id = fields.Many2one('stock.inventory', 'Inventory')
|
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')
|
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')
|
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(
|
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'")
|
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")
|
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).")
|
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.one
|
||||||
@api.depends('product_id', 'product_uom', 'product_uom_qty')
|
@api.depends('product_id', 'product_uom', 'product_uom_qty')
|
||||||
def _compute_product_qty(self):
|
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):
|
def _set_product_qty(self):
|
||||||
""" The meaning of product_qty field changed lately and is now a functional field computing the quantity
|
""" 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`.'))
|
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.one
|
||||||
@api.depends('linked_move_operation_ids.qty')
|
|
||||||
def _get_remaining_qty(self):
|
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.one
|
||||||
@api.depends('reserved_quant_ids.qty')
|
@api.depends('pack_operation_ids.product_qty')
|
||||||
def _compute_reserved_availability(self):
|
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.one
|
||||||
@api.depends('state', 'product_id', 'product_qty', 'location_id')
|
@api.depends('state', 'product_id', 'product_qty', 'location_id')
|
||||||
def _compute_product_availability(self):
|
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':
|
if self.state == 'done':
|
||||||
self.availability = self.product_qty
|
self.availability = self.product_qty
|
||||||
else:
|
else:
|
||||||
quants = self.env['stock.quant'].search([('location_id', 'child_of', self.location_id.id), ('product_id', '=', self.product_id.id), ('reservation_id', '=', False)])
|
total_availability = self.env['stock.quant']._get_available_quantity(self.product_id, self.location_id)
|
||||||
self.availability = min(self.product_qty, sum(quants.mapped('qty')))
|
self.availability = min(self.product_qty, total_availability)
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _compute_string_qty_information(self):
|
def _compute_string_qty_information(self):
|
||||||
@@ -242,6 +283,10 @@ class StockMove(models.Model):
|
|||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def write(self, vals):
|
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
|
# TDE CLEANME: it is a gros bordel + tracking
|
||||||
Picking = self.env['stock.picking']
|
Picking = self.env['stock.picking']
|
||||||
# Check that we do not modify a stock.move which is done
|
# 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):
|
if not self._context.get('do_not_propagate', False) and (propagated_date_field or propagated_changes_dict):
|
||||||
#any propagation is (maybe) needed
|
#any propagation is (maybe) needed
|
||||||
for move in self:
|
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:
|
if 'date_expected' in propagated_changes_dict:
|
||||||
propagated_changes_dict.pop('date_expected')
|
propagated_changes_dict.pop('date_expected')
|
||||||
if propagated_date_field:
|
if propagated_date_field:
|
||||||
@@ -290,6 +335,43 @@ class StockMove(models.Model):
|
|||||||
pickings.message_track(pickings.fields_get(['state']), initial_values)
|
pickings.message_track(pickings.fields_get(['state']), initial_values)
|
||||||
return res
|
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
|
# Misc tools
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
@@ -297,18 +379,6 @@ class StockMove(models.Model):
|
|||||||
""" Returns the unit price to store on the quant """
|
""" Returns the unit price to store on the quant """
|
||||||
return self.price_unit or self.product_id.standard_price
|
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):
|
def _filter_closed_moves(self):
|
||||||
""" Helper methods when having to avoid working on moves that are
|
""" 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
|
already done or canceled. In a lot of cases you may handle a batch
|
||||||
@@ -323,14 +393,24 @@ class StockMove(models.Model):
|
|||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def do_unreserve(self):
|
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):
|
def _push_apply(self):
|
||||||
# TDE CLEANME: I am quite sure I already saw this code somewhere ... in routing ??
|
# TDE CLEANME: I am quite sure I already saw this code somewhere ... in routing ??
|
||||||
Push = self.env['stock.location.path']
|
Push = self.env['stock.location.path']
|
||||||
for move in self:
|
for move in self:
|
||||||
# if the move is already chained, there is no need to check push rules
|
# 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
|
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
|
# 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)
|
# 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
|
# 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):
|
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)
|
rules._apply(move)
|
||||||
return True
|
|
||||||
|
|
||||||
@api.onchange('product_id', 'product_qty')
|
@api.onchange('product_id', 'product_qty')
|
||||||
def onchange_quantity(self):
|
def onchange_quantity(self):
|
||||||
@@ -372,6 +451,19 @@ class StockMove(models.Model):
|
|||||||
if self.date_expected:
|
if self.date_expected:
|
||||||
self.date = 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
|
# TDE DECORATOR: remove that api.multi when action_confirm is migrated
|
||||||
@api.multi
|
@api.multi
|
||||||
def assign_picking(self):
|
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 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:
|
if move.move_orig_ids:
|
||||||
move_waiting |= move
|
move_waiting |= move
|
||||||
# if the move is split and some of the ancestor was preceeded, then it's waiting as well
|
|
||||||
else:
|
else:
|
||||||
inner_move = move.split_from
|
if move.procure_method == 'make_to_order':
|
||||||
while inner_move:
|
move_create_proc |= move
|
||||||
if inner_move.move_orig_ids:
|
|
||||||
move_waiting |= move
|
|
||||||
break
|
|
||||||
inner_move = inner_move.split_from
|
|
||||||
else:
|
else:
|
||||||
if move.procure_method == 'make_to_order':
|
move_to_confirm |= move
|
||||||
move_create_proc |= move
|
|
||||||
else:
|
|
||||||
move_to_confirm |= move
|
|
||||||
|
|
||||||
if not move.picking_id and move.picking_type_id:
|
if not move.picking_id and move.picking_type_id:
|
||||||
key = (move.group_id.id, move.location_id.id, move.location_dest_id.id)
|
key = (move.group_id.id, move.location_id.id, move.location_dest_id.id)
|
||||||
if key not in to_assign:
|
if key not in to_assign:
|
||||||
@@ -477,6 +560,7 @@ class StockMove(models.Model):
|
|||||||
attribute_price = set_default_price_unit_from_product
|
attribute_price = set_default_price_unit_from_product
|
||||||
|
|
||||||
def _prepare_procurement_from_move(self):
|
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 "/")
|
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
|
group_id = self.group_id and self.group_id.id or False
|
||||||
if self.rule_id:
|
if self.rule_id:
|
||||||
@@ -502,18 +586,11 @@ class StockMove(models.Model):
|
|||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def force_assign(self):
|
def force_assign(self):
|
||||||
# TDE CLEANME: removed return value
|
""" Allow to work on stock move lines even if the reservationis not possible. We just mark
|
||||||
self.write({'state': 'assigned'})
|
the move as assigned, so the view does not block the user.
|
||||||
self.check_recompute_pack_op()
|
"""
|
||||||
|
for move in self.filtered(lambda m: m.state in ['confirmed', 'waiting', 'partially_available', 'assigned']):
|
||||||
# TDE DECORATOR: internal
|
move.write({'state': 'assigned'})
|
||||||
@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})
|
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def check_tracking(self, pack_operation):
|
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)):
|
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)
|
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
|
@api.multi
|
||||||
def action_assign(self):
|
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
|
@api.multi
|
||||||
def action_cancel(self):
|
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
|
return True
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def action_done(self):
|
def _create_extra_move(self):
|
||||||
""" Process completely the moves given and if all moves are done, it will finish the picking. """
|
""" If the quantity done on a move exceeds its quantity todo, this method will create an
|
||||||
self.filtered(lambda move: move.state == 'draft').action_confirm()
|
extra move attached to a (potentially split) move line. If the previous condition is not
|
||||||
|
met, it'll return an empty recordset.
|
||||||
|
|
||||||
Uom = self.env['product.uom']
|
The rationale for the creation of an extra move is the application of a potential push
|
||||||
Quant = self.env['stock.quant']
|
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()
|
||||||
|
|
||||||
pickings = self.env['stock.picking']
|
# link it to some move lines
|
||||||
procurements = self.env['procurement.order']
|
for move_line in self.pack_operation_ids.filtered(lambda ml: ml.qty_done):
|
||||||
operations = self.env['stock.pack.operation']
|
if float_compare(move_line.qty_done, extra_move_quantity, precision_rounding=rounding) <= 0:
|
||||||
|
# move this move line to our extra move
|
||||||
remaining_move_qty = {}
|
move_line.move_id = extra_move.id
|
||||||
|
extra_move_quantity -= move_line.qty_done
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
# Check what you can do with reserved quants already
|
# split this move line and assign the new part to our extra move
|
||||||
qty_on_link = prout_move_qty[move]
|
quantity_split = float_round(
|
||||||
rounding = operation.product_id.uom_id.rounding
|
move_line.qty_done - extra_move_quantity,
|
||||||
for reserved_quant in move.reserved_quant_ids:
|
precision_rounding=self.product_uom.rounding,
|
||||||
if (reserved_quant.owner_id.id != operation.owner_id.id) or (reserved_quant.location_id.id != operation.location_id.id) or \
|
rounding_method='UP')
|
||||||
(reserved_quant.package_id.id != operation.package_id.id):
|
move_line.qty_done = quantity_split
|
||||||
continue
|
move_line.copy(default={'move_id': extra_move.id, 'qty_done': extra_move_quantity, 'product_uom_qty': 0})
|
||||||
if not reserved_quant.lot_id:
|
extra_move_quantity -= extra_move_quantity
|
||||||
false_quants += [reserved_quant]
|
if extra_move_quantity == 0.0:
|
||||||
elif float_compare(lot_quantities.get(reserved_quant.lot_id.id, 0), 0, precision_rounding=rounding) > 0:
|
break
|
||||||
if float_compare(lot_quantities[reserved_quant.lot_id.id], reserved_quant.qty, precision_rounding=rounding) >= 0:
|
return extra_move
|
||||||
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
|
|
||||||
|
|
||||||
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
|
moves = self.filtered(lambda x: x.state not in ('done', 'cancel'))
|
||||||
if operation.pack_lot_ids:
|
moves_todo = self.env['stock.move']
|
||||||
# TDE FIXME: fix call to move_quants_by_lot to ease understanding
|
# Create extra moves where necessary
|
||||||
self._move_quants_by_lot(operation, lot_quantities, quants_taken, false_quants, lot_move_qty, quant_dest_package_id)
|
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 you were already putting stock.move.lots on the next one in the work order, transfer those to the new move
|
||||||
if not operation.product_id and operation.package_id and operation.result_package_id.id != operation.package_id.parent_id.id:
|
move.pack_operation_ids.filtered(lambda x: x.qty_done == 0.0).write({'move_id': new_move})
|
||||||
operation.package_id.sudo().write({'parent_id': operation.result_package_id.id})
|
self.browse(new_move).quantity_done = 0.0
|
||||||
|
for packop in move.pack_operation_ids:
|
||||||
# Check for remaining qtys and unreserve/check move_dest_id in
|
if float_compare(packop.qty_done, 0, precision_rounding=rounding) > 0:
|
||||||
move_dest_ids = set()
|
if move.has_tracking != 'none' and (move.picking_type_id.use_create_lots or move.picking_type_id.use_existing_lots):
|
||||||
for move in self:
|
if packop.lot_name and not packop.lot_id:
|
||||||
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
|
lot = self.env['stock.production.lot'].create(
|
||||||
move.check_tracking(False) # TDE: do in batch ? redone ? check this
|
{'name': packop.lot_name, 'product_id': packop.product_id.id})
|
||||||
|
packop.write({'lot_id': lot.id})
|
||||||
preferred_domain_list = [[('reservation_id', '=', move.id)], [('reservation_id', '=', False)], ['&', ('reservation_id', '!=', move.id), ('reservation_id', '!=', False)]]
|
if not packop.lot_id:
|
||||||
quants = Quant.quants_get_preferred_domain(
|
raise UserError(_('You need to supply a lot/serial number.'))
|
||||||
remaining_move_qty[move.id], move, domain=[('qty', '>', 0)],
|
# execute `action_done` even if nothing was done, to free the reservation
|
||||||
preferred_domain_list=preferred_domain_list)
|
packop.action_done()
|
||||||
Quant.quants_move(
|
picking = self and self[0].picking_id or False
|
||||||
quants, move, move.location_dest_id,
|
moves_todo.write({'state': 'done', 'date': fields.Datetime.now()})
|
||||||
lot_id=move.restrict_lot_id.id, owner_id=move.restrict_partner_id.id)
|
moves_todo.mapped('move_dest_ids').action_assign()
|
||||||
|
if picking:
|
||||||
# If the move has a destination, add it to the list to reserve
|
moves_to_backorder = picking.move_lines.filtered(lambda x: x.state not in ('done', 'cancel'))
|
||||||
if move.move_dest_id and move.move_dest_id.state in ('waiting', 'confirmed'):
|
if moves_to_backorder:
|
||||||
move_dest_ids.add(move.move_dest_id.id)
|
backorder_picking = picking.copy({
|
||||||
|
'name': '/',
|
||||||
if move.procurement_id:
|
'move_lines': [],
|
||||||
procurements |= move.procurement_id
|
'pack_operation_ids': [],
|
||||||
|
'backorder_id': picking.id
|
||||||
# unreserve the quants and make them available for other operations/moves
|
})
|
||||||
move.quants_unreserve()
|
picking.message_post('Backorder Created') #message needs to be improved
|
||||||
|
moves_to_backorder.write({'picking_id': backorder_picking.id})
|
||||||
# Check the packages have been placed in the correct locations
|
moves_to_backorder.action_assign()
|
||||||
self.mapped('quant_ids').filtered(lambda quant: quant.package_id and quant.qty > 0).mapped('package_id')._check_location_constraint()
|
return moves_todo
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def unlink(self):
|
def unlink(self):
|
||||||
@@ -702,9 +891,9 @@ class StockMove(models.Model):
|
|||||||
'product_uom_qty': uom_qty,
|
'product_uom_qty': uom_qty,
|
||||||
'procure_method': 'make_to_stock',
|
'procure_method': 'make_to_stock',
|
||||||
'restrict_lot_id': restrict_lot_id,
|
'restrict_lot_id': restrict_lot_id,
|
||||||
'split_from': self.id,
|
|
||||||
'procurement_id': self.procurement_id.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,
|
'origin_returned_move_id': self.origin_returned_move_id.id,
|
||||||
}
|
}
|
||||||
if restrict_partner_id:
|
if restrict_partner_id:
|
||||||
@@ -717,17 +906,31 @@ class StockMove(models.Model):
|
|||||||
# ctx = context.copy()
|
# ctx = context.copy()
|
||||||
# TDE CLEANME: used only in write in this file, to clean
|
# TDE CLEANME: used only in write in this file, to clean
|
||||||
# ctx['do_not_propagate'] = True
|
# 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'):
|
# FIXME: pim fix your crap
|
||||||
new_move_prop = self.move_dest_id.split(qty)
|
self.with_context(do_not_propagate=True, do_not_unreserve=True).write({'product_uom_qty': self.product_uom_qty - uom_qty})
|
||||||
new_move.write({'move_dest_id': new_move_prop})
|
|
||||||
|
# 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
|
# 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)
|
# thus the result of action_confirm should always be a list of 1 element length)
|
||||||
new_move.action_confirm()
|
new_move.action_confirm()
|
||||||
# TDE FIXME: due to action confirm change
|
# TDE FIXME: due to action confirm change
|
||||||
return new_move.id
|
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
|
@api.multi
|
||||||
def action_show_picking(self):
|
def action_show_picking(self):
|
||||||
view = self.env.ref('stock.view_picking_form')
|
view = self.env.ref('stock.view_picking_form')
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
from odoo import api, fields, models, _
|
from odoo import api, fields, models, _
|
||||||
|
|
||||||
from odoo.addons import decimal_precision as dp
|
from odoo.addons import decimal_precision as dp
|
||||||
from odoo.exceptions import UserError, ValidationError
|
from odoo.exceptions import UserError
|
||||||
from odoo.tools.float_utils import float_round, float_compare
|
|
||||||
from odoo.tools.pycompat import izip
|
from odoo.tools.pycompat import izip
|
||||||
|
from odoo.tools.float_utils import float_round, float_compare, float_is_zero
|
||||||
|
|
||||||
|
|
||||||
class PackOperation(models.Model):
|
class PackOperation(models.Model):
|
||||||
@@ -14,104 +14,40 @@ class PackOperation(models.Model):
|
|||||||
_description = "Packing Operation"
|
_description = "Packing Operation"
|
||||||
_order = "result_package_id desc, id"
|
_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(
|
picking_id = fields.Many2one(
|
||||||
'stock.picking', 'Stock Picking',
|
'stock.picking', 'Stock Picking',
|
||||||
required=True,
|
|
||||||
help='The stock operation where the packing has been made')
|
help='The stock operation where the packing has been made')
|
||||||
product_id = fields.Many2one('product.product', 'Product', ondelete="cascade")
|
move_id = fields.Many2one(
|
||||||
product_uom_id = fields.Many2one('product.uom', 'Unit of Measure')
|
'stock.move', 'Stock Move',
|
||||||
product_qty = fields.Float('To Do', default=0.0, digits=dp.get_precision('Product Unit of Measure'), required=True)
|
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'))
|
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'))
|
qty_done = fields.Float('Done', default=0.0, digits=dp.get_precision('Product Unit of Measure'), copy=False)
|
||||||
is_done = fields.Boolean(compute='_compute_is_done', string='Done', readonly=False, oldname='processed_boolean')
|
|
||||||
package_id = fields.Many2one('stock.quant.package', 'Source Package')
|
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(
|
result_package_id = fields.Many2one(
|
||||||
'stock.quant.package', 'Destination Package',
|
'stock.quant.package', 'Destination Package',
|
||||||
ondelete='cascade', required=False,
|
ondelete='cascade', required=False,
|
||||||
help="If set, the operations are packed into this package")
|
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")
|
owner_id = fields.Many2one('res.partner', 'Owner', help="Owner of the quants")
|
||||||
linked_move_operation_ids = fields.One2many(
|
location_id = fields.Many2one('stock.location', 'From', required=True)
|
||||||
'stock.move.operation.link', 'operation_id', string='Linked Moves',
|
location_dest_id = fields.Many2one('stock.location', 'To', required=True)
|
||||||
readonly=True,
|
from_loc = fields.Char(compute='_compute_location_description')
|
||||||
help='Moves impacted by this operation for the computation of the remaining quantities')
|
to_loc = fields.Char(compute='_compute_location_description')
|
||||||
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)
|
|
||||||
lots_visible = fields.Boolean(compute='_compute_lots_visible')
|
lots_visible = fields.Boolean(compute='_compute_lots_visible')
|
||||||
state = fields.Selection(selection=[
|
state = fields.Selection(related='move_id.state')
|
||||||
('draft', 'Draft'),
|
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. ")
|
||||||
('cancel', 'Cancelled'),
|
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. ")
|
||||||
('waiting', 'Waiting Another Operation'),
|
|
||||||
('confirmed', 'Waiting Availability'),
|
|
||||||
('partially_available', 'Partially Available'),
|
|
||||||
('assigned', 'Available'),
|
|
||||||
('done', 'Done')], related='picking_id.state')
|
|
||||||
|
|
||||||
@api.one
|
@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):
|
def _compute_location_description(self):
|
||||||
for operation, operation_sudo in izip(self, self.sudo()):
|
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 '')
|
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
|
@api.one
|
||||||
def _compute_lots_visible(self):
|
def _compute_lots_visible(self):
|
||||||
if self.pack_lot_ids:
|
if self.picking_id.picking_type_id and self.product_id.tracking != 'none': # TDE FIXME: not sure correctly migrated
|
||||||
self.lots_visible = True
|
|
||||||
elif self.picking_id.picking_type_id and self.product_id.tracking != 'none': # TDE FIXME: not sure correctly migrated
|
|
||||||
picking = self.picking_id
|
picking = self.picking_id
|
||||||
self.lots_visible = picking.picking_type_id.use_existing_lots or picking.picking_type_id.use_create_lots
|
self.lots_visible = picking.picking_type_id.use_existing_lots or picking.picking_type_id.use_create_lots
|
||||||
else:
|
else:
|
||||||
self.lots_visible = self.product_id.tracking != 'none'
|
self.lots_visible = self.product_id.tracking != 'none'
|
||||||
|
|
||||||
@api.onchange('pack_lot_ids')
|
@api.one
|
||||||
def _onchange_packlots(self):
|
@api.depends('product_id', 'product_uom_id', 'product_uom_qty')
|
||||||
self.qty_done = sum([x.qty for x in self.pack_lot_ids])
|
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.multi
|
||||||
@api.onchange('product_id', 'product_uom_id')
|
@api.onchange('product_id', 'product_uom_id')
|
||||||
@@ -137,7 +80,10 @@ class PackOperation(models.Model):
|
|||||||
if self.product_id:
|
if self.product_id:
|
||||||
self.lots_visible = self.product_id.tracking != 'none'
|
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:
|
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)]}}
|
res = {'domain': {'product_uom_id': [('category_id', '=', self.product_uom_id.category_id.id)]}}
|
||||||
else:
|
else:
|
||||||
res = {'domain': {'product_uom_id': []}}
|
res = {'domain': {'product_uom_id': []}}
|
||||||
@@ -145,173 +91,230 @@ class PackOperation(models.Model):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def create(self, vals):
|
def create(self, vals):
|
||||||
vals['ordered_qty'] = vals.get('product_qty')
|
vals['ordered_qty'] = vals.get('product_uom_qty')
|
||||||
return super(PackOperation, self).create(vals)
|
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
|
@api.multi
|
||||||
def write(self, values):
|
def write(self, vals):
|
||||||
# TDE FIXME: weird stuff, protectin pack op ?
|
""" Through the interface, we allow users to change the charateristics of a move line. If a
|
||||||
values['fresh_record'] = False
|
quantity has been reserved for this move line, we impact the reservation directly to free
|
||||||
return super(PackOperation, self).write(values)
|
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
|
@api.multi
|
||||||
def unlink(self):
|
def unlink(self):
|
||||||
if any([operation.state in ('done', 'cancel') for operation in self]):
|
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||||
raise UserError(_('You can not delete pack operations of a done picking'))
|
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()
|
return super(PackOperation, self).unlink()
|
||||||
|
|
||||||
@api.multi
|
def action_done(self):
|
||||||
def split_quantities(self):
|
""" This method will finalize the work with a move line by "moving" quants to the
|
||||||
for operation in self:
|
destination location.
|
||||||
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})
|
for ml in self:
|
||||||
operation.write({'product_qty': operation.qty_done})
|
if ml.product_id.type != 'consu':
|
||||||
operation._copy_remaining_pack_lot_ids(cpy)
|
Quant = self.env['stock.quant']
|
||||||
else:
|
rounding = ml.product_uom_id.rounding
|
||||||
raise UserError(_('The quantity to split should be smaller than the quantity To Do. '))
|
|
||||||
return True
|
|
||||||
|
|
||||||
@api.multi
|
# if this move line is force assigned, unreserve elsewhere if needed
|
||||||
def save(self):
|
if float_compare(ml.qty_done, ml.product_qty, precision_rounding=rounding) > 0:
|
||||||
# TDE FIXME: does not seem to be used -> actually, it does
|
extra_qty = ml.qty_done - ml.product_qty
|
||||||
# TDE FIXME: move me somewhere else, because the return indicated a wizard, in pack op, it is quite strange
|
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)
|
||||||
# HINT: 4. How to manage lots of identical products?
|
# unreserve what's been reserved
|
||||||
# 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
|
if ml.location_id.should_impact_quants() and ml.product_id.type == 'product' and ml.product_qty:
|
||||||
for pack in self:
|
try:
|
||||||
if pack.product_id.tracking != 'none':
|
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)
|
||||||
pack.write({'qty_done': sum(pack.pack_lot_ids.mapped('qty'))})
|
except UserError:
|
||||||
return {'type': 'ir.actions.act_window_close'}
|
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
|
# move what's been actually done
|
||||||
def action_split_lots(self):
|
quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
|
||||||
action_ctx = dict(self.env.context)
|
if ml.location_id.should_impact_quants() and ml.product_id.type == 'product':
|
||||||
# If it's a returned stock move, we do not want to create a lot
|
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)
|
||||||
returned_move = self.linked_move_operation_ids.mapped('move_id').mapped('origin_returned_move_id')
|
if available_qty < 0 and ml.lot_id:
|
||||||
picking_type = self.picking_id.picking_type_id
|
# see if we can compensate the negative quants with some untracked quants
|
||||||
action_ctx.update({
|
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)
|
||||||
'serial': self.product_id.tracking == 'serial',
|
if untracked_qty:
|
||||||
'only_create': picking_type.use_create_lots and not picking_type.use_existing_lots and not returned_move,
|
taken_from_untracked_qty = min(untracked_qty, abs(quantity))
|
||||||
'create_lots': picking_type.use_create_lots,
|
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)
|
||||||
'state_done': self.picking_id.state == 'done',
|
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)
|
||||||
'show_reserved': any([lot for lot in self.pack_lot_ids if lot.qty_todo > 0.0])})
|
if ml.location_dest_id.should_impact_quants() and ml.product_id.type == 'product':
|
||||||
view_id = self.env.ref('stock.view_pack_operation_lot_form').id
|
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)
|
||||||
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}
|
|
||||||
|
|
||||||
@api.multi
|
def _free_reservation(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None):
|
||||||
def show_details(self):
|
""" When editing a done move line or validating one with some forced quantities, it is
|
||||||
# TDE FIXME: does not seem to be used
|
possible to impact quants that were not reserved. It is therefore necessary to edit or
|
||||||
view_id = self.env.ref('stock.view_pack_operation_details_form_save').id
|
unlink the move lines that reserved a quantity now unavailable.
|
||||||
return {
|
"""
|
||||||
'name': _('Operation Details'),
|
self.ensure_one()
|
||||||
'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}
|
|
||||||
|
|
||||||
@api.multi
|
# Check the available quantity, with the `strict` kw set to `True`. If the available
|
||||||
def _check_serial_number(self):
|
# quantity is greather than the quantity now unavailable, there is nothing to do.
|
||||||
for operation in self:
|
available_quantity = self.env['stock.quant']._get_available_quantity(
|
||||||
if operation.picking_id and \
|
product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True
|
||||||
(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 \
|
if quantity > available_quantity:
|
||||||
operation.qty_done > 0.0:
|
# We now have to find the move lines that reserved our now unavailable quantity. We
|
||||||
if not operation.pack_lot_ids:
|
# take care to exclude ourselves and the move lines were work had already been done.
|
||||||
raise UserError(_('You need to provide a Lot/Serial Number for product %s') % operation.product_id.name)
|
oudated_move_lines_domain = [
|
||||||
if operation.product_id.tracking == 'serial':
|
('move_id.state', 'not in', ['done', 'cancel']),
|
||||||
for opslot in operation.pack_lot_ids:
|
('product_id', '=', product_id.id),
|
||||||
if opslot.qty not in (1.0, 0.0):
|
('lot_id', '=', lot_id.id if lot_id else False),
|
||||||
raise UserError(_('You should provide a different serial number for each piece'))
|
('location_id', '=', location_id.id),
|
||||||
check_tracking = _check_serial_number
|
('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
|
# As the move's state is not computed over the move lines, we'll have to manually
|
||||||
def _copy_remaining_pack_lot_ids(self, new_operation):
|
# recompute the moves which we adapted their lines.
|
||||||
for op in self:
|
move_to_recompute_state = self.env['stock.move']
|
||||||
for lot in op.pack_lot_ids:
|
|
||||||
new_qty_todo = lot.qty_todo - lot.qty
|
|
||||||
|
|
||||||
if float_compare(new_qty_todo, 0, precision_rounding=op.product_uom_id.rounding) > 0:
|
rounding = self.product_uom_id.rounding
|
||||||
lot.copy({
|
for candidate in oudated_candidates:
|
||||||
'operation_id': new_operation.id,
|
if float_compare(candidate.product_qty, quantity, precision_rounding=rounding) <= 0:
|
||||||
'qty_todo': new_qty_todo,
|
quantity -= candidate.product_qty
|
||||||
'qty': 0,
|
move_to_recompute_state |= candidate.move_id
|
||||||
})
|
candidate.unlink()
|
||||||
|
else:
|
||||||
|
# split this move line and assign the new part to our extra move
|
||||||
class PackOperationLot(models.Model):
|
quantity_split = float_round(
|
||||||
_name = "stock.pack.operation.lot"
|
candidate.product_qty - quantity,
|
||||||
_description = "Lot/Serial number for pack ops"
|
precision_rounding=self.product_uom_id.rounding,
|
||||||
|
rounding_method='UP')
|
||||||
operation_id = fields.Many2one('stock.pack.operation')
|
candidate.product_uom_qty = self.product_id.uom_id._compute_quantity(quantity_split, self.product_uom_id, rounding_method='HALF-UP')
|
||||||
qty = fields.Float('Done', default=1.0)
|
quantity -= quantity_split
|
||||||
lot_id = fields.Many2one('stock.production.lot', 'Lot/Serial Number')
|
move_to_recompute_state |= candidate.move_id
|
||||||
lot_name = fields.Char('Lot/Serial Number')
|
if quantity == 0.0:
|
||||||
qty_todo = fields.Float('To Do', default=0.0)
|
break
|
||||||
plus_visible = fields.Boolean(compute='_compute_plus_visible', default=True)
|
move_to_recompute_state._recompute_state()
|
||||||
|
|
||||||
_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")
|
|
||||||
|
|||||||
@@ -261,18 +261,10 @@ class Picking(models.Model):
|
|||||||
default=lambda self: self.env['res.company']._company_default_get('stock.picking'),
|
default=lambda self: self.env['res.company']._company_default_get('stock.picking'),
|
||||||
index=True, required=True,
|
index=True, required=True,
|
||||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
||||||
# TDE FIXME: separate those two kind of pack operations
|
|
||||||
pack_operation_ids = fields.One2many(
|
pack_operation_ids = fields.One2many(
|
||||||
'stock.pack.operation', 'picking_id', 'Related Packing Operations',
|
'stock.pack.operation', 'picking_id', 'Related Packing Operations',
|
||||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
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(
|
pack_operation_exist = fields.Boolean(
|
||||||
'Has Pack Operations', compute='_compute_pack_operation_exist',
|
'Has Pack Operations', compute='_compute_pack_operation_exist',
|
||||||
@@ -285,16 +277,13 @@ class Picking(models.Model):
|
|||||||
printed = fields.Boolean('Printed')
|
printed = fields.Boolean('Printed')
|
||||||
# Used to search on pickings
|
# Used to search on pickings
|
||||||
product_id = fields.Many2one('product.product', 'Product', related='move_lines.product_id')
|
product_id = fields.Many2one('product.product', 'Product', related='move_lines.product_id')
|
||||||
recompute_pack_op = fields.Boolean(
|
show_operations = fields.Boolean(related='picking_type_id.show_operations')
|
||||||
'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)
|
|
||||||
|
|
||||||
_sql_constraints = [
|
_sql_constraints = [
|
||||||
('name_uniq', 'unique(name, company_id)', 'Reference must be unique per company!'),
|
('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
|
@api.one
|
||||||
def _compute_state(self):
|
def _compute_state(self):
|
||||||
''' State of a picking depends on the state of its related stock.move
|
''' 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
|
- one of the move is assigned or partially available: partially available
|
||||||
- otherwise in waiting or confirmed state
|
- otherwise in waiting or confirmed state
|
||||||
'''
|
'''
|
||||||
if not self.move_lines and self.launch_pack_operations:
|
# FIXME: goes in 3 times at each write?
|
||||||
self.state = 'assigned'
|
if not self.move_lines:
|
||||||
elif not self.move_lines:
|
|
||||||
self.state = 'draft'
|
self.state = 'draft'
|
||||||
elif any(move.state == 'draft' for move in self.move_lines): # TDE FIXME: should be all ?
|
elif any(move.state == 'draft' for move in self.move_lines): # TDE FIXME: should be all ?
|
||||||
self.state = 'draft'
|
self.state = 'draft'
|
||||||
@@ -318,16 +306,34 @@ class Picking(models.Model):
|
|||||||
elif all(move.state in ['cancel', 'done'] for move in self.move_lines):
|
elif all(move.state in ['cancel', 'done'] for move in self.move_lines):
|
||||||
self.state = 'done'
|
self.state = 'done'
|
||||||
else:
|
else:
|
||||||
# We sort our moves by importance of state: "confirmed" should be first, then we'll have
|
# We sort our moves by importance of state:
|
||||||
# "waiting" and finally "assigned" at the end.
|
# ------------- 0
|
||||||
|
# | Confirmed |
|
||||||
|
# -------------
|
||||||
|
# | Partial |
|
||||||
|
# -------------
|
||||||
|
# | Waiting |
|
||||||
|
# -------------
|
||||||
|
# | Assigned |
|
||||||
|
# ------------- len - 1
|
||||||
|
sort_map = {
|
||||||
|
'assigned': 4,
|
||||||
|
'waiting': 3,
|
||||||
|
'partially_available': 2,
|
||||||
|
'confirmed': 1,
|
||||||
|
}
|
||||||
moves_todo = self.move_lines\
|
moves_todo = self.move_lines\
|
||||||
.filtered(lambda move: move.state not in ['cancel', 'done'])\
|
.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':
|
if self.move_type == 'one':
|
||||||
self.state = moves_todo[0].state or 'draft'
|
if moves_todo[0].state in ('partially_available', 'confirmed'):
|
||||||
elif moves_todo[0].state != 'assigned' and any(x.partially_available or x.state == 'assigned' for x in moves_todo):
|
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'
|
self.state = 'partially_available'
|
||||||
else:
|
else:
|
||||||
|
# take the less important state among all move_lines.
|
||||||
self.state = moves_todo[-1].state or 'draft'
|
self.state = moves_todo[-1].state or 'draft'
|
||||||
|
|
||||||
@api.one
|
@api.one
|
||||||
@@ -443,10 +449,14 @@ class Picking(models.Model):
|
|||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def action_confirm(self):
|
def action_confirm(self):
|
||||||
self.filtered(lambda picking: not picking.move_lines).write({'launch_pack_operations': True})
|
# call `action_confirm` on every draft move
|
||||||
# TDE CLEANME: use of launch pack operation, really useful ?
|
self.mapped('move_lines')\
|
||||||
self.mapped('move_lines').filtered(lambda move: move.state == 'draft').action_confirm()
|
.filtered(lambda move: move.state == 'draft')\
|
||||||
self.filtered(lambda picking: picking.location_id.usage in ('supplier', 'inventory', 'production')).force_assign()
|
.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
|
return True
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
@@ -468,7 +478,7 @@ class Picking(models.Model):
|
|||||||
""" Changes state of picking to available if moves are confirmed or waiting.
|
""" Changes state of picking to available if moves are confirmed or waiting.
|
||||||
@return: True
|
@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
|
return True
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
@@ -485,419 +495,157 @@ class Picking(models.Model):
|
|||||||
"""
|
"""
|
||||||
# TDE FIXME: remove decorator when migration the remaining
|
# TDE FIXME: remove decorator when migration the remaining
|
||||||
# TDE FIXME: draft -> automatically done, if waiting ?? CLEAR ME
|
# 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', 'partially_available', 'assigned', 'confirmed'])
|
||||||
todo_moves = self.mapped('move_lines').filtered(lambda self: self.state in ['draft', 'assigned', 'confirmed'])
|
# Check if there are ops not linked to moves yet
|
||||||
draft_moves.action_confirm()
|
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()
|
todo_moves.action_done()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _prepare_pack_ops(self, quants, forced_qties):
|
do_transfer = action_done #TODO:replace later
|
||||||
""" 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
|
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def do_prepare_partial(self):
|
def _check_entire_pack(self):
|
||||||
# TDE CLEANME: oh dear ...
|
""" This function check if entire packs are moved in the picking"""
|
||||||
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()
|
|
||||||
for picking in self:
|
for picking in self:
|
||||||
forced_qties = {} # Quantity remaining after calculating reserved quants
|
origin_packages = picking.pack_operation_ids.mapped("package_id")
|
||||||
picking_quants = self.env['stock.quant']
|
for pack in origin_packages:
|
||||||
# Calculate packages, reserved quants, qtys of this picking's moves
|
all_in = True
|
||||||
for move in picking.move_lines:
|
packops = picking.pack_operation_ids.filtered(lambda x: x.package_id == pack)
|
||||||
if move.state not in ('assigned', 'confirmed', 'waiting'):
|
keys = ['product_id', 'lot_id']
|
||||||
continue
|
|
||||||
move_quants = move.reserved_quant_ids
|
grouped_quants = {}
|
||||||
picking_quants += move_quants
|
for k, g in groupby(sorted(pack.quant_ids, key=itemgetter(*keys)), key=itemgetter(*keys)):
|
||||||
forced_qty = 0.0
|
grouped_quants[k] = sum(self.env['stock.quant'].concat(*list(g)).mapped('quantity'))
|
||||||
if move.state == 'assigned':
|
|
||||||
qty = move.product_uom._compute_quantity(move.product_uom_qty, move.product_id.uom_id, round=False)
|
grouped_ops = {}
|
||||||
forced_qty = qty - sum([x.qty for x in move_quants])
|
for k, g in groupby(sorted(packops, key=itemgetter(*keys)), key=itemgetter(*keys)):
|
||||||
# if we used force_assign() on the move, or if the move is incoming, forced_qty > 0
|
grouped_ops[k] = sum(self.env['stock.pack.operation'].concat(*list(g)).mapped('product_qty'))
|
||||||
if float_compare(forced_qty, 0, precision_rounding=move.product_id.uom_id.rounding) > 0:
|
if any(grouped_quants[key] - grouped_ops.get(key, 0) != 0 for key in grouped_quants)\
|
||||||
if forced_qties.get(move.product_id):
|
or any(grouped_ops[key] - grouped_quants[key] != 0 for key in grouped_ops):
|
||||||
forced_qties[move.product_id] += forced_qty
|
all_in = False
|
||||||
else:
|
if all_in and packops:
|
||||||
forced_qties[move.product_id] = forced_qty
|
packops.write({'result_package_id': pack.id})
|
||||||
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})
|
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def do_unreserve(self):
|
def do_unreserve(self):
|
||||||
return True
|
for move in self:
|
||||||
|
for move_line in move.move_lines:
|
||||||
def recompute_remaining_qty(self, done_qtys=False):
|
move_line.do_unreserve()
|
||||||
|
self.write({'state': 'confirmed'})
|
||||||
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)
|
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def do_new_transfer(self):
|
def button_validate(self):
|
||||||
for pick in self:
|
self.ensure_one()
|
||||||
if pick.state == 'done':
|
pack_operations_delete = self.env['stock.pack.operation']
|
||||||
raise UserError(_('The pick is already validated'))
|
if not self.move_lines and not self.pack_operation_ids:
|
||||||
pack_operations_delete = self.env['stock.pack.operation']
|
raise UserError(_('Please add some lines to move'))
|
||||||
if not pick.move_lines and not pick.pack_operation_ids:
|
# In draft or with no pack operations edited yet, ask if we can just do everything
|
||||||
raise UserError(_('Please create some Initial Demand or Mark as Todo and create some Operations. '))
|
if self.state == 'draft' or all([x.qty_done == 0.0 for x in self.pack_operation_ids]):
|
||||||
# In draft or with no pack operations edited yet, ask if we can just do everything
|
# If no lots when needed, raise error
|
||||||
if pick.state == 'draft' or all([x.qty_done == 0.0 for x in pick.pack_operation_ids]):
|
picking_type = self.picking_type_id
|
||||||
# If no lots when needed, raise error
|
if (picking_type.use_create_lots or picking_type.use_existing_lots):
|
||||||
picking_type = pick.picking_type_id
|
for pack in self.pack_operation_ids:
|
||||||
if (picking_type.use_create_lots or picking_type.use_existing_lots):
|
if pack.product_id and pack.product_id.tracking != 'none':
|
||||||
for pack in pick.pack_operation_ids:
|
raise UserError(_('Some products require lots/serial numbers, so you need to specify those first!'))
|
||||||
if pack.product_id and pack.product_id.tracking != 'none':
|
view = self.env.ref('stock.view_immediate_transfer')
|
||||||
raise UserError(_('Some products require lots/serial numbers, so you need to specify those first!'))
|
wiz = self.env['stock.immediate.transfer'].create({'pick_id': self.id})
|
||||||
view = self.env.ref('stock.view_immediate_transfer')
|
return {
|
||||||
wiz = self.env['stock.immediate.transfer'].create({'pick_id': pick.id})
|
'name': _('Immediate Transfer?'),
|
||||||
# TDE FIXME: a return in a loop, what a good idea. Really.
|
'type': 'ir.actions.act_window',
|
||||||
return {
|
'view_type': 'form',
|
||||||
'name': _('Immediate Transfer?'),
|
'view_mode': 'form',
|
||||||
'type': 'ir.actions.act_window',
|
'res_model': 'stock.immediate.transfer',
|
||||||
'view_type': 'form',
|
'views': [(view.id, 'form')],
|
||||||
'view_mode': 'form',
|
'view_id': view.id,
|
||||||
'res_model': 'stock.immediate.transfer',
|
'target': 'new',
|
||||||
'views': [(view.id, 'form')],
|
'res_id': wiz.id,
|
||||||
'view_id': view.id,
|
'context': self.env.context,
|
||||||
'target': 'new',
|
}
|
||||||
'res_id': wiz.id,
|
|
||||||
'context': self.env.context,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check backorder should check for other barcodes
|
# Check backorder should check for other barcodes
|
||||||
if pick.check_backorder():
|
if self.check_backorder():
|
||||||
view = self.env.ref('stock.view_backorder_confirmation')
|
view = self.env.ref('stock.view_backorder_confirmation')
|
||||||
wiz = self.env['stock.backorder.confirmation'].create({'pick_id': pick.id})
|
wiz = self.env['stock.backorder.confirmation'].create({'pick_id': self.id})
|
||||||
# TDE FIXME: same reamrk as above actually
|
return {
|
||||||
return {
|
'name': _('Create Backorder?'),
|
||||||
'name': _('Create Backorder?'),
|
'type': 'ir.actions.act_window',
|
||||||
'type': 'ir.actions.act_window',
|
'view_type': 'form',
|
||||||
'view_type': 'form',
|
'view_mode': 'form',
|
||||||
'view_mode': 'form',
|
'res_model': 'stock.backorder.confirmation',
|
||||||
'res_model': 'stock.backorder.confirmation',
|
'views': [(view.id, 'form')],
|
||||||
'views': [(view.id, 'form')],
|
'view_id': view.id,
|
||||||
'view_id': view.id,
|
'target': 'new',
|
||||||
'target': 'new',
|
'res_id': wiz.id,
|
||||||
'res_id': wiz.id,
|
'context': self.env.context,
|
||||||
'context': self.env.context,
|
}
|
||||||
}
|
for operation in self.pack_operation_ids:
|
||||||
for operation in pick.pack_operation_ids:
|
if operation.qty_done < 0:
|
||||||
if operation.qty_done < 0:
|
raise UserError(_('No negative quantities allowed'))
|
||||||
raise UserError(_('No negative quantities allowed'))
|
if operation.qty_done > 0:
|
||||||
if operation.qty_done > 0:
|
pass
|
||||||
operation.write({'product_qty': operation.qty_done})
|
#operation.write({'product_qty': operation.qty_done})
|
||||||
else:
|
else:
|
||||||
pack_operations_delete |= operation
|
pack_operations_delete |= operation
|
||||||
if pack_operations_delete:
|
if pack_operations_delete:
|
||||||
pack_operations_delete.unlink()
|
pack_operations_delete.unlink()
|
||||||
self.do_transfer()
|
self.action_done()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
do_new_transfer = button_validate #TODO: replace later
|
||||||
|
|
||||||
def check_backorder(self):
|
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:
|
for move in self.move_lines:
|
||||||
if float_compare(move.remaining_qty, 0, precision_rounding=move.product_id.uom_id.rounding) != 0:
|
quantity_todo.setdefault(move.product_id.id, 0)
|
||||||
return True
|
quantity_done.setdefault(move.product_id.id, 0)
|
||||||
return False
|
quantity_todo[move.product_id.id] += move.product_qty
|
||||||
|
quantity_done[move.product_id.id] += move.quantity_done #TODO: convert to base units
|
||||||
@api.multi
|
for ops in self.pack_operation_ids.filtered(lambda x: x.package_id and not x.product_id and not x.move_id):
|
||||||
def do_transfer(self):
|
for quant in ops.package_id.quant_ids:
|
||||||
""" If no pack operation, we do simple action_done of the picking.
|
quantity_done.setdefault(quant.product_id.id, 0)
|
||||||
Otherwise, do the pack operations. """
|
quantity_done[quant.product_id.id] += quant.qty
|
||||||
# TDE CLEAN ME: reclean me, please
|
for pack in self.pack_operation_ids.filtered(lambda x: x.product_id and not x.move_id):
|
||||||
self._create_lots_for_picking()
|
quantity_done.setdefault(pack.product_id.id, 0)
|
||||||
|
quantity_done[pack.product_id.id] += pack.qty_done
|
||||||
no_pack_op_pickings = self.filtered(lambda picking: not picking.pack_operation_ids)
|
return any(quantity_done[x] < quantity_todo.get(x, 0) for x in quantity_done)
|
||||||
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
|
|
||||||
|
|
||||||
def _create_extra_moves(self):
|
def _create_extra_moves(self):
|
||||||
'''This function creates move lines on a picking, at the time of do_transfer, based on
|
'''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})
|
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})
|
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
|
op = new_operation
|
||||||
pack_operation_ids |= op
|
pack_operation_ids |= op
|
||||||
@@ -1012,7 +753,6 @@ class Picking(models.Model):
|
|||||||
raise UserError(_('Please process some quantities to put in the pack first!'))
|
raise UserError(_('Please process some quantities to put in the pack first!'))
|
||||||
return package
|
return package
|
||||||
|
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def put_in_pack(self):
|
def put_in_pack(self):
|
||||||
return self._put_in_pack()
|
return self._put_in_pack()
|
||||||
|
|||||||
@@ -521,7 +521,9 @@ class Warehouse(models.Model):
|
|||||||
'action': 'move',
|
'action': 'move',
|
||||||
'picking_type_id': routing.picking_type.id,
|
'picking_type_id': routing.picking_type.id,
|
||||||
'procure_method': first_rule is True and 'make_to_stock' or 'make_to_order',
|
'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(values or {})
|
||||||
route_pull_values.update(pull_values or {})
|
route_pull_values.update(pull_values or {})
|
||||||
pull_rules_list.append(route_pull_values)
|
pull_rules_list.append(route_pull_values)
|
||||||
|
|||||||
@@ -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_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_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_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_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_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
|
access_product_price_history_stock_manager,prices.history stock manager,product.model_product_price_history,stock.group_stock_manager,1,1,1,1
|
||||||
|
|||||||
|
@@ -178,7 +178,7 @@
|
|||||||
<field name="location_dest_id" />
|
<field name="location_dest_id" />
|
||||||
<field name="partner_id" context="{'contact_display':'partner'}" />
|
<field name="partner_id" context="{'contact_display':'partner'}" />
|
||||||
<field name="picking_partner_id" context="{'contact_display':'partner'}"/>
|
<field name="picking_partner_id" context="{'contact_display':'partner'}"/>
|
||||||
<field name="move_dest_id" groups="base.group_no_one" readonly="1"/>
|
<field name="move_dest_ids" groups="base.group_no_one" readonly="1"/>
|
||||||
</group>
|
</group>
|
||||||
<group name="quants_grp" string="Reserved Quants" colspan="2" groups="base.group_no_one" attrs="{'invisible': [('state', '=', 'done')]}">
|
<group name="quants_grp" string="Reserved Quants" colspan="2" groups="base.group_no_one" attrs="{'invisible': [('state', '=', 'done')]}">
|
||||||
<field name="string_availability_info"/>
|
<field name="string_availability_info"/>
|
||||||
@@ -222,7 +222,7 @@
|
|||||||
<field name="procure_method" attrs="{'readonly': [('state', '!=', 'draft')]}" groups="stock.group_adv_location"/>
|
<field name="procure_method" attrs="{'readonly': [('state', '!=', 'draft')]}" groups="stock.group_adv_location"/>
|
||||||
<field name="create_date" invisible="1"/>
|
<field name="create_date" invisible="1"/>
|
||||||
<field name="date_expected"/>
|
<field name="date_expected"/>
|
||||||
<field name="move_dest_id" groups="base.group_no_one" readonly="1"/>
|
<field name="move_dest_ids" groups="base.group_no_one" readonly="1"/>
|
||||||
</group>
|
</group>
|
||||||
<group name="quants_grp" string="Reserved" attrs="{'invisible': [('state', '=', 'draft')]}">
|
<group name="quants_grp" string="Reserved" attrs="{'invisible': [('state', '=', 'draft')]}">
|
||||||
<field name="string_availability_info"/>
|
<field name="string_availability_info"/>
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<odoo>
|
|
||||||
<record id="view_pack_operation_details_form" model="ir.ui.view">
|
|
||||||
<field name="name">stock.pack.operation.details.form</field>
|
|
||||||
<field name="model">stock.pack.operation</field>
|
|
||||||
<field eval="12" name="priority"/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<form>
|
|
||||||
<group name="qty_to">
|
|
||||||
<group string="Products">
|
|
||||||
<field name="lots_visible" invisible="1"/>
|
|
||||||
<field name="picking_source_location_id" invisible="1"/>
|
|
||||||
<field name="picking_destination_location_id" invisible="1"/>
|
|
||||||
<field name="product_id" attrs="{'required': [('package_id', '=', False)]}" readonly="1"/>
|
|
||||||
<label for="qty_done"/>
|
|
||||||
<div>
|
|
||||||
<field name="qty_done" nolabel="1" attrs="{'readonly': [('lots_visible','=',True)]}" class="oe_inline"/>
|
|
||||||
/
|
|
||||||
<field name="product_qty" class="oe_inline" readonly="1"/>
|
|
||||||
<field name="product_uom_id" options="{'no_open': True, 'no_create': True}" groups="product.group_uom" class="oe_inline"/>
|
|
||||||
</div>
|
|
||||||
</group>
|
|
||||||
<group string="Locations" groups="stock.group_stock_multi_locations">
|
|
||||||
<field name="location_id" domain="[('id', 'child_of', picking_source_location_id)]" options="{'no_create': True}"/>
|
|
||||||
<field name="location_dest_id" domain="[('id', 'child_of', picking_destination_location_id)]" options="{'no_create': True}"/>
|
|
||||||
</group>
|
|
||||||
<group string="Packages" groups="stock.group_tracking_lot">
|
|
||||||
<field name="package_id" attrs="{'required': [('product_id', '=', False)]}" domain="[('location_id', '=', location_id)]" context="{'default_location_id': location_id}"/>
|
|
||||||
<field name="result_package_id" domain="['|', ('location_id', '=', False), ('location_id', '=', location_dest_id)]" context="{'default_location_id': location_dest_id}"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="view_pack_operation_details_form_save" model="ir.ui.view">
|
|
||||||
<field name="name">stock.pack.operation.details.form.save</field>
|
|
||||||
<field name="model">stock.pack.operation</field>
|
|
||||||
<field name="mode">primary</field>
|
|
||||||
<field name="inherit_id" ref="stock.view_pack_operation_details_form"/>
|
|
||||||
<field eval="15" name="priority"/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<group name="qty_to" position="after">
|
|
||||||
<footer>
|
|
||||||
<button name="save" type="object" string="Save" class="oe_highlight"/>
|
|
||||||
<button string="Discard" special="cancel"/>
|
|
||||||
</footer>
|
|
||||||
</group>
|
|
||||||
<field name="product_uom_id" position="after">
|
|
||||||
<button name="split_quantities" attrs="{'invisible': ['|', ('qty_done', '<=', 0.0), ('product_qty', '<=', 0.0)]}" class="oe_link" string="Split" type="object"/>
|
|
||||||
</field>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="pack_details" model="ir.actions.act_window">
|
|
||||||
<field name="name">Pack Details</field>
|
|
||||||
<field name="type">ir.actions.act_window</field>
|
|
||||||
<field name="res_model">stock.pack.operation</field>
|
|
||||||
<field name="view_id" ref="view_pack_operation_details_form_save"/>
|
|
||||||
<field name="view_type">form</field>
|
|
||||||
<field name="view_mode">form</field>
|
|
||||||
<field name="target">new</field>
|
|
||||||
<field name="context">{}</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="view_pack_operation_lot_form" model="ir.ui.view">
|
|
||||||
<field name="name">stock.pack.operation.lots.form</field>
|
|
||||||
<field name="model">stock.pack.operation</field>
|
|
||||||
<field eval="20" name="priority"/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<form>
|
|
||||||
<group>
|
|
||||||
<field name="product_id" readonly="1"/>
|
|
||||||
<label for="qty_done"/>
|
|
||||||
<span>
|
|
||||||
<field name="qty_done" class="oe_inline" readonly="1"/> of
|
|
||||||
<field name="product_qty" class="oe_inline" readonly="1"/>
|
|
||||||
<field name="product_uom_id" class="oe_inline" groups="product.group_uom" readonly="1"/>
|
|
||||||
<field name="state" invisible="1"/>
|
|
||||||
</span>
|
|
||||||
</group>
|
|
||||||
<group>
|
|
||||||
<field name="pack_lot_ids" nolabel="1" attrs="{'readonly': [('state', '=', 'done')]}">
|
|
||||||
<tree editable="bottom" decoration-success="qty_todo==qty" decoration-danger="(qty_todo > 0) and (qty>qty_todo)">
|
|
||||||
<field name="lot_name" invisible="not context.get('only_create', False)"/>
|
|
||||||
<field name="lot_id" invisible="context.get('only_create', False)"
|
|
||||||
domain="[('product_id','=', parent.product_id)]"
|
|
||||||
context="{'default_product_id': parent.product_id, 'active_pack_operation': parent.id}"/>
|
|
||||||
<field name="qty_todo" invisible="not context.get('show_reserved') or context.get('serial') or context.get('state_done')" readonly="1"/>
|
|
||||||
<field name="qty" invisible="context.get('serial')"/>
|
|
||||||
<button name="do_minus" type="object" icon="fa-minus-square" attrs="{'invisible': [('qty', '<=', 0.99)]}" invisible="not context.get('show_reserved') or context.get('state_done')"/>
|
|
||||||
<button name="do_plus" type="object" icon="fa-plus-square" attrs="{'invisible': [('plus_visible', '=', False)]}" invisible="not context.get('show_reserved') or context.get('state_done')"/>
|
|
||||||
<field name="plus_visible" invisible="1"/>
|
|
||||||
</tree>
|
|
||||||
</field>
|
|
||||||
</group>
|
|
||||||
<footer>
|
|
||||||
<button name="save" type="object" string="Save" class="oe_highlight" attrs="{'invisible':[('state','=','done')]}"/>
|
|
||||||
<button string="Discard" special="cancel"/>
|
|
||||||
</footer>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
<record id="view_pack_operation_lot_lot_form" model="ir.ui.view">
|
|
||||||
<field name="name">stock.pack.operation.lots.form</field>
|
|
||||||
<field name="model">stock.pack.operation.lot</field>
|
|
||||||
<field eval="20" name="priority"/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<form>
|
|
||||||
<group>
|
|
||||||
<field name="lot_id"/>
|
|
||||||
<field name="qty"/>
|
|
||||||
</group>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
</odoo>
|
|
||||||
@@ -209,9 +209,10 @@
|
|||||||
<button name="%(action_report_delivery)d" string="Print" states="done" type="action" groups="base.group_user"/>
|
<button name="%(action_report_delivery)d" string="Print" states="done" type="action" groups="base.group_user"/>
|
||||||
<button name="%(act_stock_return_picking)d" string="Return" states="done" type="action" groups="base.group_user"/>
|
<button name="%(act_stock_return_picking)d" string="Return" states="done" type="action" groups="base.group_user"/>
|
||||||
<button name="action_cancel" states="assigned,confirmed,partially_available,draft,waiting" string="Cancel" groups="base.group_user" type="object"/>
|
<button name="action_cancel" states="assigned,confirmed,partially_available,draft,waiting" string="Cancel" groups="base.group_user" type="object"/>
|
||||||
<button name="do_prepare_partial" attrs="{'invisible': ['|', ('launch_pack_operations', '=', True), '|', ('state', 'not in', ('assigned', 'partially_available')), ('pack_operation_ids', '!=', [])]}"
|
<button name="do_unreserve" string="Unreserve" groups="base.group_user" type="object" attrs="{'invisible': ['|', ('state', 'not in', ('assigned', 'partially_available')), ('location_id', 'in', ('supplier', 'inventory', 'production'))]}"/>
|
||||||
string="Reset Operations" type="object"/>
|
<!--<button name="do_prepare_partial" attrs="{'invisible': ['|', ('launch_pack_operations', '=', True), '|', ('state', 'not in', ('assigned', 'partially_available')), ('pack_operation_ids', '!=', [])]}"-->
|
||||||
<field name="launch_pack_operations" invisible="1"/>
|
<!--string="Reset Operations" type="object"/>-->
|
||||||
|
<!--<field name="launch_pack_operations" invisible="1"/>-->
|
||||||
<button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': ['|', ('state', 'in', ('draft', 'cancel')), '&', ('picking_type_code', '=', 'incoming'), ('state', '!=', 'done')]}"/>
|
<button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': ['|', ('state', 'in', ('draft', 'cancel')), '&', ('picking_type_code', '=', 'incoming'), ('state', '!=', 'done')]}"/>
|
||||||
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed,partially_available,assigned,done" />
|
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed,partially_available,assigned,done" />
|
||||||
</header>
|
</header>
|
||||||
@@ -247,48 +248,35 @@
|
|||||||
<page string="Operations" attrs="{'invisible': [('state', 'in', ('draft', 'confirmed', 'waiting'))]}">
|
<page string="Operations" attrs="{'invisible': [('state', 'in', ('draft', 'confirmed', 'waiting'))]}">
|
||||||
<button name="do_prepare_partial" type="object" string="Recompute" attrs="{'invisible': [('recompute_pack_op','=', False)]}"/>
|
<button name="do_prepare_partial" type="object" string="Recompute" attrs="{'invisible': [('recompute_pack_op','=', False)]}"/>
|
||||||
<field name="recompute_pack_op" invisible="1"/>
|
<field name="recompute_pack_op" invisible="1"/>
|
||||||
<field name="pack_operation_ids" invisible="1"/>
|
<field name="pack_operation_ids" options="{'reload_on_button': True}" context="{'default_picking_id': id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}" mode="tree,kanban">
|
||||||
<field name="pack_operation_product_ids" options="{'reload_on_button': True}" context="{'default_picking_id': id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}" mode="tree,kanban">
|
<tree editable="bottom">
|
||||||
<tree editable="bottom" decoration-muted="result_package_id" decoration-danger="qty_done>product_qty" decoration-success="qty_done==product_qty and state!='done' and not result_package_id">
|
<field name="product_id" required="1"/>
|
||||||
<field name="package_id" groups="stock.group_tracking_lot" invisible="1"/>
|
<field name="product_uom_id" attrs="{'readonly': [('state', '!=', 'draft')]}" groups="product.group_uom"/>
|
||||||
<field name="product_id" required="1" attrs="{'readonly': [('fresh_record', '=', False)]}"/>
|
|
||||||
<field name="fresh_record" invisible="1"/>
|
|
||||||
<field name="product_uom_id" attrs="{'readonly': [('fresh_record', '=', False)]}" groups="product.group_uom"/>
|
|
||||||
<field name="lots_visible" invisible="1"/>
|
|
||||||
<field name="owner_id" groups="stock.group_tracking_owner"/>
|
|
||||||
<field name="location_id" domain="[('id', 'child_of', parent.location_id)]" invisible="1"/>
|
|
||||||
<field name="location_dest_id" domain="[('id', 'child_of', parent.location_dest_id)]" invisible="1"/>
|
|
||||||
<field name="from_loc" groups="stock.group_stock_multi_locations,stock.group_tracking_lot"/>
|
<field name="from_loc" groups="stock.group_stock_multi_locations,stock.group_tracking_lot"/>
|
||||||
<field name="to_loc" groups="stock.group_stock_multi_locations,stock.group_tracking_lot"/>
|
<field name="to_loc" groups="stock.group_stock_multi_locations,stock.group_tracking_lot"/>
|
||||||
<field name="result_package_id" groups="stock.group_tracking_lot" context="{'default_location_id': location_dest_id}" invisible="1"/>
|
|
||||||
<field name="state" invisible="1"/>
|
<field name="state" invisible="1"/>
|
||||||
<field name="product_qty" readonly="1" attrs="{'required': [('product_id', '!=', False)]}"/>
|
<field name="product_qty" readonly="1" attrs="{'required': [('product_id', '!=', False)]}"/>
|
||||||
<field name="qty_done" attrs="{'readonly': [('lots_visible', '=', True)]}"/>
|
<field name="qty_done"/>
|
||||||
<button name="action_split_lots" string="Lot Split" type="object" icon="fa-list" groups="stock.group_production_lot"
|
<button name="action_split_lots" string="Lot Split" type="object" icon="fa-list" groups="stock.group_production_lot"
|
||||||
attrs="{'invisible': ['|', ('lots_visible', '=', False), ('state', 'not in', ['confirmed', 'assigned', 'waiting', 'partially_available','done'])]}"/>
|
attrs="{'invisible': ['|', ('state', 'not in', ['confirmed', 'assigned', 'waiting', 'partially_available','done'])]}"/>
|
||||||
<button name="show_details" string="Modify" type="object" icon="fa-pencil" groups="stock.group_stock_multi_locations"
|
<button name="show_details" string="Modify" type="object" icon="fa-pencil" groups="stock.group_stock_multi_locations"
|
||||||
states="confirmed,assigned,waiting,partially_available"/>
|
states="confirmed,assigned,waiting,partially_available"/>
|
||||||
</tree>
|
</tree>
|
||||||
<kanban class="o_kanban_mobile">
|
<kanban class="o_kanban_mobile">
|
||||||
<field name="package_id" groups="stock.group_tracking_lot" invisible="1"/>
|
|
||||||
<field name="product_id" required="1" attrs="{'readonly': [('fresh_record', '=', False)]}"/>
|
<field name="product_id" required="1" attrs="{'readonly': [('fresh_record', '=', False)]}"/>
|
||||||
<field name="fresh_record" invisible="1"/>
|
<field name="fresh_record" invisible="1"/>
|
||||||
<field name="product_uom_id" attrs="{'readonly': [('fresh_record', '=', False)]}" groups="product.group_uom"/>
|
<field name="product_uom_id" attrs="{'readonly': [('fresh_record', '=', False)]}" groups="product.group_uom"/>
|
||||||
<field name="lots_visible" invisible="1"/>
|
|
||||||
<field name="owner_id" groups="stock.group_tracking_owner"/>
|
<field name="owner_id" groups="stock.group_tracking_owner"/>
|
||||||
<field name="location_id" domain="[('id', 'child_of', parent.location_id)]" invisible="1"/>
|
<field name="location_id" domain="[('id', 'child_of', parent.location_id)]" invisible="1"/>
|
||||||
<field name="location_dest_id" domain="[('id', 'child_of', parent.location_dest_id)]" invisible="1"/>
|
<field name="location_dest_id" domain="[('id', 'child_of', parent.location_dest_id)]" invisible="1"/>
|
||||||
<field name="from_loc" groups="stock.group_stock_multi_locations,stock.group_tracking_lot"/>
|
<field name="from_loc" groups="stock.group_stock_multi_locations,stock.group_tracking_lot"/>
|
||||||
<field name="to_loc" groups="stock.group_stock_multi_locations,stock.group_tracking_lot"/>
|
<field name="to_loc" groups="stock.group_stock_multi_locations,stock.group_tracking_lot"/>
|
||||||
<field name="result_package_id" groups="stock.group_tracking_lot" context="{'default_location_id': location_dest_id}" invisible="1"/>
|
|
||||||
<field name="state" invisible="1"/>
|
<field name="state" invisible="1"/>
|
||||||
<field name="product_qty" readonly="1" attrs="{'required': [('product_id', '!=', False)]}"/>
|
<field name="product_qty" readonly="1" attrs="{'required': [('product_id', '!=', False)]}"/>
|
||||||
<field name="qty_done" attrs="{'readonly': [('lots_visible', '=', True)]}"/>
|
<field name="qty_done"/>
|
||||||
<templates>
|
<templates>
|
||||||
<t t-name="kanban-box">
|
<t t-name="kanban-box">
|
||||||
<div t-attf-class="oe_kanban_card oe_kanban_global_click
|
<div>
|
||||||
#{record.qty_done.raw_value>record.product_qty.raw_value ? 'oe_kanban_color_2' : ''}
|
|
||||||
#{record.qty_done.raw_value==record.product_qty.raw_value && record.state.raw_value!='done' && ! record.result_package_id.raw_value ? 'oe_kanban_color_3' : ''}">
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
<strong><span><t t-esc="record.product_id.value"/></span></strong>
|
<strong><span><t t-esc="record.product_id.value"/></span></strong>
|
||||||
@@ -310,54 +298,32 @@
|
|||||||
</kanban>
|
</kanban>
|
||||||
</field>
|
</field>
|
||||||
<field name="picking_type_entire_packs" invisible="1"/>
|
<field name="picking_type_entire_packs" invisible="1"/>
|
||||||
<field name="pack_operation_pack_ids" options="{'reload_on_button': True}" attrs="{'invisible': [('pack_operation_pack_ids', '=', []), ('picking_type_entire_packs', '=', False)]}" context="{'default_picking_id': id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_picking_id': id}" groups="stock.group_tracking_lot">
|
|
||||||
<tree editable="bottom" decoration-muted="qty_done>0">
|
|
||||||
<field name="package_id" required="1" string="Package To Move" domain="[('location_id', '=', location_id)]"/>
|
|
||||||
<field name="location_id" domain="[('id', 'child_of', parent.location_id)]" invisible="1"/>
|
|
||||||
<field name="from_loc" groups="stock.group_stock_multi_locations,stock.group_tracking_lot"/>
|
|
||||||
<field name="to_loc" groups="stock.group_stock_multi_locations,stock.group_tracking_lot"/>
|
|
||||||
<field name="location_dest_id" domain="[('id', 'child_of', parent.location_dest_id)]" invisible="1"/>
|
|
||||||
<field name="result_package_id" groups="stock.group_tracking_lot" context="{'default_location_id': location_dest_id}" invisible="1"/>
|
|
||||||
<field name="state" invisible="1"/>
|
|
||||||
<field name="qty_done" invisible="1"/>
|
|
||||||
<field name="lots_visible" invisible="1"/>
|
|
||||||
<field name="is_done"/>
|
|
||||||
<button name="show_details" string="Modify" type="object" icon="fa-pencil"
|
|
||||||
states="confirmed,assigned,waiting,partially_available" groups="base.group_no_one"/>
|
|
||||||
</tree>
|
|
||||||
</field>
|
|
||||||
<button class="oe_link oe_right" name="put_in_pack" type="object" string="↳Put in Pack" attrs="{'invisible': [('state', 'in', ('done', 'cancel'))]}" groups="stock.group_tracking_lot"/>
|
<button class="oe_link oe_right" name="put_in_pack" type="object" string="↳Put in Pack" attrs="{'invisible': [('state', 'in', ('done', 'cancel'))]}" groups="stock.group_tracking_lot"/>
|
||||||
</page>
|
</page>
|
||||||
<page string="Initial Demand">
|
<page string="Operations">
|
||||||
<field name="move_lines" attrs="{'readonly': [('state', 'in', ('cancel', 'partially_available', 'assigned', 'done'))]}" options="{'reload_on_button': True}" mode="tree,kanban" context="{'address_in_id': partner_id, 'form_view_ref':'stock.view_move_picking_form', 'tree_view_ref':'stock.view_move_picking_tree', 'default_picking_type_id': picking_type_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
|
<field name="id" invisible="1"/>
|
||||||
<kanban class="o_kanban_mobile">
|
<field name="move_lines" attrs="{'readonly': [('state', 'in', ('cancel', 'done'))]}" context="{'default_picking_id': id, 'form_view_ref':'stock.view_move_picking_form', 'address_in_id': partner_id, 'default_picking_type_id': picking_type_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
|
||||||
<field name="product_id"/>
|
<tree decoration-danger="quantity_done > reserved_availability" decoration-muted="scrapped == True" string="Stock Moves" editable="bottom">
|
||||||
<field name="availability"/>
|
<field name="name" invisible="1"/>
|
||||||
<field name="product_uom_qty"/>
|
<field name="date_expected" invisible="1"/>
|
||||||
<field name="product_uom" options="{'no_open': True, 'no_create': True}"/>
|
<field name="state" invisible="1"/>
|
||||||
<field name="location_dest_id"/>
|
<field name="picking_type_id" invisible="1"/>
|
||||||
<field name="scrapped"/>
|
<field name="location_id" invisible="1"/>
|
||||||
<field name="state"/>
|
<field name="location_dest_id" invisible="1"/>
|
||||||
<templates>
|
<field name="scrapped" invisible="1"/>
|
||||||
<t t-name="kanban-box">
|
<field name="picking_code" invisible="1"/>
|
||||||
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
|
<field name="product_type" invisible="1"/>
|
||||||
<div class="row">
|
<field name="show_details_visible" invisible="1"/>
|
||||||
<div class="col-xs-12">
|
<field name="show_operations" invisible="1"/>
|
||||||
<strong><span><t t-esc="record.product_id.value"/></span></strong>
|
<field name="additional" invisible="1"/>
|
||||||
</div>
|
|
||||||
</div>
|
<field name="product_id" required="1" attrs="{'readonly': ['&', ('state', '!=', 'draft'), ('additional', '=', False)]}"/>
|
||||||
<div class="row">
|
<field name="product_uom_qty" string="Initial Demand" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
|
||||||
<div class="col-xs-8 text-muted">
|
<field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft')]}" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="product.group_uom"/>
|
||||||
<span><t t-esc="record.availability.value"/> on <t t-esc="record.product_uom_qty.value"/> <t t-esc="record.product_uom.value"/></span>
|
<field name="reserved_availability" string="Reserved"/>
|
||||||
</div>
|
<field name="quantity_done" string="Done" attrs="{'readonly': [('show_details_visible', '=', True)], 'invisible': ['|', ('state', 'in', ['draft', 'confirmed', 'waiting']), ('show_operations', '=', True)]}"/>
|
||||||
<div class="col-xs-4 text-muted">
|
<button name="action_show_details" string="Register lots, packs, location" type="object" icon="fa-list" attrs="{'invisible': [('show_details_visible', '=', False)]}"/>
|
||||||
<span class="pull-right text-right"><t t-esc="record.location_dest_id.value"/></span>
|
</tree>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</t>
|
|
||||||
</templates>
|
|
||||||
</kanban>
|
|
||||||
</field>
|
</field>
|
||||||
<field name="pack_operation_exist" invisible="1"/>
|
<field name="pack_operation_exist" invisible="1"/>
|
||||||
<field name="note" placeholder="Add an internal note that will be printed on the Picking Operations sheet" class="oe_inline"/>
|
<field name="note" placeholder="Add an internal note that will be printed on the Picking Operations sheet" class="oe_inline"/>
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ class StockBackorderConfirmation(models.TransientModel):
|
|||||||
|
|
||||||
@api.one
|
@api.one
|
||||||
def _process(self, cancel_backorder=False):
|
def _process(self, cancel_backorder=False):
|
||||||
operations_to_delete = self.pick_id.pack_operation_ids.filtered(lambda o: o.qty_done <= 0)
|
|
||||||
for pack in self.pick_id.pack_operation_ids - operations_to_delete:
|
|
||||||
pack.product_qty = pack.qty_done
|
|
||||||
operations_to_delete.unlink()
|
|
||||||
self.pick_id.do_transfer()
|
self.pick_id.do_transfer()
|
||||||
if cancel_backorder:
|
if cancel_backorder:
|
||||||
backorder_pick = self.env['stock.picking'].search([('backorder_id', '=', self.pick_id.id)])
|
backorder_pick = self.env['stock.picking'].search([('backorder_id', '=', self.pick_id.id)])
|
||||||
|
|||||||
@@ -28,9 +28,10 @@ class StockImmediateTransfer(models.TransientModel):
|
|||||||
self.pick_id.action_assign()
|
self.pick_id.action_assign()
|
||||||
if self.pick_id.state != 'assigned':
|
if self.pick_id.state != 'assigned':
|
||||||
raise UserError(_("Could not reserve all requested products. Please use the \'Mark as Todo\' button to handle the reservation manually."))
|
raise UserError(_("Could not reserve all requested products. Please use the \'Mark as Todo\' button to handle the reservation manually."))
|
||||||
for pack in self.pick_id.pack_operation_ids:
|
for move in self.pick_id.move_lines:
|
||||||
if pack.product_qty > 0:
|
if move.pack_operation_ids:
|
||||||
pack.write({'qty_done': pack.product_qty})
|
for move_line in move.pack_operation_ids:
|
||||||
|
move_line.qty_done = move_line.product_uom_qty
|
||||||
else:
|
else:
|
||||||
pack.unlink()
|
move.quantity_done = move.product_uom_qty
|
||||||
self.pick_id.do_transfer()
|
self.pick_id.do_transfer()
|
||||||
|
|||||||
Reference in New Issue
Block a user