mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
15
stock_inventory/models/res_company.py
Normal file
15
stock_inventory/models/res_company.py
Normal 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,
|
||||
)
|
||||
12
stock_inventory/models/res_config_settings.py
Normal file
12
stock_inventory/models/res_config_settings.py
Normal 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
|
||||
)
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
11
stock_inventory/security/security.xml
Normal file
11
stock_inventory/security/security.xml
Normal 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>
|
||||
@@ -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")
|
||||
|
||||
28
stock_inventory/views/res_config_settings_view.xml
Normal file
28
stock_inventory/views/res_config_settings_view.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user