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 @@
+
+
+
+
+