[IMP] stock, product_expiry: Improve the traceability of the products
PURPOSE ======= Improve the traceability of the products. 1. If you have an issue with a serial or lot number, you want to easily find from which supplier it's coming and to which customer it has been delivered, to contact them in case of problem. 2. We have the possibility to define expiration dates on products but the users are not informed when this date is reached. Now we can easily inform them with the next activities. SPECIFICATIONS ============== 1. Locate precisely a SN/LN (find to which customer it has been delivered and from which supplier it's coming from) Add fields "Purchase Orders" and "Sale Orders" (must be a many2many) and displays PO's and SO's related to this LN/SN. 2. Log a next activity for the product responsible when expiration date is reached for a product [OK] - Check the alert_date on the stock.production.lot, if the date is in the past or today, log a next activity on the stock.production.lot - Assigned to = the product responsible - Due date = today - Activity = Alert Date Reached - Text = The alert date has been reached for this lot/serial number. - Raise a next activity only for products that are still in stock = that are in a location of type internal. - This cron checking the alert dates should be added in the existing scheduler cron - The next activity is only create one time for the each lot. If a next activity has already be create for this lot previously, next activity will not be create again.
This commit is contained in:
@@ -4,5 +4,12 @@
|
||||
<field name="name">First Expiry First Out (FEFO)</field>
|
||||
<field name="method">fefo</field>
|
||||
</record>
|
||||
<record id="mail_activity_type_alert_date_reached" model="mail.activity.type">
|
||||
<field name="name">Alert Date Reached</field>
|
||||
<field name="category">default</field>
|
||||
<field name="res_model_id" ref="stock.model_stock_production_lot"/>
|
||||
<field name="icon">fa-tasks</field>
|
||||
<field name="days">0</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -18,6 +18,7 @@
|
||||
'views/stock_views.xml',
|
||||
'views/stock_rule_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/stock_production_lot_views.xml',
|
||||
'report/purchase_report_views.xml',
|
||||
'report/purchase_report_templates.xml',
|
||||
],
|
||||
|
||||
@@ -212,3 +212,19 @@ class Orderpoint(models.Model):
|
||||
result['domain'] = "[('id','in',%s)]" % (purchase_ids.ids)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class ProductionLot(models.Model):
|
||||
_inherit = 'stock.production.lot'
|
||||
|
||||
purchase_order_ids = fields.Many2many('purchase.order', string="Purchase Orders", compute='_compute_purchase_order_ids', readonly=True, store=False)
|
||||
|
||||
@api.depends('name')
|
||||
def _compute_purchase_order_ids(self):
|
||||
for lot in self:
|
||||
stock_moves = self.env['stock.move.line'].search([
|
||||
('lot_id', '=', lot.id),
|
||||
('state', '=', 'done')
|
||||
]).mapped('move_id').filtered(
|
||||
lambda move: move.picking_id.location_id.usage == 'supplier' and move.state == 'done')
|
||||
lot.purchase_order_ids = stock_moves.mapped('purchase_line_id.order_id')
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="stock_production_lot_view_form" model="ir.ui.view">
|
||||
<field name="name">stock.production.lot.view.form</field>
|
||||
<field name="model">stock.production.lot</field>
|
||||
<field name="inherit_id" ref="stock.view_production_lot_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='main_group']" position="after">
|
||||
<group>
|
||||
<field name="purchase_order_ids" widget="many2many" readonly="True">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="date_order"/>
|
||||
<field name="state" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="stock_production_lot_view_form_simple" model="ir.ui.view">
|
||||
<field name="name">stock.production.lot.view.form.simple</field>
|
||||
<field name="model">stock.production.lot</field>
|
||||
<field name="inherit_id" ref="stock.view_production_lot_form_simple"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='main_group']" position="after">
|
||||
<group>
|
||||
<field name="purchase_order_ids" widget="many2many" readonly="True">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="date_order"/>
|
||||
<field name="state" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -36,6 +36,7 @@ You can choose flexible invoicing methods:
|
||||
'views/sale_stock_portal_template.xml',
|
||||
'views/product_template_views.xml',
|
||||
'views/res_users_views.xml',
|
||||
'views/stock_production_lot_views.xml',
|
||||
'report/sale_order_report_templates.xml',
|
||||
'report/stock_report_deliveryslip.xml',
|
||||
'data/sale_stock_data.xml',
|
||||
|
||||
@@ -101,3 +101,18 @@ class StockPicking(models.Model):
|
||||
self._log_activity(_render_note_exception_quantity, documents)
|
||||
|
||||
return super(StockPicking, self)._log_less_quantities_than_expected(moves)
|
||||
|
||||
class ProductionLot(models.Model):
|
||||
_inherit = 'stock.production.lot'
|
||||
|
||||
sale_order_ids = fields.Many2many('sale.order', string="Sales Orders", compute='_compute_sale_order_ids')
|
||||
|
||||
@api.depends('name')
|
||||
def _compute_sale_order_ids(self):
|
||||
for lot in self:
|
||||
stock_moves = self.env['stock.move.line'].search([
|
||||
('lot_id', '=', lot.id),
|
||||
('state', '=', 'done')
|
||||
]).mapped('move_id').filtered(
|
||||
lambda move: move.picking_id.location_dest_id.usage == 'customer' and move.state == 'done')
|
||||
lot.sale_order_ids = stock_moves.mapped('sale_line_id.order_id')
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="stock_production_lot_view_form" model="ir.ui.view">
|
||||
<field name="name">stock.production.lot.view.form</field>
|
||||
<field name="model">stock.production.lot</field>
|
||||
<field name="inherit_id" ref="stock.view_production_lot_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='main_group']" position="after">
|
||||
<group>
|
||||
<field name="sale_order_ids" widget="many2many" readonly="True">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="confirmation_date"/>
|
||||
<field name="state" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="stock_production_lot_view_form_simple" model="ir.ui.view">
|
||||
<field name="name">stock.production.lot.view.form.simple</field>
|
||||
<field name="model">stock.production.lot</field>
|
||||
<field name="inherit_id" ref="stock.view_production_lot_form_simple"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='main_group']" position="after">
|
||||
<group>
|
||||
<field name="sale_order_ids" widget="many2many" readonly="True">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="confirmation_date"/>
|
||||
<field name="state" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -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(
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers"/>
|
||||
<field name="activity_ids" widget="mail_activity"/>
|
||||
<field name="message_ids" widget="mail_thread"/>
|
||||
</div>
|
||||
</form>
|
||||
@@ -61,6 +62,11 @@
|
||||
<field name="ref"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers"/>
|
||||
<field name="activity_ids" widget="mail_activity"/>
|
||||
<field name="message_ids" widget="mail_thread"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
Reference in New Issue
Block a user