[IMP] mrp, stock: update reservation logic
This commit adds 2 small feature additions to reservation logic:
1. Add another # days parameter for reserve "before scheduled date"
option for priority moves (i.e. so they can be marked for reservation
earlier)
2. Add Reserve button in the forecast report
It also cleans up some auto-reserving logic:
- 'at confirm' now has a reservation_date = day of confirmation:
- removes need to check `reservation_method` when doing automated
_action_assign (i.e. in scheduler + completed incoming move).
- allows ordering by reservation_date during automated _action_assign
for more consistent and logical reservations.
Task: 2418907
Upgrade PR: odoo/upgrade#2143
This commit is contained in:
@@ -15,11 +15,19 @@
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='unreserve_link']" position="after">
|
||||
<button t-if="line['move_out'] and line['move_out'].raw_material_production_id and line['move_out'].raw_material_production_id.unreserve_visible"
|
||||
class="btn btn-primary o_report_replenish_unreserve"
|
||||
class="btn btn-sm btn-primary o_report_replenish_unreserve"
|
||||
t-attf-model="mrp.production"
|
||||
t-att-model-id="line['move_out'].raw_material_production_id.id">
|
||||
Unreserve
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='reserve_link']" position="after">
|
||||
<button t-if="line['move_out'] and line['move_out'].raw_material_production_id and line['move_out'].raw_material_production_id.reserve_visible"
|
||||
class="btn btn-sm btn-primary o_report_replenish_reserve"
|
||||
t-attf-model="mrp.production"
|
||||
t-att-model-id="line['move_out'].raw_material_production_id.id">
|
||||
Reserve
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='change_priority_link']" position="after">
|
||||
<button t-if="line['move_out'] and line['move_out'].raw_material_production_id"
|
||||
|
||||
@@ -496,13 +496,14 @@ class StockMove(models.Model):
|
||||
move_lines_commands.append((0, 0, move_line_vals))
|
||||
move.write({'move_line_ids': move_lines_commands})
|
||||
|
||||
@api.depends('picking_type_id', 'date')
|
||||
@api.depends('picking_type_id', 'date', 'priority')
|
||||
def _compute_reservation_date(self):
|
||||
for move in self:
|
||||
if move.state not in ['draft', 'confirmed', 'waiting', 'partially_available']:
|
||||
continue
|
||||
if move.picking_type_id and move.picking_type_id.reservation_method == 'by_date':
|
||||
move.reservation_date = fields.Date.to_date(move.date) - timedelta(days=move.picking_type_id.reservation_days_before)
|
||||
if move.picking_type_id.reservation_method == 'by_date' and move.state in ['draft', 'confirmed', 'waiting', 'partially_available']:
|
||||
days = move.picking_type_id.reservation_days_before
|
||||
if move.priority == '1':
|
||||
days = move.picking_type_id.reservation_days_before_priority
|
||||
move.reservation_date = fields.Date.to_date(move.date) - timedelta(days=days)
|
||||
|
||||
@api.constrains('product_uom')
|
||||
def _check_uom(self):
|
||||
@@ -1144,6 +1145,9 @@ class StockMove(models.Model):
|
||||
|
||||
move_to_confirm.write({'state': 'confirmed'})
|
||||
(move_waiting | move_create_proc).write({'state': 'waiting'})
|
||||
# procure_method sometimes changes with certain workflows so just in case, apply to all moves
|
||||
(move_to_confirm | move_waiting | move_create_proc).filtered(lambda m: m.picking_type_id.reservation_method == 'at_confirm')\
|
||||
.write({'reservation_date': fields.Date.today()})
|
||||
|
||||
# assign picking in batch for all confirmed move that share the same details
|
||||
for moves_ids in to_assign.values():
|
||||
@@ -1826,8 +1830,9 @@ class StockMove(models.Model):
|
||||
domains = []
|
||||
for move in self:
|
||||
domains.append([('product_id', '=', move.product_id.id), ('location_id', '=', move.location_dest_id.id)])
|
||||
static_domain = [('state', 'in', ['confirmed', 'partially_available']), ('procure_method', '=', 'make_to_stock')]
|
||||
reservation_domain = ['|', ('picking_type_id.reservation_method', '=', 'at_confirm'),
|
||||
('reservation_date', '<=', fields.Date.today())]
|
||||
moves_to_reserve = self.env['stock.move'].search(expression.AND([static_domain, expression.OR(domains), reservation_domain]))
|
||||
static_domain = [('state', 'in', ['confirmed', 'partially_available']),
|
||||
('procure_method', '=', 'make_to_stock'),
|
||||
('reservation_date', '<=', fields.Date.today())]
|
||||
moves_to_reserve = self.env['stock.move'].search(expression.AND([static_domain, expression.OR(domains)]),
|
||||
order='reservation_date, priority desc, date asc')
|
||||
moves_to_reserve._action_assign()
|
||||
|
||||
@@ -69,11 +69,11 @@ class PickingType(models.Model):
|
||||
help="If this checkbox is ticked, Odoo will automatically pre-fill the detailed "
|
||||
"operations with the corresponding products, locations and lot/serial numbers.")
|
||||
reservation_method = fields.Selection(
|
||||
[('by_date', 'before scheduled date'), ('at_confirm', 'At Confirmation'), ('manual', 'Manually')],
|
||||
[('at_confirm', 'At Confirmation'), ('manual', 'Manually'), ('by_date', 'Before scheduled date')],
|
||||
'Reservation Method', required=True, default='at_confirm',
|
||||
help="How products in transfers of this operation type should be reserved.")
|
||||
reservation_days_before = fields.Integer('Days', help="Maximum number of days before scheduled date that products should be reserved.")
|
||||
|
||||
reservation_days_before_priority = fields.Integer('Days when starred', help="Maximum number of days before scheduled date that priority picking products should be reserved.")
|
||||
|
||||
count_picking_draft = fields.Integer(compute='_compute_picking_count')
|
||||
count_picking_ready = fields.Integer(compute='_compute_picking_count')
|
||||
|
||||
@@ -483,10 +483,9 @@ class ProcurementGroup(models.Model):
|
||||
def _get_moves_to_assign_domain(self, company_id):
|
||||
moves_domain = [
|
||||
('state', 'in', ['confirmed', 'partially_available']),
|
||||
('product_uom_qty', '!=', 0.0)
|
||||
('product_uom_qty', '!=', 0.0),
|
||||
('reservation_date', '<=', fields.Date.today())
|
||||
]
|
||||
moves_domain = expression.AND([moves_domain, ['|', ('picking_type_id.reservation_method', '=', 'at_confirm'),
|
||||
('reservation_date', '<=', fields.Date.today())]])
|
||||
if company_id:
|
||||
moves_domain = expression.AND([[('company_id', '=', company_id)], moves_domain])
|
||||
return moves_domain
|
||||
@@ -506,7 +505,7 @@ class ProcurementGroup(models.Model):
|
||||
# Search all confirmed stock_moves and try to assign them
|
||||
domain = self._get_moves_to_assign_domain(company_id)
|
||||
moves_to_assign = self.env['stock.move'].search(domain, limit=None,
|
||||
order='priority desc, date asc')
|
||||
order='reservation_date, priority desc, date asc')
|
||||
for moves_chunk in split_every(100, moves_to_assign.ids):
|
||||
self.env['stock.move'].browse(moves_chunk).sudo()._action_assign()
|
||||
if use_new_cursor:
|
||||
|
||||
@@ -128,7 +128,15 @@
|
||||
</button>
|
||||
</t>
|
||||
<t t-elif="line['replenishment_filled']">
|
||||
<t t-if="line['document_out']">Inventory On Hand</t>
|
||||
<t t-if="line['document_out']">Inventory On Hand
|
||||
<button t-if="line['move_out'] and line['move_out'].picking_id"
|
||||
class="btn btn-sm btn-primary o_report_replenish_reserve"
|
||||
t-attf-model="stock.picking"
|
||||
t-att-model-id="line['move_out'].picking_id.id"
|
||||
name="reserve_link">
|
||||
Reserve
|
||||
</button>
|
||||
</t>
|
||||
<t t-else="">Free Stock</t>
|
||||
</t>
|
||||
<span t-else="" class="text-muted">Not Available</span>
|
||||
|
||||
@@ -227,6 +227,7 @@ const ReplenishReport = clientAction.extend({
|
||||
rr.on('mouseenter', '.o_report_replenish_change_priority', this._onMouseEnterPriority.bind(this));
|
||||
rr.on('mouseleave', '.o_report_replenish_change_priority', this._onMouseLeavePriority.bind(this));
|
||||
rr.on('click', '.o_report_replenish_unreserve', this._onClickUnreserve.bind(this));
|
||||
rr.on('click', '.o_report_replenish_reserve', this._onClickReserve.bind(this));
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -316,13 +317,26 @@ const ReplenishReport = clientAction.extend({
|
||||
_onClickUnreserve: function(ev) {
|
||||
const model = ev.target.getAttribute('model');
|
||||
const modelId = parseInt(ev.target.getAttribute('model-id'));
|
||||
this._rpc( {
|
||||
model: model,
|
||||
return this._rpc( {
|
||||
model,
|
||||
args: [[modelId]],
|
||||
method: 'do_unreserve'
|
||||
}).then((result) => {
|
||||
return this._reloadReport();
|
||||
});
|
||||
}).then(() => this._reloadReport());
|
||||
},
|
||||
|
||||
/**
|
||||
* Reserve the specified model/id, then reload this report.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onClickReserve: function(ev) {
|
||||
const model = ev.target.getAttribute('model');
|
||||
const modelId = parseInt(ev.target.getAttribute('model-id'));
|
||||
return this._rpc( {
|
||||
model,
|
||||
args: [[modelId]],
|
||||
method: 'action_assign'
|
||||
}).then(() => this._reloadReport());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -3138,6 +3138,7 @@ class TestAutoAssign(TestStockCommon):
|
||||
4. Check that only the correct outgoing pickings are auto_assigned
|
||||
5. Additionally check that auto-assignment at confirmation correctly works when products are in stock
|
||||
Note, default reservation method is expected to be covered by other tests.
|
||||
Also check reservation_dates are as expected
|
||||
"""
|
||||
|
||||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||||
@@ -3168,7 +3169,7 @@ class TestAutoAssign(TestStockCommon):
|
||||
# 'by_date' picking w/ 10 days before scheduled date auto-assign setting, set to 5 days in advance => should auto-assign
|
||||
customer_picking3 = customer_picking2.copy({'name': "Delivery 3", 'picking_type_id': picking_type_out3.id})
|
||||
customer_picking4 = customer_picking3.copy({'name': "Delivery 4", 'picking_type_id': picking_type_out3.id})
|
||||
|
||||
# 'at_confirm' picking
|
||||
customer_picking5 = customer_picking1.copy({'name': "Delivery 5", 'picking_type_id': picking_type_out4.id})
|
||||
|
||||
# create their associated moves (needs to be in form view so compute functions properly trigger)
|
||||
@@ -3209,6 +3210,11 @@ class TestAutoAssign(TestStockCommon):
|
||||
self.assertEqual(customer_picking2.move_lines.reserved_availability, 0, "There should be no products available to reserve yet.")
|
||||
self.assertEqual(customer_picking3.move_lines.reserved_availability, 0, "There should be no products available to reserve yet.")
|
||||
|
||||
self.assertFalse(customer_picking1.move_lines.reservation_date, "Reservation Method: 'manual' shouldn't have a reservation_date")
|
||||
self.assertEqual(customer_picking2.move_lines.reservation_date, (customer_picking2.scheduled_date - timedelta(days=1)).date(),
|
||||
"Reservation Method: 'by_date' should have a reservation_date = scheduled_date - reservation_days_before")
|
||||
self.assertFalse(customer_picking5.move_lines.reservation_date, "Reservation Method: 'at_confirm' shouldn't have a reservation_date until confirmed")
|
||||
|
||||
# create supplier picking and move
|
||||
supplier_picking = self.env['stock.picking'].create({
|
||||
'location_id': self.customer_location,
|
||||
@@ -3232,10 +3238,10 @@ class TestAutoAssign(TestStockCommon):
|
||||
self.assertEqual(customer_picking3.move_lines.reserved_availability, 10, "Reservation Method: 'by_date' should auto-assign when within reservation date range")
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.productA, stock_location), 40)
|
||||
|
||||
customer_picking4.action_assign()
|
||||
customer_picking5.action_assign()
|
||||
self.assertEqual(customer_picking4.move_lines.reserved_availability, 10, "Reservation Method: 'at_confirm' should auto-assign at confirmation")
|
||||
self.assertEqual(customer_picking5.move_lines.reserved_availability, 10, "Reservation Method: 'by_date' should auto-assign when within reservation date range at confirmation")
|
||||
customer_picking4.action_confirm()
|
||||
customer_picking5.action_confirm()
|
||||
self.assertEqual(customer_picking4.move_lines.reserved_availability, 10, "Reservation Method: 'by_date' should auto-assign when within reservation date range at confirmation")
|
||||
self.assertEqual(customer_picking5.move_lines.reserved_availability, 10, "Reservation Method: 'at_confirm' should auto-assign at confirmation")
|
||||
|
||||
def test_serial_lot_ids(self):
|
||||
self.stock_location = self.env.ref('stock.stock_location_stock')
|
||||
|
||||
@@ -44,10 +44,11 @@
|
||||
<field name="sequence_code"/>
|
||||
<field name="warehouse_id" groups="stock.group_stock_multi_warehouses" force_save="1"/>
|
||||
<field name="print_label" attrs="{'invisible': [('code', '!=', 'internal')]}"/>
|
||||
<label for="reservation_method" attrs="{'invisible': [('code', '=', 'incoming')]}"/>
|
||||
<div class="o_row" attrs="{'invisible': [('code', '=', 'incoming')]}">
|
||||
<field name="reservation_method" widget="radio"/>
|
||||
<span attrs='{"invisible": [("reservation_method", "!=", "by_date")]}' class="oe_left"><field name="reservation_days_before" style="width: 30px; padding-top: 0px"/> days</span>
|
||||
<field name="reservation_method" attrs="{'invisible': [('code', '=', 'incoming')]}" widget="radio"/>
|
||||
<label for="reservation_days_before" string="Reserve before scheduled date" attrs="{'invisible': ['|', ('code', '=', 'incoming'), ('reservation_method', '!=', 'by_date')]}"/>
|
||||
<div class="o_row" attrs="{'invisible': ['|', ('code', '=', 'incoming'), ('reservation_method', '!=', 'by_date')]}">
|
||||
<span><field name="reservation_days_before" style="width: 23px;"/> days before/</span>
|
||||
<span><field name="reservation_days_before_priority" style="width: 23px;"/> days before when starred</span>
|
||||
</div>
|
||||
</group>
|
||||
<group>
|
||||
|
||||
Reference in New Issue
Block a user