From 2e1a64a40f18c6d75d6c62a188fc6d0840e39e9d Mon Sep 17 00:00:00 2001 From: Jordi Ballester Date: Mon, 13 Nov 2017 13:15:41 +0100 Subject: [PATCH] add stock_request in 11.0 --- stock_request/README.rst | 88 ++++ stock_request/__init__.py | 2 + stock_request/__manifest__.py | 26 ++ .../data/stock_request_sequence_data.xml | 14 + stock_request/models/__init__.py | 7 + stock_request/models/procurement_rule.py | 20 + stock_request/models/stock_move.py | 28 ++ stock_request/models/stock_move_line.py | 64 +++ stock_request/models/stock_picking.py | 37 ++ stock_request/models/stock_request.py | 334 +++++++++++++++ .../models/stock_request_allocation.py | 65 +++ stock_request/security/ir.model.access.csv | 13 + .../security/stock_request_security.xml | 72 ++++ stock_request/static/description/icon.png | Bin 0 -> 4857 bytes stock_request/tests/__init__.py | 2 + stock_request/tests/test_stock_request.py | 390 ++++++++++++++++++ stock_request/views/stock_move_views.xml | 46 +++ stock_request/views/stock_picking_views.xml | 24 ++ .../views/stock_request_allocation_views.xml | 51 +++ stock_request/views/stock_request_views.xml | 143 +++++++ 20 files changed, 1426 insertions(+) create mode 100644 stock_request/README.rst create mode 100644 stock_request/__init__.py create mode 100644 stock_request/__manifest__.py create mode 100644 stock_request/data/stock_request_sequence_data.xml create mode 100644 stock_request/models/__init__.py create mode 100644 stock_request/models/procurement_rule.py create mode 100644 stock_request/models/stock_move.py create mode 100644 stock_request/models/stock_move_line.py create mode 100644 stock_request/models/stock_picking.py create mode 100644 stock_request/models/stock_request.py create mode 100644 stock_request/models/stock_request_allocation.py create mode 100644 stock_request/security/ir.model.access.csv create mode 100644 stock_request/security/stock_request_security.xml create mode 100644 stock_request/static/description/icon.png create mode 100644 stock_request/tests/__init__.py create mode 100644 stock_request/tests/test_stock_request.py create mode 100644 stock_request/views/stock_move_views.xml create mode 100644 stock_request/views/stock_picking_views.xml create mode 100644 stock_request/views/stock_request_allocation_views.xml create mode 100644 stock_request/views/stock_request_views.xml diff --git a/stock_request/README.rst b/stock_request/README.rst new file mode 100644 index 000000000..f3d0cca06 --- /dev/null +++ b/stock_request/README.rst @@ -0,0 +1,88 @@ +.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg + :target: https://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 + +============= +Stock Request +============= + +This module was written to allow users to request products that are +frequently stocked by the company, to be transferred to their chosen location. + + +Configuration +============= + +Users should be assigned to the groups 'Stock Request / User' or 'Stock +Request / Manager'. + +Group Stock Request / User +-------------------------- + +* Can see her/his own Stock Requests, and others that she/he's been granted + permission to follow. + +* Can create/update only her/his Stock Requests. + +Group Stock Request / Manager +----------------------------- + +* Can fully manage all Stock Requests + + +Usage +===== + +Creation +-------- +* Go to 'Stock Requests / Stock Requests' and create a new Request. +* Indicate a product, quantity and location. +* Press 'Confirm'. + +Upon confirmation the request will be evaluated using the procurement rules +for the selected location. + +In case that transfers are created, the user will be able to access to them +from the button 'Transfers' available in the Stock Request. + +Cancel +------ +When the user cancels a Stock Request, the related pending stock moves will be +also cancelled. + + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/153/11.0 + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of +trouble, please check there if your issue has already been reported. If you +spotted it first, help us smash it by providing detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Jordi Ballester (EFICENT) . + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/stock_request/__init__.py b/stock_request/__init__.py new file mode 100644 index 000000000..a9e337226 --- /dev/null +++ b/stock_request/__init__.py @@ -0,0 +1,2 @@ + +from . import models diff --git a/stock_request/__manifest__.py b/stock_request/__manifest__.py new file mode 100644 index 000000000..309374eb5 --- /dev/null +++ b/stock_request/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright 2017 Eficent Business and IT Consulting Services, S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +{ + "name": "Stock Request", + "summary": "Internal request for stock", + "version": "11.0.1.0.0", + "license": "LGPL-3", + "website": "https://github.com/stock-logistics-warehouse", + "author": "Eficent, " + "Odoo Community Association (OCA)", + "category": "Warehouse Management", + "depends": [ + "stock", + ], + "data": [ + "security/stock_request_security.xml", + "security/ir.model.access.csv", + "views/stock_request_views.xml", + "views/stock_request_allocation_views.xml", + "views/stock_move_views.xml", + "views/stock_picking_views.xml", + "data/stock_request_sequence_data.xml" + ], + "installable": True, +} diff --git a/stock_request/data/stock_request_sequence_data.xml b/stock_request/data/stock_request_sequence_data.xml new file mode 100644 index 000000000..3f2f9fd8a --- /dev/null +++ b/stock_request/data/stock_request_sequence_data.xml @@ -0,0 +1,14 @@ + + + + + + Stock Request + stock.request + SR/ + 5 + + + + + diff --git a/stock_request/models/__init__.py b/stock_request/models/__init__.py new file mode 100644 index 000000000..a7e453e0c --- /dev/null +++ b/stock_request/models/__init__.py @@ -0,0 +1,7 @@ + +from . import stock_request +from . import stock_request_allocation +from . import stock_move +from . import stock_picking +from . import procurement_rule +from . import stock_move_line diff --git a/stock_request/models/procurement_rule.py b/stock_request/models/procurement_rule.py new file mode 100644 index 000000000..3d811ec3e --- /dev/null +++ b/stock_request/models/procurement_rule.py @@ -0,0 +1,20 @@ +# 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 models + + +class ProcurementRule(models.Model): + _inherit = 'procurement.rule' + + def _get_stock_move_values(self, product_id, product_qty, product_uom, + location_id, name, origin, values, group_id): + result = super(ProcurementRule, self)._get_stock_move_values( + product_id, product_qty, product_uom, location_id, name, origin, + values, group_id) + if values.get('stock_request_id', False): + result['allocation_ids'] = [(0, 0, { + 'stock_request_id': values['stock_request_id'], + 'requested_product_uom_qty': product_qty, + })] + return result diff --git a/stock_request/models/stock_move.py b/stock_request/models/stock_move.py new file mode 100644 index 000000000..2beddd42f --- /dev/null +++ b/stock_request/models/stock_move.py @@ -0,0 +1,28 @@ +# 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 + + +class StockMove(models.Model): + _inherit = 'stock.move' + + allocation_ids = fields.One2many(comodel_name='stock.request.allocation', + inverse_name='stock_move_id', + string='Stock Request Allocation') + + stock_request_ids = fields.One2many(comodel_name='stock.request', + string='Stock Requests', + compute='_compute_stock_request_ids') + + @api.depends('allocation_ids') + def _compute_stock_request_ids(self): + for rec in self: + rec.stock_request_ids = rec.allocation_ids.mapped( + 'stock_request_id') + + def _merge_moves_fields(self): + res = super(StockMove, self)._merge_moves_fields() + res['allocation_ids'] = [(4, m.id) for m in + self.mapped('allocation_ids')] + return res diff --git a/stock_request/models/stock_move_line.py b/stock_request/models/stock_move_line.py new file mode 100644 index 000000000..13f67a87d --- /dev/null +++ b/stock_request/models/stock_move_line.py @@ -0,0 +1,64 @@ +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl-3.0). + +from odoo import _, api, models + + +class StockMoveLine(models.Model): + _inherit = "stock.move.line" + + @api.model + def _stock_request_confirm_done_message_content(self, message_data): + title = _('Receipt confirmation %s for your Request %s') % ( + message_data['picking_name'], message_data['request_name']) + message = '

%s

' % title + message += _('The following requested items from Stock Request %s ' + 'have now been received in %s using Picking %s:') % ( + message_data['request_name'], message_data['location_name'], + message_data['picking_name']) + message += '
    ' + message += _( + '
  • %s: Transferred quantity %s %s
  • ' + ) % (message_data['product_name'], + message_data['product_qty'], + message_data['product_uom'], + ) + message += '
' + return message + + def _prepare_message_data(self, ml, request, allocated_qty): + return { + 'request_name': request.name, + 'picking_name': ml.picking_id.name, + 'product_name': ml.product_id.name_get()[0][1], + 'product_qty': allocated_qty, + 'product_uom': ml.product_uom_id.name, + 'location_name': ml.location_dest_id.name_get()[0][1], + } + + def _action_done(self): + res = super(StockMoveLine, self)._action_done() + for ml in self.filtered( + lambda m: m.move_id.allocation_ids): + qty_done = ml.product_uom_id._compute_quantity( + ml.qty_done, ml.product_id.uom_id) + + # We do sudo because potentially the user that completes the move + # may not have permissions for stock.request. + to_allocate_qty = ml.qty_done + for allocation in ml.move_id.allocation_ids.sudo(): + allocated_qty = 0.0 + if allocation.open_product_qty: + allocated_qty = min( + allocation.open_product_qty, qty_done) + allocation.allocated_product_qty += allocated_qty + to_allocate_qty -= allocated_qty + request = allocation.stock_request_id + message_data = self._prepare_message_data(ml, request, + allocated_qty) + message = \ + self._stock_request_confirm_done_message_content( + message_data) + request.message_post(body=message, subtype='mail.mt_comment') + request.check_done() + return res diff --git a/stock_request/models/stock_picking.py b/stock_request/models/stock_picking.py new file mode 100644 index 000000000..6f6e77b33 --- /dev/null +++ b/stock_request/models/stock_picking.py @@ -0,0 +1,37 @@ +# 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 + + +class StockPicking(models.Model): + _inherit = 'stock.picking' + + stock_request_ids = fields.One2many(comodel_name='stock.request', + string='Stock Requests', + compute='_compute_stock_request_ids') + stock_request_count = fields.Integer('Stock Request #', + compute='_compute_stock_request_ids') + + @api.depends('move_lines') + def _compute_stock_request_ids(self): + for rec in self: + rec.stock_request_ids = rec.move_lines.mapped('stock_request_ids') + rec.stock_request_count = len(rec.stock_request_ids) + + def action_view_stock_request(self): + """ + :return dict: dictionary value for created view + """ + action = self.env.ref( + 'stock_request.action_stock_request_form').read()[0] + + requests = self.mapped('stock_request_ids') + if len(requests) > 1: + action['domain'] = [('id', 'in', requests.ids)] + elif requests: + action['views'] = [ + (self.env.ref('stock_request.view_stock_request_form').id, + 'form')] + action['res_id'] = requests.id + return action diff --git a/stock_request/models/stock_request.py b/stock_request/models/stock_request.py new file mode 100644 index 000000000..690bbb2d7 --- /dev/null +++ b/stock_request/models/stock_request.py @@ -0,0 +1,334 @@ +# 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 UserError, ValidationError +from odoo.addons import decimal_precision as dp +from odoo.tools import float_compare + + +REQUEST_STATES = [ + ('draft', 'Draft'), + ('open', 'In progress'), + ('done', 'Done'), + ('cancel', 'Cancelled')] + + +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 + + 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): + self.product_qty = self.product_uom_id._compute_quantity( + self.product_uom_qty, self.product_id.uom_id) + + name = fields.Char( + 'Name', copy=False, required=True, readonly=True, + states={'draft': [('readonly', False)]}, + default=lambda self: self.env['ir.sequence'].next_by_code( + 'stock.request')) + state = fields.Selection(selection=REQUEST_STATES, string='Status', + copy=False, default='draft', index=True, + readonly=True, track_visibility='onchange', + ) + requested_by = fields.Many2one( + 'res.users', 'Requested by', required=True, + 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, + states={'draft': [('readonly', False)]}, + help="Date when you expect to receive the goods.", + ) + picking_policy = fields.Selection([ + ('direct', 'Receive each product when available'), + ('one', 'Receive all products at once')], + string='Shipping Policy', required=True, readonly=True, + states={'draft': [('readonly', False)]}, + default='direct', + ) + move_ids = fields.One2many(comodel_name='stock.move', + compute='_compute_move_ids', + string='Stock Moves', readonly=True, + ) + picking_ids = fields.One2many('stock.picking', + compute='_compute_picking_ids', + string='Pickings', readonly=True, + ) + qty_in_progress = fields.Float( + 'Qty In Progress', digits=dp.get_precision('Product Unit of Measure'), + readonly=True, compute='_compute_qty', store=True, + help="Quantity in progress.", + ) + qty_done = fields.Float( + 'Qty Done', digits=dp.get_precision('Product Unit of Measure'), + readonly=True, compute='_compute_qty', store=True, + help="Quantity completed", + ) + picking_count = fields.Integer(string='Delivery Orders', + compute='_compute_picking_ids', + readonly=True, + ) + route_id = fields.Many2one('stock.location.route', string='Route', + readonly=True, + states={'draft': [('readonly', False)]}, + ondelete='restrict') + + allocation_ids = fields.One2many(comodel_name='stock.request.allocation', + inverse_name='stock_request_id', + string='Stock Request Allocation') + + _sql_constraints = [ + ('name_uniq', 'unique(name, company_id)', + 'Stock Request name must be unique'), + ] + + @api.depends('allocation_ids') + def _compute_move_ids(self): + for request in self.sudo(): + request.move_ids = request.allocation_ids.mapped('stock_move_id') + + @api.depends('allocation_ids') + def _compute_picking_ids(self): + for request in self.sudo(): + request.picking_count = 0 + request.picking_ids = self.env['stock.picking'] + request.picking_ids = request.move_ids.filtered( + lambda m: m.state != 'cancel').mapped('picking_id') + request.picking_count = len(request.picking_ids) + + @api.depends('allocation_ids', 'allocation_ids.stock_move_id.state', + 'allocation_ids.stock_move_id.move_line_ids', + 'allocation_ids.stock_move_id.move_line_ids.qty_done') + def _compute_qty(self): + for request in self.sudo(): + done_qty = sum(request.allocation_ids.mapped( + 'allocated_product_qty')) + open_qty = sum(request.allocation_ids.mapped('open_product_qty')) + request.qty_done = request.product_id.uom_id._compute_quantity( + done_qty, request.product_uom_id) + request.qty_in_progress = \ + request.product_id.uom_id._compute_quantity( + open_qty, request.product_uom_id) + + @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. """ + if self.warehouse_id: + 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 {} + + @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): + if self.product_id: + self.product_uom_id = self.product_id.uom_id.id + return { + 'domain': { + 'product_uom_id': + [('category_id', '=', + self.product_id.uom_id.category_id.id)]}} + return {'domain': {'product_uom_id': []}} + + @api.multi + def _action_confirm(self): + self._action_launch_procurement_rule() + self.state = 'open' + + @api.multi + def action_confirm(self): + self._action_confirm() + return True + + def action_draft(self): + self.state = 'draft' + return True + + def action_cancel(self): + self.sudo().mapped('move_ids')._action_cancel() + self.state = 'cancel' + return True + + def action_done(self): + self.state = 'done' + return True + + def check_done(self): + precision = self.env['decimal.precision'].precision_get( + 'Product Unit of Measure') + for request in self: + allocated_qty = sum(request.allocation_ids.mapped( + 'allocated_product_qty')) + qty_done = request.product_id.uom_id._compute_quantity( + allocated_qty, request.product_uom_id) + if float_compare(qty_done, request.product_uom_qty, + precision_digits=precision) >= 0: + request.action_done() + return True + + def _prepare_procurement_values(self, group_id=False): + + """ Prepare specific key for moves or other components that + will be created from a procurement rule + coming from a stock request. This method could be override + in order to add other custom key that could be used in + move/po creation. + """ + return { + 'date_planned': self.expected_date, + 'warehouse_id': self.warehouse_id, + 'stock_request_allocation_ids': self.id, + 'group_id': group_id or self.procurement_group_id.id or False, + 'route_ids': self.route_id, + 'stock_request_id': self.id, + } + + @api.multi + def _action_launch_procurement_rule(self): + """ + Launch procurement group run method with required/custom + fields genrated by a + stock request. procurement group will launch '_run_move', + '_run_buy' or '_run_manufacture' + depending on the stock request product rule. + """ + precision = self.env['decimal.precision'].precision_get( + 'Product Unit of Measure') + errors = [] + for request in self: + if ( + request.state != 'draft' or + request.product_id.type not in ('consu', 'product') + ): + continue + qty = 0.0 + for move in request.move_ids.filtered( + lambda r: r.state != 'cancel'): + qty += move.product_qty + + if float_compare(qty, request.product_qty, + precision_digits=precision) >= 0: + continue + + values = request._prepare_procurement_values( + group_id=request.procurement_group_id) + try: + # We launch with sudo because potentially we could create + # objects that the user is not authorized to create, such + # as PO. + self.env['procurement.group'].sudo().run( + request.product_id, request.product_uom_qty, + request.product_uom_id, + request.location_id, request.name, + request.name, values) + except UserError as error: + errors.append(error.name) + if errors: + raise UserError('\n'.join(errors)) + return True + + @api.multi + def action_view_transfer(self): + action = self.env.ref('stock.action_picking_tree_all').read()[0] + + pickings = self.mapped('picking_ids') + if len(pickings) > 1: + action['domain'] = [('id', 'in', pickings.ids)] + elif pickings: + action['views'] = [ + (self.env.ref('stock.view_picking_form').id, 'form')] + action['res_id'] = pickings.id + return action diff --git a/stock_request/models/stock_request_allocation.py b/stock_request/models/stock_request_allocation.py new file mode 100644 index 000000000..fbd6c684b --- /dev/null +++ b/stock_request/models/stock_request_allocation.py @@ -0,0 +1,65 @@ +# 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 + + +class StockRequestAllocation(models.Model): + _name = 'stock.request.allocation' + _description = 'Stock Request Allocation' + + stock_request_id = fields.Many2one(string='Stock Request', + comodel_name='stock.request', + required=True, ondelete='cascade', + ) + stock_move_id = fields.Many2one(string='Stock Move', + comodel_name='stock.move', + required=True, ondelete='cascade', + ) + product_id = fields.Many2one(string='Product', + comodel_name='product.product', + related='stock_request_id.product_id', + readonly=True, + ) + product_uom_id = fields.Many2one(string='UoM', comodel_name='product.uom', + related='stock_request_id.product_uom_id', + readonly=True, + ) + requested_product_uom_qty = fields.Float( + 'Requested Quantity (UoM)', + help='Quantity of the stock request allocated to the stock move, ' + 'in the UoM of the Stock Request', + ) + requested_product_qty = fields.Float( + 'Requested Quantity', + help='Quantity of the stock request allocated to the stock move, ' + 'in the default UoM of the product', + compute='_compute_requested_product_qty' + ) + allocated_product_qty = fields.Float( + 'Allocated Quantity', + help='Quantity of the stock request allocated to the stock move, ' + 'in the default UoM of the product', + ) + open_product_qty = fields.Float('Open Quantity', + compute='_compute_open_product_qty') + + @api.depends('stock_request_id.product_id', + 'stock_request_id.product_uom_id', + 'requested_product_uom_qty') + def _compute_requested_product_qty(self): + for rec in self: + rec.requested_product_qty = rec.product_uom_id._compute_quantity( + rec.requested_product_uom_qty, rec.product_id.uom_id) + + @api.depends('requested_product_qty', 'allocated_product_qty', + 'stock_move_id', 'stock_move_id.state') + def _compute_open_product_qty(self): + for rec in self: + if rec.stock_move_id.state == 'cancel': + rec.open_product_qty = 0.0 + else: + rec.open_product_qty = \ + rec.requested_product_qty - rec.allocated_product_qty + if rec.open_product_qty < 0.0: + rec.open_product_qty = 0.0 diff --git a/stock_request/security/ir.model.access.csv b/stock_request/security/ir.model.access.csv new file mode 100644 index 000000000..ca66a1a53 --- /dev/null +++ b/stock_request/security/ir.model.access.csv @@ -0,0 +1,13 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_stock_request_user,stock request user,model_stock_request,group_stock_request_user,1,1,1, +access_stock_request_manager,stock request manager,model_stock_request,group_stock_request_manager,1,1,1,1 +access_stock_request_stock_user,stock.request stock user,model_stock_request,stock.group_stock_user,1,0,0,0 +access_stock_request_allocation_user,stock request allocation user,model_stock_request_allocation,group_stock_request_user,1,1,1,1 +access_stock_request_allocation_manager,stock request allocation manager,model_stock_request_allocation,group_stock_request_manager,1,1,1,1 +access_stock_request_allocation_stock_user,stock.request.allocation stock user,model_stock_request_allocation,stock.group_stock_user,1,0,0,0 +access_stock_request_manager,stock request manager,model_stock_request,group_stock_request_manager,1,1,1,1 +access_stock_warehouse_user,stock.warehouse.user,stock.model_stock_warehouse,group_stock_request_user,1,0,0,0 +access_stock_location_user,stock.location.user,stock.model_stock_location,group_stock_request_user,1,0,0,0 +access_stock_location_request_manager,stock.location request manager,stock.model_stock_location,group_stock_request_manager,1,0,0,0 +access_procurement_rule_request_manager,procurement_rule request_manager,stock.model_procurement_rule,group_stock_request_manager,1,0,0,0 +access_procurement_rule,procurement.rule.flow,stock.model_procurement_rule,group_stock_request_user,1,0,0,0 diff --git a/stock_request/security/stock_request_security.xml b/stock_request/security/stock_request_security.xml new file mode 100644 index 000000000..ecb96c13e --- /dev/null +++ b/stock_request/security/stock_request_security.xml @@ -0,0 +1,72 @@ + + + + + Stock Request + + 10 + + + + Stock Request User + + + + + + Stock Request Manager + + + + + + + + + + + + stock_request multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + + Follow Stock Request + + + + + + + ['|',('requested_by','=',user.id), + ('message_partner_ids', 'in', [user.partner_id.id])] + + + + Stock Request User + + + + + + + [('requested_by','=',user.id)] + + + + Stock Request Manager + + + + + + + + + + diff --git a/stock_request/static/description/icon.png b/stock_request/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a7c11c7a1e3df3654b45284d5b92ca7c86e56b4e GIT binary patch literal 4857 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 + + + + stock.move.operations.form + stock.move + + + + + + + + + + + stock.move.form + stock.move + + + + + + + + + + + + + stock.move.form + stock.move + + + + + + + + + + + + diff --git a/stock_request/views/stock_picking_views.xml b/stock_request/views/stock_picking_views.xml new file mode 100644 index 000000000..1ab3802d9 --- /dev/null +++ b/stock_request/views/stock_picking_views.xml @@ -0,0 +1,24 @@ + + + + + stock.picking.form + stock.picking + + + +
+ +
+
+
+ +
diff --git a/stock_request/views/stock_request_allocation_views.xml b/stock_request/views/stock_request_allocation_views.xml new file mode 100644 index 000000000..74afe8913 --- /dev/null +++ b/stock_request/views/stock_request_allocation_views.xml @@ -0,0 +1,51 @@ + + + + + + stock.request.allocation.tree + stock.request.allocation + + + + + + + + + + + + + + + + stock.request.allocation.form + stock.request.allocation + +
+ + + + + + + + + + + + + + + + +
+
+
+ +
diff --git a/stock_request/views/stock_request_views.xml b/stock_request/views/stock_request_views.xml new file mode 100644 index 000000000..9734edc16 --- /dev/null +++ b/stock_request/views/stock_request_views.xml @@ -0,0 +1,143 @@ + + + + + + stock.request.tree + stock.request + + + + + + + + + + + + + + + + + + + stock.request.search + stock.request + + + + + + + + + + + + + + + + + + stock.request.form + stock.request + +
+
+
+ +
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+ + + Stock Requests + stock.request + ir.actions.act_window + form + tree,form + + + +

+ Click to add a Stock Request. +

+
+
+ + + + + +