diff --git a/addons/product_expiry/data/product_expiry_data.xml b/addons/product_expiry/data/product_expiry_data.xml index d9c2831bfa0..ca621cef17e 100644 --- a/addons/product_expiry/data/product_expiry_data.xml +++ b/addons/product_expiry/data/product_expiry_data.xml @@ -4,5 +4,12 @@ First Expiry First Out (FEFO) fefo + + Alert Date Reached + default + + fa-tasks + 0 + diff --git a/addons/product_expiry/models/production_lot.py b/addons/product_expiry/models/production_lot.py index 45e465ea146..5ddaf4a5f60 100644 --- a/addons/product_expiry/models/production_lot.py +++ b/addons/product_expiry/models/production_lot.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import datetime -from odoo import api, fields, models +from odoo import api, fields, models, _ class StockProductionLot(models.Model): @@ -55,3 +55,41 @@ class StockProductionLot(models.Model): dates_dict = self._get_dates() for field, value in dates_dict.items(): setattr(self, field, value) + + @api.model + def _alert_date_exceeded(self): + # if the alert_date is in the past and the lot is linked to an internal quant + # log a next activity on the next.production.lot + alert_lot_ids = self.env['stock.production.lot'].search([('alert_date', '<=', fields.Date.today())]) + mail_activity_type = self.env.ref('product_expiry.mail_activity_type_alert_date_reached').id + stock_quants = self.env['stock.quant'].search([ + ('lot_id', 'in', alert_lot_ids.ids), + ('quantity', '>', 0)]).filtered(lambda quant: quant.location_id.usage == 'internal' ) + lots = stock_quants.mapped('lot_id') + + # only for lots that do not have already an activity + # or that do not have a done alert activity, i.e. a mail.message + lots = lots.filtered(lambda lot: + not self.env['mail.activity'].search_count([ + ('res_model', '=', 'stock.production.lot'), + ('res_id', '=', lot.id), + ('activity_type_id','=', mail_activity_type)]) + and not self.env['mail.message'].search_count([ + ('model', '=', 'stock.production.lot'), + ('res_id', '=', lot.id), + ('subtype_id', '=', self.env.ref('mail.mt_activities').id), + ('mail_activity_type_id','=', mail_activity_type)])) + for lot in lots: + lot.activity_schedule('product_expiry.mail_activity_type_alert_date_reached', + user_id=lot.product_id.responsible_id.id, note=_("The alert date has been reached for this lot/serial number") + ) + +class ProcurementGroup(models.Model): + _inherit = 'procurement.group' + + @api.model + def _run_scheduler_tasks(self, use_new_cursor=False, company_id=False): + super(ProcurementGroup, self)._run_scheduler_tasks(use_new_cursor=use_new_cursor, company_id=company_id) + self.env['stock.production.lot']._alert_date_exceeded() + if use_new_cursor: + self.env.cr.commit() diff --git a/addons/product_expiry/tests/__init__.py b/addons/product_expiry/tests/__init__.py new file mode 100644 index 00000000000..858d9530d50 --- /dev/null +++ b/addons/product_expiry/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_stock_production_lot diff --git a/addons/product_expiry/tests/test_stock_production_lot.py b/addons/product_expiry/tests/test_stock_production_lot.py new file mode 100644 index 00000000000..bc369f9fa7f --- /dev/null +++ b/addons/product_expiry/tests/test_stock_production_lot.py @@ -0,0 +1,213 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime +from dateutil.relativedelta import relativedelta +from odoo import fields +from odoo.addons.stock.tests.common import TestStockCommon +from odoo.exceptions import UserError + + +class TestStockProductionLot(TestStockCommon): + + def test_00_stock_production_lot(self): + """ Test Scheduled Task on lot with an alert_date in the past creates an activity """ + + # create product + self.productAAA = self.ProductObj.create({ + 'name': 'Product AAA', + 'type': 'product', + 'tracking':'lot' + }) + + # create a new lot with with alert date in the past + self.lot1_productAAA = self.LotObj.create({ + 'name': 'Lot 1 ProductAAA', + 'product_id': self.productAAA.id, + 'alert_date': fields.Date.to_string(datetime.today() - relativedelta(days=15)) + }) + + picking_in = self.PickingObj.create({ + 'partner_id': self.partner_delta_id, + 'picking_type_id': self.picking_type_in, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location + }) + + move_a = self.MoveObj.create({ + 'name': self.productAAA.name, + 'product_id': self.productAAA.id, + 'product_uom_qty': 33, + 'product_uom': self.productAAA.uom_id.id, + 'picking_id': picking_in.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location + }) + + self.assertEqual(picking_in.move_lines.state, 'draft', 'Wrong state of move line.') + picking_in.action_confirm() + self.assertEqual(picking_in.move_lines.state, 'assigned', 'Wrong state of move line.') + + # Replace pack operation of incoming shipments. + picking_in.action_assign() + move_a.move_line_ids.qty_done = 33 + move_a.move_line_ids.lot_id = self.lot1_productAAA.id + + # Transfer Incoming Shipment. + picking_in.action_done() + + # run scheduled tasks + self.env['stock.production.lot']._alert_date_exceeded() + + # check a new activity has been created + activity_id = self.env.ref('product_expiry.mail_activity_type_alert_date_reached').id + activity_count = self.env['mail.activity'].search_count([ + ('activity_type_id', '=', activity_id), + ('res_model_id', '=', self.env.ref('stock.model_stock_production_lot').id), + ('res_id', '=', self.lot1_productAAA.id) + ]) + self.assertEqual(activity_count, 1, 'No activity created while there should be one') + + # run the scheduler a second time + self.env['stock.production.lot']._alert_date_exceeded() + + # check there is still only one activity, no additional activity is created if there is already an existing activity + activity_count = self.env['mail.activity'].search_count([ + ('activity_type_id', '=', activity_id), + ('res_model_id', '=', self.env.ref('stock.model_stock_production_lot').id), + ('res_id', '=', self.lot1_productAAA.id) + ]) + self.assertEqual(activity_count, 1, 'There should be one and only one activity') + + # mark the activity as done + mail_activity = self.env['mail.activity'].search([ + ('activity_type_id', '=', activity_id), + ('res_model_id', '=', self.env.ref('stock.model_stock_production_lot').id), + ('res_id', '=', self.lot1_productAAA.id) + ]) + mail_activity.action_done() + + # check there is no more activity (because it is already done) + activity_count = self.env['mail.activity'].search_count([ + ('activity_type_id', '=', activity_id), + ('res_model_id', '=', self.env.ref('stock.model_stock_production_lot').id), + ('res_id', '=', self.lot1_productAAA.id) + ]) + self.assertEqual(activity_count, 0,"As activity is done, there shouldn't be any related activity") + + # run the scheduler a third time + self.env['stock.production.lot']._alert_date_exceeded() + + # check there is no activity created + activity_count = self.env['mail.activity'].search_count([ + ('activity_type_id', '=', activity_id), + ('res_model_id', '=', self.env.ref('stock.model_stock_production_lot').id), + ('res_id', '=',self.lot1_productAAA.id) + ]) + self.assertEqual(activity_count, 0, "As there is already an activity marked as done, there shouldn't be any related activity created for this lot") + + + def test_01_stock_production_lot(self): + """ Test Scheduled Task on lot with an alert_date in future does not create an activity """ + + # create product + self.productBBB = self.ProductObj.create({ + 'name': 'Product BBB', + 'type': 'product', + 'tracking':'lot' + }) + + # create a new lot with with alert date in the past + self.lot1_productBBB = self.LotObj.create({ + 'name': 'Lot 1 ProductBBB', + 'product_id': self.productBBB.id, + 'alert_date': fields.Date.to_string(datetime.today() + relativedelta(days=15)) + }) + + picking_in = self.PickingObj.create({ + 'partner_id': self.partner_delta_id, + 'picking_type_id': self.picking_type_in, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + + move_b = self.MoveObj.create({ + 'name': self.productBBB.name, + 'product_id': self.productBBB.id, + 'product_uom_qty': 44, + 'product_uom': self.productBBB.uom_id.id, + 'picking_id': picking_in.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + + self.assertEqual(picking_in.move_lines.state, 'draft', 'Wrong state of move line.') + picking_in.action_confirm() + self.assertEqual(picking_in.move_lines.state, 'assigned', 'Wrong state of move line.') + + # Replace pack operation of incoming shipments. + picking_in.action_assign() + move_b.move_line_ids.qty_done = 44 + move_b.move_line_ids.lot_id = self.lot1_productBBB.id + + # Transfer Incoming Shipment. + picking_in.action_done() + + # run scheduled tasks + self.env['stock.production.lot']._alert_date_exceeded() + + # check a new activity has not been created + activity_id = self.env.ref('product_expiry.mail_activity_type_alert_date_reached').id + activity_count = self.env['mail.activity'].search_count([ + ('activity_type_id', '=', activity_id), + ('res_model_id', '=', self.env.ref('stock.model_stock_production_lot').id), + ('res_id', '=', self.lot1_productBBB.id) + ]) + self.assertEqual(activity_count, 0, "An activity has been created while it shouldn't") + + + def test_02_stock_production_lot(self): + """ Test Scheduled Task on lot without an alert_date does not create an activity """ + + # create product + self.productCCC = self.ProductObj.create({'name': 'Product CCC', 'type': 'product', 'tracking':'lot'}) + + # create a new lot with with alert date in the past + self.lot1_productCCC = self.LotObj.create({'name': 'Lot 1 ProductCCC', 'product_id': self.productCCC.id}) + + picking_in = self.PickingObj.create({ + 'partner_id': self.partner_delta_id, + 'picking_type_id': self.picking_type_in, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + + move_c = self.MoveObj.create({ + 'name': self.productCCC.name, + 'product_id': self.productCCC.id, + 'product_uom_qty': 44, + 'product_uom': self.productCCC.uom_id.id, + 'picking_id': picking_in.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + + self.assertEqual(picking_in.move_lines.state, 'draft', 'Wrong state of move line.') + picking_in.action_confirm() + self.assertEqual(picking_in.move_lines.state, 'assigned', 'Wrong state of move line.') + + # Replace pack operation of incoming shipments. + picking_in.action_assign() + move_c.move_line_ids.qty_done = 55 + move_c.move_line_ids.lot_id = self.lot1_productCCC.id + + # Transfer Incoming Shipment. + picking_in.action_done() + + # run scheduled tasks + self.env['stock.production.lot']._alert_date_exceeded() + + # check a new activity has not been created + activity_id = self.env.ref('product_expiry.mail_activity_type_alert_date_reached').id + activity_count = self.env['mail.activity'].search_count([ + ('activity_type_id', '=', activity_id), + ('res_model_id', '=', self.env.ref('stock.model_stock_production_lot').id), + ('res_id', '=', self.lot1_productCCC.id) + ]) + self.assertEqual(activity_count, 0, "An activity has been created while it shouldn't") diff --git a/addons/stock/models/stock_production_lot.py b/addons/stock/models/stock_production_lot.py index c34451a73ee..dac5d276a9f 100644 --- a/addons/stock/models/stock_production_lot.py +++ b/addons/stock/models/stock_production_lot.py @@ -7,7 +7,7 @@ from odoo.exceptions import UserError class ProductionLot(models.Model): _name = 'stock.production.lot' - _inherit = ['mail.thread'] + _inherit = ['mail.thread','mail.activity.mixin'] _description = 'Lot/Serial' name = fields.Char( diff --git a/addons/stock/views/stock_production_lot_views.xml b/addons/stock/views/stock_production_lot_views.xml index 9c5a5f64d8c..239db0ed1b5 100644 --- a/addons/stock/views/stock_production_lot_views.xml +++ b/addons/stock/views/stock_production_lot_views.xml @@ -37,6 +37,7 @@
+
@@ -61,6 +62,11 @@ +
+ + + +