diff --git a/stock_request/models/__init__.py b/stock_request/models/__init__.py index fd3b718a9..d1c9d267e 100644 --- a/stock_request/models/__init__.py +++ b/stock_request/models/__init__.py @@ -1,3 +1,4 @@ +from . import stock_request_abstract from . import stock_request from . import stock_request_allocation from . import stock_request_order diff --git a/stock_request/models/res_config_settings.py b/stock_request/models/res_config_settings.py index 416e2f2bd..e776dce90 100644 --- a/stock_request/models/res_config_settings.py +++ b/stock_request/models/res_config_settings.py @@ -12,3 +12,6 @@ class ResConfigSettings(models.TransientModel): module_stock_request_purchase = fields.Boolean( string='Stock Requests for Purchases') + + module_stock_request_kanban = fields.Boolean( + string='Stock Requests Kanban integration') diff --git a/stock_request/models/stock_request.py b/stock_request/models/stock_request.py index 2f28f85e4..a5f3baaa4 100644 --- a/stock_request/models/stock_request.py +++ b/stock_request/models/stock_request.py @@ -6,7 +6,6 @@ from odoo.exceptions import UserError, ValidationError from odoo.addons import decimal_precision as dp from odoo.tools import float_compare - REQUEST_STATES = [ ('draft', 'Draft'), ('open', 'In progress'), @@ -17,33 +16,14 @@ REQUEST_STATES = [ class StockRequest(models.Model): _name = "stock.request" _description = "Stock Request" - _inherit = ['mail.thread', 'mail.activity.mixin'] - - @api.model - def default_get(self, fields): - res = super(StockRequest, 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 + _inherit = 'stock.request.abstract' def _get_default_requested_by(self): return self.env['res.users'].browse(self.env.uid) - @api.depends('product_id', 'product_uom_id', 'product_uom_qty') - def _compute_product_qty(self): - for rec in self: - rec.product_qty = rec.product_uom_id._compute_quantity( - rec.product_uom_qty, rec.product_id.uom_id) - name = fields.Char( - 'Name', copy=False, required=True, readonly=True, - states={'draft': [('readonly', False)]}, - default='/') + states={'draft': [('readonly', False)]} + ) state = fields.Selection(selection=REQUEST_STATES, string='Status', copy=False, default='draft', index=True, readonly=True, track_visibility='onchange', @@ -53,53 +33,6 @@ class StockRequest(models.Model): track_visibility='onchange', default=lambda s: s._get_default_requested_by(), ) - warehouse_id = fields.Many2one( - 'stock.warehouse', 'Warehouse', readonly=True, - ondelete="cascade", required=True, - states={'draft': [('readonly', False)]}) - location_id = fields.Many2one( - 'stock.location', 'Location', readonly=True, - domain=[('usage', 'in', ['internal', 'transit'])], - ondelete="cascade", required=True, - states={'draft': [('readonly', False)]}, - ) - product_id = fields.Many2one( - 'product.product', 'Product', readonly=True, - states={'draft': [('readonly', False)]}, - domain=[('type', 'in', ['product', 'consu'])], ondelete='cascade', - required=True, - ) - product_uom_id = fields.Many2one( - 'product.uom', 'Product Unit of Measure', - readonly=True, required=True, - states={'draft': [('readonly', False)]}, - default=lambda self: self._context.get('product_uom_id', False), - ) - product_uom_qty = fields.Float( - 'Quantity', digits=dp.get_precision('Product Unit of Measure'), - states={'draft': [('readonly', False)]}, - readonly=True, required=True, - help="Quantity, specified in the unit of measure indicated in the " - "request.", - ) - product_qty = fields.Float( - 'Real Quantity', compute='_compute_product_qty', - store=True, readonly=True, copy=False, - help='Quantity in the default UoM of the product', - ) - procurement_group_id = fields.Many2one( - 'procurement.group', 'Procurement Group', readonly=True, - states={'draft': [('readonly', False)]}, - help="Moves created through this stock request will be put in this " - "procurement group. If none is given, the moves generated by " - "procurement rules will be grouped into one big picking.", - ) - company_id = fields.Many2one( - 'res.company', 'Company', required=True, readonly=True, - states={'draft': [('readonly', False)]}, - default=lambda self: self.env['res.company']._company_default_get( - 'stock.request'), - ) expected_date = fields.Datetime( 'Expected Date', default=fields.Datetime.now, index=True, required=True, readonly=True, @@ -135,26 +68,37 @@ class StockRequest(models.Model): compute='_compute_picking_ids', readonly=True, ) - route_id = fields.Many2one('stock.location.route', string='Route', - readonly=True, - states={'draft': [('readonly', False)]}, - domain="[('id', 'in', route_ids)]", - ondelete='restrict') - - route_ids = fields.Many2many( - 'stock.location.route', string='Route', - compute='_compute_route_ids', - readonly=True, - ) - allocation_ids = fields.One2many(comodel_name='stock.request.allocation', inverse_name='stock_request_id', string='Stock Request Allocation') - order_id = fields.Many2one( 'stock.request.order', readonly=True, ) + warehouse_id = fields.Many2one( + states={'draft': [('readonly', False)]}, readonly=True + ) + location_id = fields.Many2one( + states={'draft': [('readonly', False)]}, readonly=True + ) + product_id = fields.Many2one( + states={'draft': [('readonly', False)]}, readonly=True + ) + product_uom_id = fields.Many2one( + states={'draft': [('readonly', False)]}, readonly=True + ) + product_uom_qty = fields.Float( + states={'draft': [('readonly', False)]}, readonly=True + ) + procurement_group_id = fields.Many2one( + states={'draft': [('readonly', False)]}, readonly=True + ) + company_id = fields.Many2one( + states={'draft': [('readonly', False)]}, readonly=True + ) + route_id = fields.Many2one( + states={'draft': [('readonly', False)]}, readonly=True + ) _sql_constraints = [ ('name_uniq', 'unique(name, company_id)', @@ -189,65 +133,6 @@ class StockRequest(models.Model): request.product_id.uom_id._compute_quantity( open_qty, request.product_uom_id) - @api.depends('product_id', 'warehouse_id', 'location_id') - def _compute_route_ids(self): - for record in self: - routes = self.env['stock.location.route'] - if record.product_id: - routes += record.product_id.mapped( - 'route_ids') | record.product_id.mapped( - 'categ_id').mapped('total_route_ids') - if record.warehouse_id: - routes |= self.env['stock.location.route'].search( - [('warehouse_ids', 'in', record.warehouse_id.ids)]) - parents = record.get_parents().ids - record.route_ids = routes.filtered(lambda r: any( - p.location_id.id in parents for p in r.pull_ids)) - - def get_parents(self): - location = self.location_id.sudo() - result = location - while location.location_id: - location = location.location_id - result |= location - return result - - @api.constrains('company_id', 'product_id', 'warehouse_id', - 'location_id', 'route_id') - def _check_company_constrains(self): - """ Check if the related models have the same company """ - for rec in self: - if rec.product_id.company_id and \ - rec.product_id.company_id != rec.company_id: - raise ValidationError( - _('You have entered a product that is assigned ' - 'to another company.')) - if rec.location_id.company_id and \ - rec.location_id.company_id != rec.company_id: - raise ValidationError( - _('You have entered a location that is ' - 'assigned to another company.')) - if rec.warehouse_id.company_id != rec.company_id: - raise ValidationError( - _('You have entered a warehouse that is ' - 'assigned to another company.')) - if rec.route_id and rec.route_id.company_id and \ - rec.route_id.company_id != rec.company_id: - raise ValidationError( - _('You have entered a route that is ' - 'assigned to another company.')) - - @api.constrains('product_id') - def _check_product_uom(self): - ''' Check if the UoM has the same category as the - product standard UoM ''' - if any(request.product_id.uom_id.category_id != - request.product_uom_id.category_id for request in self): - raise ValidationError( - _('You have to select a product unit of measure in the ' - 'same category than the default unit ' - 'of measure of the product')) - @api.constrains('order_id', 'requested_by') def check_order_requested_by(self): if self.order_id and self.order_id.requested_by != self.requested_by: @@ -303,54 +188,6 @@ class StockRequest(models.Model): 'The picking policy must be equal to the order' )) - @api.onchange('warehouse_id') - def onchange_warehouse_id(self): - """ Finds location id for changed warehouse. """ - res = {'domain': {}} - if self.warehouse_id: - # search with sudo because the user may not have permissions - loc_wh = self.location_id.sudo().get_warehouse() - if self.warehouse_id != loc_wh: - self.location_id = self.warehouse_id.lot_stock_id.id - if self.warehouse_id.company_id != self.company_id: - self.company_id = self.warehouse_id.company_id - return res - - @api.onchange('location_id') - def onchange_location_id(self): - res = {'domain': {}} - if self.location_id: - loc_wh = self.location_id.get_warehouse() - if self.warehouse_id != loc_wh: - self.warehouse_id = loc_wh - return res - - @api.onchange('company_id') - def onchange_company_id(self): - """ Sets a default warehouse when the company is changed and limits - the user selection of warehouses. """ - if self.company_id and ( - not self.warehouse_id or - self.warehouse_id.company_id != self.company_id): - self.warehouse_id = self.env['stock.warehouse'].search( - [('company_id', '=', self.company_id.id)], limit=1) - self.onchange_warehouse_id() - - return { - 'domain': { - 'warehouse_id': [('company_id', '=', self.company_id.id)]}} - - @api.onchange('product_id') - def onchange_product_id(self): - res = {'domain': {}} - if self.product_id: - self.product_uom_id = self.product_id.uom_id.id - res['domain']['product_uom_id'] = [ - ('category_id', '=', self.product_id.uom_id.category_id.id)] - return res - res['domain']['product_uom_id'] = [] - return res - @api.multi def _action_confirm(self): self._action_launch_procurement_rule() diff --git a/stock_request/models/stock_request_abstract.py b/stock_request/models/stock_request_abstract.py new file mode 100644 index 000000000..b59cef52e --- /dev/null +++ b/stock_request/models/stock_request_abstract.py @@ -0,0 +1,197 @@ +# Copyright 2017 Eficent Business and IT Consulting Services, S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError +from odoo.addons import decimal_precision as dp + + +class StockRequest(models.AbstractModel): + _name = "stock.request.abstract" + _description = "Stock Request Template" + _inherit = ['mail.thread', 'mail.activity.mixin'] + + @api.model + def default_get(self, fields): + res = super(StockRequest, 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 + + @api.depends('product_id', 'product_uom_id', 'product_uom_qty') + def _compute_product_qty(self): + for rec in self: + rec.product_qty = rec.product_uom_id._compute_quantity( + rec.product_uom_qty, rec.product_id.uom_id) + + name = fields.Char( + 'Name', copy=False, required=True, readonly=True, + default='/') + warehouse_id = fields.Many2one( + 'stock.warehouse', 'Warehouse', + ondelete="cascade", required=True, + ) + location_id = fields.Many2one( + 'stock.location', 'Location', + domain=[('usage', 'in', ['internal', 'transit'])], + ondelete="cascade", required=True, + ) + product_id = fields.Many2one( + 'product.product', 'Product', + domain=[('type', 'in', ['product', 'consu'])], ondelete='cascade', + required=True, + ) + product_uom_id = fields.Many2one( + 'product.uom', 'Product Unit of Measure', + required=True, + default=lambda self: self._context.get('product_uom_id', False), + ) + product_uom_qty = fields.Float( + 'Quantity', digits=dp.get_precision('Product Unit of Measure'), + required=True, + help="Quantity, specified in the unit of measure indicated in the " + "request.", + ) + product_qty = fields.Float( + 'Real Quantity', compute='_compute_product_qty', + store=True, copy=False, + help='Quantity in the default UoM of the product', + ) + procurement_group_id = fields.Many2one( + 'procurement.group', 'Procurement Group', + help="Moves created through this stock request will be put in this " + "procurement group. If none is given, the moves generated by " + "procurement rules will be grouped into one big picking.", + ) + company_id = fields.Many2one( + 'res.company', 'Company', required=True, + default=lambda self: self.env['res.company']._company_default_get( + 'stock.request'), + ) + route_id = fields.Many2one('stock.location.route', string='Route', + domain="[('id', 'in', route_ids)]", + ondelete='restrict') + + route_ids = fields.Many2many( + 'stock.location.route', string='Route', + compute='_compute_route_ids', + readonly=True, + ) + + _sql_constraints = [ + ('name_uniq', 'unique(name, company_id)', + 'Name must be unique'), + ] + + @api.depends('product_id', 'warehouse_id', 'location_id') + def _compute_route_ids(self): + for record in self: + routes = self.env['stock.location.route'] + if record.product_id: + routes += record.product_id.mapped( + 'route_ids') | record.product_id.mapped( + 'categ_id').mapped('total_route_ids') + if record.warehouse_id: + routes |= self.env['stock.location.route'].search( + [('warehouse_ids', 'in', record.warehouse_id.ids)]) + parents = record.get_parents().ids + record.route_ids = routes.filtered(lambda r: any( + p.location_id.id in parents for p in r.pull_ids)) + + def get_parents(self): + location = self.location_id.sudo() + result = location + while location.location_id: + location = location.location_id + result |= location + return result + + @api.constrains('company_id', 'product_id', 'warehouse_id', + 'location_id', 'route_id') + def _check_company_constrains(self): + """ Check if the related models have the same company """ + for rec in self: + if rec.product_id.company_id and \ + rec.product_id.company_id != rec.company_id: + raise ValidationError( + _('You have entered a product that is assigned ' + 'to another company.')) + if rec.location_id.company_id and \ + rec.location_id.company_id != rec.company_id: + raise ValidationError( + _('You have entered a location that is ' + 'assigned to another company.')) + if rec.warehouse_id.company_id != rec.company_id: + raise ValidationError( + _('You have entered a warehouse that is ' + 'assigned to another company.')) + if rec.route_id and rec.route_id.company_id and \ + rec.route_id.company_id != rec.company_id: + raise ValidationError( + _('You have entered a route that is ' + 'assigned to another company.')) + + @api.constrains('product_id') + def _check_product_uom(self): + ''' Check if the UoM has the same category as the + product standard UoM ''' + if any(request.product_id.uom_id.category_id != + request.product_uom_id.category_id for request in self): + raise ValidationError( + _('You have to select a product unit of measure 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. """ + res = {'domain': {}} + if self.warehouse_id: + # search with sudo because the user may not have permissions + loc_wh = self.location_id.sudo().get_warehouse() + if self.warehouse_id != loc_wh: + self.location_id = self.warehouse_id.lot_stock_id.id + if self.warehouse_id.company_id != self.company_id: + self.company_id = self.warehouse_id.company_id + return res + + @api.onchange('location_id') + def onchange_location_id(self): + res = {'domain': {}} + if self.location_id: + loc_wh = self.location_id.sudo().get_warehouse() + if self.warehouse_id != loc_wh: + self.warehouse_id = loc_wh + self.onchange_warehouse_id() + return res + + @api.onchange('company_id') + def onchange_company_id(self): + """ Sets a default warehouse when the company is changed and limits + the user selection of warehouses. """ + if self.company_id and ( + not self.warehouse_id or + self.warehouse_id.company_id != self.company_id): + self.warehouse_id = self.env['stock.warehouse'].search( + [('company_id', '=', self.company_id.id)], limit=1) + self.onchange_warehouse_id() + + return { + 'domain': { + 'warehouse_id': [('company_id', '=', self.company_id.id)]}} + + @api.onchange('product_id') + def onchange_product_id(self): + res = {'domain': {}} + if self.product_id: + self.product_uom_id = self.product_id.uom_id.id + res['domain']['product_uom_id'] = [ + ('category_id', '=', self.product_id.uom_id.category_id.id)] + return res + res['domain']['product_uom_id'] = [] + return res diff --git a/stock_request/views/res_config_settings_views.xml b/stock_request/views/res_config_settings_views.xml index 9f3873a83..a6a4c9981 100644 --- a/stock_request/views/res_config_settings_views.xml +++ b/stock_request/views/res_config_settings_views.xml @@ -46,6 +46,21 @@ +