[IMP] stock: quantity done on stock.move is always editable
Quantity done will be always editable. Different use case: 1. If there is not location/package/tracing/owner -> same behavior than before 2. If the details are needed: - Immediate transfer. In case of increase the quants are reserved and the `qty_done` on `stock.move.line` is increased depending the reservation. If the quantity couldn't be fully reserve then default `stock.move.line` are create (depending the putaway rules or multiple ones if the products are tracked). In case of decrease, the quantity reserved are unreserve but the `stock.move.line` are not unlink until the validation. - Planned transfer. The quantity done will fill the reservation. If the picking is not assign, it will reserve the quantity needed. In case of decrease, it will only unreserve the extra quantity (compare to the initial demand). To unreserve the quantity bellow the intial demand, the `stock.move.line` could be unlink or use the `unreserve` button. force_qty in `_action_assign` is needed because we don't reserve only base on the initial demand (`product_uom_qty`) but sometimes on the quantity_done. In order to never block the user. The action_show_details is always available. If the id is not available, it will save and open the record (we take the record based on the new existing line with same product and highest id). closes odoo/odoo#102738 Task-id: 2960983 X-original-commit: cacc677a08066bc23e1c89df36027599d05f2a08 Related: odoo/enterprise#32555 Signed-off-by: William Henrotin (whe) <whe@odoo.com>
This commit is contained in:
@@ -302,8 +302,8 @@ class StockMove(models.Model):
|
||||
super().write({'move_line_ids': move_line_vals})
|
||||
return super().write(vals)
|
||||
|
||||
def _action_assign(self):
|
||||
res = super(StockMove, self)._action_assign()
|
||||
def _action_assign(self, force_qty=False):
|
||||
res = super(StockMove, self)._action_assign(force_qty=force_qty)
|
||||
for move in self.filtered(lambda x: x.production_id or x.raw_material_production_id):
|
||||
if move.move_line_ids:
|
||||
move.move_line_ids.write({'production_id': move.raw_material_production_id.id,
|
||||
|
||||
@@ -320,7 +320,14 @@ class StockMove(models.Model):
|
||||
else:
|
||||
move.delay_alert_date = False
|
||||
|
||||
@api.depends('move_line_ids.qty_done', 'move_line_ids.product_uom_id', 'move_line_nosuggest_ids.qty_done', 'picking_type_id.show_reserved')
|
||||
def _quantity_done_sml(self):
|
||||
self.ensure_one()
|
||||
quantity = 0
|
||||
for move_line in self._get_move_lines():
|
||||
quantity += move_line.product_uom_id._compute_quantity(move_line.qty_done, self.product_uom, round=False)
|
||||
return quantity
|
||||
|
||||
@api.depends('move_line_ids.qty_done', 'move_line_ids.product_uom_id', 'move_line_nosuggest_ids.qty_done')
|
||||
def _quantity_done_compute(self):
|
||||
""" This field represents the sum of the move lines `qty_done`. It allows the user to know
|
||||
if there is still work to do.
|
||||
@@ -333,11 +340,7 @@ class StockMove(models.Model):
|
||||
if not any(self._ids):
|
||||
# onchange
|
||||
for move in self:
|
||||
quantity_done = 0
|
||||
for move_line in move._get_move_lines():
|
||||
quantity_done += move_line.product_uom_id._compute_quantity(
|
||||
move_line.qty_done, move.product_uom, round=False)
|
||||
move.quantity_done = quantity_done
|
||||
move.quantity_done = move._quantity_done_sml()
|
||||
else:
|
||||
# compute
|
||||
move_lines_ids = set()
|
||||
@@ -362,19 +365,59 @@ class StockMove(models.Model):
|
||||
)
|
||||
|
||||
def _quantity_done_set(self):
|
||||
quantity_done = self[0].quantity_done # any call to create will invalidate `move.quantity_done`
|
||||
def _process_decrease(move, quantity):
|
||||
"""Prioritize decrease the ml without reserved qty"""
|
||||
res_mls = move._get_move_lines().sorted(lambda ml: float_is_zero(ml.reserved_uom_qty, precision_rounding=ml.product_uom_id.rounding), reverse=True)
|
||||
qty_to_unreserve = move.reserved_availability - move.product_uom_qty
|
||||
for ml in res_mls:
|
||||
if float_is_zero(quantity, precision_rounding=move.product_uom.rounding):
|
||||
break
|
||||
qty_ml_dec = min(ml.qty_done, ml.product_uom_id._compute_quantity(quantity, ml.product_uom_id, round=False))
|
||||
if float_is_zero(qty_ml_dec, precision_rounding=ml.product_uom_id.rounding):
|
||||
continue
|
||||
ml.qty_done -= qty_ml_dec
|
||||
quantity -= move.product_uom._compute_quantity(qty_ml_dec, move.product_uom, round=False)
|
||||
# Unreserve
|
||||
if (not move.picking_id.immediate_transfer and move.reserved_availability < move.product_uom_qty):
|
||||
continue
|
||||
if float_compare(ml.reserved_uom_qty, ml.qty_done, precision_rounding=ml.product_uom_id.rounding) <= 0:
|
||||
continue
|
||||
if move.picking_id.immediate_transfer:
|
||||
ml.reserved_uom_qty = ml.qty_done
|
||||
elif float_compare(qty_to_unreserve, 0, precision_rounding=move.product_uom.rounding) > 0:
|
||||
qty_unreserved = min(qty_to_unreserve, ml.reserved_qty - ml.qty_done)
|
||||
ml.reserved_uom_qty = ml.reserved_qty - qty_unreserved
|
||||
qty_to_unreserve -= qty_unreserved
|
||||
|
||||
def _process_increase(move, quantity):
|
||||
moves = move
|
||||
if move.picking_id.immediate_transfer:
|
||||
moves = move._action_confirm(merge=False)
|
||||
# Kits, already handle in action_explode, should be clean in master
|
||||
if len(moves) > 1:
|
||||
return
|
||||
if move.reserved_availability < move.quantity_done and move.state not in ['done', 'cancel']:
|
||||
move._action_assign(force_qty=move.quantity_done)
|
||||
move._set_quantity_done(quantity)
|
||||
|
||||
err = []
|
||||
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.move.line'].create(dict(move._prepare_move_line_vals(), qty_done=quantity_done))
|
||||
move.write({'move_line_ids': [(4, move_line.id)]})
|
||||
move_line._apply_putaway_strategy()
|
||||
elif len(move_lines) == 1:
|
||||
move_lines[0].qty_done = quantity_done
|
||||
else:
|
||||
move._multi_line_quantity_done_set(quantity_done)
|
||||
uom_qty = float_round(move.quantity_done, precision_rounding=move.product_uom.rounding, rounding_method='HALF-UP')
|
||||
precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
qty = float_round(move.quantity_done, precision_digits=precision_digits, rounding_method='HALF-UP')
|
||||
if float_compare(uom_qty, qty, precision_digits=precision_digits) != 0:
|
||||
err.append(_("""
|
||||
The quantity done for the product %s doesn't respect the rounding precision defined on the unit of measure %s.
|
||||
Please change the quantity done or the rounding precision of your unit of measure.""",
|
||||
move.product_id.display_name, move.product_uom.display_name))
|
||||
continue
|
||||
delta_qty = move.quantity_done - move._quantity_done_sml()
|
||||
if float_compare(delta_qty, 0, precision_rounding=move.product_uom.rounding) > 0:
|
||||
_process_increase(move, delta_qty)
|
||||
elif float_compare(delta_qty, 0, precision_rounding=move.product_uom.rounding) < 0:
|
||||
_process_decrease(move, abs(delta_qty))
|
||||
if err:
|
||||
raise UserError('\n'.join(err))
|
||||
|
||||
def _multi_line_quantity_done_set(self, quantity_done):
|
||||
move_lines = self._get_move_lines()
|
||||
@@ -595,6 +638,13 @@ class StockMove(models.Model):
|
||||
move.location_id.name, move.location_dest_id.name)))
|
||||
return res
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('quantity_done') and 'lot_ids' in vals:
|
||||
vals.pop('lot_ids')
|
||||
return super().create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
# Handle the write on the initial demand by updating the reserved quantity and logging
|
||||
# messages according to the state of the stock.move records.
|
||||
@@ -1433,7 +1483,7 @@ class StockMove(models.Model):
|
||||
return self.product_qty
|
||||
return self.env['stock.quant']._get_available_quantity(self.product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict, allow_negative=allow_negative)
|
||||
|
||||
def _action_assign(self):
|
||||
def _action_assign(self, force_qty=False):
|
||||
""" Reserve stock moves by creating their stock move lines. A stock move is
|
||||
considered reserved once the sum of `reserved_qty` for all its move lines is
|
||||
equal to its `product_qty`. If it is less, the stock move is considered
|
||||
@@ -1489,9 +1539,16 @@ class StockMove(models.Model):
|
||||
# Once the quantities are assigned, we want to find a better destination location thanks
|
||||
# to the putaway rules. This redirection will be applied on moves of `moves_to_redirect`.
|
||||
moves_to_redirect = OrderedSet()
|
||||
for move in self.filtered(lambda m: m.state in ['confirmed', 'waiting', 'partially_available']):
|
||||
moves_to_assign = self
|
||||
if not force_qty:
|
||||
moves_to_assign = self.filtered(lambda m: m.state in ['confirmed', 'waiting', 'partially_available'])
|
||||
for move in moves_to_assign:
|
||||
rounding = roundings[move]
|
||||
missing_reserved_uom_quantity = move.product_uom_qty - reserved_availability[move]
|
||||
if not force_qty:
|
||||
missing_reserved_uom_quantity = move.product_uom_qty
|
||||
else:
|
||||
missing_reserved_uom_quantity = force_qty
|
||||
missing_reserved_uom_quantity -= reserved_availability[move]
|
||||
missing_reserved_quantity = move.product_uom._compute_quantity(missing_reserved_uom_quantity, move.product_id.uom_id, rounding_method='HALF-UP')
|
||||
if move._should_bypass_reservation():
|
||||
# create the move line(s) but do not impact quants
|
||||
@@ -1803,7 +1860,7 @@ class StockMove(models.Model):
|
||||
rounding = move.product_uom.rounding
|
||||
if move.state in ('cancel', 'done', 'draft'):
|
||||
continue
|
||||
elif float_compare(move.reserved_availability, move.product_uom_qty, precision_rounding=rounding) == 0:
|
||||
elif float_compare(move.reserved_availability, move.product_uom_qty, precision_rounding=rounding) >= 0:
|
||||
moves_state_to_write['assigned'].add(move.id)
|
||||
elif move.reserved_availability and float_compare(move.reserved_availability, move.product_uom_qty, precision_rounding=rounding) <= 0:
|
||||
moves_state_to_write['partially_available'].add(move.id)
|
||||
|
||||
@@ -457,18 +457,12 @@ class StockMoveLine(models.Model):
|
||||
|
||||
res = super(StockMoveLine, self).write(vals)
|
||||
|
||||
# Update scrap object linked to move_lines to the new quantity.
|
||||
if 'qty_done' in vals:
|
||||
for move in self.mapped('move_id'):
|
||||
if move.scrapped:
|
||||
move.scrap_ids.write({'scrap_qty': move.quantity_done})
|
||||
|
||||
# As stock_account values according to a move's `product_uom_qty`, we consider that any
|
||||
# done stock move should have its `quantity_done` equals to its `product_uom_qty`, and
|
||||
# this is what move's `action_done` will do. So, we replicate the behavior here.
|
||||
if updates or 'qty_done' in vals:
|
||||
moves = self.filtered(lambda ml: ml.move_id.state == 'done').mapped('move_id')
|
||||
moves |= self.filtered(lambda ml: ml.move_id.state not in ('done', 'cancel') and ml.move_id.picking_id.immediate_transfer and not ml.reserved_uom_qty).mapped('move_id')
|
||||
moves |= self.filtered(lambda ml: ml.move_id.state not in ('done', 'cancel') and ml.move_id.picking_id.immediate_transfer).mapped('move_id')
|
||||
for move in moves:
|
||||
move.product_uom_qty = move.quantity_done
|
||||
next_moves._do_unreserve()
|
||||
|
||||
@@ -52,13 +52,22 @@ class StockScrap(models.Model):
|
||||
scrap_location_id = fields.Many2one(
|
||||
'stock.location', 'Scrap Location', default=_get_default_scrap_location_id,
|
||||
domain="[('scrap_location', '=', True), ('company_id', 'in', [company_id, False])]", required=True, states={'done': [('readonly', True)]}, check_company=True)
|
||||
scrap_qty = fields.Float('Quantity', default=1.0, required=True, states={'done': [('readonly', True)]}, digits='Product Unit of Measure')
|
||||
scrap_qty = fields.Float(
|
||||
'Quantity', required=True, states={'done': [('readonly', True)]}, digits='Product Unit of Measure',
|
||||
compute='_compute_scrap_qty', precompute=True, store=True)
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('done', 'Done')],
|
||||
string='Status', default="draft", readonly=True, tracking=True)
|
||||
date_done = fields.Datetime('Date', readonly=True)
|
||||
|
||||
@api.depends('move_id', 'move_id.move_line_ids.qty_done')
|
||||
def _compute_scrap_qty(self):
|
||||
self.scrap_qty = 1
|
||||
for scrap in self:
|
||||
if scrap.move_id:
|
||||
scrap.scrap_qty = scrap.move_id.quantity_done
|
||||
|
||||
@api.onchange('picking_id')
|
||||
def _onchange_picking_id(self):
|
||||
if self.picking_id:
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { sortBy } from "@web/core/utils/arrays";
|
||||
import { FormController } from "@web/views/form/form_controller";
|
||||
import { useBus, useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class StockPickingFormController extends FormController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.action = useService("action");
|
||||
useBus(this.env.bus, "STOCK_MOVE:UPDATED", (ev) => this._qtyUpdated(ev));
|
||||
useBus(this.env.bus, "STOCK_MOVE:SAVED", (ev) => this._actionOpenShowDetails(ev));
|
||||
}
|
||||
|
||||
// TRIGGER
|
||||
async _actionOpenShowDetails(ev) {
|
||||
let id = ev.detail.id;
|
||||
let record;
|
||||
if (id) {
|
||||
record = this.model.root.data.move_ids_without_package.records.find(
|
||||
(e) => e.data.id == id
|
||||
);
|
||||
} else {
|
||||
const moveByIdsDesc = sortBy(
|
||||
this.model.root.data.move_ids_without_package.records,
|
||||
"id",
|
||||
"desc"
|
||||
);
|
||||
record = moveByIdsDesc.find((e) => e.data.product_id[0] == ev.detail.product_id[0]);
|
||||
id = record.data.id;
|
||||
}
|
||||
const action = await this.model.orm.call("stock.move", "action_show_details", [id]);
|
||||
if (action) {
|
||||
this.action.doAction(action, { onClose: () => this._onClose() });
|
||||
}
|
||||
ev.detail.resolve();
|
||||
}
|
||||
|
||||
async _qtyUpdated(ev) {
|
||||
await this.model.root.save();
|
||||
ev.detail.resolve();
|
||||
}
|
||||
|
||||
// PRIVATE
|
||||
async _onClose() {
|
||||
await this.model.root.load();
|
||||
this.render(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Record, RelationalModel } from "@web/views/basic_relational_model";
|
||||
|
||||
export class StockPickingAutoSaveRecord extends Record {
|
||||
setup(params, state) {
|
||||
super.setup(params, state);
|
||||
}
|
||||
|
||||
async saveAndOpenDetails() {
|
||||
await new Promise((resolve) => {
|
||||
this.model.env.bus.trigger("STOCK_MOVE:UPDATED", { resolve });
|
||||
});
|
||||
await new Promise((resolve) => {
|
||||
this.model.env.bus.trigger("STOCK_MOVE:SAVED", {
|
||||
id: this.data.id,
|
||||
product_id: this.data.product_id,
|
||||
resolve,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class StockPickingModel extends RelationalModel {}
|
||||
StockPickingModel.Record = StockPickingAutoSaveRecord;
|
||||
@@ -0,0 +1,14 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
import { StockPickingModel } from "@stock/views/picking_form/picking_form_model";
|
||||
import { StockPickingFormController } from "@stock/views/picking_form/picking_form_controller";
|
||||
|
||||
export const StockPickingFormView = {
|
||||
...formView,
|
||||
Controller: StockPickingFormController,
|
||||
Model: StockPickingModel,
|
||||
};
|
||||
|
||||
registry.category("views").add("picking_form", StockPickingFormView);
|
||||
@@ -0,0 +1,33 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
import { X2ManyField } from "@web/views/fields/x2many/x2many_field";
|
||||
import { ViewButton } from "@web/views/view_button/view_button";
|
||||
|
||||
class MoveViewButton extends ViewButton {
|
||||
async onClick(ev) {
|
||||
if (this.props.clickParams.name != "action_show_details") {
|
||||
super.onClick(ev);
|
||||
} else {
|
||||
await this.props.record.saveAndOpenDetails();
|
||||
}
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
if (this.props.clickParams.name == "action_show_details") {
|
||||
return false;
|
||||
}
|
||||
return super.disabled;
|
||||
}
|
||||
}
|
||||
|
||||
MoveViewButton.props = [...ViewButton.props];
|
||||
export class MovesListRenderer extends ListRenderer {}
|
||||
|
||||
MovesListRenderer.components = { ...ListRenderer.components, ViewButton: MoveViewButton };
|
||||
|
||||
export class StockMoveX2ManyField extends X2ManyField {}
|
||||
StockMoveX2ManyField.components = { ...X2ManyField.components, ListRenderer: MovesListRenderer };
|
||||
|
||||
registry.category("fields").add("stock_move_one2many", StockMoveX2ManyField);
|
||||
@@ -104,7 +104,7 @@
|
||||
<field name="model">stock.picking</field>
|
||||
<field eval="12" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Transfer">
|
||||
<form string="Transfer" js_class="picking_form">
|
||||
|
||||
<field name="is_locked" invisible="1"/>
|
||||
<field name="show_mark_as_todo" invisible="1"/>
|
||||
@@ -246,6 +246,7 @@
|
||||
|
||||
<page string="Operations" name="operations">
|
||||
<field name="move_ids_without_package" mode="tree,kanban"
|
||||
widget="stock_move_one2many"
|
||||
attrs="{'readonly': ['&', ('state', '=', 'done'), ('is_locked', '=', True)]}"
|
||||
context="{'default_company_id': company_id, 'default_date': scheduled_date, 'default_date_deadline': date_deadline, 'picking_type_code': picking_type_code, 'default_picking_id': id, 'form_view_ref':'stock.view_move_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, 'default_partner_id': partner_id}"
|
||||
add-label="Add a Product">
|
||||
@@ -287,7 +288,7 @@
|
||||
<field name="reserved_availability" string="Reserved"
|
||||
attrs="{'column_invisible': ['|', '|', ('parent.state', 'in', ['draft', 'done']), ('parent.picking_type_code', 'in', ['incoming', 'outgoing']), ('parent.immediate_transfer', '=', True)]}"/>
|
||||
<field name="product_qty" invisible="1" readonly="1"/>
|
||||
<field name="quantity_done" string="Done" attrs="{'readonly': [('is_quantity_done_editable', '=', False)], 'column_invisible':[('parent.state', '=', 'draft'), ('parent.immediate_transfer', '=', False)]}"/>
|
||||
<field name="quantity_done" string="Done" attrs="{'readonly': [('product_id', '=', False)], 'column_invisible':[('parent.state', '=', 'draft'), ('parent.immediate_transfer', '=', False)]}"/>
|
||||
<field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft'), ('additional', '=', False)]}" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/>
|
||||
<field name="lot_ids" widget="many2many_tags"
|
||||
groups="stock.group_production_lot"
|
||||
|
||||
Reference in New Issue
Block a user