add stock_request in 11.0

This commit is contained in:
Jordi Ballester
2017-11-13 13:15:41 +01:00
committed by Kitti U
parent 432b9837ac
commit 09dfc7050b
20 changed files with 1426 additions and 0 deletions

88
stock_request/README.rst Normal file
View File

@@ -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
<https://github.com/OCA/stock-logistics-warehouse/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) <jordi.ballester@eficent.com>.
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.

View File

@@ -0,0 +1,2 @@
from . import models

View File

@@ -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,
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="seq_stock_request" model="ir.sequence">
<field name="name">Stock Request</field>
<field name="code">stock.request</field>
<field name="prefix">SR/</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
</data>
</odoo>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 = '<h3>%s</h3>' % 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 += '<ul>'
message += _(
'<li><b>%s</b>: Transferred quantity %s %s</li>'
) % (message_data['product_name'],
message_data['product_qty'],
message_data['product_uom'],
)
message += '</ul>'
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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_stock_request_user stock request user model_stock_request group_stock_request_user 1 1 1
3 access_stock_request_manager stock request manager model_stock_request group_stock_request_manager 1 1 1 1
4 access_stock_request_stock_user stock.request stock user model_stock_request stock.group_stock_user 1 0 0 0
5 access_stock_request_allocation_user stock request allocation user model_stock_request_allocation group_stock_request_user 1 1 1 1
6 access_stock_request_allocation_manager stock request allocation manager model_stock_request_allocation group_stock_request_manager 1 1 1 1
7 access_stock_request_allocation_stock_user stock.request.allocation stock user model_stock_request_allocation stock.group_stock_user 1 0 0 0
8 access_stock_request_manager stock request manager model_stock_request group_stock_request_manager 1 1 1 1
9 access_stock_warehouse_user stock.warehouse.user stock.model_stock_warehouse group_stock_request_user 1 0 0 0
10 access_stock_location_user stock.location.user stock.model_stock_location group_stock_request_user 1 0 0 0
11 access_stock_location_request_manager stock.location request manager stock.model_stock_location group_stock_request_manager 1 0 0 0
12 access_procurement_rule_request_manager procurement_rule request_manager stock.model_procurement_rule group_stock_request_manager 1 0 0 0
13 access_procurement_rule procurement.rule.flow stock.model_procurement_rule group_stock_request_user 1 0 0 0

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.module.category" id="module_category_stock_request">
<field name="name">Stock Request</field>
<field name="parent_id" ref="base.module_category_warehouse_management"/>
<field name="sequence">10</field>
</record>
<record id="group_stock_request_user" model="res.groups">
<field name="name">Stock Request User</field>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
<field name="category_id" ref="module_category_stock_request"/>
</record>
<record id="group_stock_request_manager" model="res.groups">
<field name="name">Stock Request Manager</field>
<field name="users" eval="[(4, ref('base.user_root'))]"/>
<field name="implied_ids"
eval="[(4, ref('stock_request.group_stock_request_user'))]"/>
<field name="category_id" ref="module_category_stock_request"/>
</record>
<record id="stock.group_stock_user" model="res.groups">
<field name="implied_ids"
eval="[(4, ref('group_stock_request_user'))]"/>
</record>
<data noupdate="1">
<record model="ir.rule" id="stock_picking_rule">
<field name="name">stock_request multi-company</field>
<field name="model_id" search="[('model','=','stock.request')]"
model="ir.model"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
</record>
<record id="stock_request_followers_rule" model="ir.rule">
<field name="name">Follow Stock Request</field>
<field name="model_id" ref="model_stock_request"/>
<field name="groups" eval="[(6,0, [ref('group_stock_request_user')])]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="domain_force">['|',('requested_by','=',user.id),
('message_partner_ids', 'in', [user.partner_id.id])]</field>
</record>
<record id="stock_request_rule" model="ir.rule">
<field name="name">Stock Request User</field>
<field name="model_id" ref="model_stock_request"/>
<field name="groups" eval="[(6,0, [ref('group_stock_request_user')])]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
<field name="domain_force">[('requested_by','=',user.id)]</field>
</record>
<record id="stock_request_manager_rule" model="ir.rule">
<field name="name">Stock Request Manager</field>
<field name="model_id" ref="model_stock_request"/>
<field name="groups" eval="[(6,0, [ref('group_stock_request_manager')])]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
</record>
</data>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -0,0 +1,2 @@
from . import test_stock_request

View File

@@ -0,0 +1,390 @@
# 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.tests import common
from odoo import exceptions
class TestStockRequest(common.TransactionCase):
def setUp(self):
super(TestStockRequest, self).setUp()
# common models
self.stock_request = self.env['stock.request']
# refs
self.stock_request_user_group = \
self.env.ref('stock_request.group_stock_request_user')
self.stock_request_manager_group = \
self.env.ref('stock_request.group_stock_request_manager')
self.main_company = self.env.ref('base.main_company')
self.warehouse = self.env.ref('stock.warehouse0')
self.categ_unit = self.env.ref('product.product_uom_categ_unit')
# common data
self.company_2 = self.env['res.company'].create({
'name': 'Comp2',
})
self.wh2 = self.env['stock.warehouse'].search(
[('company_id', '=', self.company_2.id)], limit=1)
self.stock_request_user = self._create_user(
'stock_request_user',
[self.stock_request_user_group.id],
[self.main_company.id, self.company_2.id])
self.stock_request_manager = self._create_user(
'stock_request_manager',
[self.stock_request_manager_group.id],
[self.main_company.id, self.company_2.id])
self.product = self._create_product('SH', 'Shoes', False)
self.ressuply_loc = self.env['stock.location'].create({
'name': 'Ressuply',
'location_id': self.warehouse.view_location_id.id,
})
self.route = self.env['stock.location.route'].create({
'name': 'Transfer',
'product_categ_selectable': False,
'product_selectable': True,
'company_id': self.main_company.id,
'sequence': 10,
})
self.uom_dozen = self.env['product.uom'].create({
'name': 'Test-DozenA',
'category_id': self.categ_unit.id,
'factor_inv': 12,
'uom_type': 'bigger',
'rounding': 0.001})
self.env['procurement.rule'].create({
'name': 'Transfer',
'route_id': self.route.id,
'location_src_id': self.ressuply_loc.id,
'location_id': self.warehouse.lot_stock_id.id,
'action': 'move',
'picking_type_id': self.warehouse.int_type_id.id,
'procure_method': 'make_to_stock',
'warehouse_id': self.warehouse.id,
'company_id': self.main_company.id,
'propagate': 'False',
})
def _create_user(self, name, group_ids, company_ids):
return self.env['res.users'].with_context(
{'no_reset_password': True}).create(
{'name': name,
'password': 'demo',
'login': name,
'email': '@'.join([name, '@test.com']),
'groups_id': [(6, 0, group_ids)],
'company_ids': [(6, 0, company_ids)]
})
def _create_product(self, default_code, name, company_id):
return self.env['product.product'].create({
'name': name,
'default_code': default_code,
'uom_id': self.env.ref('product.product_uom_unit').id,
'company_id': company_id,
'type': 'product',
})
def test_defaults(self):
vals = {
'product_id': self.product.id,
'product_uom_id': self.product.uom_id.id,
'product_uom_qty': 5.0,
}
stock_request = self.stock_request.sudo(
self.stock_request_user.id).with_context(
company_id=self.main_company.id).create(vals)
self.assertEqual(
stock_request.requested_by, self.stock_request_user)
self.assertEqual(
stock_request.warehouse_id, self.warehouse)
self.assertEqual(
stock_request.location_id, self.warehouse.lot_stock_id)
def test_onchanges(self):
vals = {
'product_id': self.product.id,
'product_uom_id': self.product.uom_id.id,
'product_uom_qty': 5.0,
'company_id': self.main_company.id,
}
stock_request = self.stock_request.sudo(
self.stock_request_user).new(vals)
self.stock_request_user.company_id = self.company_2
stock_request.default_get(['warehouse_id', 'company_id'])
stock_request.company_id = self.company_2
stock_request.onchange_company_id()
self.assertEqual(
stock_request.warehouse_id, self.wh2)
self.assertEqual(
stock_request.location_id, self.wh2.lot_stock_id)
product = self.env['product.product'].create({
'name': 'Wheat',
'uom_id': self.env.ref('product.product_uom_kgm').id,
'uom_po_id': self.env.ref('product.product_uom_kgm').id,
})
# Test onchange_product_id
stock_request.product_id = product
res = stock_request.onchange_product_id()
self.assertEqual(res['domain']['product_uom_id'],
[('category_id', '=',
product.uom_id.category_id.id)])
self.assertEqual(
stock_request.product_uom_id,
self.env.ref('product.product_uom_kgm'))
stock_request.product_id = self.env['product.product']
res = stock_request.onchange_product_id()
self.assertEqual(res['domain']['product_uom_id'], [])
# Test onchange_warehouse_id
wh2_2 = self.env['stock.warehouse'].with_context(
company_id=self.company_2.id).create({
'name': 'C2_2',
'code': 'C2_2',
'company_id': self.company_2.id
})
stock_request.warehouse_id = wh2_2
stock_request.onchange_warehouse_id()
self.assertEqual(stock_request.warehouse_id, wh2_2)
self.stock_request_user.company_id = self.main_company
stock_request.warehouse_id = self.warehouse
stock_request.onchange_warehouse_id()
self.assertEqual(
stock_request.company_id, self.main_company)
self.assertEqual(
stock_request.location_id, self.warehouse.lot_stock_id)
def test_stock_request_validations_01(self):
vals = {
'product_id': self.product.id,
'product_uom_id': self.env.ref('product.product_uom_kgm').id,
'product_uom_qty': 5.0,
'company_id': self.main_company.id,
'warehouse_id': self.warehouse.id,
'location_id': self.warehouse.lot_stock_id.id,
}
# Select a UoM that is incompatible with the product's UoM
with self.assertRaises(exceptions.ValidationError):
self.stock_request.sudo(
self.stock_request_user).create(vals)
def test_stock_request_validations_02(self):
vals = {
'product_id': self.product.id,
'product_uom_id': self.product.uom_id.id,
'product_uom_qty': 5.0,
'company_id': self.main_company.id,
'warehouse_id': self.warehouse.id,
'location_id': self.warehouse.lot_stock_id.id,
}
stock_request = self.stock_request.sudo(
self.stock_request_user).create(vals)
# With no route found, should raise an error
with self.assertRaises(exceptions.UserError):
stock_request.action_confirm()
def test_create_request_01(self):
vals = {
'product_id': self.product.id,
'product_uom_id': self.product.uom_id.id,
'product_uom_qty': 5.0,
'company_id': self.main_company.id,
'warehouse_id': self.warehouse.id,
'location_id': self.warehouse.lot_stock_id.id,
}
stock_request = self.stock_request.sudo(
self.stock_request_user).create(vals)
self.product.route_ids = [(6, 0, self.route.ids)]
stock_request.action_confirm()
self.assertEqual(stock_request.state, 'open')
self.assertEqual(len(stock_request.sudo().picking_ids), 1)
self.assertEqual(len(stock_request.sudo().move_ids), 1)
self.assertEqual(stock_request.sudo().move_ids[0].location_dest_id,
stock_request.location_id)
self.assertEqual(stock_request.qty_in_progress,
stock_request.product_uom_qty)
self.env['stock.quant'].create({
'product_id': self.product.id,
'location_id': self.ressuply_loc.id,
'quantity': 5.0})
picking = stock_request.sudo().picking_ids[0]
picking.action_confirm()
self.assertEqual(stock_request.qty_in_progress, 5.0)
self.assertEqual(stock_request.qty_done, 0.0)
picking.action_assign()
packout1 = picking.move_line_ids[0]
packout1.qty_done = 5
picking.action_done()
self.assertEqual(stock_request.qty_in_progress, 0.0)
self.assertEqual(stock_request.qty_done,
stock_request.product_uom_qty)
self.assertEqual(stock_request.state, 'done')
def test_create_request_02(self):
"""Use different UoM's"""
vals = {
'product_id': self.product.id,
'product_uom_id': self.uom_dozen.id,
'product_uom_qty': 1.0,
'company_id': self.main_company.id,
'warehouse_id': self.warehouse.id,
'location_id': self.warehouse.lot_stock_id.id,
}
stock_request = self.stock_request.sudo(
self.stock_request_user).create(vals)
self.product.route_ids = [(6, 0, self.route.ids)]
stock_request.action_confirm()
self.assertEqual(stock_request.state, 'open')
self.assertEqual(len(stock_request.sudo().picking_ids), 1)
self.assertEqual(len(stock_request.sudo().move_ids), 1)
self.assertEqual(stock_request.sudo().move_ids[0].location_dest_id,
stock_request.location_id)
self.assertEqual(stock_request.qty_in_progress,
stock_request.product_uom_qty)
self.env['stock.quant'].create({
'product_id': self.product.id,
'location_id': self.ressuply_loc.id,
'quantity': 12.0})
picking = stock_request.sudo().picking_ids[0]
picking.action_confirm()
self.assertEqual(stock_request.qty_in_progress, 1.0)
self.assertEqual(stock_request.qty_done, 0.0)
picking.action_assign()
packout1 = picking.move_line_ids[0]
packout1.qty_done = 1
picking.action_done()
self.assertEqual(stock_request.qty_in_progress, 0.0)
self.assertEqual(stock_request.qty_done,
stock_request.product_uom_qty)
self.assertEqual(stock_request.state, 'done')
def test_create_request_03(self):
"""Multiple stock requests"""
vals = {
'product_id': self.product.id,
'product_uom_id': self.product.uom_id.id,
'product_uom_qty': 4.0,
'company_id': self.main_company.id,
'warehouse_id': self.warehouse.id,
'location_id': self.warehouse.lot_stock_id.id,
}
stock_request_1 = self.env['stock.request'].sudo(
self.stock_request_user).create(vals)
stock_request_2 = self.env['stock.request'].sudo(
self.stock_request_manager).create(vals)
stock_request_2.product_uom_qty = 6.0
self.product.route_ids = [(6, 0, self.route.ids)]
stock_request_1.action_confirm()
stock_request_2.action_confirm()
self.assertEqual(len(stock_request_1.sudo().picking_ids), 1)
self.assertEqual(stock_request_1.sudo().picking_ids,
stock_request_2.sudo().picking_ids)
self.assertEqual(stock_request_1.sudo().move_ids,
stock_request_2.sudo().move_ids)
self.env['stock.quant'].create({
'product_id': self.product.id,
'location_id': self.ressuply_loc.id,
'quantity': 10.0})
picking = stock_request_1.sudo().picking_ids[0]
picking.action_confirm()
picking.action_assign()
packout1 = picking.move_line_ids[0]
packout1.qty_done = 10
picking.action_done()
def test_cancel_request(self):
vals = {
'product_id': self.product.id,
'product_uom_id': self.product.uom_id.id,
'product_uom_qty': 5.0,
'company_id': self.main_company.id,
'warehouse_id': self.warehouse.id,
'location_id': self.warehouse.lot_stock_id.id,
}
stock_request = self.stock_request.sudo(
self.stock_request_user).create(vals)
self.product.route_ids = [(6, 0, self.route.ids)]
stock_request.action_confirm()
self.assertEqual(len(stock_request.sudo().picking_ids), 1)
self.assertEqual(len(stock_request.sudo().move_ids), 1)
self.assertEqual(stock_request.sudo().move_ids[0].location_dest_id,
stock_request.location_id)
self.assertEqual(stock_request.qty_in_progress,
stock_request.product_uom_qty)
self.env['stock.quant'].create({
'product_id': self.product.id,
'location_id': self.ressuply_loc.id,
'quantity': 5.0})
picking = stock_request.sudo().picking_ids[0]
picking.action_confirm()
self.assertEqual(stock_request.qty_in_progress, 5.0)
self.assertEqual(stock_request.qty_done, 0.0)
picking.action_assign()
stock_request.action_cancel()
self.assertEqual(stock_request.qty_in_progress, 0.0)
self.assertEqual(stock_request.qty_done, 0.0)
self.assertEqual(len(stock_request.sudo().picking_ids), 0)
# Set the request back to draft
stock_request.action_draft()
self.assertEqual(stock_request.state, 'draft')
# Re-confirm. We expect new pickings to be created
stock_request.action_confirm()
self.assertEqual(len(stock_request.sudo().picking_ids), 1)
self.assertEqual(len(stock_request.sudo().move_ids), 2)
def test_view_actions(self):
vals = {
'product_id': self.product.id,
'product_uom_id': self.product.uom_id.id,
'product_uom_qty': 5.0,
'company_id': self.main_company.id,
'warehouse_id': self.warehouse.id,
'location_id': self.warehouse.lot_stock_id.id,
}
stock_request = self.stock_request.sudo().create(vals)
self.product.route_ids = [(6, 0, self.route.ids)]
stock_request.action_confirm()
action = stock_request.action_view_transfer()
self.assertEqual('domain' in action.keys(), True)
self.assertEqual('views' in action.keys(), True)
self.assertEqual(action['res_id'], stock_request.picking_ids[0].id)
action = stock_request.picking_ids[0].action_view_stock_request()
self.assertEqual(action['type'], 'ir.actions.act_window')
self.assertEqual(action['res_id'], stock_request.id)

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_stock_move_operations" model="ir.ui.view">
<field name="name">stock.move.operations.form</field>
<field name="model">stock.move</field>
<field name="inherit_id" ref="stock.view_stock_move_operations"/>
<field name="arch" type="xml">
<field name="move_line_ids" position="after">
<newline/>
<field name="allocation_ids"/>
</field>
</field>
</record>
<record id="view_move_form" model="ir.ui.view">
<field name="name">stock.move.form</field>
<field name="model">stock.move</field>
<field name="inherit_id" ref="stock.view_move_form"/>
<field name="arch" type="xml">
<group name="linked_group" position="after">
<newline/>
<group name="allocations"
string="Stock Request Allocations">
<field name="allocation_ids"/>
</group>
</group>
</field>
</record>
<record id="view_move_picking_form" model="ir.ui.view">
<field name="name">stock.move.form</field>
<field name="model">stock.move</field>
<field name="inherit_id" ref="stock.view_move_picking_form"/>
<field name="arch" type="xml">
<group name="quants_grp" position="after">
<newline/>
<group name="allocations"
string="Stock Request Allocations">
<field name="allocation_ids"/>
</group>
</group>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_picking_form" model="ir.ui.view">
<field name="name">stock.picking.form</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form"/>
<field eval="12" name="priority"/>
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button type="object"
name="action_view_stock_request"
class="oe_stat_button"
icon="fa-chain"
attrs="{'invisible':[('stock_request_ids', '=', [])]}">
<field name="stock_request_count" widget="statinfo"
string="Stock Requests"/>
<field name="stock_request_ids" invisible="1"/>
</button>
</div>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Eficent
License LGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_stock_request_allocation_tree" model="ir.ui.view">
<field name="name">stock.request.allocation.tree</field>
<field name="model">stock.request.allocation</field>
<field name="arch" type="xml">
<tree string="Stock Request Allocations">
<field name="stock_request_id"/>
<field name="stock_move_id"/>
<field name="product_id"/>
<field name="requested_product_uom_qty"/>
<field name="product_uom_id"
options="{'no_open': True, 'no_create': True}" groups="product.group_uom"/>
<field name="requested_product_qty"/>
<field name="allocated_product_qty"/>
<field name="open_product_qty" />
</tree>
</field>
</record>
<record id="view_stock_request_allocation_form" model="ir.ui.view">
<field name="name">stock.request.allocation.form</field>
<field name="model">stock.request.allocation</field>
<field name="arch" type="xml">
<form string="Stock Request Allocations">
<sheet>
<group>
<group>
<field name="stock_request_id"/>
<field name="stock_move_id"/>
</group>
<group>
<field name="product_id"/>
<field name="requested_product_uom_qty"/>
<field name="product_uom_id"
options="{'no_open': True, 'no_create': True}"
groups="product.group_uom"/>
<field name="requested_product_qty"/>
<field name="allocated_product_qty"/>
<field name="open_product_qty" />
</group>
</group>
</sheet>
</form>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,143 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Eficent
License LGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_stock_request_tree" model="ir.ui.view">
<field name="name">stock.request.tree</field>
<field name="model">stock.request</field>
<field name="arch" type="xml">
<tree string="Stock Requests" decoration-muted="state == 'cancel'" decoration-bf="message_needaction==True">
<field name="message_needaction" invisible="1"/>
<field name="name"/>
<field name="warehouse_id" groups="stock.group_stock_multi_locations"/>
<field name="location_id" groups="stock.group_stock_multi_locations"/>
<field name="route_id" options="{'no_create': True}" groups="stock.group_stock_multi_locations"/>
<field name="product_id"/>
<field name="product_uom_id"
options="{'no_open': True, 'no_create': True}" groups="product.group_uom"/>
<field name="product_uom_qty"/>
<field name="qty_in_progress" />
<field name="qty_done" />
<field name="state"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="stock_request_search">
<field name="name">stock.request.search</field>
<field name="model">stock.request</field>
<field name="arch" type="xml">
<search string="Stock Requests Search">
<field name="name" string="Stock Requests"/>
<field name="warehouse_id"/>
<field name="location_id" groups="stock.group_stock_multi_locations"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="product_id"/>
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
<group expand="0" string="Group By">
<filter string="Warehouse" domain="[]" context="{'group_by':'warehouse_id'}"/>
<filter string="Location" domain="[]" context="{'group_by':'location_id'}"/>
</group>
</search>
</field>
</record>
<record id="view_stock_request_form" model="ir.ui.view">
<field name="name">stock.request.form</field>
<field name="model">stock.request</field>
<field name="arch" type="xml">
<form string="Stock Requests">
<header>
<button name="action_confirm"
string="Confirm" type="object"
attrs="{'invisible': [('state', 'not in', ['draft'])]}"/>
<button name="action_cancel" states="draft,open"
type="object" string="Cancel"/>
<button name="action_draft" states="cancel" type="object"
string="Set to Draft"/>
<button name="action_done"
string="Done" type="object"
attrs="{'invisible': [('state', 'not in', ['open'])]}"/>
<field name="state" widget="statusbar"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<field name="picking_ids" invisible="1"/>
<button type="object"
name="action_view_transfer"
class="oe_stat_button"
icon="fa-truck"
attrs="{'invisible': [('picking_count', '=', 0)]}"
groups="stock.group_stock_user">
<field name="picking_count" widget="statinfo"
string="Transfers"/>
</button>
</div>
<div class="oe_title">
<label string="Stock Request " />
<h1>
<field name="name" readonly="1"/>
</h1>
</div>
<group>
<group>
<field name="product_id"/>
<field name="expected_date"/>
<field name="picking_policy"/>
</group>
<group>
<field name="warehouse_id" widget="selection" groups="stock.group_stock_multi_locations"/>
<field name="product_uom_id"
options="{'no_open': True, 'no_create': True}" groups="product.group_uom"/>
<field name="location_id" groups="stock.group_stock_multi_locations"/>
<field name="route_id"
options="{'no_create': True}" groups="stock.group_stock_multi_locations"/>
<field name="procurement_group_id"
groups="stock.group_adv_location"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
</group>
<group name="quantities">
<field name="product_uom_qty"/>
<field name="qty_in_progress" />
<field name="qty_done" />
</group>
</group>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="activity_ids" widget="mail_activity"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<record id="action_stock_request_form" model="ir.actions.act_window">
<field name="name">Stock Requests</field>
<field name="res_model">stock.request</field>
<field name="type">ir.actions.act_window</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_stock_request_tree"/>
<field name="search_view_id" ref="stock_request_search" />
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to add a Stock Request.
</p>
</field>
</record>
<menuitem
id="menu_stock_request_root"
name="Stock Requests"
groups="stock_request.group_stock_request_user,stock_request.group_stock_request_manager"
sequence="100"/>
<menuitem
id="menu_stock_request"
action="action_stock_request_form"
name="Stock Requests" parent="menu_stock_request_root"
sequence="10"/>
</odoo>