[MIG] stock_available_unreserved: migrate Odoo 11

This commit is contained in:
mpanarin
2018-02-13 18:11:35 +02:00
committed by Lois Rilo
parent b8afda67f7
commit 07e173114f
10 changed files with 227 additions and 211 deletions

View File

@@ -23,7 +23,7 @@ Usage
.. 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/10.0
:target: https://runbot.odoo-community.org/runbot/153/11.0
Bug Tracker
@@ -47,6 +47,7 @@ Contributors
* Jordi Ballester Alomar <jordi.ballester@eficent.com>
* Stefan Rijnhart <stefan@opener.amsterdam>
* Mykhailo Panarin <m.panarin@mobilunity.com>
Maintainer

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
# Copyright 2016 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)

View File

@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Camptocamp SA
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
# Copyright 2016 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
@@ -6,16 +6,17 @@
{
"name": "Stock Available Unreserved",
"summary": "Quantity of stock available for immediate use",
"version": "10.0.1.0.0",
"version": "11.0.1.0.0",
"author": "Eficent Business and IT Consulting Services S.L,"
"Odoo Community Association (OCA)",
"website": "https://www.odoo-community.org",
"website": "https://github.com/OCA/stock-logistics-warehouse",
"category": "Warehouse Management",
"depends": ["stock"],
"data": ["views/stock_quant_view.xml",
"views/product_view.xml"
],
"depends": [
"stock",
],
"data": [
"views/stock_quant_view.xml",
"views/product_view.xml",
],
"license": "AGPL-3",
'installable': True,
'application': False,
}

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
# Copyright 2016 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import product
from . import quant

View File

@@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Camptocamp SA
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
# Copyright 2016 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import api, fields, models, _
from odoo.tools.float_utils import float_round
from odoo import api, fields, models
from odoo.addons import decimal_precision as dp
UNIT = dp.get_precision('Product Unit of Measure')
@@ -15,33 +14,36 @@ class ProductTemplate(models.Model):
_inherit = "product.template"
qty_available_not_res = fields.Float(
string='Quantity On Hand Unreserved', digits=UNIT,
compute='_compute_product_available_not_res')
qty_available_stock_text = fields.Char(
string='Quantity On Hand Unreserved',
digits=UNIT,
compute='_compute_product_available_not_res',
string='Unreserved stock quantity')
)
@api.multi
@api.depends('product_variant_ids.qty_available_not_res')
def _compute_product_available_not_res(self):
no_new = self.filtered(
lambda x: not isinstance(x.id, models.NewId))
for tmpl in no_new:
tmpl.qty_available_not_res = sum(tmpl.mapped(
'product_variant_ids.qty_available_not_res'))
tmpl.qty_available_stock_text = "/".join(tmpl.mapped(
'product_variant_ids.qty_available_stock_text'))
for tmpl in self:
if isinstance(tmpl.id, models.NewId):
continue
tmpl.qty_available_not_res = sum(
tmpl.mapped('product_variant_ids.qty_available_not_res')
)
@api.multi
def action_open_quants_unreserved(self):
products = self.mapped('product_variant_ids').ids
products_ids = self.mapped('product_variant_ids').ids
quants = self.env['stock.quant'].search([
('product_id', 'in', products_ids),
])
quant_ids = quants.filtered(
lambda x: x.product_id.qty_available_not_res > 0
).ids
result = self.env.ref('stock.product_open_quants').read()[0]
result['domain'] = "[('product_id','in',[" + ','.join(
map(str, products)) + "]), ('reservation_id', '=', False)]"
result[
'context'] = "{'search_default_locationgroup': 1, " \
"'search_default_internal_loc': 1}"
result['domain'] = [('id', 'in', quant_ids)]
result['context'] = {
'search_default_locationgroup': 1,
'search_default_internal_loc': 1,
}
return result
@@ -49,68 +51,55 @@ class ProductProduct(models.Model):
_inherit = 'product.product'
qty_available_not_res = fields.Float(
string='Qty Available Not Reserved', digits=UNIT,
compute='_compute_qty_available_not_res')
qty_available_stock_text = fields.Char(
compute='_compute_qty_available_not_res', string='Available per stock')
@api.multi
def _compute_qty_available_not_res(self):
res = self._compute_product_available_not_res_dict()
for prod in self:
qty = res[prod.id]['qty_available_not_res']
text = res[prod.id]['qty_available_stock_text']
prod.qty_available_not_res = qty
prod.qty_available_stock_text = text
return res
@api.model
def _prepare_domain_available_not_res(self, products):
domain_products = [('product_id', 'in', products.mapped('id'))]
domain_quant = []
domain_quant_loc = products._get_domain_locations()[0]
domain_quant += domain_products
domain_quant.append(('reservation_id', '=', False))
domain_quant += domain_quant_loc
return domain_quant
string='Qty Available Not Reserved',
digits=UNIT,
compute='_compute_qty_available_not_reserved',
)
@api.multi
def _product_available_not_res_hook(self, quants):
"""Hook used to introduce possible variations"""
return False
@api.multi
def _prepare_domain_available_not_reserved(self):
domain_quant = [
('product_id', 'in', self.ids),
('contains_unreserved', '=', True),
]
domain_quant_locations = self._get_domain_locations()[0]
domain_quant.extend(domain_quant_locations)
return domain_quant
@api.multi
def _compute_product_available_not_res_dict(self):
res = {}
domain_quant = self._prepare_domain_available_not_res(self)
quants = self.env['stock.quant'].with_context(lang=False).read_group(
domain_quant = self._prepare_domain_available_not_reserved()
quants = self.env['stock.quant'].with_context(lang=False).search(
domain_quant,
['product_id', 'location_id', 'qty'],
['product_id', 'location_id'],
lazy=False)
values_prod = {}
for quant in quants:
# create a dictionary with the total value per products
values_prod.setdefault(quant['product_id'][0], 0)
values_prod[quant['product_id'][0]] += quant['qty']
for product in self.with_context(prefetch_fields=False, lang=''):
res[product.id] = {}
# get total qty for the product
qty = float_round(values_prod.get(product.id, 0.0),
precision_rounding=product.uom_id.rounding)
qty_available_not_res = qty
res[product.id].update({'qty_available_not_res':
qty_available_not_res})
text = str(qty_available_not_res) + _(" On Hand")
res[product.id].update({'qty_available_stock_text': text})
)
# TODO: this should probably be refactored performance-wise
for prod in self:
vals = {}
prod_quant = quants.filtered(lambda x: x.product_id == prod)
quantity = sum(prod_quant.mapped(
lambda x: x._get_available_quantity(
x.product_id,
x.location_id
)
))
vals['qty_available_not_res'] = quantity
res[prod.id] = vals
self._product_available_not_res_hook(quants)
return res
@api.multi
def _compute_qty_available_not_reserved(self):
res = self._compute_product_available_not_res_dict()
for prod in self:
qty = res[prod.id]['qty_available_not_res']
prod.qty_available_not_res = qty
return res

View File

@@ -0,0 +1,23 @@
# Copyright 2018 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import api, fields, models
class StockQuant(models.Model):
_inherit = "stock.quant"
contains_unreserved = fields.Boolean(
string="Contains unreserved products",
compute="_compute_contains_unreserved",
store=True,
)
@api.depends('product_id', 'location_id', 'quantity', 'reserved_quantity')
def _compute_contains_unreserved(self):
for record in self:
available = record._get_available_quantity(
record.product_id,
record.location_id,
)
record.contains_unreserved = True if available > 0 else False

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
# Copyright 2016 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)

View File

@@ -1,138 +1,142 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Camptocamp SA
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
# Copyright 2016 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# Copyright 2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo.tests.common import TransactionCase
from odoo.tests.common import SavepointCase
class TestStockLogisticsWarehouse(TransactionCase):
def test01_stock_levels(self):
"""checking that qty_available_not_res actually reflects \
the variations in stock, both on product and template"""
pickingObj = self.env['stock.picking']
productObj = self.env['product.product']
templateObj = self.env['product.template']
supplier_location = self.env.ref('stock.stock_location_suppliers')
stock_location = self.env.ref('stock.stock_location_stock')
customer_location = self.env.ref('stock.stock_location_customers')
uom_unit = self.env.ref('product.product_uom_unit')
class TestStockLogisticsWarehouse(SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.pickingObj = cls.env['stock.picking']
cls.productObj = cls.env['product.product']
cls.templateObj = cls.env['product.template']
cls.supplier_location = cls.env.ref('stock.stock_location_suppliers')
cls.stock_location = cls.env.ref('stock.stock_location_stock')
cls.customer_location = cls.env.ref('stock.stock_location_customers')
cls.uom_unit = cls.env.ref('product.product_uom_unit')
# Create product template
templateAB = templateObj.create(
{'name': 'templAB',
'uom_id': uom_unit.id,
})
cls.templateAB = cls.templateObj.create({
'name': 'templAB',
'uom_id': cls.uom_unit.id,
})
# Create product A and B
productA = productObj.create(
{'name': 'product A',
'standard_price': 1,
'type': 'product',
'uom_id': uom_unit.id,
'default_code': 'A',
'product_tmpl_id': templateAB.id,
})
cls.productA = cls.productObj.create({
'name': 'product A',
'standard_price': 1,
'type': 'product',
'uom_id': cls.uom_unit.id,
'default_code': 'A',
'product_tmpl_id': cls.templateAB.id,
})
productB = productObj.create(
{'name': 'product B',
'standard_price': 1,
'type': 'product',
'uom_id': uom_unit.id,
'default_code': 'B',
'product_tmpl_id': templateAB.id,
})
cls.productB = cls.productObj.create({
'name': 'product B',
'standard_price': 1,
'type': 'product',
'uom_id': cls.uom_unit.id,
'default_code': 'B',
'product_tmpl_id': cls.templateAB.id,
})
# Create a picking move from INCOMING to STOCK
pickingInA = pickingObj.create({
'picking_type_id': self.ref('stock.picking_type_in'),
'location_id': supplier_location.id,
'location_dest_id': stock_location.id,
cls.pickingInA = cls.pickingObj.create({
'picking_type_id': cls.env.ref('stock.picking_type_in').id,
'location_id': cls.supplier_location.id,
'location_dest_id': cls.stock_location.id,
'move_lines': [
(0, 0, {
'name': 'Test move',
'product_id': productA.id,
'product_uom': productA.uom_id.id,
'product_id': cls.productA.id,
'product_uom': cls.productA.uom_id.id,
'product_uom_qty': 2,
'location_id': supplier_location.id,
'location_dest_id': stock_location.id,
})]
'quantity_done': 2,
'location_id': cls.supplier_location.id,
'location_dest_id': cls.stock_location.id,
})
]
})
pickingInB = pickingObj.create({
'picking_type_id': self.ref('stock.picking_type_in'),
'location_id': supplier_location.id,
'location_dest_id': stock_location.id,
cls.pickingInB = cls.pickingObj.create({
'picking_type_id': cls.env.ref('stock.picking_type_in').id,
'location_id': cls.supplier_location.id,
'location_dest_id': cls.stock_location.id,
'move_lines': [
(0, 0, {
'name': 'Test move',
'product_id': productB.id,
'product_uom': productB.uom_id.id,
'product_id': cls.productB.id,
'product_uom': cls.productB.uom_id.id,
'product_uom_qty': 3,
'location_id': supplier_location.id,
'location_dest_id': stock_location.id,
})]
'quantity_done': 3,
'location_id': cls.supplier_location.id,
'location_dest_id': cls.stock_location.id,
})
]
})
def compare_qty_available_not_res(product, value):
# Refresh, because the function field is not recalculated between
# transactions
product.refresh()
self.assertEqual(product.qty_available_not_res, value)
compare_qty_available_not_res(productA, 0)
compare_qty_available_not_res(templateAB, 0)
pickingInA.action_confirm()
compare_qty_available_not_res(productA, 0)
compare_qty_available_not_res(templateAB, 0)
pickingInA.action_assign()
compare_qty_available_not_res(productA, 0)
compare_qty_available_not_res(templateAB, 0)
pickingInA.action_done()
compare_qty_available_not_res(productA, 2)
compare_qty_available_not_res(templateAB, 2)
# will directly trigger action_done on productB
pickingInB.action_done()
compare_qty_available_not_res(productA, 2)
compare_qty_available_not_res(productB, 3)
compare_qty_available_not_res(templateAB, 5)
# Create a picking from STOCK to CUSTOMER
pickingOutA = pickingObj.create({
'picking_type_id': self.ref('stock.picking_type_out'),
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
cls.pickingOutA = cls.pickingObj.create({
'picking_type_id': cls.env.ref('stock.picking_type_out').id,
'location_id': cls.stock_location.id,
'location_dest_id': cls.customer_location.id,
'move_lines': [
(0, 0, {
'name': 'Test move',
'product_id': productB.id,
'product_uom': productB.uom_id.id,
'product_id': cls.productB.id,
'product_uom': cls.productB.uom_id.id,
'product_uom_qty': 2,
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
})]
'location_id': cls.stock_location.id,
'location_dest_id': cls.customer_location.id,
})
]
})
compare_qty_available_not_res(productB, 3)
compare_qty_available_not_res(templateAB, 5)
def compare_qty_available_not_res(self, product, value):
product.invalidate_cache()
self.assertEqual(product.qty_available_not_res, value)
pickingOutA.action_confirm()
compare_qty_available_not_res(productB, 3)
compare_qty_available_not_res(templateAB, 5)
def test_stock_levels(self):
"""checking that qty_available_not_res actually reflects \
the variations in stock, both on product and template"""
pickingOutA.action_assign()
compare_qty_available_not_res(productB, 1)
compare_qty_available_not_res(templateAB, 3)
self.compare_qty_available_not_res(self.productA, 0)
self.compare_qty_available_not_res(self.templateAB, 0)
pickingOutA.action_done()
compare_qty_available_not_res(productB, 1)
compare_qty_available_not_res(templateAB, 3)
self.pickingInA.action_confirm()
self.compare_qty_available_not_res(self.productA, 0)
self.compare_qty_available_not_res(self.templateAB, 0)
templateAB.action_open_quants_unreserved()
self.pickingInA.action_assign()
self.compare_qty_available_not_res(self.productA, 0)
self.compare_qty_available_not_res(self.templateAB, 0)
self.pickingInA.button_validate()
self.compare_qty_available_not_res(self.productA, 2)
self.compare_qty_available_not_res(self.templateAB, 2)
# will directly trigger action_done on self.productB
self.pickingInB.action_done()
self.compare_qty_available_not_res(self.productA, 2)
self.compare_qty_available_not_res(self.productB, 3)
self.compare_qty_available_not_res(self.templateAB, 5)
self.compare_qty_available_not_res(self.productB, 3)
self.compare_qty_available_not_res(self.templateAB, 5)
self.pickingOutA.action_confirm()
self.compare_qty_available_not_res(self.productB, 3)
self.compare_qty_available_not_res(self.templateAB, 5)
self.pickingOutA.action_assign()
self.compare_qty_available_not_res(self.productB, 1)
self.compare_qty_available_not_res(self.templateAB, 3)
self.pickingOutA.action_done()
self.compare_qty_available_not_res(self.productB, 1)
self.compare_qty_available_not_res(self.templateAB, 3)
self.templateAB.action_open_quants_unreserved()

View File

@@ -63,9 +63,9 @@
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button class="oe_stat_button"
name="%(product_open_quants_unreserved)d"
icon="fa-building-o"
type="action" attrs="{'invisible':[('type', '!=', 'product')]}">
name="%(product_open_quants_unreserved)d"
icon="fa-building-o"
type="action" attrs="{'invisible':[('type', '!=', 'product')]}">
<field name="qty_available_not_res" widget="statinfo"
string="Unreserved"/>
</button>

View File

@@ -1,29 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="quant_search_view" model="ir.ui.view">
<field name="name">stock.quant.search</field>
<field name="model">stock.quant</field>
<field name="inherit_id" ref="stock.quant_search_view"/>
<field eval="10" name="priority"/>
<field name="arch" type="xml">
<field name="owner_id" position="after">
<field name="reservation_id"/>
</field>
<filter name="internal_loc" position="after">
<filter name='internal_unreserved'
string="Internal Unreserved"
domain="[('reservation_id','=', False), ('location_id.usage','=', 'internal')]"/>
</filter>
<record id="quant_search_view" model="ir.ui.view">
<field name="name">stock.quant.search</field>
<field name="model">stock.quant</field>
<field name="inherit_id" ref="stock.quant_search_view"/>
<field eval="10" name="priority"/>
<field name="arch" type="xml">
<field name="owner_id" position="after">
<field name="contains_unreserved"/>
</field>
</record>
<filter name="internal_loc" position="after">
<filter name='internal_unreserved'
string="Internal Unreserved"
domain="[('contains_unreserved','=', True), ('location_id.usage','=', 'internal')]"/>
</filter>
</field>
</record>
<record model="ir.actions.act_window"
id="product_open_quants_unreserved">
<field name="name">Stock On Hand (Unreserved)</field>
<field name="context">{'search_default_internal_loc': 1, 'search_default_locationgroup':1}</field>
<field name="domain">[('product_id', '=', active_id), ('reservation_id', '=', False)]</field>
<field name="res_model">stock.quant</field>
</record>
<record model="ir.actions.act_window"
id="product_open_quants_unreserved">
<field name="name">Stock On Hand (Unreserved)</field>
<field name="context">{'search_default_internal_loc': 1, 'search_default_locationgroup':1}</field>
<field name="domain">[('product_id', '=', active_id), ('contains_unreserved', '=', True)]</field>
<field name="res_model">stock.quant</field>
</record>
</odoo>