[IMP] product_expiry: expired wizard confirmation
When user tries to validate a picking with expired production lot, displays a wizard for the confirmation popup. This wizard shows which lots are expired. Task #1938656
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
||||
@@ -21,6 +21,7 @@ Also implements the removal strategy First Expiry First Out (FEFO) widely used,
|
||||
'views/product_template_views.xml',
|
||||
'views/stock_move_views.xml',
|
||||
'views/stock_quant_views.xml',
|
||||
'wizard/confirm_expiry_view.xml',
|
||||
'report/report_lot_barcode.xml',
|
||||
'data/product_expiry_data.xml'],
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ from . import production_lot
|
||||
from . import product_product
|
||||
from . import stock_move_line
|
||||
from . import stock_move
|
||||
from . import stock_picking
|
||||
from . import stock_quant
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, _
|
||||
|
||||
|
||||
class StockPicking(models.Model):
|
||||
_inherit = "stock.picking"
|
||||
|
||||
def _pre_action_done_hook(self):
|
||||
res = super()._pre_action_done_hook()
|
||||
# We use the 'skip_expired' context key to avoid to make the check when
|
||||
# user did already confirmed the wizard about expired lots.
|
||||
if res is True and not self.env.context.get('skip_expired'):
|
||||
pickings_to_warn_expired = self._check_expired_lots()
|
||||
if pickings_to_warn_expired:
|
||||
return pickings_to_warn_expired._action_generate_expired_wizard()
|
||||
return res
|
||||
|
||||
def _check_expired_lots(self):
|
||||
expired_pickings = self.move_line_ids.filtered(lambda ml: ml.lot_id.product_expiry_alert).picking_id
|
||||
return expired_pickings
|
||||
|
||||
def _action_generate_expired_wizard(self):
|
||||
expired_lot_ids = self.move_line_ids.filtered(lambda ml: ml.lot_id.product_expiry_alert).lot_id.ids
|
||||
context = dict(self.env.context)
|
||||
context.update({
|
||||
'default_picking_ids': [(6, 0, self.ids)],
|
||||
'default_lot_ids': [(6, 0, expired_lot_ids)],
|
||||
})
|
||||
return {
|
||||
'name': _('Confirmation'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'expiry.picking.confirmation',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': context,
|
||||
}
|
||||
@@ -313,3 +313,109 @@ class TestStockProductionLot(TestStockCommon):
|
||||
apple_lot.removal_date, expiration_date - timedelta(days=2), delta=time_gap)
|
||||
self.assertAlmostEqual(
|
||||
apple_lot.alert_date, expiration_date - timedelta(days=6), delta=time_gap)
|
||||
|
||||
def test_05_confirmation_on_delivery(self):
|
||||
""" Test when user tries to delivery expired lot, he/she gets a
|
||||
confirmation wizard. """
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': 'Cider & Son',
|
||||
'company_id': self.env.ref('base.main_company').id,
|
||||
})
|
||||
# Creates 3 lots (1 non-expired lot, 2 expired lots)
|
||||
lot_form = Form(self.LotObj) # Creates the lot.
|
||||
lot_form.name = 'good-apple-lot'
|
||||
lot_form.product_id = self.apple_product
|
||||
lot_form.company_id = self.env.company
|
||||
good_lot = lot_form.save()
|
||||
|
||||
lot_form = Form(self.LotObj) # Creates the lot.
|
||||
lot_form.name = 'expired-apple-lot-01'
|
||||
lot_form.product_id = self.apple_product
|
||||
lot_form.company_id = self.env.company
|
||||
expired_lot_1 = lot_form.save()
|
||||
lot_form = Form(expired_lot_1) # Edits the lot to make it expired.
|
||||
lot_form.expiration_date = datetime.today() - timedelta(days=10)
|
||||
expired_lot_1 = lot_form.save()
|
||||
|
||||
# Case #1: make a delivery with no expired lot.
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.partner_id = partner
|
||||
picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = self.apple_product
|
||||
move.product_uom_qty = 4
|
||||
# Saves and confirms it...
|
||||
delivery_1 = picking_form.save()
|
||||
delivery_1.action_confirm()
|
||||
# ... then create a move line with the non-expired lot and valids the picking.
|
||||
delivery_1.move_line_ids_without_package = [(5, 0), (0, 0, {
|
||||
'company_id': self.env.company.id,
|
||||
'location_id': delivery_1.move_lines.location_id.id,
|
||||
'location_dest_id': delivery_1.move_lines.location_dest_id.id,
|
||||
'lot_id': good_lot.id,
|
||||
'product_id': self.apple_product.id,
|
||||
'product_uom_id': self.apple_product.uom_id.id,
|
||||
'qty_done': 4,
|
||||
})]
|
||||
res = delivery_1.button_validate()
|
||||
# Validate a delivery for good products must not raise anything.
|
||||
self.assertEqual(res, True)
|
||||
|
||||
# Case #2: make a delivery with one non-expired lot and one expired lot.
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.partner_id = partner
|
||||
picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = self.apple_product
|
||||
move.product_uom_qty = 8
|
||||
# Saves and confirms it...
|
||||
delivery_2 = picking_form.save()
|
||||
delivery_2.action_confirm()
|
||||
# ... then create a move line for the non-expired lot and for an expired
|
||||
# lot and valids the picking.
|
||||
delivery_2.move_line_ids_without_package = [(5, 0), (0, 0, {
|
||||
'company_id': self.env.company.id,
|
||||
'location_id': delivery_2.move_lines.location_id.id,
|
||||
'location_dest_id': delivery_2.move_lines.location_dest_id.id,
|
||||
'lot_id': good_lot.id,
|
||||
'product_id': self.apple_product.id,
|
||||
'product_uom_id': self.apple_product.uom_id.id,
|
||||
'qty_done': 4,
|
||||
}), (0, 0, {
|
||||
'company_id': self.env.company.id,
|
||||
'location_id': delivery_2.move_lines.location_id.id,
|
||||
'location_dest_id': delivery_2.move_lines.location_dest_id.id,
|
||||
'lot_id': expired_lot_1.id,
|
||||
'product_id': self.apple_product.id,
|
||||
'product_uom_id': self.apple_product.uom_id.id,
|
||||
'qty_done': 4,
|
||||
})]
|
||||
res = delivery_2.button_validate()
|
||||
# Validate a delivery containing expired products must raise a confirmation wizard.
|
||||
self.assertNotEqual(res, True)
|
||||
self.assertEqual(res['res_model'], 'expiry.picking.confirmation')
|
||||
|
||||
# Case #3: make a delivery with only on expired lot.
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.partner_id = partner
|
||||
picking_form.picking_type_id = self.env.ref('stock.picking_type_out')
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = self.apple_product
|
||||
move.product_uom_qty = 4
|
||||
# Saves and confirms it...
|
||||
delivery_3 = picking_form.save()
|
||||
delivery_3.action_confirm()
|
||||
# ... then create two move lines with expired lot and valids the picking.
|
||||
delivery_3.move_line_ids_without_package = [(5, 0), (0, 0, {
|
||||
'company_id': self.env.company.id,
|
||||
'location_id': delivery_3.move_lines.location_id.id,
|
||||
'location_dest_id': delivery_3.move_lines.location_dest_id.id,
|
||||
'lot_id': expired_lot_1.id,
|
||||
'product_id': self.apple_product.id,
|
||||
'product_uom_id': self.apple_product.uom_id.id,
|
||||
'qty_done': 4,
|
||||
})]
|
||||
res = delivery_3.button_validate()
|
||||
# Validate a delivery containing expired products must raise a confirmation wizard.
|
||||
self.assertNotEqual(res, True)
|
||||
self.assertEqual(res['res_model'], 'expiry.picking.confirmation')
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import confirm_expiry
|
||||
@@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class ConfirmExpiry(models.TransientModel):
|
||||
_name = 'expiry.picking.confirmation'
|
||||
_description = 'Confirm Expiry'
|
||||
|
||||
lot_ids = fields.Many2many('stock.production.lot', readonly=True, required=True)
|
||||
picking_ids = fields.Many2many('stock.picking', readonly=True)
|
||||
description = fields.Char('Description', compute='_compute_descriptive_fields')
|
||||
show_lots = fields.Boolean('Show Lots', compute='_compute_descriptive_fields')
|
||||
|
||||
@api.depends('lot_ids')
|
||||
def _compute_descriptive_fields(self):
|
||||
# Shows expired lots only if we are more than one expired lot.
|
||||
self.show_lots = len(self.lot_ids) > 1
|
||||
if self.show_lots:
|
||||
# For multiple expired lots, they are listed in the wizard view.
|
||||
self.description = _(
|
||||
"You are going to deliver some product expired lots."
|
||||
"\nDo you confirm you want to proceed ?"
|
||||
)
|
||||
else:
|
||||
# For one expired lot, its name is written in the wizard message.
|
||||
self.description = _(
|
||||
"You are going to deliver the product %s, %s which is expired."
|
||||
"\nDo you confirm you want to proceed ?" % (
|
||||
self.lot_ids.product_id.display_name,
|
||||
self.lot_ids.name,
|
||||
)
|
||||
)
|
||||
|
||||
def process(self):
|
||||
picking_to_validate = self.env.context.get('button_validate_picking_ids')
|
||||
if picking_to_validate:
|
||||
picking_to_validate = self.env['stock.picking'].browse(picking_to_validate).with_context(skip_expired=True)
|
||||
return picking_to_validate.button_validate()
|
||||
return True
|
||||
|
||||
def process_no_expired(self):
|
||||
""" Don't process for concerned pickings (ones with expired lots), but
|
||||
process for all other pickings (in case of multi). """
|
||||
# Remove `self.pick_ids` from `button_validate_picking_ids` and call
|
||||
# `button_validate` with the subset (if any).
|
||||
pickings_to_validate = self.env['stock.picking'].browse(self.env.context.get('button_validate_picking_ids'))
|
||||
pickings_to_validate = pickings_to_validate - self.picking_ids
|
||||
if pickings_to_validate:
|
||||
return pickings_to_validate.with_context(skip_expired=True).button_validate()
|
||||
return True
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="confirm_expiry_view" model="ir.ui.view">
|
||||
<field name="name">Confirm</field>
|
||||
<field name="model">expiry.picking.confirmation</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Confirmation">
|
||||
<p>
|
||||
<field name="description"/>
|
||||
</p>
|
||||
<field name="show_lots" invisible="1"/>
|
||||
<field name="lot_ids" attrs="{'invisible': [('show_lots', '=', False)]}">
|
||||
<tree string="Expired Lot(s)">
|
||||
<field name="product_id"/>
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
</field>
|
||||
<footer>
|
||||
<button name="process"
|
||||
string="Confirm"
|
||||
type="object"
|
||||
class="btn-primary"/>
|
||||
<button name="process_no_expired"
|
||||
string="Proceed except for expired one"
|
||||
type="object"
|
||||
class="btn-secondary"/>
|
||||
<button string="Discard"
|
||||
class="btn-secondary"
|
||||
special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user