mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-27 21:32:00 +02:00
Initial commit: Sale packages
This commit is contained in:
commit
14e3d26998
6469 changed files with 2479670 additions and 0 deletions
|
|
@ -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
|
||||
|
|
@ -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='Use 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 Date',
|
||||
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 Date',
|
||||
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 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.')
|
||||
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.')
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
# -*- 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 StockLot(models.Model):
|
||||
_inherit = 'stock.lot'
|
||||
|
||||
use_expiration_date = fields.Boolean(
|
||||
string='Use Expiration Date', related='product_id.use_expiration_date')
|
||||
expiration_date = fields.Datetime(
|
||||
string='Expiration Date', compute='_compute_expiration_date', store=True, readonly=False,
|
||||
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', 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.')
|
||||
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('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
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_expiration_date(self):
|
||||
self.expiration_date = False
|
||||
for lot in self:
|
||||
if lot.product_id.use_expiration_date and not lot.expiration_date:
|
||||
duration = lot.product_id.product_tmpl_id.expiration_time
|
||||
lot.expiration_date = datetime.datetime.now() + datetime.timedelta(days=duration)
|
||||
|
||||
@api.depends('product_id', 'expiration_date')
|
||||
def _compute_dates(self):
|
||||
for lot in self:
|
||||
if not lot.product_id.use_expiration_date:
|
||||
lot.use_date = False
|
||||
lot.removal_date = False
|
||||
lot.alert_date = False
|
||||
elif lot.expiration_date:
|
||||
# when create
|
||||
if lot.product_id != lot._origin.product_id or \
|
||||
(not lot.use_date and not lot.removal_date and not lot.alert_date) or \
|
||||
(lot.expiration_date and not lot._origin.expiration_date):
|
||||
product_tmpl = lot.product_id.product_tmpl_id
|
||||
lot.use_date = lot.expiration_date - datetime.timedelta(days=product_tmpl.use_time)
|
||||
lot.removal_date = lot.expiration_date - datetime.timedelta(days=product_tmpl.removal_time)
|
||||
lot.alert_date = lot.expiration_date - datetime.timedelta(days=product_tmpl.alert_time)
|
||||
# when change
|
||||
elif lot._origin.expiration_date:
|
||||
time_delta = lot.expiration_date - lot._origin.expiration_date
|
||||
lot.use_date = lot._origin.use_date and lot._origin.use_date + time_delta
|
||||
lot.removal_date = lot._origin.removal_date and lot._origin.removal_date + time_delta
|
||||
lot.alert_date = lot._origin.alert_date and lot._origin.alert_date + time_delta
|
||||
|
||||
@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.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.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")
|
||||
)
|
||||
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()
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# -*- 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
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
# -*- 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.tools.sql import column_exists, create_column
|
||||
|
||||
|
||||
class StockMoveLine(models.Model):
|
||||
_inherit = "stock.move.line"
|
||||
|
||||
expiration_date = fields.Datetime(
|
||||
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.')
|
||||
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')
|
||||
|
||||
def _auto_init(self):
|
||||
""" Create column for 'expiration_date' here to avoid MemoryError when letting
|
||||
the ORM compute it after module installation. Since both 'lot_id.expiration_date'
|
||||
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")
|
||||
return super()._auto_init()
|
||||
|
||||
@api.depends('product_id', 'lot_id.expiration_date', 'picking_id.scheduled_date')
|
||||
def _compute_expiration_date(self):
|
||||
for move_line in self:
|
||||
if move_line.lot_id.expiration_date:
|
||||
move_line.expiration_date = move_line.lot_id.expiration_date
|
||||
elif move_line.picking_type_use_create_lots:
|
||||
if move_line.product_id.use_expiration_date:
|
||||
if not move_line.expiration_date:
|
||||
from_date = move_line.picking_id.scheduled_date or fields.Datetime.today()
|
||||
move_line.expiration_date = from_date + datetime.timedelta(days=move_line.product_id.expiration_time)
|
||||
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.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()
|
||||
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
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# -*- 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
|
||||
view_id = self.env.ref('product_expiry.confirm_expiry_view').id
|
||||
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',
|
||||
'views': [(view_id, 'form')],
|
||||
'view_id': view_id,
|
||||
'target': 'new',
|
||||
'context': context,
|
||||
}
|
||||
|
|
@ -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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue