summaryrefslogtreecommitdiff
path: root/addons/product_expiry/models
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/product_expiry/models
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/product_expiry/models')
-rw-r--r--addons/product_expiry/models/__init__.py10
-rw-r--r--addons/product_expiry/models/product_product.py35
-rw-r--r--addons/product_expiry/models/production_lot.py134
-rw-r--r--addons/product_expiry/models/res_config_settings.py21
-rw-r--r--addons/product_expiry/models/stock_move.py22
-rw-r--r--addons/product_expiry/models/stock_move_line.py37
-rw-r--r--addons/product_expiry/models/stock_picking.py38
-rw-r--r--addons/product_expiry/models/stock_quant.py33
8 files changed, 330 insertions, 0 deletions
diff --git a/addons/product_expiry/models/__init__.py b/addons/product_expiry/models/__init__.py
new file mode 100644
index 00000000..55fcdd6e
--- /dev/null
+++ b/addons/product_expiry/models/__init__.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import production_lot
+from . import product_product
+from . import res_config_settings
+from . import stock_move_line
+from . import stock_move
+from . import stock_picking
+from . import stock_quant
diff --git a/addons/product_expiry/models/product_product.py b/addons/product_expiry/models/product_product.py
new file mode 100644
index 00000000..9f70ba56
--- /dev/null
+++ b/addons/product_expiry/models/product_product.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class Product(models.Model):
+ _inherit = "product.product"
+
+ def action_open_quants(self):
+ # Override to hide the `removal_date` column if not needed.
+ if not any(product.use_expiration_date for product in self):
+ self = self.with_context(hide_removal_date=True)
+ return super().action_open_quants()
+
+
+class ProductTemplate(models.Model):
+ _inherit = 'product.template'
+
+ use_expiration_date = fields.Boolean(string='Expiration Date',
+ help='When this box is ticked, you have the possibility to specify dates to manage'
+ ' product expiration, on the product and on the corresponding lot/serial numbers')
+ expiration_time = fields.Integer(string='Expiration Time',
+ help='Number of days after the receipt of the products (from the vendor'
+ ' or in stock after production) after which the goods may become dangerous'
+ ' and must not be consumed. It will be computed on the lot/serial number.')
+ use_time = fields.Integer(string='Best Before Time',
+ help='Number of days before the Expiration Date after which the goods starts'
+ ' deteriorating, without being dangerous yet. It will be computed on the lot/serial number.')
+ removal_time = fields.Integer(string='Removal Time',
+ help='Number of days before the Expiration Date after which the goods'
+ ' should be removed from the stock. It will be computed on the lot/serial number.')
+ alert_time = fields.Integer(string='Alert Time',
+ help='Number of days before the Expiration Date after which an alert should be'
+ ' raised on the lot/serial number. It will be computed on the lot/serial number.')
diff --git a/addons/product_expiry/models/production_lot.py b/addons/product_expiry/models/production_lot.py
new file mode 100644
index 00000000..c84d6171
--- /dev/null
+++ b/addons/product_expiry/models/production_lot.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import datetime
+from odoo import api, fields, models, SUPERUSER_ID, _
+
+
+class StockProductionLot(models.Model):
+ _inherit = 'stock.production.lot'
+
+ use_expiration_date = fields.Boolean(
+ string='Use Expiration Date', related='product_id.use_expiration_date')
+ expiration_date = fields.Datetime(string='Expiration Date',
+ help='This is the date on which the goods with this Serial Number may become dangerous and must not be consumed.')
+ use_date = fields.Datetime(string='Best before Date',
+ help='This is the date on which the goods with this Serial Number start deteriorating, without being dangerous yet.')
+ removal_date = fields.Datetime(string='Removal Date',
+ help='This is the date on which the goods with this Serial Number should be removed from the stock. This date will be used in FEFO removal strategy.')
+ alert_date = fields.Datetime(string='Alert Date',
+ help='Date to determine the expired lots and serial numbers using the filter "Expiration Alerts".')
+ product_expiry_alert = fields.Boolean(compute='_compute_product_expiry_alert', help="The Expiration Date has been reached.")
+ product_expiry_reminded = fields.Boolean(string="Expiry has been reminded")
+
+ @api.depends('expiration_date')
+ def _compute_product_expiry_alert(self):
+ current_date = fields.Datetime.now()
+ for lot in self:
+ if lot.expiration_date:
+ lot.product_expiry_alert = lot.expiration_date <= current_date
+ else:
+ lot.product_expiry_alert = False
+
+ def _get_dates(self, product_id=None):
+ """Returns dates based on number of days configured in current lot's product."""
+ mapped_fields = {
+ 'expiration_date': 'expiration_time',
+ 'use_date': 'use_time',
+ 'removal_date': 'removal_time',
+ 'alert_date': 'alert_time'
+ }
+ res = dict.fromkeys(mapped_fields, False)
+ product = self.env['product.product'].browse(product_id) or self.product_id
+ if product:
+ for field in mapped_fields:
+ duration = getattr(product, mapped_fields[field])
+ if duration:
+ date = datetime.datetime.now() + datetime.timedelta(days=duration)
+ res[field] = fields.Datetime.to_string(date)
+ return res
+
+ # Assign dates according to products data
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ dates = self._get_dates(vals.get('product_id') or self.env.context.get('default_product_id'))
+ for d in dates:
+ if not vals.get(d):
+ vals[d] = dates[d]
+ return super().create(vals_list)
+
+ @api.onchange('expiration_date')
+ def _onchange_expiration_date(self):
+ if not self._origin or not (self.expiration_date and self._origin.expiration_date):
+ return
+ time_delta = self.expiration_date - self._origin.expiration_date
+ # As we compare expiration_date with _origin.expiration_date, we need to
+ # use `_get_date_values` with _origin to keep a stability in the values.
+ # Otherwise it will recompute from the updated values if the user calls
+ # this onchange multiple times without save between each onchange.
+ vals = self._origin._get_date_values(time_delta)
+ self.update(vals)
+
+ @api.onchange('product_id')
+ def _onchange_product(self):
+ dates_dict = self._get_dates()
+ for field, value in dates_dict.items():
+ setattr(self, field, value)
+
+ @api.model
+ def _alert_date_exceeded(self):
+ """Log an activity on internally stored lots whose alert_date has been reached.
+
+ No further activity will be generated on lots whose alert_date
+ has already been reached (even if the alert_date is changed).
+ """
+ alert_lots = self.env['stock.production.lot'].search([
+ ('alert_date', '<=', fields.Date.today()),
+ ('product_expiry_reminded', '=', False)])
+
+ lot_stock_quants = self.env['stock.quant'].search([
+ ('lot_id', 'in', alert_lots.ids),
+ ('quantity', '>', 0),
+ ('location_id.usage', '=', 'internal')])
+ alert_lots = lot_stock_quants.mapped('lot_id')
+
+ for lot in alert_lots:
+ lot.activity_schedule(
+ 'product_expiry.mail_activity_type_alert_date_reached',
+ user_id=lot.product_id.responsible_id.id or SUPERUSER_ID,
+ note=_("The alert date has been reached for this lot/serial number")
+ )
+ alert_lots.write({
+ 'product_expiry_reminded': True
+ })
+
+ def _update_date_values(self, new_date):
+ if new_date:
+ time_delta = new_date - (self.expiration_date or fields.Datetime.now())
+ vals = self._get_date_values(time_delta)
+ vals['expiration_date'] = new_date
+ self.write(vals)
+
+ def _get_date_values(self, time_delta):
+ ''' Return a dict with different date values updated depending of the
+ time_delta. Used in the onchange of `expiration_date` and when user
+ defines a date at the receipt. '''
+ vals = {}
+ if self.use_date:
+ vals['use_date'] = self.use_date + time_delta
+ if self.removal_date:
+ vals['removal_date'] = self.removal_date + time_delta
+ if self.alert_date:
+ vals['alert_date'] = self.alert_date + time_delta
+ return vals
+
+
+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/models/res_config_settings.py b/addons/product_expiry/models/res_config_settings.py
new file mode 100644
index 00000000..bb565c56
--- /dev/null
+++ b/addons/product_expiry/models/res_config_settings.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = 'res.config.settings'
+
+ group_expiry_date_on_delivery_slip = fields.Boolean("Display Expiration Dates on Delivery Slips",
+ implied_group='product_expiry.group_expiry_date_on_delivery_slip')
+
+ @api.onchange('group_lot_on_delivery_slip')
+ def _onchange_group_lot_on_delivery_slip(self):
+ if not self.group_lot_on_delivery_slip:
+ self.group_expiry_date_on_delivery_slip = False
+
+ @api.onchange('module_product_expiry')
+ def _onchange_module_product_expiry(self):
+ if not self.module_product_expiry:
+ self.group_expiry_date_on_delivery_slip = False
diff --git a/addons/product_expiry/models/stock_move.py b/addons/product_expiry/models/stock_move.py
new file mode 100644
index 00000000..d0fc75be
--- /dev/null
+++ b/addons/product_expiry/models/stock_move.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import datetime
+
+from odoo import fields, models
+
+
+class StockMove(models.Model):
+ _inherit = "stock.move"
+ use_expiration_date = fields.Boolean(
+ string='Use Expiration Date', related='product_id.use_expiration_date')
+
+ def _generate_serial_move_line_commands(self, lot_names, origin_move_line=None):
+ """Override to add a default `expiration_date` into the move lines values."""
+ move_lines_commands = super()._generate_serial_move_line_commands(lot_names, origin_move_line=origin_move_line)
+ if self.product_id.use_expiration_date:
+ date = fields.Datetime.today() + datetime.timedelta(days=self.product_id.expiration_time)
+ for move_line_command in move_lines_commands:
+ move_line_vals = move_line_command[2]
+ move_line_vals['expiration_date'] = date
+ return move_lines_commands
diff --git a/addons/product_expiry/models/stock_move_line.py b/addons/product_expiry/models/stock_move_line.py
new file mode 100644
index 00000000..b71993d8
--- /dev/null
+++ b/addons/product_expiry/models/stock_move_line.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import datetime
+
+from odoo import api, fields, models
+
+
+class StockMoveLine(models.Model):
+ _inherit = "stock.move.line"
+
+ expiration_date = fields.Datetime(string='Expiration Date',
+ help='This is the date on which the goods with this Serial Number may'
+ ' become dangerous and must not be consumed.')
+
+ @api.onchange('product_id', 'product_uom_id')
+ def _onchange_product_id(self):
+ res = super(StockMoveLine, self)._onchange_product_id()
+ if self.picking_type_use_create_lots:
+ if self.product_id.use_expiration_date:
+ self.expiration_date = fields.Datetime.today() + datetime.timedelta(days=self.product_id.expiration_time)
+ else:
+ self.expiration_date = False
+ return res
+
+ @api.onchange('lot_id')
+ def _onchange_lot_id(self):
+ if not self.picking_type_use_existing_lots or not self.product_id.use_expiration_date:
+ return
+ if self.lot_id:
+ self.expiration_date = self.lot_id.expiration_date
+ else:
+ self.expiration_date = False
+
+ def _assign_production_lot(self, lot):
+ super()._assign_production_lot(lot)
+ self.lot_id._update_date_values(self.expiration_date)
diff --git a/addons/product_expiry/models/stock_picking.py b/addons/product_expiry/models/stock_picking.py
new file mode 100644
index 00000000..35d275ba
--- /dev/null
+++ b/addons/product_expiry/models/stock_picking.py
@@ -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,
+ }
diff --git a/addons/product_expiry/models/stock_quant.py b/addons/product_expiry/models/stock_quant.py
new file mode 100644
index 00000000..34e31b54
--- /dev/null
+++ b/addons/product_expiry/models/stock_quant.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class StockQuant(models.Model):
+ _inherit = 'stock.quant'
+
+ removal_date = fields.Datetime(related='lot_id.removal_date', store=True, readonly=False)
+ use_expiration_date = fields.Boolean(related='product_id.use_expiration_date', readonly=True)
+
+ @api.model
+ def _get_inventory_fields_create(self):
+ """ Returns a list of fields user can edit when he want to create a quant in `inventory_mode`.
+ """
+ res = super()._get_inventory_fields_create()
+ res += ['removal_date']
+ return res
+
+ @api.model
+ def _get_inventory_fields_write(self):
+ """ Returns a list of fields user can edit when he want to edit a quant in `inventory_mode`.
+ """
+ res = super()._get_inventory_fields_write()
+ res += ['removal_date']
+ return res
+
+ @api.model
+ def _get_removal_strategy_order(self, removal_strategy):
+ if removal_strategy == 'fefo':
+ return 'removal_date, in_date, id'
+ return super(StockQuant, self)._get_removal_strategy_order(removal_strategy)