[13.0][MIG] stock_account_valuation_report

[IMP] stock_account_valuation_report: filter by discrepancies
This commit is contained in:
AaronHForgeFlow
2021-05-06 17:37:55 +02:00
committed by Bernat Puig Font
parent 00b7ebda20
commit f790b34bcb
14 changed files with 666 additions and 151 deletions

View File

@@ -14,13 +14,13 @@ Stock Account Valuation Report
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--reporting-lightgray.png?logo=github
:target: https://github.com/OCA/stock-logistics-reporting/tree/12.0/stock_account_valuation_report
:target: https://github.com/OCA/stock-logistics-reporting/tree/13.0/stock_account_valuation_report
:alt: OCA/stock-logistics-reporting
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/stock-logistics-reporting-12-0/stock-logistics-reporting-12-0-stock_account_valuation_report
:target: https://translation.odoo-community.org/projects/stock-logistics-reporting-13-0/stock-logistics-reporting-13-0-stock_account_valuation_report
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/151/12.0
:target: https://runbot.odoo-community.org/runbot/151/13.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
@@ -30,7 +30,8 @@ perpetual inventory, you should be able to reconcile the valuation
from an inventory perspective with the valuation
from an accounting perspective.
This module changes the report in *Inventory / Reporting / Inventory Valuation*
This module changes the report in *Inventory / Reporting /
Dual Inventory Valuation*
to display separately the Quantity and Value of each product for the
Inventory and the Accounting systems .
@@ -45,7 +46,7 @@ Bug Tracker
Bugs are tracked on `GitHub Issues <https://github.com/OCA/stock-logistics-reporting/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/stock-logistics-reporting/issues/new?body=module:%20stock_account_valuation_report%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
`feedback <https://github.com/OCA/stock-logistics-reporting/issues/new?body=module:%20stock_account_valuation_report%0Aversion:%2013.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
@@ -55,12 +56,13 @@ Credits
Authors
~~~~~~~
* Eficent
* ForgeFlow S.L.
Contributors
~~~~~~~~~~~~
* Jordi Ballester Alomar <jordi.ballester@eficent.com>
* Jordi Ballester Alomar <jordi.ballester@forgeflow.com>
* Aaron Henriquez <ahenriquez@forgeflow.com>
Maintainers
~~~~~~~~~~~
@@ -75,6 +77,6 @@ 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.
This module is part of the `OCA/stock-logistics-reporting <https://github.com/OCA/stock-logistics-reporting/tree/12.0/stock_account_valuation_report>`_ project on GitHub.
This module is part of the `OCA/stock-logistics-reporting <https://github.com/OCA/stock-logistics-reporting/tree/13.0/stock_account_valuation_report>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@@ -1,17 +1,16 @@
# Copyright 2018 Eficent Business and IT Consulting Services, S.L.
# (<https://www.eficent.com>)
# Copyright 2018 Aleph Objects, Inc.
# Copyright 2020 ForgeFlow S.L.
# Copyright 2019 Aleph Objects, Inc.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Stock Account Valuation Report",
"version": "12.0.1.0.0",
"version": "13.0.1.0.0",
"summary": "Improves logic of the Inventory Valuation Report",
"author": "Eficent, Odoo Community Association (OCA)",
"author": "ForgeFlow S.L., Odoo Community Association (OCA)",
"website": "https://github.com/OCA/stock-logistics-reporting",
"category": "Warehouse Management",
"depends": ["stock_account"],
"license": "AGPL-3",
"data": ["views/product_product_views.xml"],
"data": ["views/product_product_views.xml", "wizards/stock_valuation_history.xml"],
"installable": True,
}

View File

@@ -1,4 +1,5 @@
# Copyright 2018 Eficent Business and IT Consulting Services, S.L.
# Copyright 2020 ForgeFlow S.L.
# Copyright 2019 Aleph Objects, Inc.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields, models

View File

@@ -1,9 +1,9 @@
# Copyright 2018 Eficent Business and IT Consulting Services, S.L.
# (<https://www.eficent.com>)
# Copyright 2018 Aleph Objects, Inc.
# Copyright 2020 ForgeFlow S.L.
# Copyright 2019 Aleph Objects, Inc.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import _, fields, models
from odoo import _, api, fields, models
class ProductProduct(models.Model):
@@ -18,67 +18,104 @@ class ProductProduct(models.Model):
stock_fifo_real_time_aml_ids = fields.Many2many(
"account.move.line", compute="_compute_inventory_value"
)
stock_fifo_manual_move_ids = fields.Many2many(
"stock.move", compute="_compute_inventory_value"
stock_valuation_layer_ids = fields.Many2many(
"stock.valuation.layer", compute="_compute_inventory_value"
)
valuation_discrepancy = fields.Float(
compute="_compute_inventory_value", search="_search_valuation_discrepancy",
)
qty_discrepancy = fields.Float(
compute="_compute_inventory_value", search="_search_qty_discrepancy",
)
valuation = fields.Selection(
related="product_tmpl_id.valuation", search="_search_valuation"
)
@api.model
def _search_valuation(self, operator, value):
domain = [
"|",
("categ_id.property_valuation", operator, value),
("property_valuation", operator, value),
]
products = self.env["product.product"].search(domain)
if value:
return [("id", "in", products.ids)]
else:
return [("id", "not in", products.ids)]
@api.model
def _search_qty_discrepancy(self, operator, value):
products = self.env["product.product"].search([("type", "=", "product")])
pp_list = []
for pp in products:
if pp.qty_at_date != pp.account_qty_at_date:
pp_list.append(pp.id)
return [("id", "in", pp_list)]
@api.model
def _search_valuation_discrepancy(self, operator, value):
products = self.env["product.product"].search([("type", "=", "product")])
pp_list = []
for pp in products:
if pp.stock_value != pp.account_value:
pp_list.append(pp.id)
return [("id", "in", pp_list)]
def _compute_inventory_value(self):
stock_move = self.env["stock.move"]
self.env["account.move.line"].check_access_rights("read")
to_date = self.env.context.get("to_date", False)
location = self.env.context.get("location", False)
to_date = self.env.context.get("at_date", False)
accounting_values = {}
if not location:
layer_values = {}
# pylint: disable=E8103
query = """
SELECT aml.product_id, aml.account_id,
sum(aml.debit) - sum(aml.credit), sum(quantity),
sum(aml.balance), sum(quantity),
array_agg(aml.id)
FROM account_move_line AS aml
WHERE aml.product_id IN %%s
AND aml.company_id=%%s %s
GROUP BY aml.product_id, aml.account_id"""
params = (tuple(self._ids,), self.env.user.company_id.id)
params = (tuple(self._ids,), self.env.company.id)
if to_date:
# pylint: disable=sql-injection
query = query % ("AND aml.date <= %s",)
params = params + (to_date,)
else:
query = query % ("",)
# pylint: disable=E8103
self.env.cr.execute(query, params=params)
res = self.env.cr.fetchall()
for row in res:
accounting_values[(row[0], row[1])] = (row[2], row[3], list(row[4]))
stock_move_domain = [
("product_id", "in", self._ids),
("date", "<=", to_date),
] + stock_move._get_all_base_domain()
moves = stock_move.search(stock_move_domain)
history = {}
if to_date:
# pylint: disable=E8103
query = """
SELECT DISTINCT ON ("product_id") product_id, cost
FROM "product_price_history"
WHERE datetime <= %s::date
AND product_id IN %s
ORDER BY "product_id", "datetime" DESC NULLS LAST
SELECT DISTINCT ON ("product_id") product_id, sum(quantity),
sum(value), array_agg(svl.id)
FROM "stock_valuation_layer" AS svl
WHERE svl.product_id IN %%s
AND svl.company_id=%%s %s
GROUP BY product_id
ORDER BY "product_id" DESC NULLS LAST
"""
args = (to_date, tuple(self._ids))
self.env.cr.execute(query, args)
for row in self.env.cr.dictfetchall():
history.update({row["product_id"]: row["cost"]})
quantities_dict = self._compute_quantities_dict(
self._context.get("lot_id"),
self._context.get("owner_id"),
self._context.get("package_id"),
self._context.get("from_date"),
self._context.get("to_date"),
)
params = (tuple(self._ids,), self.env.company.id)
if to_date:
# pylint: disable=sql-injection
query = query % ("AND svl.create_date <= %s",)
params = params + (to_date,)
else:
query = query % ("",)
# pylint: disable=E8103
self.env.cr.execute(query, params=params)
res = self.env.cr.fetchall()
aml_ids = self.env["account.move.line"]
for row in res:
layer_values[row[0]] = (row[1], row[2], list(row[3]))
for product in self:
qty_available = quantities_dict[product.id]["qty_available"]
# Retrieve the values from accounting
# We cannot provide location-specific accounting valuation,
# so better, leave the data empty in that case:
if product.valuation == "real_time" and not location:
if product.valuation == "real_time":
valuation_account_id = (
product.categ_id.property_stock_valuation_account_id.id
)
@@ -90,31 +127,31 @@ class ProductProduct(models.Model):
product.stock_fifo_real_time_aml_ids = self.env[
"account.move.line"
].browse(aml_ids)
else:
product.account_value = 0.0
product.account_qty_at_date = 0.0
product.stock_fifo_real_time_aml_ids = []
# Retrieve the values from inventory
if product.cost_method in ["standard", "average"]:
price_used = product.standard_price
if to_date:
price_used = history.get(product.id, 0)
product.stock_value = price_used * qty_available
product.qty_at_date = qty_available
elif product.cost_method == "fifo":
if to_date:
if product.product_tmpl_id.valuation == "manual_periodic":
product.stock_value = sum(moves.mapped("value"))
product.qty_at_date = qty_available
product.stock_fifo_manual_move_ids = stock_move.browse(
moves.ids
quantity, value, svl_ids = layer_values.get(product.id) or (0, 0, [])
product.stock_value = value
product.qty_at_date = quantity
product.stock_valuation_layer_ids = self.env[
"stock.valuation.layer"
].browse(svl_ids)
if product.valuation == "real_time":
product.valuation_discrepancy = (
product.stock_value - product.account_value
)
product.qty_discrepancy = (
product.qty_at_date - product.account_qty_at_date
)
else:
product.stock_value, moves = product._sum_remaining_values()
product.qty_at_date = qty_available
product.stock_fifo_manual_move_ids = moves
product.valuation_discrepancy = 0.0
product.qty_discrepancy = 0.0
def action_view_amls(self):
self.ensure_one()
to_date = self.env.context.get("to_date")
tree_view_ref = self.env.ref("stock_account.view_stock_account_aml")
tree_view_ref = self.env.ref("account.view_move_line_tree")
form_view_ref = self.env.ref("account.view_move_line_form")
action = {
"name": _("Accounting Valuation at date"),
@@ -123,36 +160,23 @@ class ProductProduct(models.Model):
"view_mode": "tree,form",
"context": self.env.context,
"res_model": "account.move.line",
"domain": [
(
"id",
"in",
self.with_context(to_date=to_date).stock_fifo_real_time_aml_ids.ids,
)
],
"domain": [("id", "in", self.stock_fifo_real_time_aml_ids.ids,)],
"views": [(tree_view_ref.id, "tree"), (form_view_ref.id, "form")],
}
return action
def action_view_stock_moves(self):
def action_view_valuation_layers(self):
self.ensure_one()
to_date = self.env.context.get("to_date")
tree_view_ref = self.env.ref("stock_account.view_move_tree_valuation_at_date")
form_view_ref = self.env.ref("stock.view_move_form")
tree_view_ref = self.env.ref("stock_account.stock_valuation_layer_tree")
form_view_ref = self.env.ref("stock_account.stock_valuation_layer_form")
action = {
"name": _("Inventory Valuation"),
"type": "ir.actions.act_window",
"view_type": "form",
"view_mode": "tree,form",
"context": self.env.context,
"res_model": "stock.move",
"domain": [
(
"id",
"in",
self.with_context(to_date=to_date).stock_fifo_manual_move_ids.ids,
)
],
"res_model": "stock.valuation.layer",
"domain": [("id", "in", self.stock_valuation_layer_ids.ids,)],
"views": [(tree_view_ref.id, "tree"), (form_view_ref.id, "form")],
}
return action

View File

@@ -1 +1,2 @@
* Jordi Ballester Alomar <jordi.ballester@eficent.com>
* Jordi Ballester Alomar <jordi.ballester@forgeflow.com>
* Aaron Henriquez <ahenriquez@forgeflow.com>

View File

@@ -3,6 +3,7 @@ perpetual inventory, you should be able to reconcile the valuation
from an inventory perspective with the valuation
from an accounting perspective.
This module changes the report in *Inventory / Reporting / Inventory Valuation*
This module changes the report in *Inventory / Reporting /
Dual Inventory Valuation*
to display separately the Quantity and Value of each product for the
Inventory and the Accounting systems .

View File

@@ -3,7 +3,7 @@
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
<meta name="generator" content="Docutils 0.14: http://docutils.sourceforge.net/" />
<title>Stock Account Valuation Report</title>
<style type="text/css">
@@ -367,12 +367,13 @@ ul.auto-toc {
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/stock-logistics-reporting/tree/12.0/stock_account_valuation_report"><img alt="OCA/stock-logistics-reporting" src="https://img.shields.io/badge/github-OCA%2Fstock--logistics--reporting-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/stock-logistics-reporting-12-0/stock-logistics-reporting-12-0-stock_account_valuation_report"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/151/12.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/stock-logistics-reporting/tree/13.0/stock_account_valuation_report"><img alt="OCA/stock-logistics-reporting" src="https://img.shields.io/badge/github-OCA%2Fstock--logistics--reporting-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/stock-logistics-reporting-13-0/stock-logistics-reporting-13-0-stock_account_valuation_report"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/151/13.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>When you trigger a report of inventory valuation, and you use
perpetual inventory, you should be able to reconcile the valuation
from an inventory perspective with the valuation
from an accounting perspective.</p>
<p>This module changes the report in <em>Inventory / Reporting / Inventory Valuation</em>
<p>This module changes the report in <em>Inventory / Reporting /
Dual Inventory Valuation</em>
to display separately the Quantity and Value of each product for the
Inventory and the Accounting systems .</p>
<p><strong>Table of contents</strong></p>
@@ -392,7 +393,7 @@ Inventory and the Accounting systems .</p>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/stock-logistics-reporting/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/stock-logistics-reporting/issues/new?body=module:%20stock_account_valuation_report%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<a class="reference external" href="https://github.com/OCA/stock-logistics-reporting/issues/new?body=module:%20stock_account_valuation_report%0Aversion:%2013.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
@@ -400,13 +401,14 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id3">Authors</a></h2>
<ul class="simple">
<li>Eficent</li>
<li>ForgeFlow S.L.</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id4">Contributors</a></h2>
<ul class="simple">
<li>Jordi Ballester Alomar &lt;<a class="reference external" href="mailto:jordi.ballester&#64;eficent.com">jordi.ballester&#64;eficent.com</a>&gt;</li>
<li>Jordi Ballester Alomar &lt;<a class="reference external" href="mailto:jordi.ballester&#64;forgeflow.com">jordi.ballester&#64;forgeflow.com</a>&gt;</li>
<li>Aaron Henriquez &lt;<a class="reference external" href="mailto:ahenriquez&#64;forgeflow.com">ahenriquez&#64;forgeflow.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
@@ -416,7 +418,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
<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>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/stock-logistics-reporting/tree/12.0/stock_account_valuation_report">OCA/stock-logistics-reporting</a> project on GitHub.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/stock-logistics-reporting/tree/13.0/stock_account_valuation_report">OCA/stock-logistics-reporting</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>

View File

@@ -0,0 +1 @@
from . import test_stock_account_valuation_report

View File

@@ -0,0 +1,372 @@
# 2020 Copyright ForgeFlow, S.L. (https://www.forgeflow.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from dateutil.relativedelta import relativedelta
from odoo import fields
from odoo.tests.common import TransactionCase
class TestStockAccountValuationReport(TransactionCase):
def setUp(self):
super(TestStockAccountValuationReport, self).setUp()
# Get required Model
self.product_model = self.env["product.product"]
self.template_model = self.env["product.template"]
self.product_ctg_model = self.env["product.category"]
self.account_model = self.env["account.account"]
self.quant_model = self.env["stock.quant"]
self.layer_model = self.env["stock.valuation.layer"]
self.stock_location_model = self.env["stock.location"]
self.res_users_model = self.env["res.users"]
self.account_move_model = self.env["account.move"]
self.aml_model = self.env["account.move.line"]
self.journal_model = self.env["account.journal"]
# Get required Model data
self.product_uom = self.env.ref("uom.product_uom_unit")
self.company = self.env.ref("base.main_company")
self.stock_picking_type_out = self.env.ref("stock.picking_type_out")
self.stock_picking_type_in = self.env.ref("stock.picking_type_in")
self.stock_location_id = self.ref("stock.stock_location_stock")
self.stock_location_customer_id = self.ref("stock.stock_location_customers")
self.stock_location_supplier_id = self.ref("stock.stock_location_suppliers")
# Account types
expense_type = self.env.ref("account.data_account_type_expenses")
equity_type = self.env.ref("account.data_account_type_equity")
asset_type = self.env.ref("account.data_account_type_fixed_assets")
# Create account for Goods Received Not Invoiced
name = "Goods Received Not Invoiced"
code = "grni"
acc_type = equity_type
self.account_grni = self._create_account(acc_type, name, code, self.company)
# Create account for Cost of Goods Sold
name = "Cost of Goods Sold"
code = "cogs"
acc_type = expense_type
self.account_cogs = self._create_account(acc_type, name, code, self.company)
# Create account for Goods Delivered Not Invoiced
name = "Goods Delivered Not Invoiced"
code = "gdni"
acc_type = expense_type
self.account_gdni = self._create_account(acc_type, name, code, self.company)
# Create account for Inventory
name = "Inventory"
code = "inventory"
acc_type = asset_type
self.account_inventory = self._create_account(
acc_type, name, code, self.company
)
self.stock_journal = self.env["account.journal"].create(
{"name": "Stock journal", "type": "general", "code": "STK00"}
)
# Create product category
self.product_ctg = self._create_product_category()
# Create partners
self.supplier = self.env["res.partner"].create({"name": "Test supplier"})
self.customer = self.env["res.partner"].create({"name": "Test customer"})
# Create a Product with real cost
standard_price = 10.0
list_price = 20.0
self.product = self._create_product(standard_price, False, list_price)
# Create a vendor
self.vendor_partner = self.env["res.partner"].create(
{"name": "dropship vendor"}
)
def _create_user(self, login, groups, company):
""" Create a user."""
group_ids = [group.id for group in groups]
user = self.res_users_model.with_context({"no_reset_password": True}).create(
{
"name": "Test User",
"login": login,
"password": "demo",
"email": "test@yourcompany.com",
"company_id": company.id,
"company_ids": [(4, company.id)],
"groups_id": [(6, 0, group_ids)],
}
)
return user
def _create_account(self, acc_type, name, code, company):
"""Create an account."""
account = self.account_model.create(
{
"name": name,
"code": code,
"user_type_id": acc_type.id,
"company_id": company.id,
}
)
return account
def _create_product_category(self):
product_ctg = self.product_ctg_model.create(
{
"name": "test_product_ctg",
"property_stock_valuation_account_id": self.account_inventory.id,
"property_stock_account_input_categ_id": self.account_grni.id,
"property_account_expense_categ_id": self.account_cogs.id,
"property_stock_account_output_categ_id": self.account_gdni.id,
"property_valuation": "real_time",
"property_cost_method": "fifo",
"property_stock_journal": self.stock_journal.id,
}
)
return product_ctg
def _create_product(self, standard_price, template, list_price):
"""Create a Product variant."""
if not template:
template = self.template_model.create(
{
"name": "test_product",
"categ_id": self.product_ctg.id,
"type": "product",
"standard_price": standard_price,
"valuation": "real_time",
"invoice_policy": "delivery",
}
)
return template.product_variant_ids[0]
product = self.product_model.create(
{"product_tmpl_id": template.id, "list_price": list_price}
)
return product
def _create_delivery(self, product, qty, price_unit=10.0):
return self.env["stock.picking"].create(
{
"name": self.stock_picking_type_out.sequence_id._next(),
"partner_id": self.customer.id,
"picking_type_id": self.stock_picking_type_out.id,
"location_id": self.stock_location_id,
"location_dest_id": self.stock_location_customer_id,
"move_lines": [
(
0,
0,
{
"name": product.name,
"product_id": product.id,
"product_uom": product.uom_id.id,
"product_uom_qty": qty,
"price_unit": price_unit,
"location_id": self.stock_location_id,
"location_dest_id": self.stock_location_customer_id,
"procure_method": "make_to_stock",
},
)
],
}
)
def _create_drophip_picking(self, product, qty, price_unit=10.0):
return self.env["stock.picking"].create(
{
"name": self.stock_picking_type_out.sequence_id._next(),
"partner_id": self.customer.id,
"picking_type_id": self.stock_picking_type_out.id,
"location_id": self.stock_location_supplier_id,
"location_dest_id": self.stock_location_customer_id,
"move_lines": [
(
0,
0,
{
"name": product.name,
"product_id": product.id,
"product_uom": product.uom_id.id,
"product_uom_qty": qty,
"price_unit": price_unit,
"location_id": self.stock_location_supplier_id,
"location_dest_id": self.stock_location_customer_id,
},
)
],
}
)
def _create_receipt(self, product, qty, move_dest_id=False, price_unit=10.0):
move_dest_id = [(4, move_dest_id)] if move_dest_id else False
return self.env["stock.picking"].create(
{
"name": self.stock_picking_type_in.sequence_id._next(),
"partner_id": self.vendor_partner.id,
"picking_type_id": self.stock_picking_type_in.id,
"location_id": self.stock_location_supplier_id,
"location_dest_id": self.stock_location_id,
"move_lines": [
(
0,
0,
{
"name": product.name,
"product_id": product.id,
"product_uom": product.uom_id.id,
"product_uom_qty": qty,
"price_unit": price_unit,
"move_dest_ids": move_dest_id,
"location_id": self.stock_location_supplier_id,
"location_dest_id": self.stock_location_id,
"procure_method": "make_to_stock",
},
)
],
}
)
def _do_picking(self, picking, date, qty):
"""Do picking with only one move on the given date."""
picking.write({"date": date})
picking.move_lines.write({"date": date})
picking.action_confirm()
picking.action_assign()
picking.move_lines.quantity_done = qty
picking.button_validate()
# hacking the create_date of the layer in order to test
self.env.cr.execute(
"""UPDATE stock_valuation_layer SET create_date = %s WHERE id in %s""",
(date, tuple(picking.move_lines.stock_valuation_layer_ids.ids)),
)
return True
def test_01_stock_receipt(self):
"""Receive into stock and ship to the customer
"""
# Create receipt
in_picking = self._create_receipt(self.product, 1.0)
# Receive one unit.
self._do_picking(in_picking, fields.Datetime.now(), 1.0)
# This will create an entry:
# dr cr
# GRNI 10
# Inventory 10
# Inventory is 10
aml = self.aml_model.search([("product_id", "=", self.product.id)])
inv_aml = aml.filtered(lambda l: l.account_id == self.account_inventory)
balance_inv = sum(inv_aml.mapped("balance"))
self.assertEquals(balance_inv, 10.0)
move = in_picking.move_lines
layer = self.layer_model.search([("stock_move_id", "=", move.id)])
self.assertEquals(layer.remaining_value, 10.0)
# The accounting value and the stock value match
self.assertEquals(self.product.stock_value, 10.0)
self.assertEquals(self.product.account_value, 10.0)
# The qty also match
self.assertEquals(self.product.qty_at_date, 1.0)
self.assertEquals(self.product.account_qty_at_date, 1.0)
# Create an out picking
out_picking = self._create_delivery(self.product, 1)
self._do_picking(out_picking, fields.Datetime.now(), 1.0)
# The original layer must have been reduced.
self.assertEquals(layer.remaining_qty, 0.0)
self.assertEquals(layer.remaining_value, 0.0)
# The layer out took that out
move = out_picking.move_lines
layer = self.layer_model.search([("stock_move_id", "=", move.id)])
self.assertEquals(layer.value, -10.0)
# The report shows the material is gone
self.product._compute_inventory_value()
self.assertEquals(self.product.stock_value, 0.0)
self.assertEquals(self.product.account_value, 0.0)
self.assertEquals(self.product.qty_at_date, 0.0)
self.assertEquals(self.product.account_qty_at_date, 0.0)
def test_02_drop_ship(self):
"""Drop shipment from vendor to customer
"""
# Create drop_shipment
dropship_picking = self._create_drophip_picking(self.product, 1.0)
# Receive one unit.
self._do_picking(dropship_picking, fields.Datetime.now(), 1.0)
# This will create the following entries
# dr cr
# GRNI 10
# Inventory 10
# dr cr
# Inventory 10
# GDNI 10
aml = self.aml_model.search([("product_id", "=", self.product.id)])
# Inventory is 0
inv_aml = aml.filtered(lambda l: l.account_id == self.account_inventory)
balance_inv = sum(inv_aml.mapped("balance"))
self.assertEquals(balance_inv, 0.0)
# There are two a stock valuation layers associated to this product
move = dropship_picking.move_lines
layers = self.layer_model.search([("stock_move_id", "=", move.id)])
self.assertEquals(len(layers), 2)
in_layer = layers.filtered(lambda l: l.quantity > 0)
# Check that the layer created for the outgoing move
self.assertEquals(in_layer.remaining_qty, 0.0)
self.assertEquals(in_layer.remaining_value, 0.0)
# The report shows the material is gone
self.assertEquals(self.product.stock_value, 0.0)
self.assertEquals(self.product.account_value, 0.0)
self.assertEquals(self.product.qty_at_date, 0.0)
self.assertEquals(self.product.account_qty_at_date, 0.0)
def test_03_stock_receipt_several_costs_several_dates(self):
"""Receive into stock at different cost
"""
# Create receipt
in_picking = self._create_receipt(self.product, 1.0)
# Receive one unit.
self._do_picking(in_picking, fields.Datetime.now(), 1.0)
# This will create an entry:
# dr cr
# GRNI 10
# Inventory 10
# Inventory is 10
aml = self.aml_model.search([("product_id", "=", self.product.id)])
inv_aml = aml.filtered(lambda l: l.account_id == self.account_inventory)
balance_inv = sum(inv_aml.mapped("balance"))
self.assertEquals(balance_inv, 10.0)
move = in_picking.move_lines
layer = self.layer_model.search([("stock_move_id", "=", move.id)])
self.assertEquals(layer.remaining_value, 10.0)
# Receive more
in_picking2 = self._create_receipt(self.product, 2.0, False, 20.0)
# Receive two unitsat double cost.
self._do_picking(
in_picking2, fields.Datetime.now() + relativedelta(days=3), 2.0
)
# This will create an entry:
# dr cr
# GRNI 40
# Inventory 40
# Inventory is 50
aml = self.aml_model.search([("product_id", "=", self.product.id)])
inv_aml = aml.filtered(lambda l: l.account_id == self.account_inventory)
balance_inv = sum(inv_aml.mapped("balance"))
self.assertEquals(balance_inv, 50.0)
move2 = in_picking2.move_lines
layer = self.layer_model.search([("stock_move_id", "=", move2.id)])
self.assertEquals(layer.remaining_value, 40.0)
# Now we check the report reflects the same
self.assertEquals(self.product.stock_value, 50.0)
self.assertEquals(self.product.account_value, 50.0)
self.assertEquals(self.product.qty_at_date, 3.0)
self.assertEquals(self.product.account_qty_at_date, 3.0)
# That is the value tomorrow, today it is less
# We hack the date in the account move, not a topic for this module
aml_layer = layer.account_move_id.line_ids
self.env.cr.execute(
"""UPDATE account_move_line SET date = %s WHERE id in %s""",
(fields.Datetime.now() + relativedelta(days=3), tuple(aml_layer.ids)),
)
self.product.with_context(
at_date=fields.Datetime.now() + relativedelta(days=1)
)._compute_inventory_value()
self.assertEquals(self.product.stock_value, 10.0)
self.assertEquals(self.product.account_value, 10.0)
self.assertEquals(self.product.qty_at_date, 1.0)
self.assertEquals(self.product.account_qty_at_date, 1.0)

View File

@@ -1,30 +1,28 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_stock_product_tree2" model="ir.ui.view">
<field name="name">product.stock.tree.2.inherit</field>
<field name="name">product.stock.tree.2</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="stock_account.view_stock_product_tree2" />
<field name="arch" type="xml">
<field name="cost_method" position="after">
<tree>
<field name="display_name" />
<field name="qty_at_date" />
<field name="uom_id" groups="uom.group_uom" />
<field name="currency_id" invisible="1" />
<field name="cost_currency_id" invisible="1" />
<field name="valuation" invisible="1" />
</field>
<button name="action_valuation_at_date_details" position="attributes">
<attribute
name="attrs"
>{'invisible': [('valuation', '!=', 'real_time')]}</attribute>
</button>
<button name="action_valuation_at_date_details" position="attributes">
<attribute name="invisible">True</attribute>
</button>
<button name="action_valuation_at_date_details" position="after">
<field
name="stock_value"
sum="Stock Valuation"
widget="monetary"
options="{'currency_field': 'cost_currency_id'}"
/>
<button
name="action_view_stock_moves"
name="action_view_valuation_layers"
type="object"
icon="fa-info-circle"
attrs="{'invisible': [('cost_method', '!=', 'fifo')]}"
attrs="{'invisible': [('valuation', '!=', 'real_time')]}"
/>
</button>
<field name="stock_value" position="after">
<field
name="account_qty_at_date"
sum="Accounting Qty"
@@ -42,7 +40,60 @@
icon="fa-info-circle"
attrs="{'invisible': [('valuation', '!=', 'real_time')]}"
/>
</field>
<field name="qty_discrepancy" sum="Qty Discrepancy" />
<field name="valuation_discrepancy" sum="Valuation Discrepancy" />
</tree>
</field>
</record>
<record id="product_valuation_action" model="ir.actions.act_window">
<field name="name">Dual Inventory Valuation</field>
<field name="res_model">product.product</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_stock_product_tree2" />
<field name="domain">[('type', '=', 'product')]</field>
<field
name="context"
>{'company_owned': True, 'create': False, 'edit' : False}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a product to see its name and valuation.
</p>
</field>
</record>
<record id="product_search_form_view" model="ir.ui.view">
<field name="name">product.product.search</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_search_form_view" />
<field name="arch" type="xml">
<field name="name" position="after">
<field name="valuation_discrepancy" />
</field>
<filter name="inactive" position="after">
<separator />
<filter
string="Real time"
name="real_valuation"
domain="[('valuation', '=', 'real_time')]"
/>
<filter
string="Manual"
name="manual_valuation"
domain="[('valuation', '=', 'manual_periodic')]"
/>
<filter
string="Qty Discrepancy"
name="qty_discrepancy_not_null"
domain="[('qty_discrepancy', '!=', 0.0), ('valuation', '=', 'real_time')]"
/>
<filter
string="Valuation Discrepancy"
name="valuation_discrepancy_not_null"
domain="[('valuation_discrepancy', '!=', 0.0), ('valuation', '=', 'real_time')]"
/>
</filter>
</field>
</record>
</odoo>

View File

@@ -1 +1 @@
from . import stock_quantity_history
from . import stock_valuation_history

View File

@@ -1,13 +0,0 @@
# Copyright 2019 Eficent Business and IT Consulting Services, S.L.
# Copyright 2019 Aleph Objects, Inc.
from odoo import models
class StockQuantityHistory(models.TransientModel):
_inherit = "stock.quantity.history"
def open_table(self):
action = super(StockQuantityHistory, self).open_table()
if self.compute_at_date:
action["name"] = "%s (%s)" % (action["name"], self.date)
return action

View File

@@ -0,0 +1,36 @@
# Copyright 2019 Eficent Business and IT Consulting Services, S.L.
# Copyright 2019 Aleph Objects, Inc.
from odoo import fields, models
from odoo.osv import expression
from odoo.tools.safe_eval import safe_eval
class StockValuationHistory(models.TransientModel):
_name = "stock.valuation.history"
_description = "Stock Valuation History"
inventory_datetime = fields.Datetime(
"Dual Valuation at Date",
help="Choose a date to get the valuation at that date",
default=fields.Datetime.now,
)
def open_at_date(self):
action = self.env["ir.actions.act_window"].for_xml_id(
"stock_account_valuation_report", "product_valuation_action"
)
domain = [("type", "=", "product")]
product_id = self.env.context.get("product_id", False)
product_tmpl_id = self.env.context.get("product_tmpl_id", False)
if product_id:
domain = expression.AND([domain, [("id", "=", product_id)]])
elif product_tmpl_id:
domain = expression.AND(
[domain, [("product_tmpl_id", "=", product_tmpl_id)]]
)
action["domain"] = domain
if self.inventory_datetime:
action_context = safe_eval(action["context"])
action_context["at_date"] = self.inventory_datetime
action["context"] = action_context
return action

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_stock_valuation_history" model="ir.ui.view">
<field name="name">Dual Valuation Report at Date</field>
<field name="model">stock.valuation.history</field>
<field name="arch" type="xml">
<form string="Choose your date">
<group>
<field name="inventory_datetime" />
</group>
<footer>
<button
name="open_at_date"
string="Confirm"
type="object"
class="btn-primary"
/>
<button string="Cancel" class="btn-secondary" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="history_valuation_action" model="ir.actions.act_window">
<field name="name">Dual Inventory Valuation</field>
<field name="res_model">stock.valuation.history</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_stock_valuation_history" />
</record>
<menuitem
id="menu_dual_valuation"
name="Dual Inventory Valuation"
parent="stock.menu_warehouse_report"
sequence="120"
action="history_valuation_action"
/>
</odoo>