mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
@@ -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,
|
||||
|
||||
@@ -6,3 +6,5 @@ from . import stock_location
|
||||
from . import stock_inventory
|
||||
from . import stock_warehouse
|
||||
from . import stock_move
|
||||
from . import stock_move_line
|
||||
from . import stock_quant
|
||||
|
||||
@@ -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 @@ class StockCycleCount(models.Model):
|
||||
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 @@ class StockCycleCount(models.Model):
|
||||
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
|
||||
|
||||
@@ -160,6 +160,7 @@ class StockCycleCountRule(models.Model):
|
||||
"date": fields.Datetime.from_string(date),
|
||||
"location": location,
|
||||
"rule_type": self,
|
||||
"company_id": location.company_id,
|
||||
}
|
||||
return cycle_count
|
||||
|
||||
@@ -172,7 +173,7 @@ class StockCycleCountRule(models.Model):
|
||||
.search(
|
||||
[
|
||||
("location_ids", "in", [loc.id]),
|
||||
("state", "in", ["confirm", "done", "draft"]),
|
||||
("state", "in", ["in_progress", "done", "draft"]),
|
||||
],
|
||||
order="date desc",
|
||||
limit=1,
|
||||
|
||||
@@ -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 @@ class StockInventory(models.Model):
|
||||
("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 @@ class StockInventory(models.Model):
|
||||
|
||||
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 @@ class StockInventory(models.Model):
|
||||
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
|
||||
|
||||
@@ -106,7 +106,7 @@ class StockLocation(models.Model):
|
||||
)
|
||||
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",
|
||||
|
||||
15
stock_cycle_count/models/stock_move_line.py
Normal file
15
stock_cycle_count/models/stock_move_line.py
Normal file
@@ -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)
|
||||
44
stock_cycle_count/models/stock_quant.py
Normal file
44
stock_cycle_count/models/stock_quant.py
Normal file
@@ -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
|
||||
@@ -70,10 +70,11 @@ class StockWarehouse(models.Model):
|
||||
@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 @@ class StockWarehouse(models.Model):
|
||||
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 @@ class StockWarehouse(models.Model):
|
||||
)
|
||||
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):
|
||||
|
||||
11
stock_cycle_count/security/security.xml
Normal file
11
stock_cycle_count/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_cycle_count_comp_rule">
|
||||
<field name="name">Stock Cycle Count multi-company</field>
|
||||
<field name="model_id" ref="model_stock_cycle_count" />
|
||||
<field name="global" eval="True" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -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 @@ pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
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.option {
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic {
|
||||
span.problematic, pre.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
@@ -513,7 +514,9 @@ If you spotted it first, help us to smash it by providing a detailed and welcome
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#toc-entry-14">Maintainers</a></h2>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org">
|
||||
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
|
||||
</a>
|
||||
<p>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.</p>
|
||||
|
||||
@@ -58,10 +58,13 @@ class TestStockCycleCount(common.TransactionCase):
|
||||
]
|
||||
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 @@ class TestStockCycleCount(common.TransactionCase):
|
||||
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 @@ class TestStockCycleCount(common.TransactionCase):
|
||||
"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 @@ class TestStockCycleCount(common.TransactionCase):
|
||||
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 @@ class TestStockCycleCount(common.TransactionCase):
|
||||
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.",
|
||||
)
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
<tree
|
||||
decoration-muted="state == 'cancelled'"
|
||||
decoration-info="state == 'draft'"
|
||||
multi_edit="1"
|
||||
default_order="date_deadline asc"
|
||||
>
|
||||
<field name="name" />
|
||||
<field name="location_id" />
|
||||
@@ -17,6 +19,7 @@
|
||||
<field name="responsible_id" />
|
||||
<field name="date_deadline" />
|
||||
<field name="state" />
|
||||
<field name="company_id" optional="hide" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -124,6 +127,7 @@
|
||||
domain="[('state','=', 'cancelled')]"
|
||||
help="Cycle Counts Cancelled"
|
||||
/>
|
||||
<separator />
|
||||
<filter
|
||||
name="assigned_to_user"
|
||||
string="Assigned to me"
|
||||
@@ -150,6 +154,12 @@
|
||||
domain="[]"
|
||||
context="{'group_by':'responsible_id'}"
|
||||
/>
|
||||
<filter
|
||||
string="Required Date"
|
||||
name="date_deadline"
|
||||
domain="[]"
|
||||
context="{'group_by': 'date_deadline:day'}"
|
||||
/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<field name="inherit_id" ref="stock_inventory.view_inventory_group_tree" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="name" position="after">
|
||||
<field name="location_ids" />
|
||||
<field name="cycle_count_id" />
|
||||
<field name="inventory_accuracy" />
|
||||
</field>
|
||||
|
||||
55
stock_cycle_count/views/stock_move_line_view.xml
Normal file
55
stock_cycle_count/views/stock_move_line_view.xml
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!-- Copyright 2017 ForgeFlow S.L.
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
<record id="view_stock_move_line_tree" model="ir.ui.view">
|
||||
<field name="name">Stock Move Line Tree - cycle count extension</field>
|
||||
<field name="model">stock.move.line</field>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="stock_inventory.view_stock_move_line_inventory_tree"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="product_id" position="after">
|
||||
<field name="is_inventory" invisible="1" />
|
||||
<field
|
||||
name="theoretical_qty"
|
||||
attrs="{'invisible': [('is_inventory', '=', False)]}"
|
||||
/>
|
||||
<field
|
||||
name="counted_qty"
|
||||
attrs="{'invisible': [('is_inventory', '=', False)]}"
|
||||
/>
|
||||
<field
|
||||
name="line_accuracy"
|
||||
attrs="{'invisible': [('is_inventory', '=', False)]}"
|
||||
widget="percentage"
|
||||
/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_stock_move_line_form" model="ir.ui.view">
|
||||
<field name="name">Stock Move Line Form - cycle count extension</field>
|
||||
<field name="model">stock.move.line</field>
|
||||
<field name="inherit_id" ref="stock.view_move_line_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="lot_id" position="after">
|
||||
<field name="is_inventory" invisible="1" />
|
||||
<field
|
||||
name="theoretical_qty"
|
||||
attrs="{'invisible': [('is_inventory', '=', False)]}"
|
||||
/>
|
||||
<field
|
||||
name="counted_qty"
|
||||
attrs="{'invisible': [('is_inventory', '=', False)]}"
|
||||
/>
|
||||
<field
|
||||
name="line_accuracy"
|
||||
attrs="{'invisible': [('is_inventory', '=', False)]}"
|
||||
class="oe_inline"
|
||||
widget="percentage"
|
||||
/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user