[IMP] stock: allow user to correct pickings

Introduces a lock/unlock mechanism on stock.picking. When a picking is
unlocked a user is allowed to:

- change initial demand when picking is not done
- change quantity done when picking is done
This commit is contained in:
Joren Van Onder
2017-09-12 15:23:10 +02:00
committed by Simon Lejeune
parent c5f680200c
commit 261b45521c
5 changed files with 56 additions and 48 deletions
+19 -27
View File
@@ -60,8 +60,7 @@ class StockMove(models.Model):
"be moved. Lowering this quantity does not generate a "
"backorder. Changing this quantity on assigned moves affects "
"the product reservation, and should be done with care.")
product_uom = fields.Many2one(
'product.uom', 'Unit of Measure', required=True, states={'done': [('readonly', True)]})
product_uom = fields.Many2one('product.uom', 'Unit of Measure', required=True)
# TDE FIXME: make it stored, otherwise group will not work
product_tmpl_id = fields.Many2one(
'product.template', 'Product Template',
@@ -72,11 +71,11 @@ class StockMove(models.Model):
help="It specifies attributes of packaging like type, quantity of packaging,etc.")
location_id = fields.Many2one(
'stock.location', 'Source Location',
auto_join=True, index=True, required=True, states={'done': [('readonly', True)]},
auto_join=True, index=True, required=True,
help="Sets a location if you produce at a fixed location. This can be a partner location if you subcontract the manufacturing operations.")
location_dest_id = fields.Many2one(
'stock.location', 'Destination Location',
auto_join=True, index=True, required=True, states={'done': [('readonly', True)]},
auto_join=True, index=True, required=True,
help="Location where the system will stock the finished products.")
partner_id = fields.Many2one(
'res.partner', 'Destination Address ',
@@ -148,19 +147,18 @@ class StockMove(models.Model):
route_ids = fields.Many2many('stock.location.route', 'stock_location_route_move', 'move_id', 'route_id', 'Destination route', help="Preferred route")
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 Done', compute='_quantity_done_compute', digits=dp.get_precision('Product Unit of Measure'), inverse='_quantity_done_set',
states={'done': [('readonly', True)]})
quantity_done = fields.Float('Quantity Done', compute='_quantity_done_compute', digits=dp.get_precision('Product Unit of Measure'), inverse='_quantity_done_set')
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_locked = fields.Boolean(related='picking_id.is_locked', readonly=True)
is_initial_demand_editable = fields.Boolean('Is initial demand editable', compute='_compute_is_initial_demand_editable')
@api.multi
@api.depends('has_tracking', 'move_line_ids', 'location_id', 'location_dest_id', 'is_editable')
@api.depends('product_id', 'has_tracking', 'move_line_ids', 'location_id', 'location_dest_id')
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.
@@ -170,17 +168,13 @@ class StockMove(models.Model):
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
has_package = move.move_line_ids.mapped('package_id') | move.move_line_ids.mapped('result_package_id')
consignment_enabled = self.user_has_groups('stock.group_tracking_owner')
if move.picking_id.picking_type_id.show_operations is False\
and move.state not in ['cancel', 'draft', 'confirmed']\
and move.state != 'draft'\
and (multi_locations_enabled or move.has_tracking != 'none' or len(move.move_line_ids) > 1 or has_package or consignment_enabled):
move.show_details_visible = True
else:
@@ -195,17 +189,15 @@ class StockMove(models.Model):
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
@api.depends('state', 'picking_id')
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')
if move.state == 'draft':
move.is_initial_demand_editable = True
elif move.state != 'done' and move.picking_id and not move.picking_id.is_locked:
move.is_initial_demand_editable = True
else:
move.is_initial_demand_editable = False
@api.one
@api.depends('product_id', 'product_uom', 'product_uom_qty')
@@ -319,7 +311,11 @@ class StockMove(models.Model):
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']:
if picking_id.state == 'done':
defaults['state'] = 'done'
defaults['product_uom_qty'] = 0.0
defaults['additional'] = True
elif picking_id.state not in ['draft', 'confirmed']:
defaults['state'] = 'assigned'
defaults['product_uom_qty'] = 0.0
defaults['additional'] = True
@@ -359,10 +355,6 @@ class StockMove(models.Model):
# TDE CLEANME: it is a gros bordel + tracking
Picking = self.env['stock.picking']
# Check that we do not modify a stock.move which is done
frozen_fields = ['product_qty', 'product_uom', 'location_id', 'location_dest_id', 'product_id']
if any(fname in frozen_fields for fname in vals) and any(move.state == 'done' for move in self):
raise UserError(_('Quantities, Units of Measure, Products and Locations cannot be modified on stock moves that have already been processed (except by the Administrator).'))
propagated_changes_dict = {}
#propagation of expected date:
+2
View File
@@ -43,6 +43,8 @@ class StockMoveLine(models.Model):
to_loc = fields.Char(compute='_compute_location_description')
lots_visible = fields.Boolean(compute='_compute_lots_visible')
state = fields.Selection(related='move_id.state', store=True)
is_initial_demand_editable = fields.Boolean(related='move_id.is_initial_demand_editable')
is_locked = fields.Boolean(related='move_id.is_locked', default=True, readonly=True)
consume_line_ids = fields.Many2many('stock.move.line', 'stock_move_line_consume_rel', 'consume_line_id', 'produce_line_id', help="Technical link to see who consumed what. ")
produce_line_ids = fields.Many2many('stock.move.line', 'stock_move_line_consume_rel', 'produce_line_id', 'consume_line_id', help="Technical link to see which line was produced with this. ")
+8 -3
View File
@@ -265,9 +265,7 @@ class Picking(models.Model):
index=True, required=True,
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
move_line_ids = fields.One2many(
'stock.move.line', 'picking_id', 'Operations',
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
move_line_ids = fields.One2many('stock.move.line', 'picking_id', 'Operations')
move_line_exist = fields.Boolean(
'Has Pack Operations', compute='_compute_move_line_exist',
@@ -282,6 +280,9 @@ class Picking(models.Model):
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
help="Default Owner")
printed = fields.Boolean('Printed')
is_locked = fields.Boolean(default=True, help='When the picking is not done this allows changing the '
'initial demand. When the picking is done this allows '
'changing the done quantities.')
# Used to search on pickings
product_id = fields.Many2one('product.product', 'Product', related='move_lines.product_id')
show_operations = fields.Boolean(related='picking_type_id.show_operations')
@@ -649,6 +650,10 @@ class Picking(models.Model):
'context': self.env.context,
}
def action_toggle_is_locked(self):
self.is_locked = not self.is_locked
return True
def check_backorder(self):
self.ensure_one()
quantity_todo = {}
+6 -3
View File
@@ -141,7 +141,7 @@
<field name="location_id" invisible="1"/>
<field name="location_dest_id" invisible="1"/>
<field name="picking_id" invisible="1"/>
<field name="is_editable" invisible="1"/>
<field name="is_locked" invisible="1"/>
<group>
<group>
<field name="product_id" readonly="1"/>
@@ -160,7 +160,7 @@
</group>
</group>
<field name="move_line_ids" attrs="{'readonly': ['&amp;', ('state', 'in', ('done', 'cancel')), ('is_editable', '=', False)]}" context="{'default_product_uom_id': product_uom, 'default_picking_id': picking_id, 'default_move_id': id, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
<field name="move_line_ids" attrs="{'readonly': ['|', ('state', '=', 'cancel'), '&amp;', ('state', '=', 'done'), ('is_locked', '=', True)]}" context="{'default_product_uom_id': product_uom, 'default_picking_id': picking_id, 'default_move_id': id, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
<tree editable="bottom" decoration-succes="product_uom_qty==qty_done" decoration-danger="qty_done > product_uom_qty)">
<field name="picking_id" invisible="1"/>
<field name="product_id" invisible="1"/>
@@ -171,8 +171,11 @@
<field name="package_id" invisible="not context.get('show_package')" groups="stock.group_tracking_lot"/>
<field name="result_package_id" groups="stock.group_tracking_lot"/>
<field name="owner_id" groups="stock.group_tracking_owner"/>
<field name="is_initial_demand_editable" invisible="1"/>
<field name="product_uom_qty" invisible="not context.get('show_reserved_quantity')" readonly="1"/>
<field name="qty_done"/>
<field name="state" invisible="1"/>
<field name="is_locked" invisible="1"/>
<field name="qty_done" attrs="{'readonly': ['|', ('is_initial_demand_editable', '=', True), '&amp;', ('state', '=', 'done'), ('is_locked', '=', True)]}"/>
<field name="product_uom_id" options="{'no_open': True, 'no_create': True}" attrs="{'readonly': [('product_uom_qty', '!=', 0.0)]}" string="Unit of Measure" groups="product.group_uom"/>
</tree>
</field>
+21 -15
View File
@@ -202,21 +202,24 @@
<field name="arch" type="xml">
<form string="Transfer">
<header>
<field name="is_locked" invisible="1"/>
<button name="action_confirm" states="draft" string="Mark as Todo" type="object" class="oe_highlight" groups="base.group_user"/>
<button name="action_assign" states="confirmed,waiting" string="Check Availability" type="object" class="oe_highlight" groups="base.group_user"/>
<button name="action_assign" states="partially_available" string="Recheck Availability" type="object" groups="base.group_user"/>
<button name="force_assign" states="confirmed,waiting,partially_available" string="Force Availability" type="object" groups="stock.group_stock_manager"/>
<button name="do_new_transfer" states="draft,partially_available,assigned" string="Validate" groups="stock.group_stock_user" type="object" class="oe_highlight"/>
<button name="do_print_picking" string="Print" groups="stock.group_stock_user" type="object" attrs="{'invisible': [('state', 'not in', ('assigned', 'partially_available'))]}"/>
<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="action_assign" attrs="{'invisible': ['|', ('state', 'not in', ('confirmed', 'waiting')), ('is_locked', '=', False)]}" string="Check Availability" type="object" class="oe_highlight" groups="base.group_user"/>
<button name="action_assign" attrs="{'invisible': ['|', ('state', '!=', 'partially_available'), ('is_locked', '=', False)]}" string="Recheck Availability" type="object" groups="base.group_user"/>
<button name="force_assign" attrs="{'invisible': ['|', ('state', 'not in', ('confirmed', 'waiting', 'partially_available')), ('is_locked', '=', False)]}" string="Force Availability" type="object" groups="stock.group_stock_manager"/>
<button name="do_new_transfer" attrs="{'invisible': ['|', ('state', 'not in', ('draft', 'partially_available', 'assigned')), ('is_locked', '=', False)]}" string="Validate" groups="stock.group_stock_user" type="object" class="oe_highlight"/>
<button name="do_print_picking" string="Print" groups="stock.group_stock_user" type="object" attrs="{'invisible': ['|', ('state', 'not in', ('assigned', 'partially_available')), ('is_locked', '=', False)]}"/>
<button name="%(action_report_delivery)d" string="Print" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('is_locked', '=', False)]}" type="action" groups="base.group_user"/>
<button name="%(act_stock_return_picking)d" string="Return" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('is_locked', '=', False)]}" type="action" groups="base.group_user"/>
<button name="action_cancel" attrs="{'invisible': ['|', ('state', 'not in', ('assigned', 'confirmed', 'partially_available', 'draft', 'waiting')), ('is_locked', '=', False)]}" string="Cancel" groups="base.group_user" type="object"/>
<field name="picking_type_code" invisible="1"/>
<button name="do_unreserve" string="Unreserve" groups="base.group_user" type="object" attrs="{'invisible': ['|', ('picking_type_code', '=', 'incoming'), ('state', 'not in', ('assigned', 'partially_available'))]}"/>
<button name="do_unreserve" string="Unreserve" groups="base.group_user" type="object" attrs="{'invisible': ['|', '|', ('picking_type_code', '=', 'incoming'), ('state', 'not in', ('assigned', 'partially_available')), ('is_locked', '=', False)]}"/>
<field name="show_operations" invisible="1" readonly="1"/>
<field name="move_line_exist" invisible="1"/>
<field name="has_packages" invisible="1"/>
<button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': ['|', '&amp;', ('picking_type_code', '!=', 'incoming'), ('state', 'in', ('draft', 'cancel', 'waiting')), '&amp;', ('picking_type_code', '=', 'incoming'), ('state', '!=', 'done')]}"/>
<button name="action_cancel" states="assigned,confirmed,partially_available,draft,waiting" string="Cancel" groups="base.group_user" type="object"/>
<button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': ['|', '|', '&amp;', ('picking_type_code', '!=', 'incoming'), ('state', 'in', ('draft', 'cancel', 'waiting')), '&amp;', ('picking_type_code', '=', 'incoming'), ('state', '!=', 'done'), ('is_locked', '=', False)]}"/>
<button name="action_toggle_is_locked" attrs="{'invisible': ['|', ('state', '=', 'draft'), ('is_locked', '=', False)]}" string="Unlock" groups="stock.group_stock_manager" type="object" help="If the picking is unlocked you can edit initial demand (for a draft picking) or done quantities (for a done picking)."/>
<button name="action_toggle_is_locked" attrs="{'invisible': [('is_locked', '=', True)]}" string="Lock" class="oe_highlight" groups="stock.group_stock_manager" type="object"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed,partially_available,assigned,done" />
<field name="picking_type_entire_packs" invisible="1"/>
</header>
@@ -265,15 +268,17 @@
<field name="owner_id" groups="stock.group_tracking_owner"/>
<field name="state" invisible="1"/>
<field name="lot_id" groups="stock.group_production_lot" attrs="{'invisible': [('lots_visible', '=', False)]}" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}"/>
<field name="product_uom_qty" readonly="1" attrs="{'required': [('product_id', '!=', False)]}"/>
<field name="qty_done"/>
<field name="is_initial_demand_editable" invisible="1"/>
<field name="product_uom_qty" attrs="{'readonly': [('is_initial_demand_editable', '=', False)], 'required': [('product_id', '!=', False)]}"/>
<field name="is_locked" invisible="1"/>
<field name="qty_done" attrs="{'readonly': ['|', ('is_initial_demand_editable', '=', True), '&amp;', ('state', '=', 'done'), ('is_locked', '=', True)]}"/>
</tree>
</field>
<button class="oe_link oe_right" name="put_in_pack" type="object" string="&#8627;Put in Pack" attrs="{'invisible': [('state', 'in', ('done', 'cancel'))]}" groups="stock.group_tracking_lot"/>
</page>
<page string="Operations">
<field name="id" invisible="1"/>
<field name="move_lines" attrs="{'readonly': [('state', 'in', ('cancel', 'done'))]}" context="{'picking_type_code': picking_type_code, '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="move_lines" attrs="{'readonly': [('state', '=', 'done'), ('is_locked', '=', True)]}" context="{'picking_type_code': picking_type_code, '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}">
<tree decoration-danger="state != 'done' and quantity_done > reserved_availability" decoration-muted="scrapped == True or state == 'cancel'" string="Stock Moves" editable="bottom">
<field name="name" invisible="1"/>
<field name="date_expected" invisible="1"/>
@@ -287,12 +292,13 @@
<field name="show_details_visible" invisible="1"/>
<field name="show_operations" invisible="1" readonly="1"/>
<field name="additional" invisible="1"/>
<field name="is_locked" invisible="1"/>
<field name="product_id" required="1" attrs="{'readonly': ['&amp;', ('state', '!=', 'draft'), ('additional', '=', False)]}"/>
<field name="is_initial_demand_editable" invisible="1"/>
<field name="product_uom_qty" string="Initial Demand" attrs="{'readonly': [('is_initial_demand_editable', '=', False), ('state', '!=', 'draft')]}"/>
<field name="product_uom_qty" string="Initial Demand" attrs="{'readonly': [('is_initial_demand_editable', '=', False)]}"/>
<field name="reserved_availability" string="Reserved" invisible="context.get('picking_type_code') == 'incoming'"/>
<field name="quantity_done" string="Done" attrs="{'readonly': ['|', ('show_details_visible', '=', True), ('state', '=', 'cancel')], 'invisible': ['|', ('state', 'in', ['draft', 'confirmed', 'waiting']), ('show_operations', '=', True)]}"/>
<field name="quantity_done" string="Done" attrs="{'readonly': ['|', '|', ('show_details_visible', '=', True), ('state', '=', 'cancel'), '&amp;', ('state', '=', 'done'), ('is_locked', '=', True)], 'invisible': ['|', ('state', 'in', ['draft', 'confirmed', 'waiting']), ('show_operations', '=', True)]}"/>
<field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft'), ('additional', '=', False)]}" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="product.group_uom"/>
<button name="action_show_details" string="Register lots, packs, location" type="object" icon="fa-list" attrs="{'invisible': [('show_details_visible', '=', False)]}"/>
</tree>