Merge PR #2029 into 16.0

Signed-off-by LoisRForgeFlow
This commit is contained in:
OCA-git-bot
2024-05-16 14:47:04 +00:00
10 changed files with 328 additions and 29 deletions

View File

@@ -11,9 +11,11 @@
"depends": ["stock"],
"data": [
"security/ir.model.access.csv",
"security/security.xml",
"views/stock_inventory.xml",
"views/stock_quant.xml",
"views/stock_move_line.xml",
"views/res_config_settings_view.xml",
],
"installable": True,
"application": False,

View File

@@ -1,3 +1,5 @@
from . import stock_inventory
from . import stock_quant
from . import stock_move_line
from . import res_company
from . import res_config_settings

View File

@@ -0,0 +1,15 @@
# Copyright 2024 ForgeFlow S.L. (http://www.forgeflow.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import fields, models
class ResCompany(models.Model):
_inherit = "res.company"
stock_inventory_auto_complete = fields.Boolean(
help="If enabled, when all the quants prepared for the adjustment "
"are done, the adjustment is automatically set to done.",
default=False,
)

View File

@@ -0,0 +1,12 @@
# Copyright 2024 ForgeFlow S.L. (http://www.forgeflow.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
stock_inventory_auto_complete = fields.Boolean(
related="company_id.stock_inventory_auto_complete", readonly=False
)

View File

@@ -8,9 +8,28 @@ class InventoryAdjustmentsGroup(models.Model):
_description = "Inventory Adjustment Group"
_order = "date desc, id desc"
name = fields.Char(required=True, default="Inventory", string="Inventory Reference")
name = fields.Char(
required=True,
default="Inventory",
string="Inventory Reference",
states={"draft": [("readonly", False)]},
readonly=True,
)
date = fields.Datetime(default=lambda self: fields.Datetime.now())
date = fields.Datetime(
default=lambda self: fields.Datetime.now(),
states={"draft": [("readonly", False)]},
readonly=True,
)
company_id = fields.Many2one(
comodel_name="res.company",
readonly=True,
index=True,
states={"draft": [("readonly", False)]},
default=lambda self: self.env.company,
required=True,
)
state = fields.Selection(
[("draft", "Draft"), ("in_progress", "In Progress"), ("done", "Done")],
@@ -22,7 +41,10 @@ class InventoryAdjustmentsGroup(models.Model):
)
location_ids = fields.Many2many(
"stock.location", string="Locations", domain="[('usage', '=', 'internal')]"
"stock.location",
string="Locations",
domain="[('usage', '=', 'internal'), "
"'|', ('company_id', '=', company_id), ('company_id', '=', False)]",
)
product_selection = fields.Selection(
@@ -37,15 +59,24 @@ class InventoryAdjustmentsGroup(models.Model):
required=True,
)
product_ids = fields.Many2many("product.product", string="Products")
product_ids = fields.Many2many(
"product.product",
string="Products",
domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]",
)
stock_quant_ids = fields.Many2many("stock.quant", string="Inventory Adjustment")
stock_quant_ids = fields.Many2many(
"stock.quant",
string="Inventory Adjustment",
domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]",
)
category_id = fields.Many2one("product.category", string="Product Category")
lot_ids = fields.Many2many(
"stock.lot",
string="Lot/Serial Numbers",
domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]",
)
stock_move_ids = fields.One2many(
@@ -66,14 +97,24 @@ class InventoryAdjustmentsGroup(models.Model):
compute="_compute_count_stock_moves", string="Stock Moves Lines"
)
exclude_sublocation = fields.Boolean(
help="If enabled, it will only take into account "
"the locations selected, and not their children."
)
responsible_id = fields.Many2one(
comodel_name="res.users",
string="Assigned to",
states={"draft": [("readonly", False)]},
readonly=True,
tracking=True,
help="Specific responsible of Inventory Adjustment.",
)
@api.depends("stock_quant_ids")
def _compute_count_stock_quants(self):
self.count_stock_quants = len(self.stock_quant_ids)
count_todo = len(
self.stock_quant_ids.search(
[("id", "in", self.stock_quant_ids.ids), ("to_do", "=", "True")]
)
)
count_todo = len(self.stock_quant_ids.filtered(lambda sq: sq.to_do))
self.count_stock_quants_string = "{} / {}".format(
count_todo, self.count_stock_quants
)
@@ -99,11 +140,15 @@ class InventoryAdjustmentsGroup(models.Model):
return self.env["stock.quant"].search(domain)
def _get_base_domain(self, locations):
return [
"|",
("location_id", "in", locations.mapped("id")),
("location_id", "in", locations.child_ids.ids),
]
return (
[
("location_id", "in", locations.mapped("id")),
]
if self.exclude_sublocation
else [
("location_id", "child_of", locations.child_internal_location_ids.ids),
]
)
def _get_domain_all_quants(self, base_domain):
return base_domain
@@ -146,13 +191,15 @@ class InventoryAdjustmentsGroup(models.Model):
]
)
def refresh_stock_quant_ids(self):
for rec in self:
rec.stock_quant_ids = rec._get_quants(rec.location_ids)
def action_state_to_in_progress(self):
active_rec = self.env["stock.inventory"].search(
[
("state", "=", "in_progress"),
"|",
("location_ids", "in", self.location_ids.mapped("id")),
("location_ids", "in", self.location_ids.child_ids.ids),
("location_ids", "child_of", self.location_ids.ids),
],
limit=1,
)
@@ -164,25 +211,48 @@ class InventoryAdjustmentsGroup(models.Model):
% active_rec.name
)
self.state = "in_progress"
self.stock_quant_ids = self._get_quants(self.location_ids)
self.stock_quant_ids.update({"to_do": True})
self.refresh_stock_quant_ids()
self.stock_quant_ids.update(
{
"to_do": True,
"user_id": self.responsible_id,
"inventory_date": self.date,
}
)
return
def action_state_to_done(self):
self.state = "done"
self.stock_quant_ids.update({"to_do": True})
self.stock_quant_ids.update(
{
"to_do": True,
"user_id": False,
"inventory_date": False,
}
)
return
def action_auto_state_to_done(self):
self.ensure_one()
if not any(self.stock_quant_ids.filtered(lambda sq: sq.to_do)):
self.action_state_to_done()
return
def action_state_to_draft(self):
self.state = "draft"
self.stock_quant_ids.update({"to_do": True})
self.stock_quant_ids.update(
{
"to_do": True,
"user_id": False,
"inventory_date": False,
}
)
self.stock_quant_ids = None
return
def action_view_inventory_adjustment(self):
result = self.env["stock.quant"].action_view_inventory()
ia_ids = self.mapped("stock_quant_ids").ids
result["domain"] = [("id", "in", ia_ids)]
result["domain"] = [("id", "in", self.stock_quant_ids.ids)]
result["search_view_id"] = self.env.ref("stock.quant_search_view").id
result["context"]["search_default_to_do"] = 1
return result
@@ -196,6 +266,31 @@ class InventoryAdjustmentsGroup(models.Model):
result["context"] = []
return result
@api.constrains("state", "location_ids")
def _check_inventory_in_progress_not_override(self):
inventories = self.search([("state", "=", "in_progress")])
for rec in inventories:
inventory = inventories.filtered(
lambda x: x.id != rec.id
and (
any(i in x.location_ids for i in rec.location_ids)
or (
any(
i in x.location_ids.child_internal_location_ids
for i in rec.location_ids
)
and not x.exclude_sublocation
)
)
)
if len(inventory) > 0:
raise ValidationError(
_(
"Cannot be more than one in progress inventory adjustment "
"affecting the same location at the same time."
)
)
@api.constrains("product_selection", "product_ids")
def _check_one_product_in_product_selection(self):
for rec in self:

View File

@@ -1,4 +1,4 @@
from odoo import _, fields, models
from odoo import _, api, fields, models
class StockQuant(models.Model):
@@ -15,7 +15,10 @@ class StockQuant(models.Model):
.search([("state", "=", "in_progress")])
.filtered(
lambda x: rec.location_id in x.location_ids
or rec.location_id in x.location_ids.child_ids
or (
rec.location_id in x.location_ids.child_internal_location_ids
and not x.exclude_sublocation
)
)
)
moves = record_moves.search(
@@ -36,9 +39,32 @@ class StockQuant(models.Model):
raise ValueError(_("No move lines have been created"))
move = moves[len(moves) - 1]
adjustment.stock_move_ids |= move
move.inventory_adjustment_id = adjustment
reference = move.reference
if adjustment.name and move.reference:
reference = adjustment.name + ": " + move.reference
elif adjustment.name:
reference = adjustment.name
move.write(
{
"inventory_adjustment_id": adjustment.id,
"reference": reference,
}
)
rec.to_do = False
if adjustment and self.env.company.stock_inventory_auto_complete:
adjustment.action_auto_state_to_done()
return res
def _get_inventory_fields_write(self):
return super()._get_inventory_fields_write() + ["to_do"]
@api.model
def create(self, vals):
res = super().create(vals)
if self.env.context.get(
"active_model", False
) == "stock.inventory" and self.env.context.get("active_id", False):
self.env["stock.inventory"].browse(
self.env.context.get("active_id")
).refresh_stock_quant_ids()
return res

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="0">
<record model="ir.rule" id="stock_inventory_comp_rule">
<field name="name">Stock Inventory multi-company</field>
<field name="model_id" ref="model_stock_inventory" />
<field name="global" eval="True" />
<field
name="domain_force"
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
</odoo>

View File

@@ -8,6 +8,7 @@ from odoo.tests.common import TransactionCase
class TestStockInventory(TransactionCase):
def setUp(self):
super(TestStockInventory, self).setUp()
self.env.company.stock_inventory_auto_complete = False
self.quant_model = self.env["stock.quant"]
self.move_model = self.env["stock.move.line"]
self.inventory_model = self.env["stock.inventory"]
@@ -314,3 +315,97 @@ class TestStockInventory(TransactionCase):
self.assertEqual(inventory1.stock_move_ids.product_id.id, self.product2.id)
self.assertEqual(inventory1.stock_move_ids.location_id.id, self.location3.id)
inventory1.action_state_to_done()
def test_06_exclude_sub_locations(self):
inventory1 = self.inventory_model.create(
{
"name": "Inventory_Test_1",
"product_selection": "all",
"location_ids": [self.location1.id],
"exclude_sublocation": True,
}
)
inventory1.action_state_to_in_progress()
inventory2 = self.inventory_model.create(
{
"name": "Inventory_Test_2",
"product_selection": "all",
"location_ids": [self.location1.id],
"exclude_sublocation": True,
}
)
with self.assertRaises(ValidationError), self.cr.savepoint():
inventory2.action_state_to_in_progress()
self.assertEqual(inventory1.state, "in_progress")
self.assertEqual(
inventory1.stock_quant_ids.ids,
[self.quant1.id],
)
inventory1.action_state_to_draft()
self.assertEqual(inventory1.stock_quant_ids.ids, [])
inventory1.action_state_to_in_progress()
self.assertEqual(inventory1.count_stock_moves, 0)
self.assertEqual(inventory1.count_stock_quants, 1)
self.assertEqual(inventory1.count_stock_quants_string, "1 / 1")
inventory1.action_view_inventory_adjustment()
self.quant1.inventory_quantity = 92
self.quant1.action_apply_inventory()
inventory1._compute_count_stock_quants()
inventory1.action_view_stock_moves()
self.assertEqual(inventory1.count_stock_moves, 1)
self.assertEqual(inventory1.count_stock_quants, 1)
self.assertEqual(inventory1.count_stock_quants_string, "0 / 1")
self.assertEqual(inventory1.stock_move_ids.qty_done, 8)
self.assertEqual(inventory1.stock_move_ids.product_id.id, self.product.id)
self.assertEqual(inventory1.stock_move_ids.lot_id.id, self.lot_1.id)
self.assertEqual(inventory1.stock_move_ids.location_id.id, self.location1.id)
inventory1.action_state_to_done()
def test_07_stock_inventory_auto_complete(self):
self.env.company.stock_inventory_auto_complete = True
with self.assertRaises(ValidationError), self.cr.savepoint():
inventory1 = self.inventory_model.create(
{
"name": "Inventory_Test_5",
"product_selection": "one",
"location_ids": [self.location1.id],
"product_ids": [self.product.id, self.product2.id],
}
)
inventory1 = self.inventory_model.create(
{
"name": "Inventory_Test_5",
"product_selection": "one",
"location_ids": [self.location1.id],
"product_ids": [self.product.id],
}
)
inventory1.action_state_to_in_progress()
inventory1.product_ids = [self.product.id]
self.assertEqual(
inventory1.stock_quant_ids.ids, [self.quant1.id, self.quant3.id]
)
inventory1.action_state_to_draft()
self.assertEqual(inventory1.stock_quant_ids.ids, [])
inventory1.action_state_to_in_progress()
self.assertEqual(inventory1.state, "in_progress")
self.assertEqual(inventory1.count_stock_moves, 0)
self.assertEqual(inventory1.count_stock_quants, 2)
self.assertEqual(inventory1.count_stock_quants_string, "2 / 2")
inventory1.action_view_inventory_adjustment()
self.quant3.inventory_quantity = 74
self.quant3.action_apply_inventory()
inventory1._compute_count_stock_quants()
inventory1.action_view_stock_moves()
self.assertEqual(inventory1.count_stock_moves, 1)
self.assertEqual(inventory1.count_stock_quants, 2)
self.assertEqual(inventory1.count_stock_quants_string, "1 / 2")
self.assertEqual(inventory1.stock_move_ids.qty_done, 26)
self.assertEqual(inventory1.stock_move_ids.product_id.id, self.product.id)
self.assertEqual(inventory1.stock_move_ids.lot_id.id, self.lot_3.id)
self.assertEqual(inventory1.stock_move_ids.location_id.id, self.location3.id)
self.quant1.inventory_quantity = 65
self.quant1.action_apply_inventory()
self.assertEqual(inventory1.count_stock_moves, 2)
self.assertEqual(inventory1.count_stock_quants, 2)
self.assertEqual(inventory1.state, "done")

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2019-2023 ForgeFlow S.L.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res_config_settings_view_form - stock_inventory</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="stock.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//div[@id='production_lot_info']" position='after'>
<h2>Stock Inventory</h2>
<div class="row mt16 o_settings_container">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="stock_inventory_auto_complete" />
</div>
<div class="o_setting_right_pane">
<label for="stock_inventory_auto_complete" />
<div class="text-muted">
If enabled, when all the quants prepared for the adjustment are done, the adjustment is automatically set to done.
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@@ -41,7 +41,7 @@
name="action_view_inventory_adjustment"
class="oe_stat_button"
icon="fa-pencil-square-o"
attrs="{'invisible':['|', ('state', 'in', ['draft', 'done']), ('count_stock_quants', '=', 0)]}"
attrs="{'invisible':[('state', 'in', ['draft', 'done'])]}"
>
<field
name="count_stock_quants_string"
@@ -82,9 +82,16 @@
attrs="{'readonly':[('state', 'in', ['in_progress', 'done'])]}"
required="1"
/>
<field
name="exclude_sublocation"
attrs="{'readonly':[('state', 'in', ['in_progress', 'done'])]}"
required="1"
/>
</group>
<group>
<field name="date" />
<field name="company_id" />
<field name="responsible_id" />
<field
name="owner_id"
attrs="{'readonly':[('state', '=', 'done')]}"
@@ -124,6 +131,9 @@
<field name="arch" type="xml">
<tree>
<field name="name" />
<field name="product_selection" optional="hide" />
<field name="location_ids" widget="many2many_tags" optional="hide" />
<field name="responsible_id" optional="hide" />
<field
name="state"
widget="badge"
@@ -132,6 +142,7 @@
decoration-muted="state == 'draft'"
/>
<field name="date" />
<field name="company_id" optional="hide" />
</tree>
</field>
</record>
@@ -150,5 +161,7 @@
sequence="30"
action="action_view_inventory_group_form"
/>
<delete model="ir.ui.menu" id="stock.menu_action_inventory_tree" />
<record id="stock.menu_action_inventory_tree" model="ir.ui.menu">
<field name="active" eval="False" />
</record>
</odoo>