mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-27 10:12:06 +02:00
19.0 vanilla
This commit is contained in:
parent
79f83631d5
commit
73afc09215
6267 changed files with 1534193 additions and 1130106 deletions
|
|
@ -8,3 +8,4 @@ from . import stock_move_line
|
|||
from . import stock_move
|
||||
from . import stock_picking
|
||||
from . import stock_quant
|
||||
from . import stock_rule
|
||||
|
|
|
|||
|
|
@ -1,17 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import datetime
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
class ProductProduct(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()
|
||||
def _compute_quantities_dict(self, lot_id, owner_id, package_id, from_date=False, to_date=False):
|
||||
return super(ProductProduct, self.with_context(with_expiration=datetime.date.today()))._compute_quantities_dict(lot_id, owner_id, package_id, from_date, to_date)
|
||||
|
||||
free_qty = fields.Float(help="Available quantity (computed as Quantity On Hand "
|
||||
"- reserved quantity - quantity to remove)\n"
|
||||
"In a context with a single Stock Location, this includes "
|
||||
"goods stored in this location, or any of its children.\n"
|
||||
"In a context with a single Warehouse, this includes "
|
||||
"goods stored in the Stock Location of this Warehouse, or any "
|
||||
"of its children.\n"
|
||||
"Otherwise, this includes goods stored in any Stock Location "
|
||||
"with 'internal' type.")
|
||||
|
||||
virtual_available = fields.Float(help="Forecast quantity (computed as Quantity On Hand "
|
||||
"- Outgoing + Incoming - Quantity to Remove)\n"
|
||||
"In a context with a single Stock Location, this includes "
|
||||
"goods stored in this location, or any of its children.\n"
|
||||
"In a context with a single Warehouse, this includes "
|
||||
"goods stored in the Stock Location of this Warehouse, or any "
|
||||
"of its children.\n"
|
||||
"Otherwise, this includes goods stored in any Stock Location "
|
||||
"with 'internal' type.")
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
|
|
@ -29,7 +47,8 @@ class ProductTemplate(models.Model):
|
|||
' deteriorating, without being dangerous yet. It will be computed on the lot/serial number.')
|
||||
removal_time = fields.Integer(string='Removal Date',
|
||||
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.')
|
||||
' should be removed from the stock and not be counted in the Fresh On Hand Stock anymore.'
|
||||
'It will be computed on the lot/serial number.')
|
||||
alert_time = fields.Integer(string='Alert Date',
|
||||
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.')
|
||||
|
|
|
|||
|
|
@ -15,12 +15,29 @@ class StockLot(models.Model):
|
|||
use_date = fields.Datetime(string='Best before Date', compute='_compute_dates', store=True, readonly=False,
|
||||
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', compute='_compute_dates', store=True, readonly=False,
|
||||
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.')
|
||||
help='This is the date on which the goods with this Serial Number should be removed from the stock and not be counted in the Fresh On Hand Stock anymore. This date will be used in FEFO removal strategy.')
|
||||
alert_date = fields.Datetime(string='Alert Date', compute='_compute_dates', store=True, readonly=False,
|
||||
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('use_expiration_date', 'expiration_date', 'alert_date')
|
||||
@api.depends_context('formatted_display_name')
|
||||
def _compute_display_name(self):
|
||||
lots_to_process_ids = []
|
||||
for lot in self:
|
||||
if lot.env.context.get('formatted_display_name') and lot.use_expiration_date and lot.expiration_date:
|
||||
name = f"{lot.name}"
|
||||
if fields.Datetime.now() >= lot.expiration_date:
|
||||
name += self.env._("\t--Expired--")
|
||||
elif lot.alert_date and fields.Datetime.now() >= lot.alert_date:
|
||||
name += self.env._("\t--Expire on %(date)s--", date=fields.Datetime.to_string(lot.expiration_date))
|
||||
lot.display_name = name
|
||||
else:
|
||||
lots_to_process_ids.append(lot.id)
|
||||
if lots_to_process_ids:
|
||||
super(StockLot, self.env['stock.lot'].browse(lots_to_process_ids))._compute_display_name()
|
||||
|
||||
@api.depends('expiration_date')
|
||||
def _compute_product_expiry_alert(self):
|
||||
current_date = fields.Datetime.now()
|
||||
|
|
@ -80,21 +97,11 @@ class StockLot(models.Model):
|
|||
|
||||
for lot in alert_lots:
|
||||
lot.activity_schedule(
|
||||
'product_expiry.mail_activity_type_alert_date_reached',
|
||||
'mail.mail_activity_data_todo',
|
||||
user_id=lot.product_id.with_company(lot.company_id).responsible_id.id or lot.product_id.responsible_id.id or SUPERUSER_ID,
|
||||
note=_("The alert date has been reached for this lot/serial number")
|
||||
note=_("The alert date has been reached for this lot/serial number"),
|
||||
summary=_("Alert Date Reached"),
|
||||
)
|
||||
alert_lots.write({
|
||||
'product_expiry_reminded': True
|
||||
})
|
||||
|
||||
|
||||
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.lot']._alert_date_exceeded()
|
||||
if use_new_cursor:
|
||||
self.env.cr.commit()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ class ResConfigSettings(models.TransientModel):
|
|||
if not self.group_lot_on_delivery_slip:
|
||||
self.group_expiry_date_on_delivery_slip = False
|
||||
|
||||
@api.onchange('group_stock_production_lot')
|
||||
def _onchange_group_stock_production_lot(self):
|
||||
super()._onchange_group_stock_production_lot()
|
||||
if self.group_stock_production_lot:
|
||||
self.module_product_expiry = True
|
||||
|
||||
@api.onchange('module_product_expiry')
|
||||
def _onchange_module_product_expiry(self):
|
||||
if not self.module_product_expiry:
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import datetime
|
||||
import dateutil.parser as dparser
|
||||
from re import findall as re_findall
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import get_lang
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
|
|
@ -12,12 +15,81 @@ class StockMove(models.Model):
|
|||
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):
|
||||
@api.model
|
||||
def action_generate_lot_line_vals(self, context_data, mode, first_lot, count, lot_text):
|
||||
vals_list = super().action_generate_lot_line_vals(context_data, mode, first_lot, count, lot_text)
|
||||
product = self.env['product.product'].browse(context_data.get('default_product_id'))
|
||||
picking = self.env['stock.picking'].browse(context_data.get('default_picking_id'))
|
||||
if product.use_expiration_date:
|
||||
from_date = picking.scheduled_date or fields.Datetime.today()
|
||||
expiration_date = from_date + datetime.timedelta(days=product.expiration_time)
|
||||
for vals in vals_list:
|
||||
vals['expiration_date'] = vals.get('expiration_date') or expiration_date
|
||||
return vals_list
|
||||
|
||||
def _generate_serial_move_line_commands(self, field_data, location_dest_id=False, 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)
|
||||
move_lines_commands = super()._generate_serial_move_line_commands(field_data, location_dest_id, 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
|
||||
if 'expiration_date' not in move_line_vals:
|
||||
move_line_vals['expiration_date'] = date
|
||||
return move_lines_commands
|
||||
|
||||
def _convert_string_into_field_data(self, string, options):
|
||||
res = super()._convert_string_into_field_data(string, options)
|
||||
if not res:
|
||||
try:
|
||||
datetime = dparser.parse(string, **options)
|
||||
if self and not self.use_expiration_date:
|
||||
# The datetime was correctly parsed but this move's product doesn't use expiration date.
|
||||
return "ignore"
|
||||
return {'expiration_date': datetime}
|
||||
except ValueError:
|
||||
pass
|
||||
return res
|
||||
|
||||
def _get_formating_options(self, strings):
|
||||
options = super()._get_formating_options(strings)
|
||||
separators = "-/ "
|
||||
date_regex = f'[^{separators}]+'
|
||||
for string in strings:
|
||||
# Searches for a date.
|
||||
date_data = re_findall(date_regex, string)
|
||||
if len(date_data) < 2: # Not enough data.
|
||||
continue
|
||||
value_1, value_2 = date_data[:2]
|
||||
if re_findall('[a-zA-Z]', value_1):
|
||||
# Assumes the first value is the mounth (written in letters). Don't add any option
|
||||
# as mounth as the first date's value is the default behavior for `dateutil.parse`.
|
||||
break
|
||||
# Try to guess if the first data is the day or the year.
|
||||
if int(value_1) > 31:
|
||||
options['yearfirst'] = True
|
||||
break
|
||||
elif int(value_1) > 12 and (re_findall('[a-zA-Z]', value_2) or int(value_2) <= 12):
|
||||
options['dayfirst'] = True
|
||||
break
|
||||
else: # Too ambiguous, gets the option from the user's lang's date setting.
|
||||
user_lang_format = get_lang(self.env).date_format
|
||||
if re_findall('^%[mbB]', user_lang_format): # First parameter is for month.
|
||||
return options
|
||||
elif re_findall('^%[djaA]', user_lang_format): # First parameter is for day.
|
||||
options['dayfirst'] = True
|
||||
break
|
||||
elif re_findall('^%[yY]', user_lang_format): # First parameter is for year.
|
||||
options['yearfirst'] = True
|
||||
break
|
||||
return options
|
||||
|
||||
def _update_reserved_quantity(self, need, location_id, lot_id=None, package_id=None, owner_id=None, strict=True):
|
||||
if self.product_id.use_expiration_date:
|
||||
return super(StockMove, self.with_context(with_expiration=self.date))._update_reserved_quantity(need, location_id, lot_id, package_id, owner_id, strict)
|
||||
return super()._update_reserved_quantity(need, location_id, lot_id, package_id, owner_id, strict)
|
||||
|
||||
def _get_available_quantity(self, location_id, lot_id=None, package_id=None, owner_id=None, strict=False, allow_negative=False):
|
||||
if self.product_id.use_expiration_date:
|
||||
return super(StockMove, self.with_context(with_expiration=self.date))._get_available_quantity(location_id, lot_id, package_id, owner_id, strict, allow_negative)
|
||||
return super()._get_available_quantity(location_id, lot_id, package_id, owner_id, strict, allow_negative)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class StockMoveLine(models.Model):
|
|||
string='Expiration Date', compute='_compute_expiration_date', store=True,
|
||||
help='This is the date on which the goods with this Serial Number may'
|
||||
' become dangerous and must not be consumed.')
|
||||
removal_date = fields.Datetime(string='Removal Date', compute='_compute_removal_date', readonly=False, store=True)
|
||||
is_expired = fields.Boolean(related='lot_id.product_expiry_alert')
|
||||
use_expiration_date = fields.Boolean(
|
||||
string='Use Expiration Date', related='product_id.use_expiration_date')
|
||||
|
|
@ -24,8 +25,10 @@ class StockMoveLine(models.Model):
|
|||
and 'product_id.use_expiration_date' are new fields introduced in this module,
|
||||
there is no need for an UPDATE statement here.
|
||||
"""
|
||||
if not column_exists(self._cr, "stock_move_line", "expiration_date"):
|
||||
create_column(self._cr, "stock_move_line", "expiration_date", "timestamp")
|
||||
if not column_exists(self.env.cr, "stock_move_line", "expiration_date"):
|
||||
create_column(self.env.cr, "stock_move_line", "expiration_date", "timestamp")
|
||||
if not column_exists(self.env.cr, "stock_move_line", "removal_date"):
|
||||
create_column(self.env.cr, "stock_move_line", "removal_date", "timestamp")
|
||||
return super()._auto_init()
|
||||
|
||||
@api.depends('product_id', 'lot_id.expiration_date', 'picking_id.scheduled_date')
|
||||
|
|
@ -41,37 +44,19 @@ class StockMoveLine(models.Model):
|
|||
else:
|
||||
move_line.expiration_date = False
|
||||
|
||||
@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
|
||||
@api.depends('product_id', 'expiration_date', 'lot_id.removal_date')
|
||||
def _compute_removal_date(self):
|
||||
for move_line in self:
|
||||
if move_line.lot_id.removal_date:
|
||||
move_line.removal_date = move_line.lot_id.removal_date
|
||||
elif move_line.picking_type_use_create_lots:
|
||||
if move_line.product_id.use_expiration_date and move_line.expiration_date:
|
||||
move_line.removal_date = move_line.expiration_date - datetime.timedelta(days=move_line.product_id.removal_time)
|
||||
else:
|
||||
move_line.removal_date = False
|
||||
|
||||
@api.onchange('product_id', 'product_uom_id', 'picking_id')
|
||||
def _onchange_product_id(self):
|
||||
res = super()._onchange_product_id()
|
||||
if self.picking_type_use_create_lots:
|
||||
if self.product_id.use_expiration_date:
|
||||
from_date = self.picking_id.scheduled_date or fields.Datetime.today()
|
||||
self.expiration_date = from_date + datetime.timedelta(days=self.product_id.expiration_time)
|
||||
else:
|
||||
self.expiration_date = False
|
||||
return res
|
||||
|
||||
def _assign_production_lot(self, lot):
|
||||
super()._assign_production_lot(lot)
|
||||
self.lot_id._update_date_values(self[0].expiration_date)
|
||||
|
||||
def _get_value_production_lot(self):
|
||||
res = super()._get_value_production_lot()
|
||||
def _prepare_new_lot_vals(self):
|
||||
vals = super()._prepare_new_lot_vals()
|
||||
if self.expiration_date:
|
||||
res.update({
|
||||
'expiration_date': self.expiration_date,
|
||||
'use_date': self.expiration_date - datetime.timedelta(days=self.product_id.use_time),
|
||||
'removal_date': self.expiration_date - datetime.timedelta(days=self.product_id.removal_time),
|
||||
'alert_date': self.expiration_date - datetime.timedelta(days=self.product_id.alert_time),
|
||||
})
|
||||
return res
|
||||
vals['expiration_date'] = self.expiration_date
|
||||
return vals
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import datetime
|
||||
|
||||
from odoo import models, _
|
||||
|
||||
|
|
@ -18,11 +19,11 @@ class StockPicking(models.Model):
|
|||
return res
|
||||
|
||||
def _check_expired_lots(self):
|
||||
expired_pickings = self.move_line_ids.filtered(lambda ml: ml.lot_id.product_expiry_alert).picking_id
|
||||
expired_pickings = self.move_line_ids.filtered(lambda ml: ml.lot_id.product_expiry_alert or (ml.removal_date and ml.removal_date <= datetime.datetime.now())).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
|
||||
expired_lot_ids = self.move_line_ids.filtered(lambda ml: ml.lot_id.product_expiry_alert or (ml.removal_date and ml.removal_date <= datetime.datetime.now())).lot_id.ids
|
||||
view_id = self.env.ref('product_expiry.confirm_expiry_view').id
|
||||
context = dict(self.env.context)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,27 +7,36 @@ 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)
|
||||
expiration_date = fields.Datetime(related='lot_id.expiration_date', store=True)
|
||||
removal_date = fields.Datetime(related='lot_id.removal_date', store=True)
|
||||
use_expiration_date = fields.Boolean(related='product_id.use_expiration_date')
|
||||
available_quantity = fields.Float(help="On hand quantity which hasn't been reserved on a transfer and is still fresh, in the default unit of measure of the product")
|
||||
|
||||
@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
|
||||
def _get_gs1_barcode(self, gs1_quantity_rules_ai_by_uom=False):
|
||||
barcode = super()._get_gs1_barcode(gs1_quantity_rules_ai_by_uom)
|
||||
if self.use_expiration_date:
|
||||
if self.lot_id.expiration_date:
|
||||
barcode = '17' + self.lot_id.expiration_date.strftime('%y%m%d') + barcode
|
||||
if self.lot_id.use_date:
|
||||
barcode = '15' + self.lot_id.use_date.strftime('%y%m%d') + barcode
|
||||
return barcode
|
||||
|
||||
@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)
|
||||
return super()._get_removal_strategy_order(removal_strategy)
|
||||
|
||||
@api.depends('removal_date')
|
||||
def _compute_available_quantity(self):
|
||||
super()._compute_available_quantity()
|
||||
current_date = fields.Datetime.now()
|
||||
for quant in self:
|
||||
if quant.use_expiration_date and quant.removal_date and quant.removal_date <= current_date:
|
||||
quant.available_quantity = 0
|
||||
|
||||
def _set_view_context(self):
|
||||
self_with_context = self
|
||||
if self.env.context.get('default_product_id') and self.env['product.product'].browse(self.env.context.get('default_product_id')).use_expiration_date:
|
||||
self_with_context = self.with_context(show_removal_date=True)
|
||||
return super(StockQuant, self_with_context)._set_view_context()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class StockRule(models.Model):
|
||||
_inherit = 'stock.rule'
|
||||
|
||||
@api.model
|
||||
def _run_scheduler_tasks(self, use_new_cursor=False, company_id=False):
|
||||
super()._run_scheduler_tasks(use_new_cursor=use_new_cursor, company_id=company_id)
|
||||
self.env['stock.lot']._alert_date_exceeded()
|
||||
if use_new_cursor:
|
||||
self.env['ir.cron']._commit_progress(1)
|
||||
|
||||
@api.model
|
||||
def _get_scheduler_tasks_to_do(self):
|
||||
return super()._get_scheduler_tasks_to_do() + 1
|
||||
Loading…
Add table
Add a link
Reference in a new issue