From 5d25900cd88ebc1fd16b0bd6ebba4602a13e9d76 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Wed, 3 May 2017 18:14:16 +0200 Subject: [PATCH] [REF] stock: views refactoring We adapted the views to the new stock model, and differenciate two different views: - The first one is more adapted for simple operations, or when no suggestion of location, lot, pack is needed. We have a line with the initial demand of each product and you can complete the lot numer, location, etc in the detailed window of a move. - The second one is more adapted for pick and barcode operations, all each line is fully detailed with the system reservation, you can see the lot, location, etc you must scan in the list view. You can choose the view you want to use in the picking type settings by check the box 'Show Operations' --- addons/stock/models/stock_move.py | 93 +++++++++++++++++---- addons/stock/models/stock_pack_operation.py | 10 ++- addons/stock/models/stock_picking.py | 8 +- addons/stock/views/stock_move_views.xml | 90 +++++++++++++++++++- addons/stock/views/stock_picking_views.xml | 80 ++++++------------ 5 files changed, 201 insertions(+), 80 deletions(-) diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 7d785e00358..e0a813c71bb 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -50,7 +50,7 @@ class StockMove(models.Model): digits=0, store=True, help='Quantity in the default UoM of the product') product_uom_qty = fields.Float( - 'Quantity', + 'Initial Demand', digits=dp.get_precision('Product Unit of Measure'), default=1.0, required=True, states={'done': [('readonly', True)]}, help="This is the quantity of products from an inventory " @@ -121,10 +121,6 @@ class StockMove(models.Model): "its current stock) to gather products. If we want to chain moves and have this one to wait for the previous," "this second option should be chosen.") scrapped = fields.Boolean('Scrapped', related='location_dest_id.scrap_location', readonly=True, store=True) - remaining_qty = fields.Float( - 'Remaining Quantity', compute='_get_remaining_qty', - digits=0, states={'done': [('readonly', True)]}, - help="Remaining Quantity in default UoM according to operations matched with this move") procurement_id = fields.Many2one('procurement.order', 'Procurement') group_id = fields.Many2one('procurement.group', 'Procurement Group', default=_default_group_id) rule_id = fields.Many2one('procurement.rule', 'Procurement Rule', ondelete='restrict', help='The procurement rule that created this stock move') @@ -135,10 +131,12 @@ class StockMove(models.Model): picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type') inventory_id = fields.Many2one('stock.inventory', 'Inventory') pack_operation_ids = fields.One2many('stock.pack.operation', 'move_id') + pack_operation_nosuggest_ids = fields.One2many('stock.pack.operation', 'move_id', domain=[('product_qty', '=', 0.0)]) origin_returned_move_id = fields.Many2one('stock.move', 'Origin return move', copy=False, help='Move that created the return move') returned_move_ids = fields.One2many('stock.move', 'origin_returned_move_id', 'All returned moves', help='Optional: all returned moves created from this move') reserved_availability = fields.Float( 'Quantity Reserved', compute='_compute_reserved_availability', + digits=dp.get_precision('Product Unit of Measure'), readonly=True, help='Quantity that has already been reserved for this move') availability = fields.Float( 'Forecasted Quantity', compute='_compute_product_availability', @@ -151,7 +149,63 @@ class StockMove(models.Model): route_ids = fields.Many2many('stock.location.route', 'stock_location_route_move', 'move_id', 'route_id', 'Destination route', help="Preferred route to be followed by the procurement order") warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', help="Technical field depicting the warehouse to consider for the route selection on the next procurement (if any).") has_tracking = fields.Selection(related='product_id.tracking', string='Product with Tracking') - quantity_done = fields.Float('Quantity', compute='_qty_done_compute', digits=dp.get_precision('Product Unit of Measure')) + quantity_done = fields.Float('Quantity Done', compute='_quantity_done_compute', digits=dp.get_precision('Product Unit of Measure'), inverse='_quantity_done_set', + states={'done': [('readonly', True)]}) + show_operations = fields.Boolean(related='picking_id.picking_type_id.show_operations') + show_details_visible = fields.Boolean('Details Visible', compute='_compute_show_details_visible') + show_reserved_availability = fields.Boolean('From Supplier', compute='_compute_show_reserved_availability') + picking_code = fields.Selection(related='picking_id.picking_type_id.code', readonly=True) + product_type = fields.Selection(related='product_id.type', readonly=True) + additional = fields.Boolean("Whether the move was added after the picking's confirmation", default=False) + is_editable = fields.Boolean('Is editable when done', compute='_compute_is_editable') + is_initial_demand_editable = fields.Boolean('Is initial demand editable', compute='_compute_is_initial_demand_editable') + + @api.multi + @api.depends('has_tracking', 'pack_operation_ids', 'location_id', 'location_dest_id', 'is_editable') + def _compute_show_details_visible(self): + """ According to this field, the button that calls `action_show_details` will be displayed + to work on a move from its picking form view, or not. + """ + for move in self: + if not move.product_id: + move.show_details_visible = False + continue + + if move.is_editable: + move.show_details_visible = True + continue + + multi_locations_enabled = False + if self.user_has_groups('stock.group_stock_multi_locations'): + multi_locations_enabled = move.location_id.child_ids or move.location_dest_id.child_ids + + if move.picking_id.picking_type_id.show_operations is False\ + and move.state not in ['cancel', 'draft', 'confirmed']\ + and (multi_locations_enabled or move.has_tracking != 'none' or len(move.pack_operation_ids) > 1): + move.show_details_visible = True + else: + move.show_details_visible = False + + @api.multi + def _compute_show_reserved_availability(self): + """ This field is only of use in an attrs in the picking view, in order to hide the + "available" column if the move is coming from a supplier. + """ + for move in self: + move.show_reserved_availability = not move.location_id.usage == 'supplier' + + @api.multi + def _compute_is_editable(self): + """ This field is only of use in an attrs in the picking view, in order to show + the button to edit move when they're done. + """ + for move in self: + move.is_editable = move.state == 'done' and self.user_has_groups('stock.group_stock_manager') + + @api.multi + def _compute_is_initial_demand_editable(self): + for move in self: + move.is_initial_demand_editable = move.state not in ['done', 'cancel'] and self.user_has_groups('stock.group_stock_manager') @api.one @api.depends('product_id', 'product_uom', 'product_uom_qty') @@ -194,11 +248,6 @@ class StockMove(models.Model): 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.one - def _get_remaining_qty(self): - # TODO: sle jco - pass - @api.one @api.depends('pack_operation_ids.product_qty') @@ -258,6 +307,20 @@ class StockMove(models.Model): if not self._cr.fetchone(): self._cr.execute('CREATE INDEX stock_move_product_location_index ON stock_move (product_id, location_id, location_dest_id, company_id, state)') + @api.model + def default_get(self, fields_list): + # We override the default_get to make stock moves created after the picking was confirmed + # directly as available (like a force_assign). This allows to create extra move lines in + # the fp view. + defaults = super(StockMove, self).default_get(fields_list) + if self.env.context.get('default_picking_id'): + picking_id = self.env['stock.picking'].browse(self.env.context['default_picking_id']) + if picking_id.state not in ['draft', 'confirmed']: + defaults['state'] = 'assigned' + defaults['product_uom_qty'] = 0.0 + defaults['additional'] = True + return defaults + @api.multi def name_get(self): res = [] @@ -387,10 +450,6 @@ class StockMove(models.Model): forgot some of them, use this tool instead. """ return self.filtered(lambda move: move.state not in ('done', 'cancel')) - - # Main actions - # ------------------------------------------------------------ - @api.multi def do_unreserve(self): if any(move.state in ('done', 'cancel') for move in self): @@ -443,7 +502,8 @@ class StockMove(models.Model): product = self.product_id.with_context(lang=self.partner_id.lang or self.env.user.lang) self.name = product.partner_ref self.product_uom = product.uom_id.id - self.product_uom_qty = 1.0 + if self.product_uom_qty: + self.product_uom_qty = 1.0 return {'domain': {'product_uom': [('category_id', '=', product.uom_id.category_id.id)]}} @api.onchange('date') @@ -945,4 +1005,3 @@ class StockMove(models.Model): 'target': 'new', 'res_id': self.id} show_picking = action_show_picking - diff --git a/addons/stock/models/stock_pack_operation.py b/addons/stock/models/stock_pack_operation.py index e5fd2a866e0..40dedc233ce 100644 --- a/addons/stock/models/stock_pack_operation.py +++ b/addons/stock/models/stock_pack_operation.py @@ -10,7 +10,7 @@ from odoo.tools.float_utils import float_round, float_compare, float_is_zero class PackOperation(models.Model): - _name = "stock.pack.operation" + _name = "stock.pack.operation" #TODO: change to stock.move.operation _description = "Packing Operation" _order = "result_package_id desc, id" @@ -30,11 +30,12 @@ class PackOperation(models.Model): 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'), copy=False) package_id = fields.Many2one('stock.quant.package', 'Source Package') + lot_id = fields.Many2one('stock.production.lot', 'Lot') + lot_name = fields.Char('Lot/Serial Number') result_package_id = fields.Many2one( 'stock.quant.package', 'Destination Package', ondelete='cascade', required=False, help="If set, the operations are packed into this package") - 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") location_id = fields.Many2one('stock.location', 'From', required=True) @@ -54,9 +55,10 @@ class PackOperation(models.Model): operation.to_loc = '%s%s' % (operation_sudo.location_dest_id.name, operation_sudo.result_package_id.name or '') @api.one + @api.depends('picking_id.picking_type_id', 'product_id.tracking') def _compute_lots_visible(self): - if 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 + if picking.picking_type_id and self.product_id.tracking != 'none': # TDE FIXME: not sure correctly migrated self.lots_visible = picking.picking_type_id.use_existing_lots or picking.picking_type_id.use_create_lots else: self.lots_visible = self.product_id.tracking != 'none' diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index de261ea45d6..b848d05976c 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -40,6 +40,10 @@ class PickingType(models.Model): use_existing_lots = fields.Boolean( 'Use Existing Lots/Serial Numbers', default=True, help="If this is checked, you will be able to choose the Lots/Serial Numbers. You can also decide to not put lots in this operation type. This means it will create stock with no lot or not put a restriction on the lot taken. ") + show_operations = fields.Boolean( + 'Show Operations', default=False) + show_reserved = fields.Boolean( + 'Show Reserved', default=True) # Statistics for the kanban view last_done_picking = fields.Char('Last 10 Done Pickings', compute='_compute_last_done_picking') @@ -263,7 +267,7 @@ class Picking(models.Model): states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) pack_operation_ids = fields.One2many( - 'stock.pack.operation', 'picking_id', 'Related Packing Operations', + 'stock.pack.operation', 'picking_id', 'Operations', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) pack_operation_exist = fields.Boolean( @@ -296,7 +300,6 @@ class Picking(models.Model): - one of the move is assigned or partially available: partially available - otherwise in waiting or confirmed state ''' - # FIXME: goes in 3 times at each write? if not self.move_lines: self.state = 'draft' elif any(move.state == 'draft' for move in self.move_lines): # TDE FIXME: should be all ? @@ -514,7 +517,6 @@ class Picking(models.Model): # '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) diff --git a/addons/stock/views/stock_move_views.xml b/addons/stock/views/stock_move_views.xml index 426cf829172..6ddb7de4ffb 100644 --- a/addons/stock/views/stock_move_views.xml +++ b/addons/stock/views/stock_move_views.xml @@ -131,12 +131,94 @@ - + + stock.move.operations.form + stock.move + 1000 + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ + + stock.move.operations.nosuggest.form + stock.move + 1000 + primary + + + + + + + + + + + + + + + + + + + + + + + + + + stock.move.form stock.move @@ -183,6 +265,9 @@ + + + @@ -227,6 +312,9 @@ + + +
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index 727927af368..e94bec27cad 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -46,6 +46,8 @@ + + @@ -209,12 +211,13 @@