diff --git a/stock_cycle_count/__manifest__.py b/stock_cycle_count/__manifest__.py
index fe7ae7cbf036..03c29d565d12 100644
--- a/stock_cycle_count/__manifest__.py
+++ b/stock_cycle_count/__manifest__.py
@@ -17,12 +17,14 @@
"views/stock_warehouse_view.xml",
"views/stock_inventory_view.xml",
"views/stock_location_view.xml",
+ "views/stock_move_line_view.xml",
"views/res_config_settings_view.xml",
"data/cycle_count_sequence.xml",
"data/cycle_count_ir_cron.xml",
"reports/stock_location_accuracy_report.xml",
"reports/stock_cycle_count_report.xml",
"security/ir.model.access.csv",
+ "security/security.xml",
],
"license": "AGPL-3",
"installable": True,
diff --git a/stock_cycle_count/models/__init__.py b/stock_cycle_count/models/__init__.py
index e1320941dc81..9a1035894393 100644
--- a/stock_cycle_count/models/__init__.py
+++ b/stock_cycle_count/models/__init__.py
@@ -6,3 +6,5 @@
from . import stock_inventory
from . import stock_warehouse
from . import stock_move
+from . import stock_move_line
+from . import stock_quant
diff --git a/stock_cycle_count/models/stock_cycle_count.py b/stock_cycle_count/models/stock_cycle_count.py
index 89384fca8f51..d39eb72e74d6 100644
--- a/stock_cycle_count/models/stock_cycle_count.py
+++ b/stock_cycle_count/models/stock_cycle_count.py
@@ -1,10 +1,13 @@
# Copyright 2017-18 ForgeFlow S.L.
# (http://www.forgeflow.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+import logging
from odoo import _, api, fields, models
from odoo.exceptions import UserError
+_logger = logging.getLogger(__name__)
+
class StockCycleCount(models.Model):
_name = "stock.cycle.count"
@@ -24,7 +27,7 @@ class StockCycleCount(models.Model):
comodel_name="res.users",
string="Assigned to",
readonly=True,
- states={"draft": [("readonly", False)]},
+ states={"draft": [("readonly", False)], "open": [("readonly", False)]},
tracking=True,
)
date_deadline = fields.Date(
@@ -32,6 +35,21 @@ class StockCycleCount(models.Model):
readonly=True,
states={"draft": [("readonly", False)]},
tracking=True,
+ compute="_compute_date_deadline",
+ inverse="_inverse_date_deadline",
+ store=True,
+ )
+ automatic_deadline_date = fields.Date(
+ string="Automatic Required Date",
+ readonly=True,
+ states={"draft": [("readonly", False)]},
+ tracking=True,
+ )
+ manual_deadline_date = fields.Date(
+ string="Manual Required Date",
+ readonly=True,
+ states={"draft": [("readonly", False)]},
+ tracking=True,
)
cycle_count_rule_id = fields.Many2one(
comodel_name="stock.cycle.count.rule",
@@ -99,15 +117,10 @@ def action_create_inventory_adjustment(self):
data = rec._prepare_inventory_adjustment()
inv = self.env["stock.inventory"].create(data)
if rec.company_id.auto_start_inventory_from_cycle_count:
- inv.prefill_counted_quantity = (
- rec.company_id.inventory_adjustment_counted_quantities
- )
- inv.action_state_to_in_progress()
- if inv.prefill_counted_quantity == "zero":
- inv.stock_quant_ids.write({"inventory_quantity": 0})
- else:
- for quant in inv.stock_quant_ids:
- quant.write({"inventory_quantity": quant.quantity})
+ try:
+ inv.action_state_to_in_progress()
+ except Exception as e:
+ _logger.info("Error when beginning an adjustment: %s", str(e))
self.write({"state": "open"})
return True
@@ -124,3 +137,15 @@ def action_view_inventory(self):
action["views"] = [(res and res.id or False, "form")]
action["res_id"] = adjustment_ids and adjustment_ids[0] or False
return action
+
+ @api.depends("automatic_deadline_date", "manual_deadline_date")
+ def _compute_date_deadline(self):
+ for rec in self:
+ if rec.manual_deadline_date:
+ rec.date_deadline = rec.manual_deadline_date
+ else:
+ rec.date_deadline = rec.automatic_deadline_date
+
+ def _inverse_date_deadline(self):
+ for rec in self:
+ rec.manual_deadline_date = rec.date_deadline
diff --git a/stock_cycle_count/models/stock_cycle_count_rule.py b/stock_cycle_count/models/stock_cycle_count_rule.py
index 15f7a75459fb..06e2129d06da 100644
--- a/stock_cycle_count/models/stock_cycle_count_rule.py
+++ b/stock_cycle_count/models/stock_cycle_count_rule.py
@@ -160,6 +160,7 @@ def _propose_cycle_count(self, date, location):
"date": fields.Datetime.from_string(date),
"location": location,
"rule_type": self,
+ "company_id": location.company_id,
}
return cycle_count
@@ -172,7 +173,7 @@ def _compute_rule_periodic(self, locs):
.search(
[
("location_ids", "in", [loc.id]),
- ("state", "in", ["confirm", "done", "draft"]),
+ ("state", "in", ["in_progress", "done", "draft"]),
],
order="date desc",
limit=1,
diff --git a/stock_cycle_count/models/stock_inventory.py b/stock_cycle_count/models/stock_inventory.py
index 9993856bf055..c7304ed56f10 100644
--- a/stock_cycle_count/models/stock_inventory.py
+++ b/stock_cycle_count/models/stock_inventory.py
@@ -29,25 +29,48 @@ class StockInventory(models.Model):
)
inventory_accuracy = fields.Float(
string="Accuracy",
- compute="_compute_inventory_accuracy",
digits=(3, 2),
store=True,
group_operator="avg",
+ default=False,
+ )
+ responsible_id = fields.Many2one(
+ tracking=True,
+ compute="_compute_responsible_id",
+ inverse="_inverse_responsible_id",
+ store=True,
+ readonly=False,
)
- @api.depends("state", "stock_quant_ids")
- def _compute_inventory_accuracy(self):
+ @api.depends("cycle_count_id.responsible_id")
+ def _compute_responsible_id(self):
for inv in self:
- theoretical = sum(inv.stock_quant_ids.mapped(lambda x: abs(x.quantity)))
- abs_discrepancy = sum(
- inv.stock_quant_ids.mapped(lambda x: abs(x.inventory_diff_quantity))
- )
- if theoretical:
- inv.inventory_accuracy = max(
- PERCENT * (theoretical - abs_discrepancy) / theoretical, 0.0
+ if inv.cycle_count_id:
+ inv.responsible_id = inv.cycle_count_id.responsible_id
+ inv.stock_quant_ids.write(
+ {"user_id": inv.cycle_count_id.responsible_id}
)
- if not inv.stock_quant_ids and inv.state == "done":
- inv.inventory_accuracy = PERCENT
+
+ def _inverse_responsible_id(self):
+ for inv in self:
+ if inv.cycle_count_id and inv.responsible_id:
+ inv.cycle_count_id.responsible_id = inv.responsible_id
+
+ def write(self, vals):
+ result = super().write(vals)
+ if "responsible_id" in vals:
+ if not self.env.context.get("no_propagate"):
+ if (
+ self.cycle_count_id
+ and self.cycle_count_id.responsible_id.id != vals["responsible_id"]
+ ):
+ self.cycle_count_id.with_context(no_propagate=True).write(
+ {"responsible_id": vals["responsible_id"]}
+ )
+ for quant in self.mapped("stock_quant_ids"):
+ if quant.user_id.id != vals["responsible_id"]:
+ quant.write({"user_id": vals["responsible_id"]})
+ return result
def _update_cycle_state(self):
for inv in self:
@@ -61,6 +84,26 @@ def _domain_cycle_count_candidate(self):
("location_id", "in", self.location_ids.ids),
]
+ def _calculate_inventory_accuracy(self):
+ for inv in self:
+ accuracy = 100
+ sum_line_accuracy = 0
+ sum_theoretical_qty = 0
+ if inv.stock_move_ids:
+ for line in inv.stock_move_ids:
+ sum_line_accuracy += line.theoretical_qty * line.line_accuracy
+ sum_theoretical_qty += line.theoretical_qty
+ if sum_theoretical_qty != 0:
+ accuracy = (sum_line_accuracy / sum_theoretical_qty) * 100
+ else:
+ accuracy = 0
+ inv.update(
+ {
+ "inventory_accuracy": accuracy,
+ }
+ )
+ return False
+
def _link_to_planned_cycle_count(self):
self.ensure_one()
domain = self._domain_cycle_count_candidate()
@@ -85,11 +128,13 @@ def _link_to_planned_cycle_count(self):
def action_state_to_done(self):
res = super().action_state_to_done()
+ self._calculate_inventory_accuracy()
self._update_cycle_state()
return res
def action_force_done(self):
res = super().action_force_done()
+ self._calculate_inventory_accuracy()
self._update_cycle_state()
return res
@@ -144,3 +189,15 @@ def _check_cycle_count_consistency(self):
message=msg,
)
)
+
+ def action_state_to_in_progress(self):
+ res = super().action_state_to_in_progress()
+ self.prefill_counted_quantity = (
+ self.company_id.inventory_adjustment_counted_quantities
+ )
+ if self.prefill_counted_quantity == "zero":
+ self.stock_quant_ids.write({"inventory_quantity": 0})
+ elif self.prefill_counted_quantity == "counted":
+ for quant in self.stock_quant_ids:
+ quant.write({"inventory_quantity": quant.quantity})
+ return res
diff --git a/stock_cycle_count/models/stock_location.py b/stock_cycle_count/models/stock_location.py
index 2a09fc760315..a03e30e4abef 100644
--- a/stock_cycle_count/models/stock_location.py
+++ b/stock_cycle_count/models/stock_location.py
@@ -106,7 +106,7 @@ def create_zero_confirmation_cycle_count(self):
)
self.env["stock.cycle.count"].create(
{
- "date_deadline": date,
+ "automatic_deadline_date": date,
"location_id": self.id,
"cycle_count_rule_id": rule.id,
"state": "draft",
diff --git a/stock_cycle_count/models/stock_move_line.py b/stock_cycle_count/models/stock_move_line.py
new file mode 100644
index 000000000000..23b4184bbffa
--- /dev/null
+++ b/stock_cycle_count/models/stock_move_line.py
@@ -0,0 +1,15 @@
+# Copyright 2024 ForgeFlow S.L.
+# (http://www.forgeflow.com)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+from odoo import fields, models
+
+
+class StockMoveLine(models.Model):
+ _inherit = "stock.move.line"
+
+ line_accuracy = fields.Float(
+ string="Accuracy",
+ store=True,
+ )
+ theoretical_qty = fields.Float(string="Theoretical Quantity", store=True)
+ counted_qty = fields.Float(string="Counted Quantity", store=True)
diff --git a/stock_cycle_count/models/stock_quant.py b/stock_cycle_count/models/stock_quant.py
new file mode 100644
index 000000000000..1a4d6608c121
--- /dev/null
+++ b/stock_cycle_count/models/stock_quant.py
@@ -0,0 +1,44 @@
+# Copyright 2024 ForgeFlow S.L.
+# (http://www.forgeflow.com)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
+from odoo import models
+
+
+class StockQuant(models.Model):
+ _inherit = "stock.quant"
+
+ def _apply_inventory(self):
+ accuracy_dict = {}
+ theoretical_dict = {}
+ counted_dict = {}
+ for rec in self:
+ if rec.discrepancy_percent > 100:
+ line_accuracy = 0
+ else:
+ line_accuracy = 1 - (rec.discrepancy_percent / 100)
+ accuracy_dict[rec.id] = line_accuracy
+ theoretical_dict[rec.id] = rec.quantity
+ counted_dict[rec.id] = rec.inventory_quantity
+ res = super()._apply_inventory()
+ for rec in self:
+ record_moves = self.env["stock.move.line"]
+ moves = record_moves.search(
+ [
+ ("product_id", "=", rec.product_id.id),
+ ("lot_id", "=", rec.lot_id.id),
+ "|",
+ ("location_id", "=", rec.location_id.id),
+ ("location_dest_id", "=", rec.location_id.id),
+ ]
+ + ([("company_id", "=", rec.company_id.id)] if rec.company_id else []),
+ order="create_date asc",
+ )
+ move = moves[len(moves) - 1]
+ move.write(
+ {
+ "line_accuracy": accuracy_dict[rec.id],
+ "theoretical_qty": theoretical_dict[rec.id],
+ "counted_qty": counted_dict[rec.id],
+ }
+ )
+ return res
diff --git a/stock_cycle_count/models/stock_warehouse.py b/stock_cycle_count/models/stock_warehouse.py
index 49eaa10288cd..4078af0005b2 100644
--- a/stock_cycle_count/models/stock_warehouse.py
+++ b/stock_cycle_count/models/stock_warehouse.py
@@ -70,10 +70,11 @@ def _cycle_count_rules_to_compute(self):
@api.model
def _prepare_cycle_count(self, cycle_count_proposed):
return {
- "date_deadline": cycle_count_proposed["date"],
+ "automatic_deadline_date": cycle_count_proposed["date"],
"location_id": cycle_count_proposed["location"].id,
"cycle_count_rule_id": cycle_count_proposed["rule_type"].id,
"state": "draft",
+ "company_id": cycle_count_proposed["company_id"].id,
}
def action_compute_cycle_count_rules(self):
@@ -106,12 +107,17 @@ def _process_cycle_counts(self, proposed_cycle_counts):
cycle_count_proposed = next(
filter(lambda x: x["date"] == earliest_date, proposed_for_loc)
)
- self._handle_existing_cycle_counts(loc, cycle_count_proposed)
+ existing_cycle_counts = self._handle_existing_cycle_counts(
+ loc, cycle_count_proposed
+ )
delta = (
fields.Datetime.from_string(cycle_count_proposed["date"])
- datetime.today()
)
- if delta.days < self.cycle_count_planning_horizon:
+ if (
+ not existing_cycle_counts
+ and delta.days < self.cycle_count_planning_horizon
+ ):
cc_vals = self._prepare_cycle_count(cycle_count_proposed)
cc_vals_list.append(cc_vals)
return cc_vals_list
@@ -133,10 +139,11 @@ def _handle_existing_cycle_counts(self, location, cycle_count_proposed):
)
cc_to_update.write(
{
- "date_deadline": cycle_count_proposed_date,
+ "automatic_deadline_date": cycle_count_proposed_date,
"cycle_count_rule_id": cycle_count_proposed["rule_type"].id,
}
)
+ return existing_cycle_counts
@api.model
def cron_cycle_count(self):
diff --git a/stock_cycle_count/security/security.xml b/stock_cycle_count/security/security.xml
new file mode 100644
index 000000000000..b369f0667bd5
--- /dev/null
+++ b/stock_cycle_count/security/security.xml
@@ -0,0 +1,11 @@
+
+
+
+ Stock Cycle Count multi-company
+
+
+ ['|',('company_id','=',False),('company_id', 'in', company_ids)]
+
+
diff --git a/stock_cycle_count/static/description/index.html b/stock_cycle_count/static/description/index.html
index 7cf0ee64c511..17edd7396e84 100644
--- a/stock_cycle_count/static/description/index.html
+++ b/stock_cycle_count/static/description/index.html
@@ -8,10 +8,11 @@
/*
:Author: David Goodger (goodger@python.org)
-:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
+:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
+Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
@@ -274,7 +275,7 @@
margin-left: 2em ;
margin-right: 2em }
-pre.code .ln { color: grey; } /* line numbers */
+pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
@@ -300,7 +301,7 @@
span.pre {
white-space: pre }
-span.problematic {
+span.problematic, pre.problematic {
color: red }
span.section-subtitle {
@@ -513,7 +514,9 @@
This module is maintained by the OCA.
-
+
+
+
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
diff --git a/stock_cycle_count/tests/test_stock_cycle_count.py b/stock_cycle_count/tests/test_stock_cycle_count.py
index 449fcd8dd36a..d96992984203 100644
--- a/stock_cycle_count/tests/test_stock_cycle_count.py
+++ b/stock_cycle_count/tests/test_stock_cycle_count.py
@@ -58,10 +58,13 @@ def setUpClass(cls):
]
cls.big_wh.write({"cycle_count_rule_ids": [(6, 0, cls.rule_ids)]})
- # Create a location:
+ # Create locations:
cls.count_loc = cls.stock_location_model.create(
{"name": "Place", "usage": "production"}
)
+ cls.count_loc_2 = cls.stock_location_model.create(
+ {"name": "Place 2", "usage": "production"}
+ )
cls.stock_location_model._parent_store_compute()
# Create a cycle count:
@@ -77,6 +80,9 @@ def setUpClass(cls):
cls.product1 = cls.product_model.create(
{"name": "Test Product 1", "type": "product", "default_code": "PROD1"}
)
+ cls.product2 = cls.product_model.create(
+ {"name": "Test Product 2", "type": "product", "default_code": "PROD2"}
+ )
@classmethod
def _create_user(cls, login, groups, company):
@@ -153,7 +159,7 @@ def test_cycle_count_planner(self):
"name": "To be cancelled when running cron job.",
"cycle_count_rule_id": self.rule_periodic.id,
"location_id": loc.id,
- "date_deadline": date_pre_existing_cc,
+ "automatic_deadline_date": date_pre_existing_cc,
}
)
self.assertEqual(
@@ -188,14 +194,13 @@ def test_cycle_count_planner(self):
move1._action_assign()
move1.move_line_ids[0].qty_done = 1.0
move1._action_done()
+ # Remove the pre_existing_count
+ self.inventory_model.search(
+ [("cycle_count_id", "=", pre_existing_count.id)], limit=1
+ ).unlink()
+ pre_existing_count.unlink()
+ # Execute cron for first time
wh.cron_cycle_count()
- self.assertNotEqual(
- pre_existing_count.date_deadline,
- date_pre_existing_cc,
- "Date of pre-existing cycle counts has not been " "updated.",
- )
- counts = self.cycle_count_model.search([("location_id", "in", locs.ids)])
- self.assertTrue(counts, "Cycle counts not planned")
# Zero-confirmations:
count = self.cycle_count_model.search(
[
@@ -334,5 +339,279 @@ def test_cycle_count_contrains(self):
with self.assertRaises(ValidationError):
inventory.exclude_sublocation = False
company = self.env["res.company"].create({"name": "Test"})
+
with self.assertRaises(ValidationError):
inventory.company_id = company
+
+ def test_inventory_adjustment_accuracy(self):
+ date = datetime.today() - timedelta(days=1)
+ # Create location
+ loc = self.stock_location_model.create(
+ {"name": "Test Location", "usage": "internal"}
+ )
+ # Create stock quants for specific location
+ quant1 = self.quant_model.create(
+ {
+ "product_id": self.product1.id,
+ "location_id": loc.id,
+ "quantity": 10.0,
+ }
+ )
+ quant2 = self.quant_model.create(
+ {
+ "product_id": self.product2.id,
+ "location_id": loc.id,
+ "quantity": 15.0,
+ }
+ )
+ # Create adjustments for specific location
+ adjustment = self.inventory_model.create(
+ {
+ "name": "Pre-existing inventory",
+ "location_ids": [(4, loc.id)],
+ "date": date,
+ }
+ )
+ # Start the adjustment
+ adjustment.action_state_to_in_progress()
+ # Check that there are stock quants for the specific location
+ self.assertTrue(self.env["stock.quant"].search([("location_id", "=", loc.id)]))
+ # Make the count of the stock
+ quant1.update(
+ {
+ "inventory_quantity": 5,
+ }
+ )
+ quant2.update(
+ {
+ "inventory_quantity": 10,
+ }
+ )
+ # Apply the changes
+ quant1._apply_inventory()
+ quant2._apply_inventory()
+ # Check that line_accuracy is calculated properly
+ sml = self.env["stock.move.line"].search(
+ [("location_id", "=", loc.id), ("product_id", "=", self.product1.id)]
+ )
+ self.assertEqual(sml.line_accuracy, 0.5)
+ sml = self.env["stock.move.line"].search(
+ [("location_id", "=", loc.id), ("product_id", "=", self.product2.id)]
+ )
+ self.assertEqual(sml.line_accuracy, 0.6667000000000001)
+ # Set Inventory Adjustment to Done
+ adjustment.action_state_to_done()
+ # Check that accuracy is correctly calculated
+ self.assertEqual(adjustment.inventory_accuracy, 60)
+
+ def test_zero_inventory_adjustment_accuracy(self):
+ date = datetime.today() - timedelta(days=1)
+ # Create location
+ loc = self.stock_location_model.create(
+ {"name": "Test Location", "usage": "internal"}
+ )
+ # Create stock quants for specific location
+ quant1 = self.quant_model.create(
+ {
+ "product_id": self.product1.id,
+ "location_id": loc.id,
+ "quantity": 15.0,
+ }
+ )
+ quant2 = self.quant_model.create(
+ {
+ "product_id": self.product2.id,
+ "location_id": loc.id,
+ "quantity": 10.0,
+ }
+ )
+ # Create adjustment for specific location
+ adjustment = self.inventory_model.create(
+ {
+ "name": "Pre-existing inventory qty zero",
+ "location_ids": [(4, loc.id)],
+ "date": date,
+ }
+ )
+ # Start the adjustment
+ adjustment.action_state_to_in_progress()
+ # Check that there are stock quants for the specific location
+ self.assertTrue(self.env["stock.quant"].search([("location_id", "=", loc.id)]))
+ # Make the count of the stock
+ quant1.update(
+ {
+ "inventory_quantity": 0,
+ }
+ )
+ quant2.update(
+ {
+ "inventory_quantity": 0,
+ }
+ )
+ # Apply the changes
+ quant1._apply_inventory()
+ quant2._apply_inventory()
+ # Check that line_accuracy is calculated properly
+ move_1 = adjustment.stock_move_ids.filtered(
+ lambda c: c.product_id == self.product1
+ )
+ move_2 = adjustment.stock_move_ids.filtered(
+ lambda c: c.product_id == self.product1
+ )
+ self.assertEqual(move_1.line_accuracy, 0)
+ self.assertEqual(move_2.line_accuracy, 0)
+ # Set Inventory Adjustment to Done
+ adjustment.action_state_to_done()
+ # Check that accuracy is correctly calculated
+ self.assertEqual(adjustment.inventory_accuracy, 0)
+ # Check discrepancy over 100%
+ adjustment_2 = self.inventory_model.create(
+ {
+ "name": "Adjustment 2",
+ "location_ids": [(4, loc.id)],
+ "date": date,
+ }
+ )
+ adjustment_2.action_state_to_in_progress()
+ quant1.update(
+ {
+ "inventory_quantity": 1500,
+ }
+ )
+ quant1._apply_inventory()
+ # Check that line_accuracy is calculated properly
+ sml = self.env["stock.move.line"].search(
+ [("location_id", "=", loc.id), ("product_id", "=", self.product1.id)]
+ )
+ # Check that line_accuracy is still 0
+ self.assertEqual(sml.line_accuracy, 0)
+
+ def test_auto_start_inventory_from_cycle_count(self):
+ # Set the auto_start_inventory_from_cycle_count rule to True
+ self.company.auto_start_inventory_from_cycle_count = True
+ # Create Cycle Count 1 cont_loc_2
+ cycle_count_1 = self.cycle_count_model.create(
+ {
+ "name": "Cycle Count 1",
+ "cycle_count_rule_id": self.rule_periodic.id,
+ "location_id": self.count_loc_2.id,
+ "date_deadline": "2026-11-30",
+ "manual_deadline_date": "2026-11-30",
+ }
+ )
+ cycle_count_1.flush()
+ # Confirm the Cycle Count
+ cycle_count_1.action_create_inventory_adjustment()
+ # Inventory adjustments change their state to in_progress
+ self.assertEqual(cycle_count_1.stock_adjustment_ids.state, "in_progress")
+
+ def test_prefill_counted_quantity(self):
+ self.company.inventory_adjustment_counted_quantities = "counted"
+ date = datetime.today() - timedelta(days=1)
+ # Create locations
+ loc_1 = self.stock_location_model.create(
+ {"name": "Test Location 1", "usage": "internal"}
+ )
+ loc_2 = self.stock_location_model.create(
+ {"name": "Test Location 2", "usage": "internal"}
+ )
+ # Create stock quants for different locations
+ quant_1 = self.quant_model.create(
+ {
+ "product_id": self.product1.id,
+ "location_id": loc_1.id,
+ "quantity": 25,
+ }
+ )
+ quant_2 = self.quant_model.create(
+ {
+ "product_id": self.product1.id,
+ "location_id": loc_2.id,
+ "quantity": 50,
+ }
+ )
+ # Create adjustments for different locations
+ adjustment_1 = self.inventory_model.create(
+ {
+ "name": "Adjustment Location 1",
+ "location_ids": [(4, loc_1.id)],
+ "date": date,
+ }
+ )
+ adjustment_2 = self.inventory_model.create(
+ {
+ "name": "Adjustment Location 2",
+ "location_ids": [(4, loc_2.id)],
+ "date": date,
+ }
+ )
+ # Start the adjustment 1 with prefill quantity as counted
+ adjustment_1.action_state_to_in_progress()
+ # Check that the inventory_quantity is 25
+ self.assertEqual(quant_1.inventory_quantity, 25)
+ # Change company prefill option to zero
+ self.company.inventory_adjustment_counted_quantities = "zero"
+ # Start the adjustment 2 with prefill quantity as zero
+ adjustment_2.action_state_to_in_progress()
+ # Check that the inventory_quantity is 0
+ self.assertEqual(quant_2.inventory_quantity, 0.0)
+
+ def test_responsible_id_propagation_with_inventory_adjustment(self):
+ additional_user = self._create_user(
+ "user_3", [self.g_stock_manager], self.company
+ )
+ additional_user_2 = self._create_user(
+ "user_4", [self.g_stock_manager], self.company
+ )
+ self.cycle_count_1.responsible_id = self.manager
+ self.assertEqual(
+ self.cycle_count_1.responsible_id.id,
+ self.manager,
+ "Initial responsible not correctly assigned.",
+ )
+ self.quant_model.create(
+ {
+ "product_id": self.product1.id,
+ "location_id": self.count_loc.id,
+ "quantity": 100,
+ }
+ )
+ self.cycle_count_1.action_create_inventory_adjustment()
+ inventory = self.cycle_count_1.stock_adjustment_ids[0]
+ self.assertEqual(
+ inventory.responsible_id.id,
+ self.cycle_count_1.responsible_id.id,
+ "Inventory responsible does not match cycle count responsible.",
+ )
+ for quant in inventory.stock_quant_ids:
+ self.assertEqual(
+ quant.user_id.id,
+ inventory.responsible_id.id,
+ "Quant user does not match inventory responsible.",
+ )
+ self.cycle_count_1.responsible_id = additional_user.id
+ inventory.invalidate_cache()
+ self.cycle_count_1.stock_adjustment_ids[0].stock_quant_ids.invalidate_cache()
+ self.assertEqual(
+ inventory.responsible_id.id,
+ additional_user.id,
+ "Inventory responsible not updated after cycle count responsible change.",
+ )
+ for quant in inventory.stock_quant_ids:
+ self.assertEqual(
+ quant.user_id.id,
+ additional_user.id,
+ "Quant user not updated after inventory responsible change.",
+ )
+ inventory.responsible_id = additional_user_2
+ self.assertEqual(
+ self.cycle_count_1.responsible_id.id,
+ additional_user_2.id,
+ "Cycle Count not updated after inventory responsible change.",
+ )
+ for quant in inventory.stock_quant_ids:
+ self.assertEqual(
+ quant.user_id.id,
+ additional_user_2.id,
+ "Quant user not updated after inventory responsible change.",
+ )
diff --git a/stock_cycle_count/views/stock_cycle_count_view.xml b/stock_cycle_count/views/stock_cycle_count_view.xml
index e4495efeb26e..09b7ff1175a5 100644
--- a/stock_cycle_count/views/stock_cycle_count_view.xml
+++ b/stock_cycle_count/views/stock_cycle_count_view.xml
@@ -10,6 +10,8 @@
@@ -17,6 +19,7 @@
+
@@ -124,6 +127,7 @@
domain="[('state','=', 'cancelled')]"
help="Cycle Counts Cancelled"
/>
+
+
diff --git a/stock_cycle_count/views/stock_inventory_view.xml b/stock_cycle_count/views/stock_inventory_view.xml
index 1fe85b04415d..aaf709c65eed 100644
--- a/stock_cycle_count/views/stock_inventory_view.xml
+++ b/stock_cycle_count/views/stock_inventory_view.xml
@@ -8,7 +8,6 @@
-
diff --git a/stock_cycle_count/views/stock_move_line_view.xml b/stock_cycle_count/views/stock_move_line_view.xml
new file mode 100644
index 000000000000..d05ec3711022
--- /dev/null
+++ b/stock_cycle_count/views/stock_move_line_view.xml
@@ -0,0 +1,55 @@
+
+
+
+
+ Stock Move Line Tree - cycle count extension
+ stock.move.line
+
+
+
+
+
+
+
+
+
+
+
+ Stock Move Line Form - cycle count extension
+ stock.move.line
+
+
+
+
+
+
+
+
+
+
+