From 814074a3e6f1e1d3001242198388901813643539 Mon Sep 17 00:00:00 2001 From: anusrinps96 Date: Fri, 4 Oct 2024 09:49:12 +0200 Subject: [PATCH] [16.0][IMP]stock_report_quantity_by_location: Included report generation Included pdf report generation functionality for module stock_report_quantity_by_location --- .../__manifest__.py | 2 + .../data/paperformat_data.xml | 20 + .../reports/__init__.py | 1 + .../stock_report_quantity_by_location_pdf.py | 125 +++++++ ...ock_report_quantity_by_location_report.xml | 341 ++++++++++++++++++ .../security/ir.model.access.csv | 3 + .../static/description/index.html | 12 +- .../static/src/css/report.css | 75 ++++ ...ock_report_quantity_by_location_backend.js | 116 ++++++ .../tests/__init__.py | 1 + ...t_stock_report_quantity_by_location_pdf.py | 243 +++++++++++++ ...ock_report_quantity_by_location_prepare.py | 42 +++ ...tock_report_quantity_by_location_views.xml | 10 + 13 files changed, 986 insertions(+), 5 deletions(-) create mode 100644 stock_report_quantity_by_location/data/paperformat_data.xml create mode 100644 stock_report_quantity_by_location/reports/__init__.py create mode 100644 stock_report_quantity_by_location/reports/stock_report_quantity_by_location_pdf.py create mode 100644 stock_report_quantity_by_location/reports/stock_report_quantity_by_location_report.xml create mode 100644 stock_report_quantity_by_location/static/src/css/report.css create mode 100644 stock_report_quantity_by_location/static/src/js/stock_report_quantity_by_location_backend.js create mode 100644 stock_report_quantity_by_location/tests/test_stock_report_quantity_by_location_pdf.py diff --git a/stock_report_quantity_by_location/__manifest__.py b/stock_report_quantity_by_location/__manifest__.py index a395a4bec..c7116f06b 100644 --- a/stock_report_quantity_by_location/__manifest__.py +++ b/stock_report_quantity_by_location/__manifest__.py @@ -12,6 +12,8 @@ "data": [ "wizards/stock_report_quantity_by_location_views.xml", "security/ir.model.access.csv", + "data/paperformat_data.xml", + "reports/stock_report_quantity_by_location_report.xml", ], "installable": True, } diff --git a/stock_report_quantity_by_location/data/paperformat_data.xml b/stock_report_quantity_by_location/data/paperformat_data.xml new file mode 100644 index 000000000..f7737caf9 --- /dev/null +++ b/stock_report_quantity_by_location/data/paperformat_data.xml @@ -0,0 +1,20 @@ + + + + + Stock Report Qunatity By Location A4 + + A4 + 0 + 0 + Portrait + 28 + 28 + 7 + 7 + + 24 + 90 + + + diff --git a/stock_report_quantity_by_location/reports/__init__.py b/stock_report_quantity_by_location/reports/__init__.py new file mode 100644 index 000000000..565b5de16 --- /dev/null +++ b/stock_report_quantity_by_location/reports/__init__.py @@ -0,0 +1 @@ +from . import stock_report_quantity_by_location_pdf diff --git a/stock_report_quantity_by_location/reports/stock_report_quantity_by_location_pdf.py b/stock_report_quantity_by_location/reports/stock_report_quantity_by_location_pdf.py new file mode 100644 index 000000000..e9e7d7abb --- /dev/null +++ b/stock_report_quantity_by_location/reports/stock_report_quantity_by_location_pdf.py @@ -0,0 +1,125 @@ +from odoo import api, fields, models + + +class StockReportQuantityByProductView(models.TransientModel): + _name = "stock.report.quantity.by.product.view" + _description = "Stock Report Quantity By Product View" + + report_id = fields.Many2one("report.stock.report.quantity.by.location.pdf") + name = fields.Char() + + +class StockReportQuantityByLocationView(models.TransientModel): + _name = "stock.report.quantity.by.location.view" + _description = "Stock Report Quantity By Location View" + + report_location_id = fields.Many2one("report.stock.report.quantity.by.location.pdf") + + loc_name = fields.Char() + product_name = fields.Char() + quantity_on_hand = fields.Float() + quantity_reserved = fields.Float() + quantity_unreserved = fields.Float() + + +class StockReportQuantityByLocationReport(models.TransientModel): + _name = "report.stock.report.quantity.by.location.pdf" + _description = "Stock Report Quantity By Location" + + location_ids = fields.Many2many( + comodel_name="stock.location", string="Locations", required=True + ) + + with_quantity = fields.Boolean( + string="Quantity>0", + default=True, + help="Show only the products that have existing quantity on hand", + ) + + results = fields.One2many( + comodel_name="stock.report.quantity.by.product.view", + inverse_name="report_id", + compute="_compute_results", + ) + + results_location = fields.One2many( + comodel_name="stock.report.quantity.by.location.view", + inverse_name="report_location_id", + compute="_compute_results", + ) + + @api.depends("location_ids") + def _compute_results(self): + """ + Generate report lines + """ + self.ensure_one() + results = self.env["stock.report.quantity.by.product.view"] + results_location = self.env["stock.report.quantity.by.location.view"] + products = self.env["product.product"].search([("type", "=", "product")]) + for product in products: + product_exist = False + for loc in self.location_ids: + available_products = self.env["stock.quant"].read_group( + [ + ("product_id", "=", product.id), + ("location_id", "child_of", [loc.id]), + ], + ["quantity", "reserved_quantity", "product_id"], + ["product_id"], + ) + if available_products: + qty_on_hand = available_products[0]["quantity"] + qty_reserved = available_products[0]["reserved_quantity"] + qty_unreserved = qty_on_hand - qty_reserved + if not self.with_quantity or qty_on_hand > 0: + product_exist = True + vals = { + "loc_name": loc.display_name, + "product_name": product.display_name, + "quantity_on_hand": qty_on_hand, + "quantity_reserved": qty_reserved, + "quantity_unreserved": qty_unreserved, + } + results_location |= results_location.new(vals) + if product_exist: + vals_product = { + "name": product.display_name, + } + results |= results.new(vals_product) + self.results = results + self.results_location = results_location + + def print_report(self, report_type="qweb"): + self.ensure_one() + action = ( + report_type == "xlsx" + and self.env.ref( + "stock_report_quantity_by_location_pdf." + "action_stock_report_quantity_by_location_xlsx", + raise_if_not_found=False, + ) + or self.env.ref( + "stock_report_quantity_by_location_pdf." + "action_stock_report_quantity_by_location_pdf", + raise_if_not_found=False, + ) + ) + return action.report_action(self, config=False) + + def _get_html(self): + result = {} + rcontext = {} + report = self.browse(self._context.get("active_id")) + if report: + rcontext["o"] = report + result["html"] = self.env["ir.qweb"]._render( + "stock_report_quantity_by_location_pdf." + "report_stock_report_quantity_by_location_html", + rcontext, + ) + return result + + @api.model + def get_html(self, given_context=None): + return self.with_context(**(given_context or {}))._get_html() diff --git a/stock_report_quantity_by_location/reports/stock_report_quantity_by_location_report.xml b/stock_report_quantity_by_location/reports/stock_report_quantity_by_location_report.xml new file mode 100644 index 000000000..335c3e5de --- /dev/null +++ b/stock_report_quantity_by_location/reports/stock_report_quantity_by_location_report.xml @@ -0,0 +1,341 @@ + + + + Stock Report Quantity By Location + stock_report_quantity_by_location_backend + + + + + Stock Report Quantity By Location PDF + report.stock.report.quantity.by.location.pdf + qweb-pdf + stock_report_quantity_by_location_pdf.report_stock_report_quantity_by_location_pdf + stock_report_quantity_by_location_pdf.report_stock_report_quantity_by_location_pdf + 'Stock Report Quantity By Product and Location' + + report + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stock_report_quantity_by_location/security/ir.model.access.csv b/stock_report_quantity_by_location/security/ir.model.access.csv index f027da093..0ed2e85c2 100644 --- a/stock_report_quantity_by_location/security/ir.model.access.csv +++ b/stock_report_quantity_by_location/security/ir.model.access.csv @@ -1,3 +1,6 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_stock_report_quantity_by_location_prepare,access_stock_report_quantity_by_location_prepare,model_stock_report_quantity_by_location_prepare,base.group_user,1,1,1,0 access_stock_report_quantity_by_location,access_stock_report_quantity_by_location,model_stock_report_quantity_by_location,base.group_user,1,1,1,0 +access_stock_report_quantity_by_location_pdf,access_stock_report_quantity_by_location_pdf,model_report_stock_report_quantity_by_location_pdf,stock.group_stock_user,1,1,1,1 +access_stock_report_quantity_by_location_view,access_stock_report_quantity_by_location_view,model_stock_report_quantity_by_location_view,stock.group_stock_user,1,1,1,1 +access_stock_report_quantity_by_product_view,access_stock_report_quantity_by_product_view,model_stock_report_quantity_by_product_view,stock.group_stock_user,1,1,1,1 diff --git a/stock_report_quantity_by_location/static/description/index.html b/stock_report_quantity_by_location/static/description/index.html index 69d23bcdd..fdc2b9fa3 100644 --- a/stock_report_quantity_by_location/static/description/index.html +++ b/stock_report_quantity_by_location/static/description/index.html @@ -1,4 +1,3 @@ - @@ -9,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. @@ -275,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -419,7 +419,9 @@

Contributors

Maintainers

This module is maintained by the OCA.

-Odoo Community Association + +Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

diff --git a/stock_report_quantity_by_location/static/src/css/report.css b/stock_report_quantity_by_location/static/src/css/report.css new file mode 100644 index 000000000..8b2b949bc --- /dev/null +++ b/stock_report_quantity_by_location/static/src/css/report.css @@ -0,0 +1,75 @@ +.act_as_table { + display: table !important; + background-color: white; +} +.act_as_row { + display: table-row !important; + page-break-inside: avoid; +} +.act_as_cell { + display: table-cell !important; + page-break-inside: avoid; +} +.act_as_thead { + display: table-header-group !important; +} +.data_table { + width: 100% !important; +} +.act_as_row.labels { + background-color: #f0f0f0 !important; +} +.data_table, +.total_row, +.act_as_row { + border-left: 0px; + border-right: 0px; + text-align: center; + font-size: 10px; + padding-right: 3px; + padding-left: 3px; + padding-top: 2px; + padding-bottom: 2px; + border-collapse: collapse; +} +.data_table .act_as_cell { + border: 1px solid lightGrey; + text-align: center; +} +.data_table .act_as_cell { + word-wrap: break-word; +} +.data_table .act_as_row.labels { + font-weight: bold; +} +.data_table .total_row { + background-color: #f0f0f0 !important; + border-left: 1px solid lightGrey; + border-right: 1px solid lightGrey; + border-bottom: 1px solid lightGrey; + text-align: right; + font-weight: bold; +} +.act_as_cell.amount { + word-wrap: normal; + text-align: right; +} +.act_as_cell.left { + text-align: left; +} +.act_as_cell.right { + text-align: right; +} +.custom_footer { + font-size: 7px !important; +} +.button_row { + padding-bottom: 10px; +} +.o_stock_inventory_valuation_report_page { + padding-top: 10px; + width: 90%; + margin-right: auto; + margin-left: auto; + font-family: Helvetica, Arial; +} diff --git a/stock_report_quantity_by_location/static/src/js/stock_report_quantity_by_location_backend.js b/stock_report_quantity_by_location/static/src/js/stock_report_quantity_by_location_backend.js new file mode 100644 index 000000000..6ec473edd --- /dev/null +++ b/stock_report_quantity_by_location/static/src/js/stock_report_quantity_by_location_backend.js @@ -0,0 +1,116 @@ +odoo.define( + "stock_report_quantity_by_location.stock_report_quantity_by_location_backend", + function (require) { + "use strict"; + + var core = require("web.core"); + var AbstractAction = require("web.AbstractAction"); + var ReportWidget = require("web.Widget"); + + var report_backend = AbstractAction.extend({ + hasControlPanel: true, + // Stores all the parameters of the action. + events: { + "click .o_stock_report_quantity_by_location_print": "print", + "click .o_stock_report_quantity_by_location_export": "export", + }, + init: function (parent, action) { + this._super.apply(this, arguments); + this.actionManager = parent; + this.given_context = {}; + this.odoo_context = action.context; + this.controller_url = action.context.url; + if (action.context.context) { + this.given_context = action.context.context; + } + this.given_context.active_id = + action.context.active_id || action.params.active_id; + this.given_context.model = action.context.active_model || false; + this.given_context.ttype = action.context.ttype || false; + }, + willStart: function () { + return Promise.all([ + this._super.apply(this, arguments), + this.get_html(), + ]); + }, + set_html: function () { + const self = this; + var def = Promise.resolve(); + if (!self.report_widget) { + self.report_widget = new ReportWidget(self, self.given_context); + def = self.report_widget.appendTo(self.$(".o_content")); + } + def.then(function () { + self.report_widget.$el.html(self.html); + }); + }, + start: function () { + this.set_html(); + return this._super(); + }, + // Fetches the html and is previous report.context if any, + // else create it + get_html: function () { + var self = this; + var defs = []; + return this._rpc({ + model: this.given_context.model, + method: "get_html", + args: [self.given_context], + context: self.odoo_context, + }).then(function (result) { + self.html = result.html; + defs.push(self.update_cp()); + return Promise.all(defs); + }); + }, + // Updates the control panel and render the elements that have yet + // to be rendered + update_cp: function () { + if (this.$buttons) { + var status = { + breadcrumbs: this.actionManager.get_breadcrumbs(), + cp_content: {$buttons: this.$buttons}, + }; + return this.update_control_panel(status); + } + }, + do_show: function () { + this._super(); + this.update_cp(); + }, + print: function () { + var self = this; + this._rpc({ + model: this.given_context.model, + method: "print_report", + args: [this.given_context.active_id, "qweb-pdf"], + context: self.odoo_context, + }).then(function (result) { + self.do_action(result); + }); + }, + export: function () { + var self = this; + this._rpc({ + model: this.given_context.model, + method: "print_report", + args: [this.given_context.active_id, "xlsx"], + context: self.odoo_context, + }).then(function (result) { + self.do_action(result); + }); + }, + canBeRemoved: function () { + return Promise.resolve(); + }, + }); + + core.action_registry.add( + "stock_report_quantity_by_location_backend", + report_backend + ); + return report_backend; + } +); diff --git a/stock_report_quantity_by_location/tests/__init__.py b/stock_report_quantity_by_location/tests/__init__.py index 8d067768a..4e4529b94 100644 --- a/stock_report_quantity_by_location/tests/__init__.py +++ b/stock_report_quantity_by_location/tests/__init__.py @@ -1 +1,2 @@ from . import test_stock_report_quantity_by_location +from . import test_stock_report_quantity_by_location_pdf diff --git a/stock_report_quantity_by_location/tests/test_stock_report_quantity_by_location_pdf.py b/stock_report_quantity_by_location/tests/test_stock_report_quantity_by_location_pdf.py new file mode 100644 index 000000000..c95df4c37 --- /dev/null +++ b/stock_report_quantity_by_location/tests/test_stock_report_quantity_by_location_pdf.py @@ -0,0 +1,243 @@ +from odoo.tests.common import TransactionCase +from odoo.tools import test_reports + + +class TestStockReportQuantityByLocationPdf(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + + cls.stock_report_qty_by_loc_pdf_model = cls.env[ + "report.stock.report.quantity.by.location.pdf" + ] + + cls.qweb_report_name = ( + "stock_report_quantity_by_location_pdf." + "report_stock_report_quantity_by_location_pdf" + ) + + cls.report_title = "Stock Report Quantity By Location" + + cls.base_filters = { + "with_quantity": True, + } + + cls.report = cls.stock_report_qty_by_loc_pdf_model.create(cls.base_filters) + + def test_html(self): + test_reports.try_report( + self.env.cr, + self.env.uid, + self.qweb_report_name, + [self.report.id], + report_type="qweb-html", + ) + + def test_qweb(self): + test_reports.try_report( + self.env.cr, + self.env.uid, + self.qweb_report_name, + [self.report.id], + report_type="qweb-pdf", + ) + + def test_print(self): + self.report.print_report("qweb") + + +class TestStockReportQuantityByLocationReport(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.with_quantity = True + location1 = cls.env["stock.location"].create( + { + "name": "Test Location 1", + "usage": "internal", + "display_name": "Test Location 1", + "location_id": cls.env.ref("stock.stock_location_stock").id, + } + ) + location2 = cls.env["stock.location"].create( + { + "name": "Test Location 2", + "usage": "internal", + "location_id": cls.env.ref("stock.stock_location_stock").id, + "display_name": "Test Location 2", + } + ) + location3 = cls.env["stock.location"].create( + { + "name": "Test Location 3", + "usage": "internal", + "location_id": cls.env.ref("stock.stock_location_stock").id, + "display_name": "Test Location 3", + } + ) + location4 = cls.env["stock.location"].create( + { + "name": "Test Location 4", + "usage": "internal", + "location_id": cls.env.ref("stock.stock_location_stock").id, + "display_name": "Test Location 4", + } + ) + cls.location_ids = [location1.id, location2.id, location3.id, location4.id] + + def test_get_report_html(self): + report = self.env["report.stock.report.quantity.by.location.pdf"].create( + { + "with_quantity": self.with_quantity, + "location_ids": self.location_ids, + } + ) + report._compute_results() + report.get_html(given_context={"active_id": report.id}) + + def test_wizard(self): + wizard = self.env["stock.report.quantity.by.location.prepare"].create({}) + wizard.button_export_html() + wizard.button_export_pdf() + + def test_stock_report_result(self): + """ + Check that report shows the products present + at each location + """ + + product1 = self.env["product.product"].create( + { + "name": "test product report by location", + "type": "product", + "display_name": "product1", + } + ) + + quant_pro_loc1 = self.env["stock.quant"].create( + { + "product_id": product1.id, + "location_id": self.location_ids[0], + "quantity": 100.0, + "reserved_quantity": 80.0, + } + ) + + quant_pro_loc2 = self.env["stock.quant"].create( + { + "product_id": product1.id, + "location_id": self.location_ids[1], + "quantity": 140.0, + "reserved_quantity": 60.0, + } + ) + + product2 = self.env["product.product"].create( + { + "name": "test product 2 report by location", + "type": "product", + "display_name": "product2", + } + ) + + quant_pro2_loc1 = self.env["stock.quant"].create( + { + "product_id": product2.id, + "location_id": self.location_ids[0], + "quantity": 100.0, + "reserved_quantity": 50.0, + } + ) + + product3 = self.env["product.product"].create( + { + "name": "test product 3 report by location", + "type": "product", + "display_name": "product3", + } + ) + + quant_pro3_loc3 = self.env["stock.quant"].create( + { + "product_id": product3.id, + "location_id": self.location_ids[3], + "quantity": 100.0, + "reserved_quantity": 50.0, + } + ) + + # Report should have a line with two products and all the location in which it exist + report = self.env["report.stock.report.quantity.by.location.pdf"].create( + { + "with_quantity": self.with_quantity, + "location_ids": [self.location_ids[0], self.location_ids[1]], + } + ) + product_row = report.results.filtered( + lambda r: ( + r.name == product1.display_name or r.name == product2.display_name + ) + ) + self.assertEqual( + len(product_row), + 2, + msg="There should be two product lines in the report", + ) + location1_row = report.results_location.filtered( + lambda r: ( + r.loc_name == quant_pro_loc1.location_id.display_name + and r.product_name == product1.display_name + ) + ) + self.assertEqual( + location1_row[0].quantity_on_hand, + quant_pro_loc1.quantity, + msg="The product quantity at location 1 should match", + ) + location2_row = report.results_location.filtered( + lambda r: ( + r.loc_name == quant_pro_loc2.location_id.display_name + and r.product_name == product1.display_name + ) + ) + self.assertEqual( + location2_row[0].quantity_on_hand, + quant_pro_loc2.quantity, + msg="The product quantity at location 2 should match", + ) + + # Report should not have any lines with the product + # No locations displayed as product does not exist on location 2 + report = self.env["report.stock.report.quantity.by.location.pdf"].create( + { + "with_quantity": self.with_quantity, + "location_ids": [self.location_ids[2]], + } + ) + + product_row = report.results.filtered( + lambda r: ( + r.name == product1.display_name + or r.name == product2.display_name + or r.name == product3.display_name + ) + ) + self.assertEqual( + len(product_row), + 0, + msg="There should not be any product lines in the report", + ) + location_row = report.results_location.filtered( + lambda r: ( + r.loc_name == quant_pro_loc1.location_id.display_name + or r.loc_name == quant_pro2_loc1.location_id.display_name + or r.loc_name == quant_pro3_loc3.location_id.display_name + ) + ) + self.assertEqual( + len(location_row), + 0, + msg="No locations should be displayed on the report", + ) diff --git a/stock_report_quantity_by_location/wizards/stock_report_quantity_by_location_prepare.py b/stock_report_quantity_by_location/wizards/stock_report_quantity_by_location_prepare.py index 2a370566d..e2290cae0 100644 --- a/stock_report_quantity_by_location/wizards/stock_report_quantity_by_location_prepare.py +++ b/stock_report_quantity_by_location/wizards/stock_report_quantity_by_location_prepare.py @@ -1,6 +1,11 @@ # Copyright 2019-21 ForgeFlow, S.L. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + from odoo import _, fields, models +from odoo.tools.safe_eval import safe_eval + +_logger = logging.getLogger(__name__) class StockReportByLocationPrepare(models.TransientModel): @@ -79,3 +84,40 @@ def _compute_stock_report_by_location(self): } ) self.env["stock.report.quantity.by.location"].create(vals_list) + + def button_export_html(self): + self.ensure_one() + action = self.env.ref( + "stock_report_quantity_by_location_pdf." + "action_stock_report_quantity_by_location_html" + ) + vals = action.read()[0] + new_context = vals.get("context", {}) + if isinstance(new_context, str): + try: + new_context = safe_eval(new_context) + except (TypeError, SyntaxError, NameError, ValueError): + _logger.warning( + _("Failed context evaluation: %(context)s", context=new_context) + ) + new_context = {} + model = self.env["report.stock.report.quantity.by.location.pdf"] + report = model.create(self._prepare_stock_quantity_by_location_report()) + new_context.update(active_id=report.id, active_ids=report.ids) + vals["context"] = new_context + return vals + + def button_export_pdf(self): + self.ensure_one() + model = self.env["report.stock.report.quantity.by.location.pdf"] + report = model.create(self._prepare_stock_quantity_by_location_report()) + return report.print_report() + + def _prepare_stock_quantity_by_location_report(self): + self.ensure_one() + vals = { + "with_quantity": self.with_quantity, + } + if self.location_ids: + vals["location_ids"] = self.location_ids + return vals diff --git a/stock_report_quantity_by_location/wizards/stock_report_quantity_by_location_views.xml b/stock_report_quantity_by_location/wizards/stock_report_quantity_by_location_views.xml index f98b8a6d0..9ca21b54d 100644 --- a/stock_report_quantity_by_location/wizards/stock_report_quantity_by_location_views.xml +++ b/stock_report_quantity_by_location/wizards/stock_report_quantity_by_location_views.xml @@ -23,6 +23,16 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). string="Retrieve the Inventory Quantities" type="object" class="btn-primary" + /> +