Maintainers
This module is maintained by the OCA.
- + + +OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.
diff --git a/stock_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..2d2980821 --- /dev/null +++ b/stock_report_quantity_by_location/static/src/js/stock_report_quantity_by_location_backend.js @@ -0,0 +1,101 @@ +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", + }, + 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], + context: self.odoo_context, + }).then(function (result) { + self.do_action(result); + }); + }, + }); + + 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..87a68fdf4 100644 --- a/stock_report_quantity_by_location/tests/__init__.py +++ b/stock_report_quantity_by_location/tests/__init__.py @@ -1 +1,3 @@ from . import test_stock_report_quantity_by_location +from . import test_stock_report_quantity_by_location_pdf +from . import test_stock_report_quantity_by_location_report 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..6453f0d59 --- /dev/null +++ b/stock_report_quantity_by_location/tests/test_stock_report_quantity_by_location_pdf.py @@ -0,0 +1,47 @@ +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." + "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() diff --git a/stock_report_quantity_by_location/tests/test_stock_report_quantity_by_location_report.py b/stock_report_quantity_by_location/tests/test_stock_report_quantity_by_location_report.py new file mode 100644 index 000000000..a1f9b6f02 --- /dev/null +++ b/stock_report_quantity_by_location/tests/test_stock_report_quantity_by_location_report.py @@ -0,0 +1,197 @@ +from odoo.tests.common import TransactionCase + + +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..5839c6d24 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,41 @@ 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["ir.actions.actions"]._for_xml_id( + "stock_report_quantity_by_location.action_stock_report_quantity_by_location_html" + ) + new_context = action.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) + action["context"] = new_context + return action + + 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" + /> + +