[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 :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3 :alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--reporting-lightgray.png?logo=github .. |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 :alt: OCA/stock-logistics-reporting
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png .. |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 :alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png .. |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 :alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5| |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 inventory perspective with the valuation
from an accounting perspective. 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 to display separately the Quantity and Value of each product for the
Inventory and the Accounting systems . 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>`_. 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. 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 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. Do not contact contributors directly about support or help with technical issues.
@@ -55,12 +56,13 @@ Credits
Authors Authors
~~~~~~~ ~~~~~~~
* Eficent * ForgeFlow S.L.
Contributors Contributors
~~~~~~~~~~~~ ~~~~~~~~~~~~
* Jordi Ballester Alomar <jordi.ballester@eficent.com> * Jordi Ballester Alomar <jordi.ballester@forgeflow.com>
* Aaron Henriquez <ahenriquez@forgeflow.com>
Maintainers 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 mission is to support the collaborative development of Odoo features and
promote its widespread use. 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. 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. # Copyright 2020 ForgeFlow S.L.
# (<https://www.eficent.com>) # Copyright 2019 Aleph Objects, Inc.
# Copyright 2018 Aleph Objects, Inc.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{ {
"name": "Stock Account Valuation Report", "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", "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", "website": "https://github.com/OCA/stock-logistics-reporting",
"category": "Warehouse Management", "category": "Warehouse Management",
"depends": ["stock_account"], "depends": ["stock_account"],
"license": "AGPL-3", "license": "AGPL-3",
"data": ["views/product_product_views.xml"], "data": ["views/product_product_views.xml", "wizards/stock_valuation_history.xml"],
"installable": True, "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). # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields, models from odoo import fields, models

View File

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

View File

@@ -3,7 +3,7 @@
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <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> <title>Stock Account Valuation Report</title>
<style type="text/css"> <style type="text/css">
@@ -367,12 +367,13 @@ ul.auto-toc {
!! This file is generated by oca-gen-addon-readme !! !! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !! !! 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 <p>When you trigger a report of inventory valuation, and you use
perpetual inventory, you should be able to reconcile the valuation perpetual inventory, you should be able to reconcile the valuation
from an inventory perspective with the valuation from an inventory perspective with the valuation
from an accounting perspective.</p> 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 to display separately the Quantity and Value of each product for the
Inventory and the Accounting systems .</p> Inventory and the Accounting systems .</p>
<p><strong>Table of contents</strong></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>. <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. 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 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> <p>Do not contact contributors directly about support or help with technical issues.</p>
</div> </div>
<div class="section" id="credits"> <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"> <div class="section" id="authors">
<h2><a class="toc-backref" href="#id3">Authors</a></h2> <h2><a class="toc-backref" href="#id3">Authors</a></h2>
<ul class="simple"> <ul class="simple">
<li>Eficent</li> <li>ForgeFlow S.L.</li>
</ul> </ul>
</div> </div>
<div class="section" id="contributors"> <div class="section" id="contributors">
<h2><a class="toc-backref" href="#id4">Contributors</a></h2> <h2><a class="toc-backref" href="#id4">Contributors</a></h2>
<ul class="simple"> <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> </ul>
</div> </div>
<div class="section" id="maintainers"> <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 <p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and mission is to support the collaborative development of Odoo features and
promote its widespread use.</p> 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> <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>
</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" ?> <?xml version="1.0" encoding="utf-8" ?>
<odoo> <odoo>
<record id="view_stock_product_tree2" model="ir.ui.view"> <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="model">product.product</field>
<field name="inherit_id" ref="stock_account.view_stock_product_tree2" />
<field name="arch" type="xml"> <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 name="valuation" invisible="1" />
</field> <field
<button name="action_valuation_at_date_details" position="attributes"> name="stock_value"
<attribute sum="Stock Valuation"
name="attrs" widget="monetary"
>{'invisible': [('valuation', '!=', 'real_time')]}</attribute> options="{'currency_field': 'cost_currency_id'}"
</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">
<button <button
name="action_view_stock_moves" name="action_view_valuation_layers"
type="object" type="object"
icon="fa-info-circle" icon="fa-info-circle"
attrs="{'invisible': [('cost_method', '!=', 'fifo')]}" attrs="{'invisible': [('valuation', '!=', 'real_time')]}"
/> />
</button>
<field name="stock_value" position="after">
<field <field
name="account_qty_at_date" name="account_qty_at_date"
sum="Accounting Qty" sum="Accounting Qty"
@@ -42,7 +40,60 @@
icon="fa-info-circle" icon="fa-info-circle"
attrs="{'invisible': [('valuation', '!=', 'real_time')]}" attrs="{'invisible': [('valuation', '!=', 'real_time')]}"
/> />
</field> <field name="qty_discrepancy" sum="Qty Discrepancy" />
<field name="valuation_discrepancy" sum="Valuation Discrepancy" />
</tree>
</field> </field>
</record> </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> </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>