[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:
Tiffany Chang (tic)
2021-03-29 06:54:30 +00:00
parent c0269814f6
commit 0d61d3fccb
8 changed files with 72 additions and 31 deletions
@@ -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"
+14 -9
View File
@@ -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()
+2 -2
View File
@@ -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')
+3 -4
View File
@@ -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());
}
});
+11 -5
View File
@@ -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')
+5 -4
View File
@@ -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>