[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:
Yannick Tivisse
2018-07-23 15:58:12 +02:00
12 changed files with 385 additions and 2 deletions
@@ -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>
+39 -1
View File
@@ -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()
+4
View File
@@ -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")
+1
View File
@@ -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',
],
+16
View File
@@ -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>
+1
View File
@@ -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',
+15
View File
@@ -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>
+1 -1
View File
@@ -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>