mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
@@ -1,25 +1,32 @@
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.osv import expression
|
||||
|
||||
READONLY_STATES = {
|
||||
"draft": [("readonly", False)],
|
||||
}
|
||||
|
||||
|
||||
class InventoryAdjustmentsGroup(models.Model):
|
||||
_name = "stock.inventory"
|
||||
_description = "Inventory Adjustment Group"
|
||||
_order = "date desc, id desc"
|
||||
_inherit = [
|
||||
"mail.thread",
|
||||
]
|
||||
|
||||
name = fields.Char(
|
||||
required=True,
|
||||
default="Inventory",
|
||||
string="Inventory Reference",
|
||||
states={"draft": [("readonly", False)]},
|
||||
readonly=True,
|
||||
states=READONLY_STATES,
|
||||
)
|
||||
|
||||
date = fields.Datetime(
|
||||
default=lambda self: fields.Datetime.now(),
|
||||
states={"draft": [("readonly", False)]},
|
||||
readonly=True,
|
||||
states=READONLY_STATES,
|
||||
)
|
||||
|
||||
company_id = fields.Many2one(
|
||||
@@ -32,12 +39,22 @@ class InventoryAdjustmentsGroup(models.Model):
|
||||
)
|
||||
|
||||
state = fields.Selection(
|
||||
[("draft", "Draft"), ("in_progress", "In Progress"), ("done", "Done")],
|
||||
[
|
||||
("draft", "Draft"),
|
||||
("in_progress", "In Progress"),
|
||||
("done", "Done"),
|
||||
("cancel", "Cancelled"),
|
||||
],
|
||||
default="draft",
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
owner_id = fields.Many2one(
|
||||
"res.partner", "Owner", help="This is the owner of the inventory adjustment"
|
||||
"res.partner",
|
||||
"Owner",
|
||||
help="This is the owner of the inventory adjustment",
|
||||
readonly=True,
|
||||
states=READONLY_STATES,
|
||||
)
|
||||
|
||||
location_ids = fields.Many2many(
|
||||
@@ -45,6 +62,8 @@ class InventoryAdjustmentsGroup(models.Model):
|
||||
string="Locations",
|
||||
domain="[('usage', '=', 'internal'), "
|
||||
"'|', ('company_id', '=', company_id), ('company_id', '=', False)]",
|
||||
readonly=True,
|
||||
states=READONLY_STATES,
|
||||
)
|
||||
|
||||
product_selection = fields.Selection(
|
||||
@@ -57,32 +76,47 @@ class InventoryAdjustmentsGroup(models.Model):
|
||||
],
|
||||
default="all",
|
||||
required=True,
|
||||
readonly=True,
|
||||
states=READONLY_STATES,
|
||||
)
|
||||
|
||||
product_ids = fields.Many2many(
|
||||
"product.product",
|
||||
string="Products",
|
||||
domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]",
|
||||
readonly=True,
|
||||
states=READONLY_STATES,
|
||||
)
|
||||
|
||||
stock_quant_ids = fields.Many2many(
|
||||
"stock.quant",
|
||||
string="Inventory Adjustment",
|
||||
domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]",
|
||||
readonly=True,
|
||||
states=READONLY_STATES,
|
||||
)
|
||||
|
||||
category_id = fields.Many2one("product.category", string="Product Category")
|
||||
category_id = fields.Many2one(
|
||||
"product.category",
|
||||
string="Product Category",
|
||||
readonly=True,
|
||||
states=READONLY_STATES,
|
||||
)
|
||||
|
||||
lot_ids = fields.Many2many(
|
||||
"stock.lot",
|
||||
string="Lot/Serial Numbers",
|
||||
domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]",
|
||||
readonly=True,
|
||||
states=READONLY_STATES,
|
||||
)
|
||||
|
||||
stock_move_ids = fields.One2many(
|
||||
"stock.move.line",
|
||||
"inventory_adjustment_id",
|
||||
string="Inventory Adjustments Done",
|
||||
readonly=True,
|
||||
states=READONLY_STATES,
|
||||
)
|
||||
|
||||
count_stock_quants = fields.Integer(
|
||||
@@ -96,6 +130,9 @@ class InventoryAdjustmentsGroup(models.Model):
|
||||
count_stock_moves = fields.Integer(
|
||||
compute="_compute_count_stock_moves", string="Stock Moves Lines"
|
||||
)
|
||||
action_state_to_cancel_allowed = fields.Boolean(
|
||||
compute="_compute_action_state_to_cancel_allowed"
|
||||
)
|
||||
|
||||
exclude_sublocation = fields.Boolean(
|
||||
help="If enabled, it will only take into account "
|
||||
@@ -112,18 +149,38 @@ class InventoryAdjustmentsGroup(models.Model):
|
||||
|
||||
@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.filtered(lambda sq: sq.to_do))
|
||||
self.count_stock_quants_string = "{} / {}".format(
|
||||
count_todo, self.count_stock_quants
|
||||
)
|
||||
for rec in self:
|
||||
quants = rec.stock_quant_ids
|
||||
quants_to_do = quants.filtered(lambda q: q.to_do)
|
||||
count_todo = len(quants_to_do)
|
||||
rec.count_stock_quants = len(quants)
|
||||
rec.count_stock_quants_string = "{} / {}".format(
|
||||
count_todo, rec.count_stock_quants
|
||||
)
|
||||
|
||||
@api.depends("stock_move_ids")
|
||||
def _compute_count_stock_moves(self):
|
||||
sm_ids = self.mapped("stock_move_ids").ids
|
||||
self.count_stock_moves = len(sm_ids)
|
||||
group_fname = "inventory_adjustment_id"
|
||||
group_data = self.env["stock.move.line"].read_group(
|
||||
[
|
||||
(group_fname, "in", self.ids),
|
||||
],
|
||||
[group_fname],
|
||||
[group_fname],
|
||||
)
|
||||
data_by_adj_id = {
|
||||
row[group_fname][0]: row.get(f"{group_fname}_count", 0)
|
||||
for row in group_data
|
||||
}
|
||||
for rec in self:
|
||||
rec.count_stock_moves = data_by_adj_id.get(rec.id, 0)
|
||||
|
||||
def _compute_action_state_to_cancel_allowed(self):
|
||||
for rec in self:
|
||||
rec.action_state_to_cancel_allowed = rec.state == "draft"
|
||||
|
||||
def _get_quants(self, locations):
|
||||
self.ensure_one()
|
||||
domain = []
|
||||
base_domain = self._get_base_domain(locations)
|
||||
if self.product_selection == "all":
|
||||
@@ -153,11 +210,13 @@ class InventoryAdjustmentsGroup(models.Model):
|
||||
return base_domain
|
||||
|
||||
def _get_domain_manual_quants(self, base_domain):
|
||||
self.ensure_one()
|
||||
return expression.AND(
|
||||
[base_domain, [("product_id", "in", self.product_ids.ids)]]
|
||||
)
|
||||
|
||||
def _get_domain_one_quant(self, base_domain):
|
||||
self.ensure_one()
|
||||
return expression.AND(
|
||||
[
|
||||
base_domain,
|
||||
@@ -168,6 +227,7 @@ class InventoryAdjustmentsGroup(models.Model):
|
||||
)
|
||||
|
||||
def _get_domain_lot_quants(self, base_domain):
|
||||
self.ensure_one()
|
||||
return expression.AND(
|
||||
[
|
||||
base_domain,
|
||||
@@ -179,6 +239,7 @@ class InventoryAdjustmentsGroup(models.Model):
|
||||
)
|
||||
|
||||
def _get_domain_category_quants(self, base_domain):
|
||||
self.ensure_one()
|
||||
return expression.AND(
|
||||
[
|
||||
base_domain,
|
||||
@@ -195,6 +256,7 @@ class InventoryAdjustmentsGroup(models.Model):
|
||||
rec.stock_quant_ids = rec._get_quants(rec.location_ids)
|
||||
|
||||
def action_state_to_in_progress(self):
|
||||
self.ensure_one()
|
||||
active_rec = self.env["stock.inventory"].search(
|
||||
[
|
||||
("state", "=", "in_progress"),
|
||||
@@ -203,15 +265,20 @@ class InventoryAdjustmentsGroup(models.Model):
|
||||
limit=1,
|
||||
)
|
||||
if active_rec:
|
||||
raise ValidationError(
|
||||
raise UserError(
|
||||
_(
|
||||
"There's already an Adjustment in Process using one requested Location: %s"
|
||||
)
|
||||
% active_rec.name
|
||||
)
|
||||
self.state = "in_progress"
|
||||
self.refresh_stock_quant_ids()
|
||||
self.stock_quant_ids.update(
|
||||
quants = self._get_quants(self.location_ids)
|
||||
self.write(
|
||||
{
|
||||
"state": "in_progress",
|
||||
"stock_quant_ids": [(6, 0, quants.ids)],
|
||||
}
|
||||
)
|
||||
quants.write(
|
||||
{
|
||||
"to_do": True,
|
||||
"user_id": self.responsible_id,
|
||||
@@ -221,6 +288,7 @@ class InventoryAdjustmentsGroup(models.Model):
|
||||
return
|
||||
|
||||
def action_state_to_done(self):
|
||||
self.ensure_one()
|
||||
self.state = "done"
|
||||
self.stock_quant_ids.update(
|
||||
{
|
||||
@@ -238,6 +306,7 @@ class InventoryAdjustmentsGroup(models.Model):
|
||||
return
|
||||
|
||||
def action_state_to_draft(self):
|
||||
self.ensure_one()
|
||||
self.state = "draft"
|
||||
self.stock_quant_ids.update(
|
||||
{
|
||||
@@ -249,20 +318,52 @@ class InventoryAdjustmentsGroup(models.Model):
|
||||
self.stock_quant_ids = None
|
||||
return
|
||||
|
||||
def action_state_to_cancel(self):
|
||||
self.ensure_one()
|
||||
self._check_action_state_to_cancel()
|
||||
self.write(
|
||||
{
|
||||
"state": "cancel",
|
||||
}
|
||||
)
|
||||
|
||||
def _check_action_state_to_cancel(self):
|
||||
for rec in self:
|
||||
if not rec.action_state_to_cancel_allowed:
|
||||
raise UserError(
|
||||
_(
|
||||
"You can't cancel this inventory %(display_name)s.",
|
||||
display_name=rec.display_name,
|
||||
)
|
||||
)
|
||||
|
||||
def action_view_inventory_adjustment(self):
|
||||
self.ensure_one()
|
||||
result = self.env["stock.quant"].action_view_inventory()
|
||||
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
|
||||
context = result.get("context", {})
|
||||
context.update(
|
||||
{
|
||||
"search_default_to_do": 1,
|
||||
"inventory_id": self.id,
|
||||
"default_to_do": True,
|
||||
}
|
||||
)
|
||||
result.update(
|
||||
{
|
||||
"domain": [("id", "in", self.stock_quant_ids.ids)],
|
||||
"search_view_id": self.env.ref("stock.quant_search_view").id,
|
||||
"context": context,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
def action_view_stock_moves(self):
|
||||
self.ensure_one()
|
||||
result = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"stock_inventory.action_view_stock_move_line_inventory_tree"
|
||||
)
|
||||
sm_ids = self.mapped("stock_move_ids").ids
|
||||
result["domain"] = [("id", "in", sm_ids)]
|
||||
result["context"] = []
|
||||
result["domain"] = [("inventory_adjustment_id", "=", self.id)]
|
||||
result["context"] = {}
|
||||
return result
|
||||
|
||||
@api.constrains("state", "location_ids")
|
||||
|
||||
@@ -59,9 +59,9 @@ class StockQuant(models.Model):
|
||||
def _get_inventory_fields_write(self):
|
||||
return super()._get_inventory_fields_write() + ["to_do"]
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
res = super().create(vals)
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
res = super().create(vals_list)
|
||||
if self.env.context.get(
|
||||
"active_model", False
|
||||
) == "stock.inventory" and self.env.context.get("active_id", False):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright 2022 ForgeFlow S.L
|
||||
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ class TestStockInventory(TransactionCase):
|
||||
"location_ids": [self.location1.id],
|
||||
}
|
||||
)
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
with self.assertRaises(UserError), self.cr.savepoint():
|
||||
inventory2.action_state_to_in_progress()
|
||||
self.assertEqual(inventory1.state, "in_progress")
|
||||
self.assertEqual(
|
||||
@@ -137,7 +137,7 @@ class TestStockInventory(TransactionCase):
|
||||
inventory1.action_view_inventory_adjustment()
|
||||
self.quant1.inventory_quantity = 92
|
||||
self.quant1.action_apply_inventory()
|
||||
inventory1._compute_count_stock_quants()
|
||||
inventory1.invalidate_recordset()
|
||||
inventory1.action_view_stock_moves()
|
||||
self.assertEqual(inventory1.count_stock_moves, 1)
|
||||
self.assertEqual(inventory1.count_stock_quants, 3)
|
||||
@@ -172,8 +172,7 @@ class TestStockInventory(TransactionCase):
|
||||
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()
|
||||
inventory1.invalidate_recordset()
|
||||
self.assertEqual(inventory1.count_stock_moves, 1)
|
||||
self.assertEqual(inventory1.count_stock_quants, 2)
|
||||
self.assertEqual(inventory1.count_stock_quants_string, "1 / 2")
|
||||
@@ -183,15 +182,15 @@ class TestStockInventory(TransactionCase):
|
||||
self.assertEqual(inventory1.stock_move_ids.location_id.id, self.location3.id)
|
||||
self.quant1.inventory_quantity = 65
|
||||
self.quant1.action_apply_inventory()
|
||||
inventory1._compute_count_stock_quants()
|
||||
inventory1.invalidate_recordset()
|
||||
self.assertEqual(inventory1.count_stock_moves, 2)
|
||||
self.assertEqual(inventory1.count_stock_quants, 2)
|
||||
self.assertEqual(inventory1.count_stock_quants_string, "0 / 2")
|
||||
inventory1.action_state_to_done()
|
||||
|
||||
def test_03_one_selection(self):
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
inventory1 = self.inventory_model.create(
|
||||
with self.assertRaises(UserError), self.cr.savepoint():
|
||||
self.inventory_model.create(
|
||||
{
|
||||
"name": "Inventory_Test_5",
|
||||
"product_selection": "one",
|
||||
@@ -222,7 +221,7 @@ class TestStockInventory(TransactionCase):
|
||||
inventory1.action_view_inventory_adjustment()
|
||||
self.quant3.inventory_quantity = 74
|
||||
self.quant3.action_apply_inventory()
|
||||
inventory1._compute_count_stock_quants()
|
||||
inventory1.invalidate_recordset()
|
||||
inventory1.action_view_stock_moves()
|
||||
self.assertEqual(inventory1.count_stock_moves, 1)
|
||||
self.assertEqual(inventory1.count_stock_quants, 2)
|
||||
@@ -233,15 +232,15 @@ class TestStockInventory(TransactionCase):
|
||||
self.assertEqual(inventory1.stock_move_ids.location_id.id, self.location3.id)
|
||||
self.quant1.inventory_quantity = 65
|
||||
self.quant1.action_apply_inventory()
|
||||
inventory1._compute_count_stock_quants()
|
||||
inventory1.invalidate_recordset()
|
||||
self.assertEqual(inventory1.count_stock_moves, 2)
|
||||
self.assertEqual(inventory1.count_stock_quants, 2)
|
||||
self.assertEqual(inventory1.count_stock_quants_string, "0 / 2")
|
||||
inventory1.action_state_to_done()
|
||||
|
||||
def test_04_lot_selection(self):
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
inventory1 = self.inventory_model.create(
|
||||
with self.assertRaises(UserError), self.cr.savepoint():
|
||||
self.inventory_model.create(
|
||||
{
|
||||
"name": "Inventory_Test_6",
|
||||
"product_selection": "lot",
|
||||
@@ -272,7 +271,7 @@ class TestStockInventory(TransactionCase):
|
||||
inventory1.action_view_inventory_adjustment()
|
||||
self.quant3.inventory_quantity = 74
|
||||
self.quant3.action_apply_inventory()
|
||||
inventory1._compute_count_stock_quants()
|
||||
inventory1.invalidate_recordset()
|
||||
inventory1.action_view_stock_moves()
|
||||
self.assertEqual(inventory1.count_stock_moves, 1)
|
||||
self.assertEqual(inventory1.count_stock_quants, 1)
|
||||
@@ -306,7 +305,7 @@ class TestStockInventory(TransactionCase):
|
||||
inventory1.action_view_inventory_adjustment()
|
||||
self.quant4.inventory_quantity = 74
|
||||
self.quant4.action_apply_inventory()
|
||||
inventory1._compute_count_stock_quants()
|
||||
inventory1.invalidate_recordset()
|
||||
inventory1.action_view_stock_moves()
|
||||
self.assertEqual(inventory1.count_stock_moves, 1)
|
||||
self.assertEqual(inventory1.count_stock_quants, 1)
|
||||
@@ -334,7 +333,7 @@ class TestStockInventory(TransactionCase):
|
||||
"exclude_sublocation": True,
|
||||
}
|
||||
)
|
||||
with self.assertRaises(ValidationError), self.cr.savepoint():
|
||||
with self.assertRaises(UserError), self.cr.savepoint():
|
||||
inventory2.action_state_to_in_progress()
|
||||
self.assertEqual(inventory1.state, "in_progress")
|
||||
self.assertEqual(
|
||||
|
||||
@@ -20,6 +20,13 @@
|
||||
attrs="{'invisible':['|',('state', 'in', ['draft', 'done']), ('count_stock_moves', '!=', 0)]}"
|
||||
string="Back to Draft"
|
||||
/>
|
||||
<field name="action_state_to_cancel_allowed" invisible="1" />
|
||||
<button
|
||||
type="object"
|
||||
name="action_state_to_cancel"
|
||||
attrs="{'invisible':[('action_state_to_cancel_allowed', '=', False),]}"
|
||||
string="Cancel"
|
||||
/>
|
||||
<button
|
||||
type="object"
|
||||
name="action_state_to_done"
|
||||
@@ -70,16 +77,11 @@
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field
|
||||
name="product_selection"
|
||||
widget="radio"
|
||||
attrs="{'readonly':[('state', 'in', ['in_progress', 'done'])]}"
|
||||
/>
|
||||
<field name="product_selection" widget="radio" />
|
||||
<field
|
||||
name="location_ids"
|
||||
string="Locations"
|
||||
widget="many2many_tags"
|
||||
attrs="{'readonly':[('state', 'in', ['in_progress', 'done'])]}"
|
||||
required="1"
|
||||
/>
|
||||
<field
|
||||
@@ -99,27 +101,31 @@
|
||||
<field
|
||||
name="product_ids"
|
||||
widget="many2many_tags"
|
||||
attrs="{'readonly':[('state', 'in', ['in_progress', 'done'])], 'required': [('product_selection', 'in', ['manual', 'lot'])],'invisible': [('product_selection', 'in', ['all', 'category', 'one'])]}"
|
||||
attrs="{'required': [('product_selection', 'in', ['manual', 'lot'])],'invisible': [('product_selection', 'in', ['all', 'category', 'one'])]}"
|
||||
/>
|
||||
<field
|
||||
name="product_ids"
|
||||
widget="many2many_tags"
|
||||
options="{'limit': 10}"
|
||||
attrs="{'readonly':[('state', 'in', ['in_progress', 'done'])], 'required': [('product_selection', '=', 'one')],'invisible': [('product_selection', '!=', 'one')]}"
|
||||
attrs="{'required': [('product_selection', '=', 'one')],'invisible': [('product_selection', '!=', 'one')]}"
|
||||
/>
|
||||
<field
|
||||
name="category_id"
|
||||
attrs="{'readonly':[('state', 'in', ['in_progress', 'done'])], 'required': [('product_selection', '=', 'category')],'invisible': [('product_selection', '!=', 'category')]}"
|
||||
attrs="{'required': [('product_selection', '=', 'category')],'invisible': [('product_selection', '!=', 'category')]}"
|
||||
/>
|
||||
<field
|
||||
name="lot_ids"
|
||||
widget="many2many_tags"
|
||||
domain="[('product_id', 'in', product_ids)]"
|
||||
attrs="{'readonly':[('state', 'in', ['in_progress', 'done'])], 'required': [('product_selection', '=', 'lot')],'invisible': [('product_selection', '!=', 'lot')]}"
|
||||
attrs="{'required': [('product_selection', '=', 'lot')],'invisible': [('product_selection', '!=', 'lot')]}"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers" />
|
||||
<field name="message_ids" widget="mail_thread" />
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
@@ -147,6 +153,18 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="stock_inventory_search_view">
|
||||
<field name="model">stock.inventory</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name" />
|
||||
<field name="location_ids" />
|
||||
<field name="date" />
|
||||
<field name="state" />
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_view_inventory_group_form" model="ir.actions.act_window">
|
||||
<field name="name">Inventory Adjustment Group</field>
|
||||
<field name="res_model">stock.inventory</field>
|
||||
|
||||
Reference in New Issue
Block a user