diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/product_expiry/models | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/product_expiry/models')
| -rw-r--r-- | addons/product_expiry/models/__init__.py | 10 | ||||
| -rw-r--r-- | addons/product_expiry/models/product_product.py | 35 | ||||
| -rw-r--r-- | addons/product_expiry/models/production_lot.py | 134 | ||||
| -rw-r--r-- | addons/product_expiry/models/res_config_settings.py | 21 | ||||
| -rw-r--r-- | addons/product_expiry/models/stock_move.py | 22 | ||||
| -rw-r--r-- | addons/product_expiry/models/stock_move_line.py | 37 | ||||
| -rw-r--r-- | addons/product_expiry/models/stock_picking.py | 38 | ||||
| -rw-r--r-- | addons/product_expiry/models/stock_quant.py | 33 |
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) |
