mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
[IMP] stock_available: black, isort
This commit is contained in:
committed by
Florian da Costa
parent
c7a37b0216
commit
dd90188d8a
@@ -3,18 +3,18 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
{
|
||||
'name': 'Stock available to promise',
|
||||
'version': '12.0.1.0.1',
|
||||
'author': 'Numérigraphe, Sodexis, Odoo Community Association (OCA)',
|
||||
'website': 'https://github.com/OCA/stock-logistics-warehouse',
|
||||
'development_status': 'Production/Stable',
|
||||
'category': 'Warehouse',
|
||||
'depends': ['stock'],
|
||||
'license': 'AGPL-3',
|
||||
'data': [
|
||||
'views/product_template_view.xml',
|
||||
'views/product_product_view.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
"name": "Stock available to promise",
|
||||
"version": "13.0.1.0.0",
|
||||
"author": "Numérigraphe, Sodexis, Odoo Community Association (OCA)",
|
||||
"website": "https://github.com/OCA/stock-logistics-warehouse",
|
||||
"development_status": "Production/Stable",
|
||||
"category": "Warehouse",
|
||||
"depends": ["stock"],
|
||||
"license": "AGPL-3",
|
||||
"data": [
|
||||
"views/product_template_view.xml",
|
||||
"views/product_product_view.xml",
|
||||
"views/res_config_settings_views.xml",
|
||||
],
|
||||
'installable': True,
|
||||
"installable": True,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
from odoo.addons import decimal_precision as dp
|
||||
from odoo.addons.stock.models.product import OPERATORS
|
||||
|
||||
@@ -13,27 +14,28 @@ class ProductProduct(models.Model):
|
||||
Useful implementations need to be installed through the Settings menu or by
|
||||
installing one of the modules stock_available_*
|
||||
"""
|
||||
_inherit = 'product.product'
|
||||
|
||||
_inherit = "product.product"
|
||||
|
||||
@api.multi
|
||||
def _compute_available_quantities_dict(self):
|
||||
stock_dict = self._compute_quantities_dict(
|
||||
self._context.get('lot_id'),
|
||||
self._context.get('owner_id'),
|
||||
self._context.get('package_id'),
|
||||
self._context.get('from_date'),
|
||||
self._context.get('to_date'))
|
||||
self._context.get("lot_id"),
|
||||
self._context.get("owner_id"),
|
||||
self._context.get("package_id"),
|
||||
self._context.get("from_date"),
|
||||
self._context.get("to_date"),
|
||||
)
|
||||
res = {}
|
||||
for product in self:
|
||||
res[product.id] = {
|
||||
'immediately_usable_qty': stock_dict[product.id][
|
||||
'virtual_available'],
|
||||
'potential_qty': 0.0
|
||||
"immediately_usable_qty": stock_dict[product.id]["virtual_available"],
|
||||
"potential_qty": 0.0,
|
||||
}
|
||||
return res, stock_dict
|
||||
|
||||
@api.multi
|
||||
@api.depends('virtual_available')
|
||||
@api.depends("virtual_available")
|
||||
def _compute_available_quantities(self):
|
||||
res, _ = self._compute_available_quantities_dict()
|
||||
for product in self:
|
||||
@@ -42,20 +44,22 @@ class ProductProduct(models.Model):
|
||||
product[key] = value
|
||||
|
||||
immediately_usable_qty = fields.Float(
|
||||
digits=dp.get_precision('Product Unit of Measure'),
|
||||
compute='_compute_available_quantities',
|
||||
digits=dp.get_precision("Product Unit of Measure"),
|
||||
compute="_compute_available_quantities",
|
||||
search="_search_immediately_usable_qty",
|
||||
string='Available to promise',
|
||||
string="Available to promise",
|
||||
help="Stock for this Product that can be safely proposed "
|
||||
"for sale to Customers.\n"
|
||||
"The definition of this value can be configured to suit "
|
||||
"your needs.")
|
||||
"for sale to Customers.\n"
|
||||
"The definition of this value can be configured to suit "
|
||||
"your needs.",
|
||||
)
|
||||
potential_qty = fields.Float(
|
||||
compute='_compute_available_quantities',
|
||||
digits=dp.get_precision('Product Unit of Measure'),
|
||||
string='Potential',
|
||||
compute="_compute_available_quantities",
|
||||
digits=dp.get_precision("Product Unit of Measure"),
|
||||
string="Potential",
|
||||
help="Quantity of this Product that could be produced using "
|
||||
"the materials already at hand.")
|
||||
"the materials already at hand.",
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _search_immediately_usable_qty(self, operator, value):
|
||||
@@ -73,4 +77,4 @@ class ProductProduct(models.Model):
|
||||
for product in products:
|
||||
if OPERATORS[operator](product.immediately_usable_qty, value):
|
||||
product_ids.append(product.id)
|
||||
return [('id', 'in', product_ids)]
|
||||
return [("id", "in", product_ids)]
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
# Copyright 2016 Sodexis
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import models, fields, api
|
||||
from odoo import api, fields, models
|
||||
|
||||
from odoo.addons import decimal_precision as dp
|
||||
from odoo.addons.stock.models.product import OPERATORS
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
_inherit = "product.template"
|
||||
|
||||
@api.multi
|
||||
@api.depends('product_variant_ids.immediately_usable_qty',
|
||||
'product_variant_ids.potential_qty')
|
||||
@api.depends(
|
||||
"product_variant_ids.immediately_usable_qty",
|
||||
"product_variant_ids.potential_qty",
|
||||
)
|
||||
def _compute_available_quantities(self):
|
||||
res = self._compute_available_quantities_dict()
|
||||
for product in self:
|
||||
@@ -23,40 +26,49 @@ class ProductTemplate(models.Model):
|
||||
@api.multi
|
||||
def _compute_available_quantities_dict(self):
|
||||
variants_dict, _ = self.mapped(
|
||||
'product_variant_ids')._compute_available_quantities_dict()
|
||||
"product_variant_ids"
|
||||
)._compute_available_quantities_dict()
|
||||
res = {}
|
||||
for template in self:
|
||||
immediately_usable_qty = sum(
|
||||
[variants_dict[p.id]["immediately_usable_qty"] -
|
||||
variants_dict[p.id]["potential_qty"] for p in
|
||||
template.product_variant_ids])
|
||||
[
|
||||
variants_dict[p.id]["immediately_usable_qty"]
|
||||
- variants_dict[p.id]["potential_qty"]
|
||||
for p in template.product_variant_ids
|
||||
]
|
||||
)
|
||||
potential_qty = max(
|
||||
[variants_dict[p.id]["potential_qty"] for p in
|
||||
template.product_variant_ids] or [0.0])
|
||||
[
|
||||
variants_dict[p.id]["potential_qty"]
|
||||
for p in template.product_variant_ids
|
||||
]
|
||||
or [0.0]
|
||||
)
|
||||
res[template.id] = {
|
||||
"immediately_usable_qty": immediately_usable_qty +
|
||||
potential_qty,
|
||||
"immediately_usable_qty": immediately_usable_qty + potential_qty,
|
||||
"potential_qty": potential_qty,
|
||||
}
|
||||
return res
|
||||
|
||||
immediately_usable_qty = fields.Float(
|
||||
digits=dp.get_precision('Product Unit of Measure'),
|
||||
compute='_compute_available_quantities',
|
||||
digits=dp.get_precision("Product Unit of Measure"),
|
||||
compute="_compute_available_quantities",
|
||||
search="_search_immediately_usable_qty",
|
||||
string='Available to promise',
|
||||
string="Available to promise",
|
||||
help="Stock for this Product that can be safely proposed "
|
||||
"for sale to Customers.\n"
|
||||
"The definition of this value can be configured to suit "
|
||||
"your needs")
|
||||
"for sale to Customers.\n"
|
||||
"The definition of this value can be configured to suit "
|
||||
"your needs",
|
||||
)
|
||||
potential_qty = fields.Float(
|
||||
compute='_compute_available_quantities',
|
||||
digits=dp.get_precision('Product Unit of Measure'),
|
||||
string='Potential',
|
||||
compute="_compute_available_quantities",
|
||||
digits=dp.get_precision("Product Unit of Measure"),
|
||||
string="Potential",
|
||||
help="Quantity of this Product that could be produced using "
|
||||
"the materials already at hand. "
|
||||
"If the product has several variants, this will be the biggest "
|
||||
"quantity that can be made for a any single variant.")
|
||||
"the materials already at hand. "
|
||||
"If the product has several variants, this will be the biggest "
|
||||
"quantity that can be made for a any single variant.",
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _search_immediately_usable_qty(self, operator, value):
|
||||
@@ -74,4 +86,4 @@ class ProductTemplate(models.Model):
|
||||
for product in products:
|
||||
if OPERATORS[operator](product.immediately_usable_qty, value):
|
||||
product_ids.append(product.id)
|
||||
return [('id', 'in', product_ids)]
|
||||
return [("id", "in", product_ids)]
|
||||
|
||||
@@ -8,62 +8,67 @@ from odoo import api, fields, models
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
|
||||
"""Add options to easily install the submodules"""
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
@api.model
|
||||
def _get_stock_available_mrp_based_on(self):
|
||||
"""Gets the available languages for the selection."""
|
||||
pdct_fields = self.env['ir.model.fields'].search(
|
||||
[('model', '=', 'product.product'),
|
||||
('ttype', '=', 'float')])
|
||||
pdct_fields = self.env["ir.model.fields"].search(
|
||||
[("model", "=", "product.product"), ("ttype", "=", "float")]
|
||||
)
|
||||
return [
|
||||
(field.name, field.field_description)
|
||||
for field in sorted(pdct_fields, key=lambda f: f.field_description)
|
||||
]
|
||||
|
||||
module_stock_available_immediately = fields.Boolean(
|
||||
string='Exclude incoming goods',
|
||||
string="Exclude incoming goods",
|
||||
help="This will subtract incoming quantities from the quantities "
|
||||
"available to promise.\n"
|
||||
"This installs the module stock_available_immediately.")
|
||||
"available to promise.\n"
|
||||
"This installs the module stock_available_immediately.",
|
||||
)
|
||||
|
||||
module_stock_available_sale = fields.Boolean(
|
||||
string='Exclude goods already in sale quotations',
|
||||
string="Exclude goods already in sale quotations",
|
||||
help="This will subtract quantities from the sale quotations from "
|
||||
"the quantities available to promise.\n"
|
||||
"This installs the modules stock_available_sale.\n"
|
||||
"If the modules sale and sale_delivery_date are not "
|
||||
"installed, this will install them too")
|
||||
"the quantities available to promise.\n"
|
||||
"This installs the modules stock_available_sale.\n"
|
||||
"If the modules sale and sale_delivery_date are not "
|
||||
"installed, this will install them too",
|
||||
)
|
||||
|
||||
module_stock_available_mrp = fields.Boolean(
|
||||
string='Include the production potential',
|
||||
string="Include the production potential",
|
||||
help="This will add the quantities of goods that can be "
|
||||
"immediately manufactured, to the quantities available to "
|
||||
"promise.\n"
|
||||
"This installs the module stock_available_mrp.\n"
|
||||
"If the module mrp is not installed, this will install it "
|
||||
"too")
|
||||
"immediately manufactured, to the quantities available to "
|
||||
"promise.\n"
|
||||
"This installs the module stock_available_mrp.\n"
|
||||
"If the module mrp is not installed, this will install it "
|
||||
"too",
|
||||
)
|
||||
|
||||
stock_available_mrp_based_on = fields.Selection(
|
||||
_get_stock_available_mrp_based_on,
|
||||
string='based on',
|
||||
string="based on",
|
||||
help="Choose the field of the product which will be used to compute "
|
||||
"potential.\nIf empty, Quantity On Hand is used.\n"
|
||||
"Only the quantity fields have meaning for computing stock",
|
||||
"potential.\nIf empty, Quantity On Hand is used.\n"
|
||||
"Only the quantity fields have meaning for computing stock",
|
||||
)
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
res = super(ResConfigSettings, self).get_values()
|
||||
res.update(stock_available_mrp_based_on=self.env[
|
||||
'ir.config_parameter'].sudo().get_param(
|
||||
'stock_available_mrp_based_on',
|
||||
'qty_available')
|
||||
res.update(
|
||||
stock_available_mrp_based_on=self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("stock_available_mrp_based_on", "qty_available")
|
||||
)
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def set_values(self):
|
||||
super(ResConfigSettings, self).set_values()
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'stock_available_mrp_based_on', self.stock_available_mrp_based_on)
|
||||
self.env["ir.config_parameter"].sudo().set_param(
|
||||
"stock_available_mrp_based_on", self.stock_available_mrp_based_on
|
||||
)
|
||||
|
||||
@@ -8,71 +8,74 @@ from odoo.tests.common import TransactionCase
|
||||
class TestStockLogisticsWarehouse(TransactionCase):
|
||||
def test_res_config(self):
|
||||
"""Test the config file"""
|
||||
stock_setting = self.env['res.config.settings'].create({})
|
||||
stock_setting = self.env["res.config.settings"].create({})
|
||||
|
||||
self.assertEquals(
|
||||
stock_setting.stock_available_mrp_based_on,
|
||||
'qty_available')
|
||||
stock_setting.stock_available_mrp_based_on = 'immediately_usable_qty'
|
||||
self.assertEquals(stock_setting.stock_available_mrp_based_on, "qty_available")
|
||||
stock_setting.stock_available_mrp_based_on = "immediately_usable_qty"
|
||||
stock_setting.set_values()
|
||||
self.assertEquals(
|
||||
stock_setting.stock_available_mrp_based_on,
|
||||
'immediately_usable_qty')
|
||||
stock_setting.stock_available_mrp_based_on, "immediately_usable_qty"
|
||||
)
|
||||
|
||||
def test01_stock_levels(self):
|
||||
"""checking that immediately_usable_qty actually reflects \
|
||||
the variations in stock, both on product and template"""
|
||||
moveObj = self.env['stock.move']
|
||||
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('uom.product_uom_unit')
|
||||
moveObj = self.env["stock.move"]
|
||||
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("uom.product_uom_unit")
|
||||
|
||||
# Create product template
|
||||
templateAB = templateObj.create(
|
||||
{'name': 'templAB',
|
||||
'uom_id': uom_unit.id,
|
||||
})
|
||||
templateAB = templateObj.create({"name": "templAB", "uom_id": 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,
|
||||
})
|
||||
{
|
||||
"name": "product A",
|
||||
"standard_price": 1,
|
||||
"type": "product",
|
||||
"uom_id": uom_unit.id,
|
||||
"default_code": "A",
|
||||
"product_tmpl_id": 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,
|
||||
})
|
||||
{
|
||||
"name": "product B",
|
||||
"standard_price": 1,
|
||||
"type": "product",
|
||||
"uom_id": uom_unit.id,
|
||||
"default_code": "B",
|
||||
"product_tmpl_id": templateAB.id,
|
||||
}
|
||||
)
|
||||
|
||||
# Create a stock move from INCOMING to STOCK
|
||||
stockMoveInA = moveObj.create(
|
||||
{'location_id': supplier_location.id,
|
||||
'location_dest_id': stock_location.id,
|
||||
'name': 'MOVE INCOMING -> STOCK ',
|
||||
'product_id': productA.id,
|
||||
'product_uom': productA.uom_id.id,
|
||||
'product_uom_qty': 2,
|
||||
})
|
||||
{
|
||||
"location_id": supplier_location.id,
|
||||
"location_dest_id": stock_location.id,
|
||||
"name": "MOVE INCOMING -> STOCK ",
|
||||
"product_id": productA.id,
|
||||
"product_uom": productA.uom_id.id,
|
||||
"product_uom_qty": 2,
|
||||
}
|
||||
)
|
||||
|
||||
stockMoveInB = moveObj.create(
|
||||
{'location_id': supplier_location.id,
|
||||
'location_dest_id': stock_location.id,
|
||||
'name': 'MOVE INCOMING -> STOCK ',
|
||||
'product_id': productB.id,
|
||||
'product_uom': productB.uom_id.id,
|
||||
'product_uom_qty': 3,
|
||||
})
|
||||
{
|
||||
"location_id": supplier_location.id,
|
||||
"location_dest_id": stock_location.id,
|
||||
"name": "MOVE INCOMING -> STOCK ",
|
||||
"product_id": productB.id,
|
||||
"product_uom": productB.uom_id.id,
|
||||
"product_uom_qty": 3,
|
||||
}
|
||||
)
|
||||
|
||||
def compare_product_usable_qty(product, value):
|
||||
"""
|
||||
@@ -87,16 +90,16 @@ class TestStockLogisticsWarehouse(TransactionCase):
|
||||
product.refresh()
|
||||
self.assertEqual(product.immediately_usable_qty, value)
|
||||
# Now check search function
|
||||
domain = [('immediately_usable_qty', '=', value)]
|
||||
domain = [("immediately_usable_qty", "=", value)]
|
||||
results = self.env[product._name].search(domain)
|
||||
self.assertIn(product.id, results.ids)
|
||||
domain = [('immediately_usable_qty', '!=', value)]
|
||||
domain = [("immediately_usable_qty", "!=", value)]
|
||||
results = self.env[product._name].search(domain)
|
||||
self.assertNotIn(product.id, results.ids)
|
||||
domain = [('immediately_usable_qty', '>', value-1)]
|
||||
domain = [("immediately_usable_qty", ">", value - 1)]
|
||||
results = self.env[product._name].search(domain)
|
||||
self.assertIn(product.id, results.ids)
|
||||
domain = [('immediately_usable_qty', '<', value+1)]
|
||||
domain = [("immediately_usable_qty", "<", value + 1)]
|
||||
results = self.env[product._name].search(domain)
|
||||
self.assertIn(product.id, results.ids)
|
||||
|
||||
@@ -123,14 +126,16 @@ class TestStockLogisticsWarehouse(TransactionCase):
|
||||
|
||||
# Create a stock move from STOCK to CUSTOMER
|
||||
stockMoveOutA = moveObj.create(
|
||||
{'location_id': stock_location.id,
|
||||
'location_dest_id': customer_location.id,
|
||||
'name': ' STOCK --> CUSTOMER ',
|
||||
'product_id': productA.id,
|
||||
'product_uom': productA.uom_id.id,
|
||||
'product_uom_qty': 1,
|
||||
'state': 'confirmed',
|
||||
})
|
||||
{
|
||||
"location_id": stock_location.id,
|
||||
"location_dest_id": customer_location.id,
|
||||
"name": " STOCK --> CUSTOMER ",
|
||||
"product_id": productA.id,
|
||||
"product_uom": productA.uom_id.id,
|
||||
"product_uom_qty": 1,
|
||||
"state": "confirmed",
|
||||
}
|
||||
)
|
||||
|
||||
stockMoveOutA._action_done()
|
||||
compare_product_usable_qty(productA, 1)
|
||||
|
||||
Reference in New Issue
Block a user