From d81d12daaada571fdb184b059ada34221fa70d3d Mon Sep 17 00:00:00 2001 From: Arnold Moyaux Date: Thu, 28 Nov 2019 14:46:31 +0000 Subject: [PATCH] [MOV] stock: orderpoint own file Move the code from stock warehouse to its own file. [MOV] stock: adapt warehouse and orderpoint files --- addons/stock/models/__init__.py | 1 + addons/stock/models/stock_orderpoint.py | 148 ++++++++++++++++++++++++ addons/stock/models/stock_warehouse.py | 144 +---------------------- 3 files changed, 150 insertions(+), 143 deletions(-) create mode 100644 addons/stock/models/stock_orderpoint.py diff --git a/addons/stock/models/__init__.py b/addons/stock/models/__init__.py index 2e061fd211c..115b4f42e6a 100644 --- a/addons/stock/models/__init__.py +++ b/addons/stock/models/__init__.py @@ -10,6 +10,7 @@ from . import stock_inventory from . import stock_location from . import stock_move from . import stock_move_line +from . import stock_orderpoint from . import stock_production_lot from . import stock_picking from . import stock_quant diff --git a/addons/stock/models/stock_orderpoint.py b/addons/stock/models/stock_orderpoint.py new file mode 100644 index 00000000000..c9cb8c2da8e --- /dev/null +++ b/addons/stock/models/stock_orderpoint.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime +from dateutil import relativedelta + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError, ValidationError +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT +from odoo.osv import expression + + +class StockWarehouseOrderpoint(models.Model): + """ Defines Minimum stock rules. """ + _name = "stock.warehouse.orderpoint" + _description = "Minimum Inventory Rule" + _check_company_auto = True + + @api.model + def default_get(self, fields): + res = super().default_get(fields) + warehouse = None + if 'warehouse_id' not in res and res.get('company_id'): + warehouse = self.env['stock.warehouse'].search([('company_id', '=', res['company_id'])], limit=1) + if warehouse: + res['warehouse_id'] = warehouse.id + res['location_id'] = warehouse.lot_stock_id.id + return res + + name = fields.Char( + 'Name', copy=False, required=True, readonly=True, + default=lambda self: self.env['ir.sequence'].next_by_code('stock.orderpoint')) + active = fields.Boolean( + 'Active', default=True, + help="If the active field is set to False, it will allow you to hide the orderpoint without removing it.") + warehouse_id = fields.Many2one( + 'stock.warehouse', 'Warehouse', + check_company=True, ondelete="cascade", required=True) + location_id = fields.Many2one( + 'stock.location', 'Location', + ondelete="cascade", required=True, check_company=True) + product_id = fields.Many2one( + 'product.product', 'Product', + domain="[('type', '=', 'product'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", ondelete='cascade', required=True, check_company=True) + product_uom = fields.Many2one( + 'uom.uom', 'Unit of Measure', related='product_id.uom_id') + product_uom_name = fields.Char(string='Product unit of measure label', related='product_uom.display_name', readonly=True) + product_min_qty = fields.Float( + 'Minimum Quantity', digits='Product Unit of Measure', required=True, + help="When the virtual stock equals to or goes below the Min Quantity specified for this field, Odoo generates " + "a procurement to bring the forecasted quantity to the Max Quantity.") + product_max_qty = fields.Float( + 'Maximum Quantity', digits='Product Unit of Measure', required=True, + help="When the virtual stock goes below the Min Quantity, Odoo generates " + "a procurement to bring the forecasted quantity to the Quantity specified as Max Quantity.") + qty_multiple = fields.Float( + 'Qty Multiple', digits='Product Unit of Measure', + default=1, required=True, + help="The procurement quantity will be rounded up to this multiple. If it is 0, the exact quantity will be used.") + group_id = fields.Many2one( + 'procurement.group', 'Procurement Group', copy=False, + help="Moves created through this orderpoint will be put in this procurement group. If none is given, the moves generated by stock rules will be grouped into one big picking.") + company_id = fields.Many2one( + 'res.company', 'Company', required=True, index=True, + default=lambda self: self.env.company) + lead_days = fields.Integer( + 'Lead Time', default=1, + help="Number of days after the orderpoint is triggered to receive the products or to order to the vendor") + lead_type = fields.Selection( + [('net', 'Days to get the products'), ('supplier', 'Days to purchase')], 'Lead Type', + required=True, default='supplier') + allowed_location_ids = fields.One2many(comodel_name='stock.location', compute='_compute_allowed_location_ids') + + _sql_constraints = [ + ('qty_multiple_check', 'CHECK( qty_multiple >= 0 )', 'Qty Multiple must be greater than or equal to zero.'), + ] + + @api.depends('warehouse_id') + def _compute_allowed_location_ids(self): + loc_domain = [('usage', 'in', ('internal', 'view'))] + # We want to keep only the locations + # - strictly belonging to our warehouse + # - not belonging to any warehouses + for orderpoint in self: + other_warehouses = self.env['stock.warehouse'].search([('id', '!=', orderpoint.warehouse_id.id)]) + for view_location_id in other_warehouses.mapped('view_location_id'): + loc_domain = expression.AND([loc_domain, ['!', ('id', 'child_of', view_location_id.id)]]) + loc_domain = expression.AND([loc_domain, ['|', ('company_id', '=', False), ('company_id', '=', orderpoint.company_id.id)]]) + orderpoint.allowed_location_ids = self.env['stock.location'].search(loc_domain) + + def _quantity_in_progress(self): + """Return Quantities that are not yet in virtual stock but should be deduced from orderpoint rule + (example: purchases created from orderpoints)""" + return dict(self.mapped(lambda x: (x.id, 0.0))) + + @api.constrains('product_id') + def _check_product_uom(self): + ''' Check if the UoM has the same category as the product standard UoM ''' + if any(orderpoint.product_id.uom_id.category_id != orderpoint.product_uom.category_id for orderpoint in self): + raise ValidationError(_('You have to select a product unit of measure that is in the same category than the default unit of measure of the product')) + + @api.onchange('warehouse_id') + def onchange_warehouse_id(self): + """ Finds location id for changed warehouse. """ + if self.warehouse_id: + self.location_id = self.warehouse_id.lot_stock_id.id + + @api.onchange('product_id') + def _onchange_product_id(self): + if self.product_id: + self.product_uom = self.product_id.uom_id.id + + @api.onchange('company_id') + def _onchange_company_id(self): + if self.company_id: + self.warehouse_id = self.env['stock.warehouse'].search([ + ('company_id', '=', self.company_id.id) + ], limit=1) + + def write(self, vals): + if 'company_id' in vals: + for orderpoint in self: + if orderpoint.company_id.id != vals['company_id']: + raise UserError(_("Changing the company of this record is forbidden at this point, you should rather archive it and create a new one.")) + return super().write(vals) + + def _get_date_planned(self, product_qty, start_date): + days = self.lead_days or 0.0 + if self.lead_type == 'supplier': + # These days will be substracted when creating the PO + days += self.product_id._select_seller( + quantity=product_qty, + date=fields.Date.context_today(self,start_date), + uom_id=self.product_uom).delay or 0.0 + date_planned = start_date + relativedelta.relativedelta(days=days) + return date_planned.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + + def _prepare_procurement_values(self, product_qty, date=False, group=False): + """ Prepare specific key for moves or other components that will be created from a stock rule + comming from an orderpoint. This method could be override in order to add other custom key that could + be used in move/po creation. + """ + return { + 'date_planned': date or self._get_date_planned(product_qty, datetime.today()), + 'warehouse_id': self.warehouse_id, + 'orderpoint_id': self, + 'group_id': group or self.group_id, + } diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py index 847bab73dfa..7b688933b08 100644 --- a/addons/stock/models/stock_warehouse.py +++ b/addons/stock/models/stock_warehouse.py @@ -2,13 +2,9 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from collections import namedtuple -from datetime import datetime -from dateutil import relativedelta from odoo import api, fields, models, _ -from odoo.exceptions import UserError, ValidationError -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT -from odoo.osv import expression +from odoo.exceptions import UserError import logging @@ -957,141 +953,3 @@ class Warehouse(models.Model): 'limit': 20, 'context': dict(self._context, default_warehouse_selectable=True, default_warehouse_ids=self.ids) } - - -class Orderpoint(models.Model): - """ Defines Minimum stock rules. """ - _name = "stock.warehouse.orderpoint" - _description = "Minimum Inventory Rule" - _check_company_auto = True - - @api.model - def default_get(self, fields): - res = super(Orderpoint, self).default_get(fields) - warehouse = None - if 'warehouse_id' not in res and res.get('company_id'): - warehouse = self.env['stock.warehouse'].search([('company_id', '=', res['company_id'])], limit=1) - if warehouse: - res['warehouse_id'] = warehouse.id - res['location_id'] = warehouse.lot_stock_id.id - return res - - name = fields.Char( - 'Name', copy=False, required=True, readonly=True, - default=lambda self: self.env['ir.sequence'].next_by_code('stock.orderpoint')) - active = fields.Boolean( - 'Active', default=True, - help="If the active field is set to False, it will allow you to hide the orderpoint without removing it.") - warehouse_id = fields.Many2one( - 'stock.warehouse', 'Warehouse', - check_company=True, ondelete="cascade", required=True) - location_id = fields.Many2one( - 'stock.location', 'Location', - ondelete="cascade", required=True, check_company=True) - product_id = fields.Many2one( - 'product.product', 'Product', - domain="[('type', '=', 'product'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", ondelete='cascade', required=True, check_company=True) - product_uom = fields.Many2one( - 'uom.uom', 'Unit of Measure', related='product_id.uom_id') - product_uom_name = fields.Char(string='Product unit of measure label', related='product_uom.display_name', readonly=True) - product_min_qty = fields.Float( - 'Minimum Quantity', digits='Product Unit of Measure', required=True, - help="When the virtual stock equals to or goes below the Min Quantity specified for this field, Odoo generates " - "a procurement to bring the forecasted quantity to the Max Quantity.") - product_max_qty = fields.Float( - 'Maximum Quantity', digits='Product Unit of Measure', required=True, - help="When the virtual stock goes below the Min Quantity, Odoo generates " - "a procurement to bring the forecasted quantity to the Quantity specified as Max Quantity.") - qty_multiple = fields.Float( - 'Qty Multiple', digits='Product Unit of Measure', - default=1, required=True, - help="The procurement quantity will be rounded up to this multiple. If it is 0, the exact quantity will be used.") - group_id = fields.Many2one( - 'procurement.group', 'Procurement Group', copy=False, - help="Moves created through this orderpoint will be put in this procurement group. If none is given, the moves generated by stock rules will be grouped into one big picking.") - company_id = fields.Many2one( - 'res.company', 'Company', required=True, index=True, - default=lambda self: self.env.company) - lead_days = fields.Integer( - 'Lead Time', default=1, - help="Number of days after the orderpoint is triggered to receive the products or to order to the vendor") - lead_type = fields.Selection( - [('net', 'Days to get the products'), ('supplier', 'Days to purchase')], 'Lead Type', - required=True, default='supplier') - allowed_location_ids = fields.One2many(comodel_name='stock.location', compute='_compute_allowed_location_ids') - - _sql_constraints = [ - ('qty_multiple_check', 'CHECK( qty_multiple >= 0 )', 'Qty Multiple must be greater than or equal to zero.'), - ] - - @api.depends('warehouse_id') - def _compute_allowed_location_ids(self): - loc_domain = [('usage', 'in', ('internal', 'view'))] - # We want to keep only the locations - # - strictly belonging to our warehouse - # - not belonging to any warehouses - for orderpoint in self: - other_warehouses = self.env['stock.warehouse'].search([('id', '!=', orderpoint.warehouse_id.id)]) - for view_location_id in other_warehouses.mapped('view_location_id'): - loc_domain = expression.AND([loc_domain, ['!', ('id', 'child_of', view_location_id.id)]]) - loc_domain = expression.AND([loc_domain, ['|', ('company_id', '=', False), ('company_id', '=', orderpoint.company_id.id)]]) - orderpoint.allowed_location_ids = self.env['stock.location'].search(loc_domain) - - def _quantity_in_progress(self): - """Return Quantities that are not yet in virtual stock but should be deduced from orderpoint rule - (example: purchases created from orderpoints)""" - return dict(self.mapped(lambda x: (x.id, 0.0))) - - @api.constrains('product_id') - def _check_product_uom(self): - ''' Check if the UoM has the same category as the product standard UoM ''' - if any(orderpoint.product_id.uom_id.category_id != orderpoint.product_uom.category_id for orderpoint in self): - raise ValidationError(_('You have to select a product unit of measure that is in the same category than the default unit of measure of the product')) - - @api.onchange('warehouse_id') - def onchange_warehouse_id(self): - """ Finds location id for changed warehouse. """ - if self.warehouse_id: - self.location_id = self.warehouse_id.lot_stock_id.id - - @api.onchange('product_id') - def _onchange_product_id(self): - if self.product_id: - self.product_uom = self.product_id.uom_id.id - - @api.onchange('company_id') - def _onchange_company_id(self): - if self.company_id: - self.warehouse_id = self.env['stock.warehouse'].search([ - ('company_id', '=', self.company_id.id) - ], limit=1) - - def write(self, vals): - if 'company_id' in vals: - for orderpoint in self: - if orderpoint.company_id.id != vals['company_id']: - raise UserError(_("Changing the company of this record is forbidden at this point, you should rather archive it and create a new one.")) - return super(Orderpoint, self).write(vals) - - def _get_date_planned(self, product_qty, start_date): - days = self.lead_days or 0.0 - if self.lead_type == 'supplier': - # These days will be substracted when creating the PO - days += self.product_id._select_seller( - quantity=product_qty, - date=fields.Date.context_today(self,start_date), - uom_id=self.product_uom).delay or 0.0 - date_planned = start_date + relativedelta.relativedelta(days=days) - return date_planned.strftime(DEFAULT_SERVER_DATETIME_FORMAT) - - def _prepare_procurement_values(self, product_qty, date=False, group=False): - """ Prepare specific key for moves or other components that will be created from a stock rule - comming from an orderpoint. This method could be override in order to add other custom key that could - be used in move/po creation. - """ - return { - 'date_planned': date or self._get_date_planned(product_qty, datetime.today()), - 'warehouse_id': self.warehouse_id, - 'orderpoint_id': self, - 'group_id': group or self.group_id, - }