diff --git a/inventory_advanced_reports/README.rst b/inventory_advanced_reports/README.rst new file mode 100755 index 000000000..ecb13e64f --- /dev/null +++ b/inventory_advanced_reports/README.rst @@ -0,0 +1,51 @@ +.. image:: https://img.shields.io/badge/license-LGPL--3-green.svg + :target: https://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 + +Advanced Inventory Reports +========================== +Helps to Manage different types of Inventory Reports like FSN Report, Out +Of Stock Report, etc. + +Configuration +============= +- Additional configuration not required + +License +------- +Lesser General Public License, Version 3 (LGPL v3). +(https://www.gnu.org/licenses/lgpl-3.0-standalone.html) + +Company +------- +* `Cybrosys Techno Solutions `__ + +Credits +------- +Developer: + (V16) Anusha C, + (V17) Jumana Haseen, +Contact : odoo@cybrosys.com + +Contacts +-------- +* Mail Contact : odoo@cybrosys.com +* Website : https://cybrosys.com + +Bug Tracker +----------- +Bugs are tracked on GitHub Issues. In case of trouble, please check there if +your issue has already been reported. + +Maintainer +========== +.. image:: https://cybrosys.com/images/logo.png + :target: https://cybrosys.com + +This module is maintained by Cybrosys Technologies. + +For support and more information, please visit https://www.cybrosys.com + +Further information +=================== +HTML Description: ``__ diff --git a/inventory_advanced_reports/__init__.py b/inventory_advanced_reports/__init__.py new file mode 100644 index 000000000..3ae45c247 --- /dev/null +++ b/inventory_advanced_reports/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +from . import controllers +from . import report +from . import wizard diff --git a/inventory_advanced_reports/__manifest__.py b/inventory_advanced_reports/__manifest__.py new file mode 100644 index 000000000..4d34aaa97 --- /dev/null +++ b/inventory_advanced_reports/__manifest__.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +{ + "name": "Advanced Inventory Reports", + "version": "17.0.1.0.0", + "category": 'Warehouse', + "summary": """Helps to Manage different types of Inventory Reports like FSN + Report, Out Of Stock Report, Inventory XYZ Report etc.""", + "description": """Provides efficient management of various types of + inventory reports, including FSN Reports, Out-of-Stock Reports to monitor + and prevent inventory shortages, Inventory XYZ Reports for categorizing + items based on value and volume, and other customizable reports tailored to + the specific needs of the business.""", + "author": "Cybrosys Techno Solutions", + 'company': 'Cybrosys Techno Solutions', + 'maintainer': 'Cybrosys Techno Solutions', + 'website': 'https://www.cybrosys.com', + "depends": ["stock", "purchase", "sale_management"], + "data": ["security/ir.model.access.csv", + "report/aging_report_views.xml", + "report/fsn_report_views.xml", + "report/xyz_report_views.xml", + "report/fsn_xyz_report_views.xml", + "report/out_of_stock_report_views.xml", + "report/over_stock_report_views.xml", + "report/age_breakdown_report_views.xml", + "report/stock_movement_report_views.xml", + "wizard/inventory_aging_report_views.xml", + "wizard/inventory_aging_data_report_views.xml", + "wizard/inventory_fsn_report_views.xml", + "wizard/inventory_fsn_data_report_views.xml", + "wizard/inventory_xyz_report_views.xml", + "wizard/inventory_xyz_data_report_views.xml", + "wizard/inventory_fsn_xyz_report_views.xml", + "wizard/inventory_fsn_xyz_data_report_views.xml", + "wizard/inventory_out_of_stock_report_views.xml", + "wizard/inventory_out_of_stock_data_report_views.xml", + "wizard/inventory_age_breakdown_report_views.xml", + "wizard/inventory_over_stock_report_views.xml", + "wizard/inventory_over_stock_data_report_views.xml", + "wizard/inventory_stock_movement_report_views.xml", + ], + 'assets': { + 'web.assets_backend': [ + 'inventory_advanced_reports/static/src/js/action_manager.js'] + }, + 'images': ['static/description/banner.jpg'], + 'license': 'LGPL-3', + 'installable': True, + 'auto_install': False, + 'application': False, +} diff --git a/inventory_advanced_reports/controllers/__init__.py b/inventory_advanced_reports/controllers/__init__.py new file mode 100644 index 000000000..99f43c649 --- /dev/null +++ b/inventory_advanced_reports/controllers/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +from . import inventory_advanced_reports diff --git a/inventory_advanced_reports/controllers/inventory_advanced_reports.py b/inventory_advanced_reports/controllers/inventory_advanced_reports.py new file mode 100644 index 000000000..6eaf76c88 --- /dev/null +++ b/inventory_advanced_reports/controllers/inventory_advanced_reports.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +import json +from odoo.http import content_disposition, request +from odoo import http +from odoo.tools import html_escape + + +class XLSXReportController(http.Controller): + """ This model is used to connect the frontend to the backend """ + @http.route('/xlsx_reports', type='http', auth='public', + methods=['POST'], csrf=False) + def get_report_xlsx(self, model, options, output_format, report_name): + """This function is called when a post request is made to this route""" + uid = request.session.uid + report_obj = request.env[model].with_user(uid) + options = json.loads(options) + token = 'dummy-because-api-expects-one' + try: + if output_format == 'xlsx': + response = request.make_response( + None, + headers=[ + ('Content-Type', 'application/vnd.ms-excel'), + ('Content-Disposition', + content_disposition(report_name + '.xlsx')) + ]) + report_obj.get_xlsx_report(options, response) + response.set_cookie('fileToken', token) + return response + except Exception as exception: + serialise = http.serialize_exception(exception) + error = { + 'code': 200, + 'message': 'Odoo Server Error', + 'data': serialise + } + return request.make_response(html_escape(json.dumps(error))) diff --git a/inventory_advanced_reports/doc/RELEASE_NOTES.md b/inventory_advanced_reports/doc/RELEASE_NOTES.md new file mode 100755 index 000000000..5ddba4186 --- /dev/null +++ b/inventory_advanced_reports/doc/RELEASE_NOTES.md @@ -0,0 +1,6 @@ +## Module + +#### 10.01.2025 +#### Version 17.0.1.0.0 +##### ADD +- Initial Commit for Advanced Inventory Reports diff --git a/inventory_advanced_reports/report/__init__.py b/inventory_advanced_reports/report/__init__.py new file mode 100644 index 000000000..7ddc5b28e --- /dev/null +++ b/inventory_advanced_reports/report/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +from . import age_breakdown_report +from . import aging_report +from . import fsn_report +from . import fsn_xyz_report +from . import out_of_stock_report +from . import over_stock_report +from . import stock_movement_report +from . import xyz_report diff --git a/inventory_advanced_reports/report/age_breakdown_report.py b/inventory_advanced_reports/report/age_breakdown_report.py new file mode 100644 index 000000000..c246a2844 --- /dev/null +++ b/inventory_advanced_reports/report/age_breakdown_report.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +from odoo import api, models +from odoo.exceptions import ValidationError + + +class AgeBreakdownReport(models.AbstractModel): + """ Create an abstract model for passing reporting values """ + _name = 'report.inventory_advanced_reports.report_inventory_breakdown' + _description = 'Age Breakdown Report' + + @api.model + def _get_report_values(self, docids, data=None): + """This function has working in get the pdf report.""" + values = data + product_ids = data['product_ids'] + category_ids = data['category_ids'] + company_ids = data['company_ids'] + age_breakdown_days = data['age_breakdown_days'] + params = [] + param_count = 0 + query = """ + SELECT + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END AS product_code_and_name, + c.complete_name AS category_name, + c.id AS category_id, + pp.id AS product_id, + company.id AS company_id, + company.name AS company_name, + COALESCE(SUM(svl.remaining_qty), 0) AS qty_available, + SUM(svl.remaining_value) AS stock_value, + SUM(CASE + WHEN age.days_between >= 1 AND age.days_between <= %s + THEN svl.remaining_qty + ELSE 0 + END) AS "age_breakdown_qty_1", + SUM(CASE + WHEN age.days_between >= %s+1 AND age.days_between <= %s*2 + THEN svl.remaining_qty + ELSE 0 + END) AS "age_breakdown_qty_2", + SUM(CASE + WHEN age.days_between >= (%s*2)+1 AND age.days_between <= %s*3 + THEN svl.remaining_qty + ELSE 0 + END) AS "age_breakdown_qty_3", + SUM(CASE + WHEN age.days_between >= (%s*3)+1 AND age.days_between <= %s*4 + THEN svl.remaining_qty + ELSE 0 + END) AS "age_breakdown_qty_4", + SUM(CASE + WHEN age.days_between >= (%s*4)+1 THEN svl.remaining_qty + ELSE 0 + END) AS "age_breakdown_qty_5", + SUM(CASE + WHEN age.days_between >= 1 AND age.days_between <= %s + THEN svl.remaining_value + ELSE 0 + END) AS "age_breakdown_value_1", + SUM(CASE + WHEN age.days_between >= %s+1 AND age.days_between <= %s*2 + THEN svl.remaining_value + ELSE 0 + END) AS "age_breakdown_value_2", + SUM(CASE + WHEN age.days_between >= (%s*2)+1 AND age.days_between <= %s*3 + THEN svl.remaining_value + ELSE 0 + END) AS "age_breakdown_value_3", + SUM(CASE + WHEN age.days_between >= (%s*3)+1 AND age.days_between <= %s*4 + THEN svl.remaining_value + ELSE 0 + END) AS "age_breakdown_value_4", + SUM(CASE + WHEN age.days_between >= (%s*4)+1 THEN svl.remaining_value + ELSE 0 + END) AS "age_breakdown_value_5" + FROM product_product pp + INNER JOIN product_template pt ON pp.product_tmpl_id = pt.id + INNER JOIN product_category c ON pt.categ_id = c.id + LEFT JOIN stock_move sm ON sm.product_id = pp.id + LEFT JOIN stock_picking_type spt ON sm.picking_type_id = spt.id + LEFT JOIN res_company company ON sm.company_id = company.id + LEFT JOIN LATERAL ( + SELECT EXTRACT(day FROM CURRENT_DATE - sm.date) AS days_between + ) AS age ON true + INNER JOIN stock_valuation_layer svl ON svl.stock_move_id = sm.id + WHERE pt.detailed_type = 'product' + AND sm.state = 'done' + AND svl.remaining_value IS NOT NULL + """ + params.extend([age_breakdown_days] * 16) + if product_ids or category_ids: + query += " AND (" + if product_ids: + product_ids = [product_id for product_id in product_ids] + query += "pp.id = ANY(%s)" + params.append(product_ids) + param_count += 1 + if product_ids and category_ids: + query += " OR " + if category_ids: + category_ids = [category for category in category_ids] + params.append(category_ids) + query += "(pt.categ_id = ANY(%s))" + param_count += 1 + if product_ids or category_ids: + query += ")" + if company_ids: + company_ids = [company for company in company_ids] + query += " AND (sm.company_id = ANY(%s))" # Specify the table alias + params.append(company_ids) + param_count += 1 + query += """ + GROUP BY + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', + pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END, + c.complete_name, + company.id, + c.id, + company.name, + pp.id; + """ + self.env.cr.execute(query, params) + result_data = self.env.cr.dictfetchall() + main_header = age_breakdown_days + if result_data: + return { + 'doc_ids': docids, + 'doc_model': 'report.inventory_advanced_reports.' + 'report_inventory_breakdown', + 'data': values, + 'options': result_data, + 'main_header': self.get_header(main_header) + } + else: + raise ValidationError("No records found for the given criteria!") + + def get_header(self, main_header): + """ For getting the header for the report """ + age_breakdown1 = main_header + age_breakdown2 = main_header * 2 + age_breakdown3 = main_header * 3 + age_breakdown4 = main_header * 4 + return ['1-' + str(age_breakdown1), + str(age_breakdown1 + 1) + '-' + str(age_breakdown2), + str(age_breakdown2 + 1) + '-' + str(age_breakdown3), + str(age_breakdown3 + 1) + '-' + str(age_breakdown4), + 'ABOVE ' + str(age_breakdown4)] diff --git a/inventory_advanced_reports/report/age_breakdown_report_views.xml b/inventory_advanced_reports/report/age_breakdown_report_views.xml new file mode 100644 index 000000000..4fc6ebf72 --- /dev/null +++ b/inventory_advanced_reports/report/age_breakdown_report_views.xml @@ -0,0 +1,106 @@ + + + + + Inventory Age Breakdown Report + report.inventory_advanced_reports.report_inventory_breakdown + qweb-pdf + inventory_advanced_reports.report_inventory_breakdown + inventory_advanced_reports.report_inventory_breakdown + + report + + + + diff --git a/inventory_advanced_reports/report/aging_report.py b/inventory_advanced_reports/report/aging_report.py new file mode 100644 index 000000000..5181c50d5 --- /dev/null +++ b/inventory_advanced_reports/report/aging_report.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +class AgingReport(models.AbstractModel): + """ Create an abstract model for passing reporting values """ + _name = 'report.inventory_advanced_reports.report_inventory_aging' + _description = 'Aging Report' + + @api.model + def _get_report_values(self, docids, data=None): + """ This function has working in get the pdf report """ + values = data + product_ids = data['product_ids'] + category_ids = data['category_ids'] + company_ids = data['company_ids'] + params = [] + param_count = 0 + query = """ + SELECT + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', + pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END AS product_code_and_name, + c.complete_name AS category_name, + c.id AS category_id, + pp.id AS product_id, + company.id AS company_id, + company.name AS company_name, + COALESCE(SUM(svl.remaining_qty), 0) AS qty_available, + (SELECT SUM(sm_inner.product_uom_qty) + FROM stock_move sm_inner + INNER JOIN res_company company_inner + ON sm_inner.company_id = company_inner.id + WHERE sm_inner.product_id = pp.id + AND sm_inner.state = 'done' + AND sm_inner.date < ( + SELECT MAX(sm_inner2.date) + FROM stock_move sm_inner2 + WHERE sm_inner2.product_id = pp.id + AND sm_inner2.state = 'done' + AND company_inner.id = sm_inner2.company_id + ) + ) AS prev_qty_available, + ( + SELECT MIN(sm_inner.date) + FROM stock_move sm_inner + WHERE sm_inner.product_id = pp.id + AND sm_inner.state = 'done' + AND (company.id IS NULL OR company.id = sm_inner.company_id) + ) AS receipt_date + FROM product_product pp + INNER JOIN product_template pt ON pp.product_tmpl_id = pt.id + INNER JOIN product_category c ON pt.categ_id = c.id + LEFT JOIN stock_move sm ON sm.product_id = pp.id + LEFT JOIN stock_picking_type spt ON + sm.picking_type_id = spt.id + LEFT JOIN res_company company ON sm.company_id = company.id + INNER JOIN stock_valuation_layer svl ON + svl.stock_move_id = sm.id + WHERE pt.detailed_type = 'product' + AND sm.state = 'done' + """ + if product_ids or category_ids: + query += " AND (" + if product_ids: + product_ids = [product_id for product_id in product_ids] + query += "pp.id = ANY(%s)" + params.append(product_ids) + param_count += 1 + if product_ids and category_ids: + query += " OR " + if category_ids: + category_ids = [category for category in category_ids] + params.append(category_ids) + query += "(pt.categ_id = ANY(%s))" + param_count += 1 + if product_ids or category_ids: + query += ")" + if company_ids: + company_ids = [company for company in company_ids] + query += " AND (sm.company_id = ANY(%s))" + params.append(company_ids) + param_count += 1 + query += """ + GROUP BY + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', + pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END, + c.complete_name, + company.id, + c.id, + company.name, + pp.id; + """ + self.env.cr.execute(query, params) + result_data = self.env.cr.dictfetchall() + today = fields.datetime.now().date() + for row in result_data: + receipt_date = row.get('receipt_date') + if receipt_date: + receipt_date = receipt_date.date() + row['days_since_receipt'] = (today - receipt_date).days + product = self.env['product.product'].browse(row.get('product_id')) + standard_price = product.standard_price + current_stock = row.get('qty_available') + prev_stock = row.get('prev_qty_available') + if prev_stock is None: + prev_stock = current_stock + row['prev_qty_available'] = current_stock + if standard_price and current_stock: + row['current_value'] = current_stock * standard_price + else: + row[ + 'current_value'] = 0 + row[ + 'prev_value'] = prev_stock * standard_price \ + if prev_stock is not None else 0 + total_current_stock = sum( + item.get('qty_available') for item in result_data if + item.get('qty_available') is not None) + if total_current_stock: + stock_percentage = (current_stock / total_current_stock) * 100 + else: + stock_percentage = 0.0 + row['stock_percentage'] = round(stock_percentage, 2) + current_value = row.get('current_value') + total_value = sum( + item.get('current_value', 0) for item in result_data) + if total_value: + stock_value_percentage = (current_value / total_value) * 100 + else: + stock_value_percentage = 0.0 + row['stock_value_percentage'] = round(stock_value_percentage, 2) + if result_data: + return { + 'doc_ids': docids, + 'doc_model': + 'report.inventory_advanced_reports.report_inventory_aging', + 'data': values, + 'options': result_data, + } + else: + raise ValidationError("No records found for the given criteria!") diff --git a/inventory_advanced_reports/report/aging_report_views.xml b/inventory_advanced_reports/report/aging_report_views.xml new file mode 100755 index 000000000..347ae74f9 --- /dev/null +++ b/inventory_advanced_reports/report/aging_report_views.xml @@ -0,0 +1,75 @@ + + + + + Inventory Aging Report + report.inventory_advanced_reports.report_inventory_aging + qweb-pdf + inventory_advanced_reports.report_inventory_aging + inventory_advanced_reports.report_inventory_aging + + report + + + + diff --git a/inventory_advanced_reports/report/fsn_report.py b/inventory_advanced_reports/report/fsn_report.py new file mode 100644 index 000000000..b8c73bfc5 --- /dev/null +++ b/inventory_advanced_reports/report/fsn_report.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +from odoo import api, models +from datetime import datetime +from odoo.exceptions import ValidationError + + +class FsnReport(models.AbstractModel): + """Create an abstract model for passing reporting values""" + _name = 'report.inventory_advanced_reports.report_inventory_fsn' + _description = 'FSN Report' + + @api.model + def _get_report_values(self, docids, data=None): + """This function has working in get the pdf report.""" + values = data + if data is None or not isinstance(data, dict): + raise ValueError("Invalid or missing data for the report") + product_ids = data.get('product_ids', []) + category_ids = data.get('category_ids', []) + company_ids = data.get('company_ids', []) + warehouse_ids = data.get('warehouse_ids', []) + start_date = data.get('start_date') + end_date = data.get('end_date') + fsn = data.get('fsn') + if not start_date or not end_date: + raise ValueError( + "Missing start_date or end_date in the data") + start_date = datetime.strptime(start_date, '%Y-%m-%d') + end_date = datetime.strptime(end_date, '%Y-%m-%d') + filtered_product_stock = [] + query = """ + SELECT + product_id, + product_code_and_name, + category_id, + category_name, + company_id, + warehouse_id, + opening_stock, + closing_stock, + sales, + average_stock, + CASE + WHEN sales > 0 + THEN ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END AS turnover_ratio, + CASE + WHEN + CASE + WHEN sales > 0 + THEN ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END > 3 THEN 'Fast Moving' + WHEN + CASE + WHEN sales > 0 + THEN ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END >= 1 AND + CASE + WHEN sales > 0 + THEN ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END <= 3 THEN 'Slow Moving' + ELSE 'Non Moving' + END AS fsn_classification + FROM + (SELECT + pp.id AS product_id, + pt.categ_id AS category_id, + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', + pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END AS product_code_and_name, + pc.complete_name AS category_name, + company.id AS company_id, + sw.id AS warehouse_id, + (SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END)) AS opening_stock, + (SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END)) AS closing_stock, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_dest.usage = 'customer' + THEN sm.product_uom_qty ELSE 0 END) AS sales, + ((SUM(CASE WHEN sm.date <= %s + AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END))+ + (SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END)))/2 AS average_stock + FROM + stock_move sm + JOIN + product_product pp ON sm.product_id = pp.id + JOIN + product_template pt ON pp.product_tmpl_id = pt.id + JOIN + product_category pc ON pt.categ_id = pc.id + JOIN + res_company company ON company.id = sm.company_id + JOIN + stock_warehouse sw ON sw.company_id = company.id + LEFT JOIN + stock_location sld_dest ON sm.location_dest_id = sld_dest.id + LEFT JOIN + stock_location sld_src ON sm.location_id = sld_src.id + WHERE + sm.state = 'done' + """ + params = [ + start_date, start_date, end_date, end_date, start_date, end_date, + start_date, start_date, end_date, end_date + ] + sub_queries = [] + sub_params = [] + param_count = 0 + if product_ids or category_ids: + query += " AND (" + if product_ids: + product_ids = [product_id for product_id in product_ids] + query += "pp.id = ANY(%s)" + params.append(product_ids) + param_count += 1 + if product_ids and category_ids: + query += " OR " + if category_ids: + category_ids = [category_id for category_id in category_ids] + query += "pt.categ_id IN %s" + params.append(tuple(category_ids)) + param_count += 1 + if product_ids or category_ids: + query += ")" + if company_ids: + query += f" AND company.id IN %s" + sub_params.append(tuple(company_ids)) + param_count += 1 + if warehouse_ids: + query += f" AND sw.id IN %s" + sub_params.append(tuple(warehouse_ids)) + param_count += 1 + if sub_queries: + query += " AND " + " AND ".join(sub_queries) + query += """ + GROUP BY pp.id, pt.categ_id,CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', + pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END, pc.complete_name, company.id, sw.id + ) AS subquery + """ + self.env.cr.execute(query, tuple(params + sub_params)) + result_data = self.env.cr.dictfetchall() + for fsn_data in result_data: + if fsn_data.get('fsn_classification') == str(fsn): + filtered_product_stock.append(fsn_data) + if fsn == 'All' and not result_data: + raise ValidationError("No corresponding data to print") + elif fsn != 'All' and filtered_product_stock == []: + raise ValidationError("No corresponding data to print") + return { + 'doc_ids': docids, + 'doc_model': + 'report.inventory_advanced_reports.report_inventory_fsn', + 'data': values, + 'options': result_data if fsn == 'All' else filtered_product_stock, + } diff --git a/inventory_advanced_reports/report/fsn_report_views.xml b/inventory_advanced_reports/report/fsn_report_views.xml new file mode 100644 index 000000000..6d2d03e22 --- /dev/null +++ b/inventory_advanced_reports/report/fsn_report_views.xml @@ -0,0 +1,91 @@ + + + + + Inventory FSN Report + report.inventory_advanced_reports.report_inventory_fsn + qweb-pdf + inventory_advanced_reports.report_inventory_fsn + inventory_advanced_reports.report_inventory_fsn + + report + + + + diff --git a/inventory_advanced_reports/report/fsn_xyz_report.py b/inventory_advanced_reports/report/fsn_xyz_report.py new file mode 100644 index 000000000..f29900e72 --- /dev/null +++ b/inventory_advanced_reports/report/fsn_xyz_report.py @@ -0,0 +1,260 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +class FsnXyzReport(models.AbstractModel): + """Create an abstract model for passing reporting values""" + _name = 'report.inventory_advanced_reports.report_inventory_fsn_xyz' + _description = 'FSN-XYZ Report' + + @api.model + def _get_report_values(self, docids, data=None): + """This function has working in get the pdf report.""" + values = data + if data is None or not isinstance(data, dict): + raise ValueError("Invalid or missing data for the report") + product_ids = data.get('product_ids', []) + category_ids = data.get('category_ids', []) + company_ids = data.get('company_ids', []) + warehouse_ids = data.get('warehouse_ids', []) + start_date = data.get('start_date') + end_date = data.get('end_date') + fsn = data.get('fsn') + xyz = data.get('xyz') + if not start_date or not end_date: + raise ValueError( + "Missing start_date or end_date in the data") + start_date = fields.datetime.strptime(start_date, '%Y-%m-%d') + end_date = fields.datetime.strptime(end_date, '%Y-%m-%d') + filtered_product_stock = [] + query = """ + SELECT + product_id, + product_code_and_name, + category_id, + category_name, + company_id, + warehouse_id, + opening_stock, + closing_stock, + sales, + average_stock, + current_stock, + stock_value, + stock_percentage, + CASE + WHEN sales > 0 + THEN ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END AS turnover_ratio, + CASE + WHEN + CASE + WHEN sales > 0 + THEN + ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END > 3 THEN 'Fast Moving' + WHEN + CASE + WHEN sales > 0 + THEN + ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END >= 1 AND + CASE + WHEN sales > 0 + THEN + ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END <= 3 THEN 'Slow Moving' + ELSE 'Non Moving' + END AS fsn_classification, + SUM(stock_percentage) OVER (ORDER BY stock_value DESC) + AS cumulative_stock_percentage, + CASE + WHEN SUM(stock_percentage) OVER (ORDER BY stock_value DESC) < 70 + THEN 'X' + WHEN SUM(stock_percentage) OVER (ORDER BY stock_value DESC) >= 70 + AND SUM(stock_percentage) OVER (ORDER BY stock_value DESC) <= 90 + THEN 'Y' + ELSE 'Z' + END AS xyz_classification, + CONCAT( + CASE + WHEN + CASE + WHEN sales > 0 + THEN ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END > 3 THEN 'F' + WHEN + CASE + WHEN sales > 0 + THEN ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END >= 1 AND + CASE + WHEN sales > 0 + THEN ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END <= 3 THEN 'S' + ELSE 'N' + END, + CASE + WHEN SUM(stock_percentage) OVER (ORDER BY stock_value DESC) < 70 + THEN 'X' + WHEN SUM(stock_percentage) + OVER (ORDER BY stock_value DESC) >= 70 AND SUM(stock_percentage) + OVER (ORDER BY stock_value DESC) <= 90 THEN 'Y' + ELSE 'Z' + END + ) AS combined_classification + FROM + (SELECT + pp.id AS product_id, + pt.categ_id AS category_id, + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', + pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END AS product_code_and_name, + pc.complete_name AS category_name, + company.id AS company_id, + sw.id AS warehouse_id, + SUM(svl.remaining_qty) AS current_stock, + SUM(svl.remaining_value) AS stock_value, + COALESCE(ROUND((SUM(svl.remaining_value) / + NULLIF(SUM(SUM(svl.remaining_value)) + OVER (), 0)) * 100, 2),0) AS stock_percentage, + (SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END)) AS opening_stock, + (SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END)) AS closing_stock, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_dest.usage = 'customer' + THEN sm.product_uom_qty ELSE 0 END) AS sales, + ((SUM(CASE WHEN sm.date <= %s + AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END))+ + (SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END)))/2 AS average_stock + FROM + stock_move sm + JOIN + product_product pp ON sm.product_id = pp.id + JOIN + product_template pt ON pp.product_tmpl_id = pt.id + JOIN + product_category pc ON pt.categ_id = pc.id + JOIN + res_company company ON company.id = sm.company_id + JOIN + stock_warehouse sw ON sw.company_id = company.id + JOIN + stock_valuation_layer svl ON svl.stock_move_id = sm.id + LEFT JOIN + stock_location sld_dest ON sm.location_dest_id = sld_dest.id + LEFT JOIN + stock_location sld_src ON sm.location_id = sld_src.id + WHERE + sm.state = 'done' + AND pp.active = TRUE + AND pt.active = TRUE + AND pt.type = 'product' + AND svl.remaining_value IS NOT NULL + """ + params = [ + start_date, start_date, end_date, end_date, start_date, end_date, + start_date, start_date, end_date, end_date + ] + sub_queries = [] + sub_params = [] + param_count = 0 + if product_ids or category_ids: + query += " AND (" + if product_ids: + product_ids = [product_id for product_id in product_ids] + query += "pp.id = ANY(%s)" + params.append(product_ids) + param_count += 1 + if product_ids and category_ids: + query += " OR " + if category_ids: + category_ids = [category_id for category_id in category_ids] + query += "pt.categ_id IN %s" + params.append(tuple(category_ids)) + param_count += 1 + if product_ids or category_ids: + query += ")" + if company_ids: + query += f" AND sm.company_id IN %s" + sub_params.append(tuple(company_ids)) + param_count += 1 + if warehouse_ids: + query += f" AND sw.id IN %s" + sub_params.append(tuple(warehouse_ids)) + param_count += 1 + if sub_queries: + query += " AND " + " AND ".join(sub_queries) + query += """ + GROUP BY + pp.id,pt.name, pt.categ_id,pc.complete_name, company.id, sw.id + ) AS subquery + ORDER BY stock_value DESC + """ + self.env.cr.execute(query, tuple(params + sub_params)) + result_data = self.env.cr.dictfetchall() + for fsn_data in result_data: + if ( + (fsn == 'All' and xyz == 'All') or + (fsn == 'All' and fsn_data.get('xyz_classification') == str( + xyz)) or + (xyz == 'All' and fsn_data.get('fsn_classification') == str( + fsn)) or + (fsn_data.get('fsn_classification') == str( + fsn) and fsn_data.get('xyz_classification') == str(xyz)) + ): + filtered_product_stock.append(fsn_data) + if (fsn == 'All' or xyz == 'All') and not result_data: + raise ValidationError("No corresponding data to print") + elif not filtered_product_stock: + raise ValidationError("No corresponding data to print") + return { + 'doc_ids': docids, + 'doc_model': + 'report.inventory_advanced_reports.report_inventory_fsn_xyz', + 'data': values, + 'options': filtered_product_stock, + } diff --git a/inventory_advanced_reports/report/fsn_xyz_report_views.xml b/inventory_advanced_reports/report/fsn_xyz_report_views.xml new file mode 100644 index 000000000..85892221c --- /dev/null +++ b/inventory_advanced_reports/report/fsn_xyz_report_views.xml @@ -0,0 +1,102 @@ + + + + + Inventory FSN XYZ Report + + report.inventory_advanced_reports.report_inventory_fsn_xyz + + qweb-pdf + inventory_advanced_reports.report_inventory_fsn_xyz + inventory_advanced_reports.report_inventory_fsn_xyz + + + report + + + + diff --git a/inventory_advanced_reports/report/out_of_stock_report.py b/inventory_advanced_reports/report/out_of_stock_report.py new file mode 100644 index 000000000..2e4db8534 --- /dev/null +++ b/inventory_advanced_reports/report/out_of_stock_report.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +from odoo import api, models +from odoo.exceptions import ValidationError + + +class OutOfStockReport(models.AbstractModel): + """Create an abstract model for passing reporting values""" + _name = 'report.inventory_advanced_reports.report_inventory_out_of_stock' + _description = 'OutOfStock Report' + + @api.model + def _get_report_values(self, docids, data=None): + """This function has working in get the pdf report.""" + values = data + if data is None or not isinstance(data, dict): + raise ValueError("Invalid or missing data for the report") + product_ids = data.get('product_ids', []) + category_ids = data.get('category_ids', []) + company_ids = data.get('company_ids', []) + warehouse_ids = data.get('warehouse_ids', []) + inventory_for_next_x_days = data.get('inventory_for_next_x_days', []) + start_date = data.get('start_date') + end_date = data.get('end_date') + if not start_date or not end_date: + raise ValueError( + "Missing start_date or end_date in the data") + query = """ + SELECT + product_id, + product_code_and_name, + category_id, + category_name, + company_id, + current_stock, + warehouse_id, + incoming_quantity, + outgoing_quantity, + virtual_stock, + sales, + ads, + advance_stock_days, + ROUND(advance_stock_days * ads, 0) AS demanded_quantity, + ROUND(CASE + WHEN ads = 0 THEN virtual_stock / 0.001 + ELSE virtual_stock / ads + END,0) AS in_stock_days, + ROUND(CASE + WHEN ads = 0 THEN GREATEST(advance_stock_days - + ROUND(virtual_stock / 0.001, 2), 0) + ELSE GREATEST(advance_stock_days - + ROUND(virtual_stock / ads, 2), 0) + END ,0) AS out_of_stock_days, + ROUND( + CASE + WHEN advance_stock_days = 0 THEN 0 + ELSE + CASE + WHEN ads = 0 THEN GREATEST(advance_stock_days - + ROUND(virtual_stock / 0.001, 2), 0) + ELSE GREATEST(advance_stock_days - + ROUND(virtual_stock / ads, 2), 0) + END + END, 2 + ) AS out_of_stock_ratio, + ROUND( + CASE + WHEN ads = 0 + THEN + GREATEST(advance_stock_days - + ROUND(virtual_stock / 0.001, 2), 0) + ELSE GREATEST(advance_stock_days - + ROUND(virtual_stock / ads, 2), 0) + END * ads, 0 + ) AS out_of_stock_qty, + ROUND( + CASE + WHEN virtual_stock = 0 THEN 0 + ELSE sales / virtual_stock + END, 2 + ) AS turnover_ratio, + CASE + WHEN + CASE + WHEN sales > 0 THEN + ROUND((sales / NULLIF(virtual_stock, 0)), 2) + ELSE 0 + END > 3 THEN 'Fast Moving' + WHEN + CASE + WHEN sales > 0 THEN + ROUND((sales / NULLIF(virtual_stock, 0)), 2) + ELSE 0 + END >= 1 AND + CASE + WHEN sales > 0 THEN + ROUND((sales / NULLIF(virtual_stock, 0)), 2) + ELSE 0 + END <= 3 THEN 'Slow Moving' + ELSE 'Non Moving' + END AS fsn_classification + FROM( + SELECT + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', + pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END AS product_code_and_name, + company.id AS company_id, + company.name AS company_name, + sm.product_id AS product_id, + pc.id AS category_id, + pc.complete_name AS category_name, + sw.id AS warehouse_id, + SUM(CASE + WHEN sld_dest.usage = 'internal' AND sm.state IN + ('assigned', 'confirmed', 'waiting') + THEN sm.product_uom_qty + ELSE 0 + END) AS incoming_quantity, + SUM(CASE + WHEN sld_src.usage = 'internal' AND sm.state IN + ('assigned', 'confirmed', 'waiting') + THEN sm.product_uom_qty + ELSE 0 + END) AS outgoing_quantity, + SUM(CASE + WHEN sld_dest.usage = 'internal' AND sm.state = 'done' + THEN sm.product_uom_qty + ELSE 0 + END) - + SUM(CASE + WHEN sld_src.usage = 'internal' AND sm.state = 'done' + THEN sm.product_uom_qty + ELSE 0 + END) AS current_stock, + SUM(CASE + WHEN sld_dest.usage = 'internal' AND sm.state = 'done' + THEN sm.product_uom_qty + ELSE 0 + END) - + SUM(CASE + WHEN sld_src.usage = 'internal' AND sm.state = 'done' + THEN sm.product_uom_qty + ELSE 0 + END)+ + SUM(CASE + WHEN sld_dest.usage = 'internal' AND sm.state + IN ('assigned', 'confirmed', 'waiting') THEN sm.product_uom_qty + ELSE 0 + END) - + SUM(CASE + WHEN sld_src.usage = 'internal' AND sm.state + IN ('assigned', 'confirmed', 'waiting') THEN sm.product_uom_qty + ELSE 0 + END) AS virtual_stock, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_dest.usage = 'customer' + THEN sm.product_uom_qty ELSE 0 END) AS sales, + ROUND(SUM(CASE + WHEN sm.date BETWEEN %s AND %s AND sld_src.usage = 'internal' + AND sm.state = 'done' THEN sm.product_uom_qty + ELSE 0 + END) / ((date %s - date %s)+1), 2) AS ads, + %s AS advance_stock_days + FROM stock_move sm + INNER JOIN product_product pp ON pp.id = sm.product_id + INNER JOIN product_template pt ON pt.id = pp.product_tmpl_id + INNER JOIN res_company company ON company.id = sm.company_id + INNER JOIN stock_warehouse sw ON sw.company_id = company.id + INNER JOIN product_category pc ON pc.id = pt.categ_id + LEFT JOIN ( + SELECT sm.id AS move_id, usage + FROM stock_location sld + INNER JOIN stock_move sm ON sld.id = sm.location_dest_id + ) sld_dest ON sm.id = sld_dest.move_id + LEFT JOIN ( + SELECT sm.id AS move_id, usage + FROM stock_location sld + INNER JOIN stock_move sm ON sld.id = sm.location_id + ) sld_src ON sm.id = sld_src.move_id + WHERE pp.active = TRUE + AND pt.active = TRUE + AND pt.type = 'product' + """ + params = [ + start_date, end_date, + start_date, end_date, + end_date, start_date, + inventory_for_next_x_days + ] + sub_queries = [] + sub_params = [] + param_count = 0 + if product_ids or category_ids: + query += " AND (" + if product_ids: + product_ids = [product_id for product_id in product_ids] + query += "pp.id = ANY(%s)" + params.append(product_ids) + param_count += 1 + if product_ids and category_ids: + query += " OR " + if category_ids: + category_ids = [category for category in category_ids] + params.append(category_ids) + query += "(pt.categ_id = ANY(%s))" + param_count += 1 + if product_ids or category_ids: + query += ")" + if company_ids: + company_ids = [company for company in company_ids] + query += " AND (sm.company_id = ANY(%s))" + sub_params.append(company_ids) + param_count += 1 + if warehouse_ids: + warehouse_ids = [warehouse for warehouse in warehouse_ids] + query += " AND (sw.id = ANY(%s))" + sub_params.append(warehouse_ids) + param_count += 1 + if sub_queries: + query += " AND " + " AND ".join(sub_queries) + query += """ GROUP BY pp.id, pt.name, pc.id, company.id, sm.product_id, + sw.id) AS sub_query """ + self.env.cr.execute(query, tuple(params + sub_params)) + result_data = self.env.cr.dictfetchall() + for data in result_data: + product_id = data.get('product_id') + out_of_stock_qty = data.get('out_of_stock_qty') + total_value = sum( + item.get('out_of_stock_qty', 0) for item in result_data) + if total_value: + out_of_stock_qty_percentage = \ + (out_of_stock_qty / total_value) * 100 + else: + out_of_stock_qty_percentage = 0.0 + data['out_of_stock_qty_percentage'] = round( + out_of_stock_qty_percentage, 2) + cost = self.env['product.product'].search([ + ('id', '=', product_id)]).standard_price + data['cost'] = cost + data['out_of_stock_value'] = out_of_stock_qty * cost + if result_data: + return { + 'doc_ids': docids, + 'doc_model': + 'report.inventory_advanced_reports.' + 'report_inventory_out_of_stock', + 'data': values, + 'options': result_data, + } + else: + raise ValidationError("No records found for the given criteria!") diff --git a/inventory_advanced_reports/report/out_of_stock_report_views.xml b/inventory_advanced_reports/report/out_of_stock_report_views.xml new file mode 100644 index 000000000..b69bfb5dd --- /dev/null +++ b/inventory_advanced_reports/report/out_of_stock_report_views.xml @@ -0,0 +1,158 @@ + + + + + Over Stock + + custom + 297 + 500 + Landscape + 30 + 23 + 5 + 5 + + 20 + 90 + + + + Inventory Out Of Stock Report + report.inventory_advanced_reports.report_inventory_out_of_stock + qweb-pdf + inventory_advanced_reports.report_inventory_out_of_stock + inventory_advanced_reports.report_inventory_out_of_stock + + + report + + + + diff --git a/inventory_advanced_reports/report/over_stock_report.py b/inventory_advanced_reports/report/over_stock_report.py new file mode 100644 index 000000000..f9e90219f --- /dev/null +++ b/inventory_advanced_reports/report/over_stock_report.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +class OverStockReport(models.AbstractModel): + """Create an abstract model for passing reporting values""" + _name = 'report.inventory_advanced_reports.report_inventory_over_stock' + _description = 'Over Stock Report' + + @api.model + def _get_report_values(self, docids, data=None): + """This function has working in get the pdf report.""" + values = data + if data is None or not isinstance(data, dict): + raise ValueError("Invalid or missing data for the report") + product_ids = data.get('product_ids', []) + category_ids = data.get('category_ids', []) + company_ids = data.get('company_ids', []) + warehouse_ids = data.get('warehouse_ids', []) + inventory_for_next_x_days = data.get('inventory_for_next_x_days', []) + start_date = data.get('start_date') + end_date = data.get('end_date') + if not start_date or not end_date: + raise ValueError( + "Missing start_date or end_date in the data") + processed_product_ids = [] + filtered_result_data = [] + query = """ + SELECT + product_id, + product_code_and_name, + category_id, + category_name, + company_id, + current_stock, + warehouse_id, + incoming_quantity, + outgoing_quantity, + virtual_stock, + sales, + ads, + advance_stock_days, + ROUND(advance_stock_days * ads, 0) + AS demanded_quantity, + ROUND(CASE + WHEN ads = 0 THEN virtual_stock / 0.001 + ELSE virtual_stock / ads + END,0) AS in_stock_days, + ROUND(virtual_stock-(ads*advance_stock_days),0) + AS over_stock_qty, + ROUND( + CASE + WHEN virtual_stock = 0 THEN 0 + ELSE sales / virtual_stock + END, 2 + ) AS turnover_ratio, + CASE + WHEN + CASE + WHEN sales > 0 + THEN ROUND((sales / NULLIF(virtual_stock, 0)), 2) + ELSE 0 + END > 3 THEN 'Fast Moving' + WHEN + CASE + WHEN sales > 0 + THEN ROUND((sales / NULLIF(virtual_stock, 0)), 2) + ELSE 0 + END >= 1 AND + CASE + WHEN sales > 0 + THEN ROUND((sales / NULLIF(virtual_stock, 0)), 2) + ELSE 0 + END <= 3 THEN 'Slow Moving' + ELSE 'Non Moving' + END AS fsn_classification + FROM( + SELECT + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', + pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END AS product_code_and_name, + company.id AS company_id, + company.name AS company_name, + sm.product_id AS product_id, + pc.id AS category_id, + pc.complete_name AS category_name, + sw.id AS warehouse_id, + SUM(CASE + WHEN sld_dest.usage = 'internal' AND sm.state + IN ('assigned', 'confirmed', 'waiting') THEN sm.product_uom_qty + ELSE 0 + END) AS incoming_quantity, + SUM(CASE + WHEN sld_src.usage = 'internal' AND sm.state + IN ('assigned', 'confirmed', 'waiting') THEN sm.product_uom_qty + ELSE 0 + END) AS outgoing_quantity, + SUM(CASE + WHEN sld_dest.usage = 'internal' AND sm.state = 'done' + THEN sm.product_uom_qty + ELSE 0 + END) - + SUM(CASE + WHEN sld_src.usage = 'internal' AND sm.state = 'done' + THEN sm.product_uom_qty + ELSE 0 + END) AS current_stock, + SUM(CASE + WHEN sld_dest.usage = 'internal' AND sm.state = 'done' + THEN sm.product_uom_qty + ELSE 0 + END) - + SUM(CASE + WHEN sld_src.usage = 'internal' AND sm.state = 'done' + THEN sm.product_uom_qty + ELSE 0 + END)+ + SUM(CASE + WHEN sld_dest.usage = 'internal' AND sm.state + IN ('assigned', 'confirmed', 'waiting') THEN sm.product_uom_qty + ELSE 0 + END) - + SUM(CASE + WHEN sld_src.usage = 'internal' AND sm.state + IN ('assigned', 'confirmed', 'waiting') THEN sm.product_uom_qty + ELSE 0 + END) AS virtual_stock, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_dest.usage = 'customer' + THEN sm.product_uom_qty ELSE 0 END) AS sales, + ROUND(SUM(CASE + WHEN sm.date BETWEEN %s AND %s AND sld_src.usage = 'internal' + AND sm.state = 'done' THEN sm.product_uom_qty + ELSE 0 + END) / ((date %s - date %s)+1), 2) AS ads, + %s AS advance_stock_days + FROM stock_move sm + INNER JOIN product_product pp ON pp.id = sm.product_id + INNER JOIN product_template pt ON pt.id = pp.product_tmpl_id + INNER JOIN res_company company ON company.id = sm.company_id + INNER JOIN stock_warehouse sw ON sw.company_id = company.id + INNER JOIN product_category pc ON pc.id = pt.categ_id + LEFT JOIN ( + SELECT sm.id AS move_id, usage + FROM stock_location sld + INNER JOIN stock_move sm ON sld.id = sm.location_dest_id + ) sld_dest ON sm.id = sld_dest.move_id + LEFT JOIN ( + SELECT sm.id AS move_id, usage + FROM stock_location sld + INNER JOIN stock_move sm ON sld.id = sm.location_id + ) sld_src ON sm.id = sld_src.move_id + WHERE pp.active = TRUE + AND pt.active = TRUE + AND pt.type = 'product' + """ + params = [ + start_date, end_date, + start_date, end_date, + end_date, start_date, + inventory_for_next_x_days + ] + sub_queries = [] + sub_params = [] + param_count = 0 + if product_ids or category_ids: + query += " AND (" + if product_ids: + product_ids = [product_id for product_id in product_ids] + query += "pp.id = ANY(%s)" + params.append(product_ids) + param_count += 1 + if product_ids and category_ids: + query += " OR " + if category_ids: + category_ids = [category for category in category_ids] + params.append(category_ids) + query += "(pt.categ_id = ANY(%s))" + param_count += 1 + if product_ids or category_ids: + query += ")" + if company_ids: + company_ids = [company for company in company_ids] + query += " AND (sm.company_id = ANY(%s))" + sub_params.append(company_ids) + param_count += 1 + if warehouse_ids: + warehouse_ids = [warehouse for warehouse in warehouse_ids] + query += " AND (sw.id = ANY(%s))" + sub_params.append(warehouse_ids) + param_count += 1 + if sub_queries: + query += " AND " + " AND ".join(sub_queries) + query += """ GROUP BY pp.id, pt.name, pc.id, company.id, sm.product_id, + sw.id) AS sub_query """ + self.env.cr.execute(query, tuple(params + sub_params)) + result_data = self.env.cr.dictfetchall() + for data in result_data: + product_id = data.get('product_id') + if product_id not in processed_product_ids: + processed_product_ids.append( + product_id) + filtered_result_data.append(data) + for data in filtered_result_data: + over_stock_qty = data.get('over_stock_qty') + product_id = data.get('product_id') + total_qty = sum( + item.get('over_stock_qty', 0) for item in filtered_result_data) + if total_qty: + over_stock_qty_percentage = \ + (over_stock_qty / total_qty) * 100 + else: + over_stock_qty_percentage = 0.0 + data['over_stock_qty_percentage'] = round( + over_stock_qty_percentage, 2) + cost = self.env['product.product'].search([ + ('id', '=', product_id)]).standard_price + data['cost'] = cost + data['over_stock_value'] = over_stock_qty * cost + latest_po = '' + confirmed_po = self.env['purchase.order.line'].search([ + ('product_id', '=', product_id), + ('state', '=', 'purchase'), + ]) + for po in confirmed_po: + if latest_po: + if latest_po.date_approve < po.date_approve: + latest_po = po + else: + latest_po = po + data['po_qty'] = 0 + data['po_price_total'] = 0 + if latest_po: + start_date = fields.Datetime.from_string(start_date).date() + end_date = fields.Datetime.from_string(end_date).date() + po_date = fields.Datetime.from_string(latest_po.date_approve) + if start_date <= po_date.date() <= end_date: + data['po_qty'] += latest_po.product_qty + data['po_price_total'] += latest_po.price_total + data['po_date'] = po_date + data['po_currency'] = latest_po.currency_id.name + data['po_partner'] = latest_po.partner_id.name + else: + data['po_price_total'] = None + data['po_qty'] = None + data['po_currency'] = None + data['po_partner'] = None + data[ + 'po_date'] = None + else: + data['po_price_total'] = None + data['po_qty'] = None + data['po_date'] = None + data['po_partner'] = None + data['po_currency'] = None + total_value = sum( + item.get('over_stock_value', 0) for item in filtered_result_data) + for data in filtered_result_data: + over_stock_value = data.get('over_stock_value') + if total_value: + over_stock_value_percentage = \ + (over_stock_value / total_value) * 100 + else: + over_stock_value_percentage = 0.0 + data['over_stock_value_percentage'] = round( + over_stock_value_percentage, 2) + if filtered_result_data: + return { + 'doc_ids': docids, + 'doc_model': + 'report.inventory_advanced_reports.' + 'report_inventory_over_stock', + 'data': values, + 'options': filtered_result_data, + } + else: + raise ValidationError("No records found for the given criteria!") diff --git a/inventory_advanced_reports/report/over_stock_report_views.xml b/inventory_advanced_reports/report/over_stock_report_views.xml new file mode 100644 index 000000000..f22c3d4f8 --- /dev/null +++ b/inventory_advanced_reports/report/over_stock_report_views.xml @@ -0,0 +1,153 @@ + + + + + Inventory Over Stock Report + report.inventory_advanced_reports.report_inventory_over_stock + qweb-pdf + inventory_advanced_reports.report_inventory_over_stock + inventory_advanced_reports.report_inventory_over_stock + + + report + + + + diff --git a/inventory_advanced_reports/report/stock_movement_report.py b/inventory_advanced_reports/report/stock_movement_report.py new file mode 100644 index 000000000..9eca7faba --- /dev/null +++ b/inventory_advanced_reports/report/stock_movement_report.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +from odoo import api, models +from odoo.exceptions import ValidationError + + +class StockMovementReport(models.AbstractModel): + """Create an abstract model for passing reporting values""" + _name = 'report.inventory_advanced_reports.report_inventory_movement' + _description = 'Stock Movement Report' + + @api.model + def _get_report_values(self, docids, data=None): + """This function has working in get the pdf report.""" + values = data + if data is None or not isinstance(data, dict): + raise ValueError("Invalid or missing data for the report") + product_ids = data.get('product_ids', []) + category_ids = data.get('category_ids', []) + company_ids = data.get('company_ids', []) + warehouse_ids = data.get('warehouse_ids', []) + report_up_to_certain_date = data.get('report_up_to_certain_date', []) + up_to_certain_date = data.get('up_to_certain_date', []) + start_date = data.get('start_date') + end_date = data.get('end_date') + if not start_date or not end_date: + raise ValueError( + "Missing start_date or end_date in the data") + query = """ + SELECT + pp.id as product_id, + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', + pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END AS product_code_and_name, + pc.complete_name AS category_name, + company.name AS company_name, + """ + if report_up_to_certain_date: + query += """ + SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'inventory' + THEN sm.product_uom_qty ELSE 0 END) AS opening_stock, + (SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END)) AS closing_stock, + SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'customer' + THEN sm.product_uom_qty ELSE 0 END) AS sales, + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'customer' + THEN sm.product_uom_qty ELSE 0 END) AS sales_return, + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'supplier' + THEN sm.product_uom_qty ELSE 0 END) AS purchase, + SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'supplier' + THEN sm.product_uom_qty ELSE 0 END) AS purchase_return, + SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) AS internal_in, + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) AS internal_out, + SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'inventory' + THEN sm.product_uom_qty ELSE 0 END) AS adj_in, + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'inventory' + THEN sm.product_uom_qty ELSE 0 END) AS adj_out, + SUM(CASE WHEN sm.date <= %s + AND sld_dest.usage = 'production' + THEN sm.product_uom_qty ELSE 0 END) AS production_in, + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'production' + THEN sm.product_uom_qty ELSE 0 END) AS production_out, + SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'transit' + THEN sm.product_uom_qty ELSE 0 END) AS transit_in, + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'transit' + THEN sm.product_uom_qty ELSE 0 END) AS transit_out + """ + params = [up_to_certain_date] * 15 + else: + query += """ + (SUM(CASE WHEN sm.date <= %s + AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s + AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END)) AS opening_stock, + (SUM(CASE WHEN sm.date <= %s + AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s + AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END)) AS closing_stock, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_dest.usage = 'customer' + THEN sm.product_uom_qty ELSE 0 END) AS sales, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_src.usage = 'customer' + THEN sm.product_uom_qty ELSE 0 END) AS sales_return, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_src.usage = 'supplier' + THEN sm.product_uom_qty ELSE 0 END) AS purchase, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_dest.usage = 'supplier' + THEN sm.product_uom_qty ELSE 0 END) AS purchase_return, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) AS internal_in, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) AS internal_out, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_dest.usage = 'inventory' + THEN sm.product_uom_qty ELSE 0 END) AS adj_in, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_src.usage = 'inventory' + THEN sm.product_uom_qty ELSE 0 END) AS adj_out, + SUM(CASE WHEN sm.date BETWEEN %s + AND %s AND sld_dest.usage = 'production' + THEN sm.product_uom_qty ELSE 0 END) AS production_in, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_src.usage = 'production' + THEN sm.product_uom_qty ELSE 0 END) AS production_out, + SUM(CASE WHEN sm.date BETWEEN %s + AND %s AND sld_dest.usage = 'transit' + THEN sm.product_uom_qty ELSE 0 END) AS transit_in, + SUM(CASE WHEN sm.date BETWEEN %s + AND %s AND sld_src.usage = 'transit' + THEN sm.product_uom_qty ELSE 0 END) AS transit_out + """ + params = [start_date, start_date, end_date, end_date, start_date, + end_date, start_date, end_date, start_date, end_date, + start_date, end_date, start_date, end_date, start_date, + end_date, start_date, end_date, start_date, end_date, + start_date, end_date, start_date, end_date, start_date, + end_date, start_date, end_date] + query += """ + FROM stock_move sm + INNER JOIN product_product pp ON pp.id = sm.product_id + INNER JOIN product_template pt ON pt.id = pp.product_tmpl_id + INNER JOIN res_company company ON company.id = sm.company_id + INNER JOIN stock_warehouse sw ON sw.company_id = company.id + INNER JOIN product_category pc ON pc.id = pt.categ_id + """ + query += """ + LEFT JOIN stock_location sld_dest + ON sm.location_dest_id = sld_dest.id + LEFT JOIN stock_location sld_src + ON sm.location_id = sld_src.id + WHERE + sm.state = 'done' + """ + sub_queries = [] + if product_ids: + product_ids = [product_id for product_id in product_ids] + sub_queries.append("pp.id = ANY(%s)") + params.append(product_ids) + if category_ids: + category_ids = [category for category in category_ids] + sub_queries.append("pt.categ_id = ANY(%s)") + params.append(category_ids) + if sub_queries: + query += " AND (" + " OR ".join(sub_queries) + ")" + if company_ids: + company_ids = [company for company in company_ids] + query += " AND sm.company_id = ANY(%s)" + params.append(company_ids) + if warehouse_ids: + warehouse_ids = [warehouse for warehouse in warehouse_ids] + query += " AND sw.id = ANY(%s)" + params.append(warehouse_ids) + query += """ + GROUP BY pp.id,pt.name,pc.complete_name,company.name + """ + self.env.cr.execute(query, params) + result_data = self.env.cr.dictfetchall() + if result_data: + return { + 'doc_ids': docids, + 'doc_model': 'inventory.overstock.report', + 'data': values, + 'options': result_data, + } + else: + raise ValidationError("No records found for the given criteria!") diff --git a/inventory_advanced_reports/report/stock_movement_report_views.xml b/inventory_advanced_reports/report/stock_movement_report_views.xml new file mode 100644 index 000000000..170efaff4 --- /dev/null +++ b/inventory_advanced_reports/report/stock_movement_report_views.xml @@ -0,0 +1,140 @@ + + + + + Inventory Stock Movement Report + report.inventory_advanced_reports.report_inventory_movement + qweb-pdf + inventory_advanced_reports.report_inventory_movement + inventory_advanced_reports.report_inventory_movement + + + report + + + + diff --git a/inventory_advanced_reports/report/xyz_report.py b/inventory_advanced_reports/report/xyz_report.py new file mode 100644 index 000000000..19da9df10 --- /dev/null +++ b/inventory_advanced_reports/report/xyz_report.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +from odoo import api, models +from odoo.exceptions import ValidationError + + +class XyzReport(models.AbstractModel): + """Create an abstract model for passing reporting values""" + _name = 'report.inventory_advanced_reports.report_inventory_xyz' + _description = 'XYZ Report' + + @api.model + def _get_report_values(self, docids, data=None): + """This function has working in get the pdf report.""" + values = data + product_ids = data['product_ids'] + category_ids = data['category_ids'] + company_ids = data['company_ids'] + xyz = data['xyz'] + params = [] + param_count = 0 + query = """ + SELECT + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END AS product_code_and_name, + svl.company_id, + company.name AS company_name, + svl.product_id, + pt.categ_id AS product_category_id, + c.complete_name AS category_name, + SUM(svl.remaining_qty) AS current_stock, + SUM(svl.remaining_value) AS stock_value + FROM stock_valuation_layer svl + INNER JOIN res_company company ON company.id = svl.company_id + INNER JOIN product_product pp ON pp.id = svl.product_id + INNER JOIN product_template pt ON pt.id = pp.product_tmpl_id + INNER JOIN product_category c ON c.id = pt.categ_id + WHERE pp.active = TRUE + AND pt.active = TRUE + AND pt.type = 'product' + AND svl.remaining_value IS NOT NULL + """ + if company_ids: + company_ids = [company_id for company_id in company_ids] + query += f" AND (company.id IS NULL OR company.id = ANY(%s))" + params.append(company_ids) + param_count += 1 + if product_ids or category_ids: + query += " AND (" + if product_ids: + product_ids = [product_id for product_id in product_ids] + query += f"pp.id = ANY(%s)" + params.append(product_ids) + param_count += 1 + if product_ids and category_ids: + query += " OR " + if category_ids: + category_ids = [category_id for category_id in + category_ids] + query += f"c.id = ANY(%s)" + params.append(category_ids) + param_count += 1 + query += ")" + query += """ + GROUP BY + svl.company_id, + company.name, + svl.product_id, + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END, + pt.categ_id, + c.complete_name + ORDER BY SUM(svl.remaining_value) DESC; + """ + self.env.cr.execute(query, params) + result_data = self.env.cr.dictfetchall() + total_current_value = 0 + cumulative_stock = 0 + filtered_stock = [] + for row in result_data: + current_value = row.get('stock_value') + total_current_value += current_value + for value in result_data: + current_value = value.get('stock_value') + if total_current_value != 0 and current_value: + stock_percentage = (current_value / total_current_value) * 100 + else: + stock_percentage = 0.0 + value['stock_percentage'] = round(stock_percentage, 2) + cumulative_stock += value['stock_percentage'] + value['cumulative_stock_percentage'] = round(cumulative_stock, 2) + if cumulative_stock < 70: + xyz_classification = 'X' + elif 70 <= cumulative_stock <= 90: + xyz_classification = 'Y' + else: + xyz_classification = 'Z' + value['xyz_classification'] = xyz_classification + if result_data: + for xyz_class in result_data: + if xyz_class.get('xyz_classification') == str(xyz): + filtered_stock.append(xyz_class) + if xyz == 'All' and not result_data: + raise ValidationError("No corresponding data to print") + elif xyz != 'All' and filtered_stock == []: + raise ValidationError("No corresponding data to print") + return { + 'doc_ids': docids, + 'doc_model': + 'report.inventory_advanced_reports.report_inventory_xyz', + 'data': values, + 'options': result_data if xyz == 'All' else filtered_stock, + } + else: + raise ValidationError("No records found for the given criteria!") diff --git a/inventory_advanced_reports/report/xyz_report_views.xml b/inventory_advanced_reports/report/xyz_report_views.xml new file mode 100644 index 000000000..4e534d652 --- /dev/null +++ b/inventory_advanced_reports/report/xyz_report_views.xml @@ -0,0 +1,67 @@ + + + + + Inventory XYZ Report + report.inventory_advanced_reports.report_inventory_xyz + qweb-pdf + inventory_advanced_reports.report_inventory_xyz + inventory_advanced_reports.report_inventory_xyz + + report + + + + diff --git a/inventory_advanced_reports/security/ir.model.access.csv b/inventory_advanced_reports/security/ir.model.access.csv new file mode 100644 index 000000000..1f4afec03 --- /dev/null +++ b/inventory_advanced_reports/security/ir.model.access.csv @@ -0,0 +1,16 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_inventory_aging_report_user,access.inventory.aging.report.user,model_inventory_aging_report,base.group_user,1,1,1,1 +access_inventory_age_data_report_user,access.inventory.age.data.report.user,model_inventory_aging_data_report,base.group_user,1,1,1,1 +access_inventory_fsn_report_user,access.inventory.fsn.report.user,model_inventory_fsn_report,base.group_user,1,1,1,1 +access_inventory_aging_data_report_user,access.inventory.aging.data.report.user,model_inventory_aging_data_report,base.group_user,1,1,1,1 +access_inventory_fsn_data_report_user,access.inventory.fsn.data.report.user,model_inventory_fsn_data_report,base.group_user,1,1,1,1 +access_inventory_xyz_report_user,access.inventory.xyz.report.user,model_inventory_xyz_report,base.group_user,1,1,1,1 +access_inventory_xyz_data_report_user,access.inventory.xyz.data.report.user,model_inventory_xyz_data_report,base.group_user,1,1,1,1 +access_inventory_fsn_xyz_report_user,access.inventory.fsn.xyz.report.user,model_inventory_fsn_xyz_report,base.group_user,1,1,1,1 +access_inventory_fsn_xyz_data_report_user,access.inventory.fsn.xyz.data.report.user,model_inventory_fsn_xyz_data_report,base.group_user,1,1,1,1 +access_inventory_out_of_stock_report_user,access.inventory.out.of.stock.report.user,model_inventory_out_of_stock_report,base.group_user,1,1,1,1 +access_inventory_out_of_stock_data_report_user,access.inventory.out.of.stock.data.report.user,model_inventory_out_of_stock_data_report,base.group_user,1,1,1,1 +access_inventory_age_breakdown_report_user,access.inventory.age.breakdown.report.user,model_inventory_age_breakdown_report,base.group_user,1,1,1,1 +access_inventory_over_stock_report_user,access.inventory.over.stock.report.user,model_inventory_over_stock_report,base.group_user,1,1,1,1 +access_inventory_over_stock_data_report_user,access.inventory.over.stock.data.report.user,model_inventory_over_stock_data_report,base.group_user,1,1,1,1 +access_inventory_stock_movement_report_user,access.inventory.stock.movement.report.user,model_inventory_stock_movement_report,base.group_user,1,1,1,1 diff --git a/inventory_advanced_reports/static/description/assets/icons/check.png b/inventory_advanced_reports/static/description/assets/icons/check.png new file mode 100644 index 000000000..c8e85f51d Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/check.png differ diff --git a/inventory_advanced_reports/static/description/assets/icons/chevron.png b/inventory_advanced_reports/static/description/assets/icons/chevron.png new file mode 100644 index 000000000..2089293d6 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/chevron.png differ diff --git a/inventory_advanced_reports/static/description/assets/icons/cogs.png b/inventory_advanced_reports/static/description/assets/icons/cogs.png new file mode 100644 index 000000000..95d0bad62 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/cogs.png differ diff --git a/inventory_advanced_reports/static/description/assets/icons/consultation.png b/inventory_advanced_reports/static/description/assets/icons/consultation.png new file mode 100644 index 000000000..8319d4baa Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/consultation.png differ diff --git a/inventory_advanced_reports/static/description/assets/icons/ecom-black.png b/inventory_advanced_reports/static/description/assets/icons/ecom-black.png new file mode 100644 index 000000000..a9385ff13 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/ecom-black.png differ diff --git a/inventory_advanced_reports/static/description/assets/icons/education-black.png b/inventory_advanced_reports/static/description/assets/icons/education-black.png new file mode 100644 index 000000000..3eb09b27b Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/education-black.png differ diff --git a/inventory_advanced_reports/static/description/assets/icons/hotel-black.png b/inventory_advanced_reports/static/description/assets/icons/hotel-black.png new file mode 100644 index 000000000..130f613be Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/hotel-black.png differ diff --git a/inventory_advanced_reports/static/description/assets/icons/license.png b/inventory_advanced_reports/static/description/assets/icons/license.png new file mode 100644 index 000000000..a5869797e Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/license.png differ diff --git a/inventory_advanced_reports/static/description/assets/icons/lifebuoy.png b/inventory_advanced_reports/static/description/assets/icons/lifebuoy.png new file mode 100644 index 000000000..658d56ccc Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/lifebuoy.png differ diff --git a/inventory_advanced_reports/static/description/assets/icons/manufacturing-black.png b/inventory_advanced_reports/static/description/assets/icons/manufacturing-black.png new file mode 100644 index 000000000..697eb0e9f Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/manufacturing-black.png differ diff --git a/inventory_advanced_reports/static/description/assets/icons/pos-black.png b/inventory_advanced_reports/static/description/assets/icons/pos-black.png new file mode 100644 index 000000000..97c0f90c1 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/pos-black.png differ diff --git a/inventory_advanced_reports/static/description/assets/icons/puzzle.png b/inventory_advanced_reports/static/description/assets/icons/puzzle.png new file mode 100644 index 000000000..65cf854e7 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/puzzle.png differ diff --git a/inventory_advanced_reports/static/description/assets/icons/restaurant-black.png b/inventory_advanced_reports/static/description/assets/icons/restaurant-black.png new file mode 100644 index 000000000..4a35eb939 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/restaurant-black.png differ diff --git a/inventory_advanced_reports/static/description/assets/icons/service-black.png b/inventory_advanced_reports/static/description/assets/icons/service-black.png new file mode 100644 index 000000000..301ab51cb Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/service-black.png differ diff --git a/inventory_advanced_reports/static/description/assets/icons/trading-black.png b/inventory_advanced_reports/static/description/assets/icons/trading-black.png new file mode 100644 index 000000000..9398ba2f1 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/trading-black.png differ diff --git a/inventory_advanced_reports/static/description/assets/icons/training.png b/inventory_advanced_reports/static/description/assets/icons/training.png new file mode 100644 index 000000000..884ca024d Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/training.png differ diff --git a/inventory_advanced_reports/static/description/assets/icons/update.png b/inventory_advanced_reports/static/description/assets/icons/update.png new file mode 100644 index 000000000..ecbc5a01a Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/update.png differ diff --git a/inventory_advanced_reports/static/description/assets/icons/user.png b/inventory_advanced_reports/static/description/assets/icons/user.png new file mode 100644 index 000000000..6ffb23d9f Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/user.png differ diff --git a/inventory_advanced_reports/static/description/assets/icons/wrench.png b/inventory_advanced_reports/static/description/assets/icons/wrench.png new file mode 100644 index 000000000..6c04dea0f Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/icons/wrench.png differ diff --git a/inventory_advanced_reports/static/description/assets/misc/categories.png b/inventory_advanced_reports/static/description/assets/misc/categories.png new file mode 100644 index 000000000..bedf1e0b1 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/misc/categories.png differ diff --git a/inventory_advanced_reports/static/description/assets/misc/check-box.png b/inventory_advanced_reports/static/description/assets/misc/check-box.png new file mode 100644 index 000000000..42caf24b9 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/misc/check-box.png differ diff --git a/inventory_advanced_reports/static/description/assets/misc/compass.png b/inventory_advanced_reports/static/description/assets/misc/compass.png new file mode 100644 index 000000000..d5fed8faa Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/misc/compass.png differ diff --git a/inventory_advanced_reports/static/description/assets/misc/corporate.png b/inventory_advanced_reports/static/description/assets/misc/corporate.png new file mode 100644 index 000000000..2eb13edbf Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/misc/corporate.png differ diff --git a/inventory_advanced_reports/static/description/assets/misc/customer-support.png b/inventory_advanced_reports/static/description/assets/misc/customer-support.png new file mode 100644 index 000000000..79efc72ed Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/misc/customer-support.png differ diff --git a/inventory_advanced_reports/static/description/assets/misc/cybrosys-logo.png b/inventory_advanced_reports/static/description/assets/misc/cybrosys-logo.png new file mode 100644 index 000000000..cc3cc0ccf Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/misc/cybrosys-logo.png differ diff --git a/inventory_advanced_reports/static/description/assets/misc/features.png b/inventory_advanced_reports/static/description/assets/misc/features.png new file mode 100644 index 000000000..b41769f77 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/misc/features.png differ diff --git a/inventory_advanced_reports/static/description/assets/misc/logo.png b/inventory_advanced_reports/static/description/assets/misc/logo.png new file mode 100644 index 000000000..478462d3e Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/misc/logo.png differ diff --git a/inventory_advanced_reports/static/description/assets/misc/pictures.png b/inventory_advanced_reports/static/description/assets/misc/pictures.png new file mode 100644 index 000000000..56d255fe9 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/misc/pictures.png differ diff --git a/inventory_advanced_reports/static/description/assets/misc/pie-chart.png b/inventory_advanced_reports/static/description/assets/misc/pie-chart.png new file mode 100644 index 000000000..426e05244 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/misc/pie-chart.png differ diff --git a/inventory_advanced_reports/static/description/assets/misc/reports-icon.png b/inventory_advanced_reports/static/description/assets/misc/reports-icon.png new file mode 100644 index 000000000..571820062 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/misc/reports-icon.png differ diff --git a/inventory_advanced_reports/static/description/assets/misc/right-arrow.png b/inventory_advanced_reports/static/description/assets/misc/right-arrow.png new file mode 100644 index 000000000..730984a06 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/misc/right-arrow.png differ diff --git a/inventory_advanced_reports/static/description/assets/misc/star.png b/inventory_advanced_reports/static/description/assets/misc/star.png new file mode 100644 index 000000000..2eb9ab29f Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/misc/star.png differ diff --git a/inventory_advanced_reports/static/description/assets/misc/support.png b/inventory_advanced_reports/static/description/assets/misc/support.png new file mode 100644 index 000000000..4f18b8b82 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/misc/support.png differ diff --git a/inventory_advanced_reports/static/description/assets/misc/whatsapp.png b/inventory_advanced_reports/static/description/assets/misc/whatsapp.png new file mode 100644 index 000000000..d513a5356 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/misc/whatsapp.png differ diff --git a/inventory_advanced_reports/static/description/assets/modules/1.png b/inventory_advanced_reports/static/description/assets/modules/1.png new file mode 100644 index 000000000..958601ec8 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/modules/1.png differ diff --git a/inventory_advanced_reports/static/description/assets/modules/2.png b/inventory_advanced_reports/static/description/assets/modules/2.png new file mode 100644 index 000000000..773a67300 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/modules/2.png differ diff --git a/inventory_advanced_reports/static/description/assets/modules/3.png b/inventory_advanced_reports/static/description/assets/modules/3.png new file mode 100644 index 000000000..2a52f4d41 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/modules/3.png differ diff --git a/inventory_advanced_reports/static/description/assets/modules/4.png b/inventory_advanced_reports/static/description/assets/modules/4.png new file mode 100644 index 000000000..923a3beea Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/modules/4.png differ diff --git a/inventory_advanced_reports/static/description/assets/modules/5.png b/inventory_advanced_reports/static/description/assets/modules/5.png new file mode 100644 index 000000000..dacb9817d Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/modules/5.png differ diff --git a/inventory_advanced_reports/static/description/assets/modules/6.png b/inventory_advanced_reports/static/description/assets/modules/6.png new file mode 100644 index 000000000..be55ec3ed Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/modules/6.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/AGE BREAKDOWN 1.png b/inventory_advanced_reports/static/description/assets/screenshots/AGE BREAKDOWN 1.png new file mode 100644 index 000000000..56bc9ce9a Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/AGE BREAKDOWN 1.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/AGE BREAKDOWN 2.png b/inventory_advanced_reports/static/description/assets/screenshots/AGE BREAKDOWN 2.png new file mode 100644 index 000000000..bf120714a Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/AGE BREAKDOWN 2.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/AGE BREAKDOWN 3.png b/inventory_advanced_reports/static/description/assets/screenshots/AGE BREAKDOWN 3.png new file mode 100644 index 000000000..d9507626d Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/AGE BREAKDOWN 3.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/AGE BREAKDOWN 4.png b/inventory_advanced_reports/static/description/assets/screenshots/AGE BREAKDOWN 4.png new file mode 100644 index 000000000..5676fe9ab Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/AGE BREAKDOWN 4.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/AGE BREAKDOWN 5.png b/inventory_advanced_reports/static/description/assets/screenshots/AGE BREAKDOWN 5.png new file mode 100644 index 000000000..f8f3d7b6c Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/AGE BREAKDOWN 5.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/AGING 0.png b/inventory_advanced_reports/static/description/assets/screenshots/AGING 0.png new file mode 100644 index 000000000..52e2a0e5f Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/AGING 0.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/AGING 1.png b/inventory_advanced_reports/static/description/assets/screenshots/AGING 1.png new file mode 100644 index 000000000..6179cf789 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/AGING 1.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/AGING 2.png b/inventory_advanced_reports/static/description/assets/screenshots/AGING 2.png new file mode 100644 index 000000000..259dc4898 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/AGING 2.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/AGING 3.png b/inventory_advanced_reports/static/description/assets/screenshots/AGING 3.png new file mode 100644 index 000000000..ffbae96ea Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/AGING 3.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/AGING 4.png b/inventory_advanced_reports/static/description/assets/screenshots/AGING 4.png new file mode 100644 index 000000000..33200b143 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/AGING 4.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/AGING 5.png b/inventory_advanced_reports/static/description/assets/screenshots/AGING 5.png new file mode 100644 index 000000000..d8cb1f6e2 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/AGING 5.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/AGING 6.png b/inventory_advanced_reports/static/description/assets/screenshots/AGING 6.png new file mode 100644 index 000000000..8310d473b Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/AGING 6.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/AGING 7.png b/inventory_advanced_reports/static/description/assets/screenshots/AGING 7.png new file mode 100644 index 000000000..8a1780d4f Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/AGING 7.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/AGING 8.png b/inventory_advanced_reports/static/description/assets/screenshots/AGING 8.png new file mode 100644 index 000000000..8f072a1be Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/AGING 8.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN 1.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN 1.png new file mode 100644 index 000000000..eef53d2a4 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN 1.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN 10.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN 10.png new file mode 100644 index 000000000..0ce8fe321 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN 10.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN 2.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN 2.png new file mode 100644 index 000000000..888d998d4 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN 2.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN 3.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN 3.png new file mode 100644 index 000000000..52ac2bbdd Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN 3.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN 4.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN 4.png new file mode 100644 index 000000000..a6bc1c651 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN 4.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN 5.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN 5.png new file mode 100644 index 000000000..9f3c3d2df Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN 5.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN 6.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN 6.png new file mode 100644 index 000000000..2aaa41f26 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN 6.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN 7.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN 7.png new file mode 100644 index 000000000..bfedbfd73 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN 7.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN 8.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN 8.png new file mode 100644 index 000000000..b9795e6f7 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN 8.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN 9.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN 9.png new file mode 100644 index 000000000..d5c94ac4b Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN 9.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 1.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 1.png new file mode 100644 index 000000000..6af732d4e Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 1.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 2.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 2.png new file mode 100644 index 000000000..c0618d810 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 2.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 3.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 3.png new file mode 100644 index 000000000..9547b14f3 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 3.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 4.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 4.png new file mode 100644 index 000000000..71233a673 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 4.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 5.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 5.png new file mode 100644 index 000000000..d13ec3b16 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 5.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 6.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 6.png new file mode 100644 index 000000000..6221f5bf1 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 6.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 7.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 7.png new file mode 100644 index 000000000..4e5b12cb0 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 7.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 8.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 8.png new file mode 100644 index 000000000..b44e5d180 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 8.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 9.png b/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 9.png new file mode 100644 index 000000000..ec61c576d Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/FSN XYZ 9.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 1.png b/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 1.png new file mode 100644 index 000000000..1e742b743 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 1.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 2.png b/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 2.png new file mode 100644 index 000000000..ebe90617b Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 2.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 3.png b/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 3.png new file mode 100644 index 000000000..c7f080491 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 3.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 4.png b/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 4.png new file mode 100644 index 000000000..9efd12622 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 4.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 5.png b/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 5.png new file mode 100644 index 000000000..46e4d8be7 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 5.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 6.png b/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 6.png new file mode 100644 index 000000000..8fc4d6ee2 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 6.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 7.png b/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 7.png new file mode 100644 index 000000000..d3a56d87e Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 7.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 8.png b/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 8.png new file mode 100644 index 000000000..6a38e26ea Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 8.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 9.png b/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 9.png new file mode 100644 index 000000000..c460776c3 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/OUT OF STOCK 9.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 1.png b/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 1.png new file mode 100644 index 000000000..a96f0572d Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 1.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 2.png b/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 2.png new file mode 100644 index 000000000..07326a1de Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 2.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 3.png b/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 3.png new file mode 100644 index 000000000..060eabf3c Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 3.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 4.png b/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 4.png new file mode 100644 index 000000000..09712a9f6 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 4.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 5.png b/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 5.png new file mode 100644 index 000000000..6aa9a8c3e Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 5.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 6.png b/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 6.png new file mode 100644 index 000000000..c03ee8594 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 6.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 7.png b/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 7.png new file mode 100644 index 000000000..72e9ec4c0 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 7.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 8.png b/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 8.png new file mode 100644 index 000000000..1ea72a0fe Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 8.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 9.png b/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 9.png new file mode 100644 index 000000000..f7e614d51 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/OVER STOCK 9.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/STOCK MOVE 1.png b/inventory_advanced_reports/static/description/assets/screenshots/STOCK MOVE 1.png new file mode 100644 index 000000000..0a87708c2 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/STOCK MOVE 1.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/STOCK MOVE 2.png b/inventory_advanced_reports/static/description/assets/screenshots/STOCK MOVE 2.png new file mode 100644 index 000000000..41bbf6916 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/STOCK MOVE 2.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/STOCK MOVE 3.png b/inventory_advanced_reports/static/description/assets/screenshots/STOCK MOVE 3.png new file mode 100644 index 000000000..a6377a508 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/STOCK MOVE 3.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/STOCK MOVE 4.png b/inventory_advanced_reports/static/description/assets/screenshots/STOCK MOVE 4.png new file mode 100644 index 000000000..e4cf8473e Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/STOCK MOVE 4.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/STOCK MOVE 5.png b/inventory_advanced_reports/static/description/assets/screenshots/STOCK MOVE 5.png new file mode 100644 index 000000000..43d642359 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/STOCK MOVE 5.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/XYZ 1.png b/inventory_advanced_reports/static/description/assets/screenshots/XYZ 1.png new file mode 100644 index 000000000..c0b822dcf Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/XYZ 1.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/XYZ 2.png b/inventory_advanced_reports/static/description/assets/screenshots/XYZ 2.png new file mode 100644 index 000000000..e9fff759e Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/XYZ 2.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/XYZ 3.png b/inventory_advanced_reports/static/description/assets/screenshots/XYZ 3.png new file mode 100644 index 000000000..eca5ab07c Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/XYZ 3.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/XYZ 4.png b/inventory_advanced_reports/static/description/assets/screenshots/XYZ 4.png new file mode 100644 index 000000000..c9b23a57f Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/XYZ 4.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/XYZ 5.png b/inventory_advanced_reports/static/description/assets/screenshots/XYZ 5.png new file mode 100644 index 000000000..5d47d221f Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/XYZ 5.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/XYZ 6.png b/inventory_advanced_reports/static/description/assets/screenshots/XYZ 6.png new file mode 100644 index 000000000..159fa3e4a Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/XYZ 6.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/XYZ 7.png b/inventory_advanced_reports/static/description/assets/screenshots/XYZ 7.png new file mode 100644 index 000000000..3fa78fc7d Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/XYZ 7.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/XYZ 8.png b/inventory_advanced_reports/static/description/assets/screenshots/XYZ 8.png new file mode 100644 index 000000000..43686f70f Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/XYZ 8.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/XYZ 9.png b/inventory_advanced_reports/static/description/assets/screenshots/XYZ 9.png new file mode 100644 index 000000000..759788eb7 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/XYZ 9.png differ diff --git a/inventory_advanced_reports/static/description/assets/screenshots/hero.gif b/inventory_advanced_reports/static/description/assets/screenshots/hero.gif new file mode 100644 index 000000000..0ddf2f587 Binary files /dev/null and b/inventory_advanced_reports/static/description/assets/screenshots/hero.gif differ diff --git a/inventory_advanced_reports/static/description/banner.jpg b/inventory_advanced_reports/static/description/banner.jpg new file mode 100644 index 000000000..37e4b05f6 Binary files /dev/null and b/inventory_advanced_reports/static/description/banner.jpg differ diff --git a/inventory_advanced_reports/static/description/icon.png b/inventory_advanced_reports/static/description/icon.png new file mode 100644 index 000000000..62b648fdb Binary files /dev/null and b/inventory_advanced_reports/static/description/icon.png differ diff --git a/inventory_advanced_reports/static/description/index.html b/inventory_advanced_reports/static/description/index.html new file mode 100644 index 000000000..37de296fe --- /dev/null +++ b/inventory_advanced_reports/static/description/index.html @@ -0,0 +1,1969 @@ + + + + + + + + Document + + + +
+ +
+ +
+
+ Community +
+
+ Enterprise +
+
+ Odoo.sh +
+
+
+ +
+
+
+ +

+ Inventory Advanced Reports

+

+ Inventory Reports to Help You to Manage Your Inventory + Properly.

+ + +
+
+
+
+ +
+
+ +
+

+ Explore This + Module

+
+ + + +
+
+ +
+

+ Overview +

+
+
+
+ This module allows you to generate inventory reports, + users can track Aging analysis, FSN classification + (inventory movement frequency classification), XYZ classification + (inventory classification based on stock value), FSN-XYZ combined + classification to define sales strategies for the existing inventories. + Overstock analysis, Out of Stock analysis and Stock movements (inventory + rotation). +
+
+ + +
+
+ +
+

+ Features +

+
+
+
+
+ + Inventory Aging Report. +
+
+ + Inventory Age Breakdown Report. +
+
+ + Inventory FSN Report. +
+
+ + Inventory XYZ Report. +
+
+ + Inventory FSN-XYZ Report. +
+
+ + Inventory Out Of Stock Report. +
+
+ + Inventory Over Stock Report. +
+
+ + Inventory Stock Movement Report. +
+
+
+
+
+ + +
+
+ +
+

+ Reports +

+
+
+
+

+ Inventory Advanced Reports +

+
+
+
+ + +
+ +
+
+
+
+
    +
  • + + +
    +

    Inventory Aging Report +

    +
    +

    +
    + This report categorizes inventory items based on their age, helping users identify aging stock that might need special attention. It categorizes products based on their time in stock, providing insights into how long items have been held. This report calculates the days since receipt, current stock levels, and their corresponding values, allowing businesses to identify slow-moving or obsolete items. By visually representing this data and offering export options, it helps companies make informed decisions about inventory management, such as restocking, discounting, or discontinuing products, ultimately optimizing their stock and reducing carrying costs. +

    + + +
    +

    Advantages of Inventory Aging Report +

    +

    +
    +
  • + arrow + Users can select specific products and categories they want to generate the report for. +
  • +
  • arrow The report can be filtered by the company. +
  • +
  • arrow The report categorizes products based on their age, allowing you to see how long items have been in stock. +
  • +
  • arrow The report calculates the age of each product from its date of receipt and shows the number of days since receipt. +
  • +
  • arrow Displays the current quantity available in stock for each product and current value of the product in stock, taking into account its standard price. +
  • +
  • arrow Percentage of Total Quantity and Percentage of Total Value helps help you understand the relative significance of a particular product in your overall stock. +
  • +
  • arrow Users can export the report to Excel or PDF formats for further analysis and sharing. +
  • +
  • arrow Graphical and tree views are also available. +
  • +

    +
    +
    +

    + Screenshots +

    +
    + +
    + +
+
+
+
+
+ +
+
+
+
+
    +
  • + + +
    +

    Inventory Age Breakdown Report +

    +
    +

    +
    +This report provides a detailed analysis of how long products have been sitting in your inventory. It categorizes products into different age groups based on the number of days they have been in stock, making it easy to identify slow-moving items and potential issues with inventory turnover. The report shows the quantity and value of products in each age category, helping businesses make informed decisions about inventory management and sales strategies. This report helps in optimizing stock levels and reducing holding costs while ensuring that products are available to meet customer demand. + + +

    +

    + Advantages of Inventory Age Breakdown Report +

    +
    +
    +
    +
  • arrow Users can customize the age periods for analysis. +
  • +
  • arrow The report displays both the quantity and value of products. +
  • +
  • arrow Businesses can use this report to make informed decisions about restocking, discounting, or discontinuing specific products. +
  • +
  • arrow Users can filter the report by company,product, category and age period. +
  • +
  • arrow The report is available in both PDF and Excel formats. +
  • +
  • arrow The report can be displayed in graphical and tree views. +
  • +

    +
    +
    +

    + Screenshots +

    +
    + +
    + + +
+
+
+
+
+ + +
+
+
+
+
    +
  • + + +
    +

    Inventory FSN Report +

    +
    +

    +
    + The FSN Report (Fast Moving, Slow Moving, Non-Moving Report) is used for analyzing and categorizing inventory items based on their movement and turnover rates within a specified time frame. + This report provides insights into the performance of products, helping businesses make informed decisions about stock management and optimization.The report provides valuable insights into product performance, + enabling companies to maximize profits and ensure efficient stock management.

    + F (Fast): Stock turnover ratio > 3.
    + S (Slow): Stock turnover ratio between 1 and 3.
    + N (Non-moving): Stock turnover ratio < 1.
    + + +

    +

    + Advantages of Inventory FSN Report +

    +
    +
    +
    +
  • arrow Products are classified into three main categories - Fast Moving, Slow Moving, and Non-Moving. +
  • +
  • arrow Fast-moving products are those with high turnover rates, while slow-moving products have moderate turnover, and non-moving products have low or no sales. +
  • +
  • arrow The report allows users to set a specific start and end date for analysis. +
  • +
  • arrow Users can filter the report by selecting specific products, product categories, companies and warehouses. +
  • +
  • arrow The report provides data on opening and closing stock, sales, average stock, turnover ratio, and the FSN classification for each product. +
  • +
  • arrow Users can choose between graphical and list views to visualize the data. +
  • +
  • arrow The report can be exported to PDF and Excel formats. +
  • +

    +
    +
    +

    + Screenshots +

    +
    + +
    + +
+
+
+
+
+ +
+
+
+
+
    +
  • + + +
    +

    Inventory XYZ Report +

    +
    +

    +
    + The XYZ Report used for inventory management that categorizes products based on their value and significance in a company's stock. This classification + helps businesses identify the products that are vital for their operations and profitability. The report groups products into three categories: X, Y, and Z. 'X' represents the most important and valuable products (high value), 'Y' includes products of moderate significance, and 'Z' consists of less critical items (low value). + This categorization is based on factors such as stock value, stock percentage, and cumulative stock percentage. The XYZ Report helps businesses focus on the items that affect their profits the most.

    + X Class: Top 70% of inventory value.
    + Y Class: The next 20% of inventory value.
    + Z Class: The bottom 10% of inventory value. +

    + + +
    +

    Advantages of Inventory XYZ Report +

    +
    +
  • +
  • arrow Classifies products into three categories - X, Y, and Z, based on their importance and value +
  • +
  • arrow Users can select specific products, product categories, companies, warehouses for analysis. +
  • +
  • arrow The report calculates and displays the cumulative stock percentages, helping businesses understand the collective impact of their inventory. +
  • +
  • arrow Users can generate and export reports in both PDF and Excel formats. +
  • +
  • arrow Provides graphical and tree representations. +
  • +
  • arrow The report provides valuable insights for making informed decisions about inventory priorities and management. +
  • +

    + + +
    +
    +

    + Screenshots +

    +
    + +
    +
+
+
+
+
+ + +
+
+
+
+
    +
  • + + +
    +

    Inventory FSN-XYZ Report +

    +
    +

    +
    + The FSN-XYZ Report helps to analyze and classify the inventory based on two key dimensions. + Firstly, it categorizes items into Fast-Moving (F), Slow-Moving (S), and Non-Moving (N) based + on their turnover ratios, helping companies identify which products are selling rapidly and which + are stagnating. Secondly, it classifies inventory into X, Y, and Z categories based on their contribution + to the overall stock value, enabling businesses to focus their resources on high-value items. This report + offers essential insights for optimizing inventory management and making strategic decisions to improve profitability. + +

    +

    Advantages of Inventory FSN-XYZ Report +

    +
    +
  • +
  • arrow The report classifies items into Fast-Moving (F), Slow-Moving (S), or Non-Moving (N) based on turnover ratios, enabling quick identification of sales performance.
  • +
  • arrow Inventory is labeled as X, Y, or Z to prioritize high-value items for management decisions. +
  • +
  • arrow Users can specify a start and end date for the report, allowing for time-bound analysis of inventory performance. +
  • +
  • arrow Users can filter the report based on specific products, product categories, companies, warehouses, f/s/n and x/y/z +
  • +
  • arrow The report can be exported in PDF and Excel formats. +
  • +
  • arrow Users have the option to view the report data in graphical and tree formats. +
  • +
  • arrow The report offers a combined classification that combines FSN and XYZ categories, simplifying decision-making based on both turnover and stock value. +
  • +
  • arrow It calculates the cumulative stock percentage, helping users understand the collective contribution of items to the stock value. +
  • +

    + + +
    +
    +

    + Screenshots +

    +
    + +
    +
+
+
+
+
+ + +
+
+
+
+
    +
  • + + +
    +

    Inventory Out Of Stock Report +

    +
    +

    +
    + The Out of Stock report provide insights into the availability of products within a specified time period. This report offers valuable information regarding current stock levels, incoming and outgoing quantities, virtual stock, sales data, and more. It calculates essential + performance metrics such as Advanced Stock Days (ADS), Demanded Quantity, In Stock Days, Out of Stock Days, and Turnover Ratio. Additionally, the report categorizes products into Fast-Moving, Slow-Moving, or Non-Moving classifications based on their sales performance. By identifying + items that are low or out of stock, this report helps businesses optimize their inventory management and ensure products are available to meet customer demand.

    + + +
    +

    Advantages of Inventory Out Of Stock Report +

    +
    +
  • +
  • arrow The report provides current stock levels, incoming and outgoing quantities, virtual stock, and sales data.
  • +
  • arrow It calculates the Advanced Stock Days, helping businesses understand how many days their current stock will last based on sales data. +
  • +
  • arrow The report calculates the quantity of a product demanded by customers, allowing for better stock planning. +
  • +
  • arrow It measures the number of days a product is expected to remain in stock, providing insights into stock sufficiency. +
  • +
  • arrowThe report also calculates the number of days a product is projected to be out of stock, helping in preventing stockouts. +
  • +
  • arrow The report computes the Turnover Ratio, which is the ratio of sales to virtual stock. It assists in identifying fast-moving and slow-moving products. +
  • +
  • arrow Users can filter the report based on criteria like date range, specific warehouses, product selection, product categories, and company. +
  • +
  • arrow The report includes cost information, allowing users to assess the financial impact of out-of-stock situations. +
  • +
  • arrow It offers the flexibility to export the report data in PDF or Excel formats. +
  • +
  • arrow The report often includes graphical and tree representations. +
  • +

    + + +
    +
    +

    + Screenshots +

    +
    + +
    +
+
+
+
+
+ + +
+
+
+
+
    +
  • + + +
    +

    Inventory Over Stock Report +

    +
    +

    + The Over Stock report helps to analyze and understand situations where a surplus + of products exists in their stock. This report provides detailed insights into + various inventory parameters, such as current stock levels, sales history, + advanced stock days, and turnover ratios. By identifying overstocked items, + businesses can take informed actions to mitigate financial losses, reduce storage costs, + and optimize their inventory strategies. Additionally, it offers a way to categorize products + as Fast-Moving, Slow-Moving, or Non-Moving based on their sales performance, aiding in better + decision-making for inventory control. The Over Stock Report ultimately assists organizations in + maintaining a healthy balance of stock, enhancing overall efficiency, and minimizing excess holding + of goods.

    + + +
    +

    Advantages of Inventory Over Stock Report +

    +
    +
  • +
  • arrow The report provides insights into advanced stock days and sales history, allowing users to forecast how many days the current inventory will last.
  • +
  • arrow Various inventory metrics are calculated and displayed in the report, including current stock, incoming and outgoing quantities, virtual stock, sales, ADS, and more. +
  • +
  • arrow The report identifies products with excessive stock levels and calculates overstock quantities and percentages. +
  • +
  • arrow It calculates the turnover ratio for each product, indicating how quickly items are sold. +
  • +
  • arrow Products are classified as "Fast Moving," "Slow Moving," or "Non Moving" based on their sales performance. +
  • +
  • arrow The report includes information about the last purchase order date, quantity, price, currency, and partner for each product. +
  • +
  • arrow Users can filter the report based on criteria like date range, specific warehouses, product selection, product categories, and company. +
  • +
  • arrow Users can generate the report in both PDF and Excel formats. +
  • +
  • arrow Users can switch between tree and graph views for more comprehensive data analysis. +

    + + +
    +
    +

    + Screenshots +

    +
    + +
    +
+
+
+
+
+ + +
+
+
+
+
    +
  • + + +
    +

    Inventory Stock Movement Report +

    +
    +

    + The Stock Movement Report provides insights into the flow of products. It includes information on the opening and closing stock quantities, sales, sales returns, purchases, + purchase returns, internal movements, adjustments, production-related movements, and transit-related movements for various products. The report offers a breakdown by product, product category, and company, + making it a valuable tool for tracking stock movements, optimizing inventory levels, and assessing the overall performance of the supply chain. The report can be generated in both PDF and Excel formats for + easy sharing and analysis, providing a comprehensive view of how inventory items have been managed within the specified date range or up to a certain date.

    + + +
    +

    Advantages of Inventory Stock Movement Report +

    +
    +
  • +
  • arrow The report breaks down stock movements into various categories, such as sales, sales returns, purchases, purchase returns, internal movements, adjustments, production-related movements, and transit-related movements.
  • +
  • arrow The report provides information on the opening stock (stock at the beginning of the selected period) and closing stock (stock at the end of the selected period or up to a certain date). +
  • +
  • arrow Users can choose to generate the report in PDF or Excel format. +
  • +
  • arrow The report includes detailed information about each product, its category, the company it belongs to, and the quantity of stock involved in various types of movements. +
  • +
  • arrow Users can generate the report for a specified date range or up to a certain date, depending on their reporting needs. +
  • +
  • arrow Users can filter the report based on criteria like date range, specific warehouses, product selection, product categories, and company. +
  • +

    + + +
    +
    +

    + Screenshots +

    +
    + +
    +
+
+
+
+
+
+
+
+
+ + +
+
+ +
+

+ Related + Products +

+
+
+
+ +
+
+ + +
+
+ +
+

+ Our Services +

+
+
+
+
+
+ +
+
+ Odoo + Customization
+
+
+
+ +
+
+ Odoo + Implementation
+
+
+
+ +
+
+ Odoo + Support
+
+
+
+ +
+
+ Hire + Odoo + Developer
+
+
+
+ +
+
+ Odoo + Integration
+
+
+
+ +
+
+ Odoo + Migration
+
+
+
+ +
+
+ Odoo + Consultancy
+
+
+
+ +
+
+ Odoo + Implementation
+
+
+
+ +
+
+ Odoo + Licensing Consultancy
+
+
+
+ + +
+
+ +
+

+ Our + Industries +

+
+
+
+
+
+ +
+ Trading +
+

+ Easily procure + and + sell your products

+
+
+
+
+ +
+ POS +
+

+ Easy + configuration + and convivial experience

+
+
+
+
+ +
+ Education +
+

+ A platform for + educational management

+
+
+
+
+ +
+ Manufacturing +
+

+ Plan, track and + schedule your operations

+
+
+
+
+ +
+ E-commerce & Website +
+

+ Mobile + friendly, + awe-inspiring product pages

+
+
+
+
+ +
+ Service Management +
+

+ Keep track of + services and invoice

+
+
+
+
+ +
+ Restaurant +
+

+ Run your bar or + restaurant methodically

+
+
+
+
+ +
+ Hotel Management +
+

+ An + all-inclusive + hotel management application

+
+
+
+
+ + +
+
+ +
+

+ Support +

+
+
+
+
+
+
+ +
+
+

Need Help?

+

Got questions or need help? + Get in touch.

+ +

+ odoo@cybrosys.com

+
+
+
+
+
+
+
+ +
+
+

WhatsApp

+

Say hi to us on WhatsApp!

+ +

+ +91 86068 + 27707

+
+
+
+
+
+
+
+ +
+
+
+ + + + + + + + + diff --git a/inventory_advanced_reports/static/src/js/action_manager.js b/inventory_advanced_reports/static/src/js/action_manager.js new file mode 100755 index 000000000..7984cbf57 --- /dev/null +++ b/inventory_advanced_reports/static/src/js/action_manager.js @@ -0,0 +1,16 @@ +/** @odoo-module */ +import { registry } from "@web/core/registry"; +import { BlockUI } from "@web/core/ui/block_ui"; +import { download } from "@web/core/network/download"; +// Action manager for printing xlsx report +registry.category("ir.actions.report handlers").add("xlsx", async (action) => { + if (action.report_type === 'xlsx') { + BlockUI; + await download({ + url: '/xlsx_reports', + data: action.data, + complete: () => unblockUI, + error: (error) => self.call('crash_manager', 'rpc_error', error), + }); + } +}); diff --git a/inventory_advanced_reports/wizard/__init__.py b/inventory_advanced_reports/wizard/__init__.py new file mode 100644 index 000000000..bd67c6653 --- /dev/null +++ b/inventory_advanced_reports/wizard/__init__.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +from . import inventory_age_breakdown_report +from . import inventory_aging_data_report +from . import inventory_aging_report +from . import inventory_fsn_data_report +from . import inventory_fsn_report +from . import inventory_fsn_xyz_data_report +from . import inventory_fsn_xyz_report +from . import inventory_out_of_stock_data_report +from . import inventory_out_of_stock_report +from . import inventory_over_stock_data_report +from . import inventory_over_stock_report +from . import inventory_stock_movement_report +from . import inventory_xyz_data_report +from . import inventory_xyz_report diff --git a/inventory_advanced_reports/wizard/inventory_age_breakdown_report.py b/inventory_advanced_reports/wizard/inventory_age_breakdown_report.py new file mode 100644 index 000000000..cd3c3693a --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_age_breakdown_report.py @@ -0,0 +1,276 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +import io +import json +from odoo import fields, models +from odoo.exceptions import ValidationError + +try: + from odoo.tools.misc import xlsxwriter +except ImportError: + import xlsxwriter + + +class InventoryAgeBreakdownReport(models.TransientModel): + """This model is for creating a wizard for inventory age breakdown report""" + _name = "inventory.age.breakdown.report" + _description = "Inventory Age Breakdown Report" + + product_ids = fields.Many2many( + "product.product", string="Products", + help="Select the products you want to generate the report for") + category_ids = fields.Many2many( + "product.category", string="Product Categories", + help="Select the product categories you want to generate the report for" + ) + company_ids = fields.Many2many( + 'res.company', string="Company", + help="Select the companies you want to generate the report for" + ) + age_breakdown_days = fields.Integer( + string="Age Breakdown Days", default=30, + help="Time interval in days used to categorize the age of records.") + + def get_report_data(self): + """Function to return necessary data for printing""" + params = [] + param_count = 0 + query = """ + SELECT + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END AS product_code_and_name, + c.complete_name AS category_name, + c.id AS category_id, + pp.id AS product_id, + company.id AS company_id, + company.name AS company_name, + COALESCE(SUM(svl.remaining_qty), 0) AS qty_available, + SUM(svl.remaining_value) AS stock_value, + SUM(CASE + WHEN age.days_between >= 1 AND age.days_between <= %s + THEN svl.remaining_qty + ELSE 0 + END) AS "age_breakdown_qty_1", + SUM(CASE + WHEN age.days_between >= %s+1 AND age.days_between <= %s*2 + THEN svl.remaining_qty + ELSE 0 + END) AS "age_breakdown_qty_2", + SUM(CASE + WHEN age.days_between >= (%s*2)+1 AND age.days_between <= %s*3 + THEN svl.remaining_qty + ELSE 0 + END) AS "age_breakdown_qty_3", + SUM(CASE + WHEN age.days_between >= (%s*3)+1 AND age.days_between <= %s*4 + THEN svl.remaining_qty + ELSE 0 + END) AS "age_breakdown_qty_4", + SUM(CASE + WHEN age.days_between >= (%s*4)+1 THEN svl.remaining_qty + ELSE 0 + END) AS "age_breakdown_qty_5", + SUM(CASE + WHEN age.days_between >= 1 AND age.days_between <= %s + THEN svl.remaining_value + ELSE 0 + END) AS "age_breakdown_value_1", + SUM(CASE + WHEN age.days_between >= %s+1 AND age.days_between <= %s*2 + THEN svl.remaining_value + ELSE 0 + END) AS "age_breakdown_value_2", + SUM(CASE + WHEN age.days_between >= (%s*2)+1 AND age.days_between <= %s*3 + THEN svl.remaining_value + ELSE 0 + END) AS "age_breakdown_value_3", + SUM(CASE + WHEN age.days_between >= (%s*3)+1 AND age.days_between <= %s*4 + THEN svl.remaining_value + ELSE 0 + END) AS "age_breakdown_value_4", + SUM(CASE + WHEN age.days_between >= (%s*4)+1 THEN svl.remaining_value + ELSE 0 + END) AS "age_breakdown_value_5" + FROM product_product pp + INNER JOIN product_template pt ON pp.product_tmpl_id = pt.id + INNER JOIN product_category c ON pt.categ_id = c.id + LEFT JOIN stock_move sm ON sm.product_id = pp.id + LEFT JOIN stock_picking_type spt ON sm.picking_type_id = spt.id + LEFT JOIN res_company company ON sm.company_id = company.id + LEFT JOIN LATERAL ( + SELECT EXTRACT(day FROM CURRENT_DATE - sm.date) AS days_between + ) AS age ON true + INNER JOIN stock_valuation_layer svl ON svl.stock_move_id = sm.id + WHERE pt.detailed_type = 'product' + AND sm.state = 'done' + AND svl.remaining_value IS NOT NULL + """ + params.extend([self.age_breakdown_days] * 16) + if self.product_ids or self.category_ids: + query += " AND (" + if self.product_ids: + product_ids = [product_id.id for product_id in self.product_ids] + query += "pp.id = ANY(%s)" + params.append(product_ids) + param_count += 1 + if self.product_ids and self.category_ids: + query += " OR " + if self.category_ids: + category_ids = [category.id for category in self.category_ids] + params.append(category_ids) + query += "(pt.categ_id = ANY(%s))" + param_count += 1 + if self.product_ids or self.category_ids: + query += ")" + if self.company_ids: + company_ids = [company.id for company in self.company_ids] + query += " AND (sm.company_id = ANY(%s))" + params.append(company_ids) + param_count += 1 + query += """ + GROUP BY + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END, + c.complete_name, + company.id, + c.id, + company.name, + pp.id; + """ + self.env.cr.execute(query, params) + result_data = self.env.cr.dictfetchall() + main_header = self.age_breakdown_days + if result_data: + data = { + 'result_data': result_data, + 'main_header': main_header + } + return data + else: + raise ValidationError("No records found for the given criteria!") + + def get_header(self, main_header): + """This function for getting the header in report""" + age_breakdown1 = main_header + age_breakdown2 = main_header * 2 + age_breakdown3 = main_header * 3 + age_breakdown4 = main_header * 4 + return ['1-' + str(age_breakdown1), + str(age_breakdown1 + 1) + '-' + str(age_breakdown2), + str(age_breakdown2 + 1) + '-' + str(age_breakdown3), + str(age_breakdown3 + 1) + '-' + str(age_breakdown4), + 'ABOVE ' + str(age_breakdown4)] + + def action_pdf(self): + """This function is for printing pdf report""" + data = { + 'model_id': self.id, + 'product_ids': self.product_ids.ids, + 'category_ids': self.category_ids.ids, + 'company_ids': self.company_ids.ids, + 'age_breakdown_days': self.age_breakdown_days, + } + return ( + self.env.ref( + 'inventory_advanced_reports.' + 'report_inventory_age_breakdown_action') + .report_action(None, data=data)) + + def action_excel(self): + """This function is for printing excel report""" + data = self.get_report_data() + return { + 'type': 'ir.actions.report', + 'data': {'model': 'inventory.age.breakdown.report', + 'options': json.dumps( + data, default=fields.date_utils.json_default), + 'output_format': 'xlsx', + 'report_name': 'Excel Report', + }, + 'report_type': 'xlsx', + } + + def get_xlsx_report(self, data, response): + """Excel sheet format for printing the data""" + datas = data['result_data'] + main_header = data['main_header'] + output = io.BytesIO() + workbook = xlsxwriter.Workbook(output, {'in_memory': True}) + sheet = workbook.add_worksheet() + sheet.set_margins(0.5, 0.5, 0.5, 0.5) + cell_format = workbook.add_format( + {'font_size': '12px', 'align': 'left'}) + header_style = workbook.add_format( + {'font_name': 'Times', 'bold': True, 'left': 1, 'bottom': 1, + 'right': 1, 'top': 1, 'align': 'center'}) + text_style = workbook.add_format( + {'font_name': 'Times', 'left': 1, 'bottom': 1, 'right': 1, 'top': 1, + 'align': 'left'}) + head = workbook.add_format( + {'align': 'center', 'bold': True, 'font_size': '20px'}) + sheet.merge_range('C2:I3', 'Inventory Age Breakdown Report', head) + + headers = ['Product', 'Category', 'Total Stock', 'Stock Value', 'Stock', + 'Value', 'Stock', 'Value', 'Stock', 'Value', 'Stock', + 'Value', 'Stock', 'Value'] + main_headers = self.get_header(main_header) + for col, header in enumerate(main_headers): + sheet.merge_range(7, col * 2 + 4, 7, col * 2 + 5, header, + header_style) + for col, header in enumerate(headers): + sheet.write(8, col, header, header_style) + sheet.set_column('A:B', 27, cell_format) + sheet.set_column('C:D', 13, cell_format) + row = 9 + number = 1 + for val in datas: + sheet.write(row, 0, val['product_code_and_name'], text_style) + sheet.write(row, 1, val['category_name'], text_style) + sheet.write(row, 2, val['qty_available'], text_style) + sheet.write(row, 3, val['stock_value'], text_style) + sheet.write(row, 4, val['age_breakdown_qty_1'], text_style) + sheet.write(row, 5, val['age_breakdown_value_1'], text_style) + sheet.write(row, 6, val['age_breakdown_qty_2'], text_style) + sheet.write(row, 7, val['age_breakdown_value_2'], text_style) + sheet.write(row, 8, val['age_breakdown_qty_3'], text_style) + sheet.write(row, 9, val['age_breakdown_value_3'], text_style) + sheet.write(row, 10, val['age_breakdown_qty_4'], text_style) + sheet.write(row, 11, val['age_breakdown_value_4'], text_style) + sheet.write(row, 12, val['age_breakdown_qty_5'], text_style) + sheet.write(row, 13, val['age_breakdown_value_5'], text_style) + row += 1 + number += 1 + workbook.close() + output.seek(0) + response.stream.write(output.read()) + output.close() diff --git a/inventory_advanced_reports/wizard/inventory_age_breakdown_report_views.xml b/inventory_advanced_reports/wizard/inventory_age_breakdown_report_views.xml new file mode 100644 index 000000000..b54ea107e --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_age_breakdown_report_views.xml @@ -0,0 +1,45 @@ + + + + + inventory.age.breakdown.report.view.form + inventory.age.breakdown.report + +
+ + + + + + + + + + + +
+
+
+
+
+
+ + + Inventory Age Breakdown Report + ir.actions.act_window + inventory.age.breakdown.report + form + + new + + + +
diff --git a/inventory_advanced_reports/wizard/inventory_aging_data_report.py b/inventory_advanced_reports/wizard/inventory_aging_data_report.py new file mode 100644 index 000000000..e20e7dc2f --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_aging_data_report.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +from odoo import fields, models + + +class InventoryAgingDataReport(models.TransientModel): + """This model is for creating a wizard for viewing the report data""" + _name = "inventory.aging.data.report" + _description = "Inventory Aging Data Report" + + product_id = fields.Many2one("product.product", + string="Product", + help="Name of Product") + category_id = fields.Many2one("product.category", + string="Category", help="Product Category") + company_id = fields.Many2one("res.company", string="Company", + help="Select the Company") + qty_available = fields.Float(string="Current Stock", + help="On hand quantity of product") + current_value = fields.Float(string="Current Value", + help="Current value of Stock") + stock_percentage = fields.Float(string="Stock Qty(%)", + help="Percentage of current stock") + stock_value_percentage = fields.Float(string="Stock Value(%)", + help="Value of current stock") + days_since_receipt = fields.Integer( + string="Oldest Stock Age", + help="Number of days since the receipt of the oldest stock item") + prev_qty_available = fields.Float( + string="Oldest Qty", + help="Quantity of the oldest stock available.") + prev_value = fields.Float( + string="Oldest Stock Value", + help="Total monetary value of oldest stock.") + data_id = fields.Many2one('inventory.aging.report', + string="Aging Data", + help="Reference of corresponding Aging data.") diff --git a/inventory_advanced_reports/wizard/inventory_aging_data_report_views.xml b/inventory_advanced_reports/wizard/inventory_aging_data_report_views.xml new file mode 100644 index 000000000..b7a32582a --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_aging_data_report_views.xml @@ -0,0 +1,34 @@ + + + + + inventory.aging.data.report.view.graph + inventory.aging.data.report + + + + + + + + + + + inventory.aging.data.report.view.tree + inventory.aging.data.report + + + + + + + + + + + + + + + + diff --git a/inventory_advanced_reports/wizard/inventory_aging_report.py b/inventory_advanced_reports/wizard/inventory_aging_report.py new file mode 100644 index 000000000..dab1bd466 --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_aging_report.py @@ -0,0 +1,300 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +import io +import json +from odoo import _, fields, models +from odoo.exceptions import ValidationError + +try: + from odoo.tools.misc import xlsxwriter +except ImportError: + import xlsxwriter + + +class InventoryAgingReport(models.TransientModel): + """This model is for creating a wizard for inventory aging report""" + _name = "inventory.aging.report" + _description = "Inventory Aging Report" + + product_ids = fields.Many2many( + "product.product", string="Products", + help="Select the products you want to generate the report for") + category_ids = fields.Many2many( + "product.category", string="Product Categories", + help="Select the product categories you want to generate the report for" + ) + company_ids = fields.Many2many( + 'res.company', string="Company", + help="Select the companies you want to generate the report for" + ) + + def get_report_data(self): + """Function for returning datas for printing""" + params = [] + param_count = 0 + query = """ + SELECT + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END AS product_code_and_name, + c.complete_name AS category_name, + c.id AS category_id, + pp.id AS product_id, + company.id AS company_id, + company.name AS company_name, + COALESCE(SUM(svl.remaining_qty), 0) AS qty_available, + (SELECT SUM(sm_inner.product_uom_qty) + FROM stock_move sm_inner + INNER JOIN res_company company_inner + ON sm_inner.company_id = company_inner.id + WHERE sm_inner.product_id = pp.id + AND sm_inner.state = 'done' + AND sm_inner.date < ( + SELECT MAX(sm_inner2.date) + FROM stock_move sm_inner2 + WHERE sm_inner2.product_id = pp.id + AND sm_inner2.state = 'done' + AND company_inner.id = sm_inner2.company_id + ) + ) AS prev_qty_available, + ( + SELECT MIN(sm_inner.date) + FROM stock_move sm_inner + WHERE sm_inner.product_id = pp.id + AND sm_inner.state = 'done' + AND (company.id IS NULL OR company.id = sm_inner.company_id) + ) AS receipt_date + FROM product_product pp + INNER JOIN product_template pt ON pp.product_tmpl_id = pt.id + INNER JOIN product_category c ON pt.categ_id = c.id + LEFT JOIN stock_move sm ON sm.product_id = pp.id + LEFT JOIN stock_picking_type spt ON sm.picking_type_id = spt.id + LEFT JOIN res_company company ON sm.company_id = company.id + INNER JOIN stock_valuation_layer svl ON svl.stock_move_id = sm.id + WHERE pt.detailed_type = 'product' + AND sm.state = 'done' + """ + if self.product_ids or self.category_ids: + query += " AND (" + if self.product_ids: + product_ids = [product_id.id for product_id in self.product_ids] + query += "pp.id = ANY(%s)" + params.append(product_ids) + param_count += 1 + if self.product_ids and self.category_ids: + query += " OR " + if self.category_ids: + category_ids = [category.id for category in self.category_ids] + params.append(category_ids) + query += "(pt.categ_id = ANY(%s))" + param_count += 1 + if self.product_ids or self.category_ids: + query += ")" + if self.company_ids: + company_ids = [company.id for company in self.company_ids] + query += " AND (sm.company_id = ANY(%s))" + params.append(company_ids) + param_count += 1 + query += """ + GROUP BY + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END, + c.complete_name, + company.id, + c.id, + company.name, + pp.id; + """ + self.env.cr.execute(query, params) + result_data = self.env.cr.dictfetchall() + today = fields.datetime.now().date() + for row in result_data: + receipt_date = row.get('receipt_date') + if receipt_date: + receipt_date = receipt_date.date() # Ensure it's a date object + row['days_since_receipt'] = (today - receipt_date).days + product = self.env['product.product'].browse(row.get('product_id')) + standard_price = product.standard_price + current_stock = row.get('qty_available') + prev_stock = row.get('prev_qty_available') + if prev_stock is None: + prev_stock = current_stock + row['prev_qty_available'] = current_stock + if standard_price and current_stock: + row['current_value'] = current_stock * standard_price + else: + row[ + 'current_value'] = 0 + row[ + 'prev_value'] = prev_stock * standard_price \ + if prev_stock is not None else 0 + total_current_stock = sum( + item.get('qty_available') for item in result_data if + item.get('qty_available') is not None) + if total_current_stock: + stock_percentage = (current_stock / total_current_stock) * 100 + else: + stock_percentage = 0.0 + row['stock_percentage'] = round(stock_percentage, 2) + current_value = row.get('current_value') + total_value = sum( + item.get('current_value', 0) for item in result_data) + if total_value: + stock_value_percentage = (current_value / total_value) * 100 + else: + stock_value_percentage = 0.0 + row['stock_value_percentage'] = round(stock_value_percentage, 2) + if result_data: + data = { + 'result_data': result_data, + } + return data + else: + raise ValidationError("No records found for the given criteria!") + + def action_pdf(self): + """Function for printing the pdf report""" + data = { + 'model_id': self.id, + 'product_ids': self.product_ids.ids, + 'category_ids': self.category_ids.ids, + 'company_ids': self.company_ids.ids, + + } + return ( + self.env.ref( + 'inventory_advanced_reports.report_inventory_aging_action') + .report_action(None, data=data)) + + def action_excel(self): + """This function is for printing excel report""" + data = self.get_report_data() + return { + 'type': 'ir.actions.report', + 'data': {'model': 'inventory.aging.report', + 'options': json.dumps( + data, default=fields.date_utils.json_default), + 'output_format': 'xlsx', + 'report_name': 'Excel Report', + }, + 'report_type': 'xlsx', + } + + def get_xlsx_report(self, data, response): + """Excel sheet format for printing the data""" + datas = data['result_data'] + output = io.BytesIO() + workbook = xlsxwriter.Workbook(output, {'in_memory': True}) + sheet = workbook.add_worksheet() + sheet.set_margins(0.5, 0.5, 0.5, 0.5) + cell_format = workbook.add_format( + {'font_size': '12px', 'align': 'left'}) + header_style = workbook.add_format( + {'font_name': 'Times', 'bold': True, 'left': 1, 'bottom': 1, + 'right': 1, 'top': 1, 'align': 'center'}) + text_style = workbook.add_format( + {'font_name': 'Times', 'left': 1, 'bottom': 1, 'right': 1, 'top': 1, + 'align': 'left'}) + head = workbook.add_format( + {'align': 'center', 'bold': True, 'font_size': '20px'}) + sheet.merge_range('C2:F3', 'Inventory Aging Report', head) + + headers = ['Product', 'Category', 'Current Stock', 'Current Value', + 'Stock Quant(%)', 'Stock Value(%)', 'Oldest Stock Age', + 'Oldest Stock', 'Oldest Stock Value'] + for col, header in enumerate(headers): + sheet.write(8, col, header, header_style) + sheet.set_column('A:B', 27, cell_format) + sheet.set_column('C:D', 13, cell_format) + sheet.set_column('E:F', 13, cell_format) + sheet.set_column('G:H', 13, cell_format) + sheet.set_column('I:J', 15, cell_format) + row = 9 + number = 1 + for val in datas: + sheet.write(row, 0, val['product_code_and_name'], text_style) + sheet.write(row, 1, val['category_name'], text_style) + sheet.write(row, 2, val['qty_available'], text_style) + sheet.write(row, 3, val['current_value'], text_style) + sheet.write(row, 4, val['stock_percentage'], text_style) + sheet.write(row, 5, val['stock_value_percentage'], text_style) + sheet.write(row, 6, val['days_since_receipt'], text_style) + sheet.write(row, 7, val['prev_qty_available'], text_style) + sheet.write(row, 8, val['prev_value'], text_style) + row += 1 + number += 1 + workbook.close() + output.seek(0) + response.stream.write(output.read()) + output.close() + + def display_report_views(self): + """Function for viewing tree and graph view""" + data = self.get_report_data() + for data_values in data.get('result_data'): + data_values['data_id'] = self.id + self.generate_data(data_values) + graph_view_id = self.env.ref( + 'inventory_advanced_reports.' + 'inventory_aging_data_report_view_graph').id + tree_view_id = self.env.ref( + 'inventory_advanced_reports.' + 'inventory_aging_data_report_view_tree').id + graph_report = self.env.context.get("graph_report", False) + report_views = [(tree_view_id, 'tree'), + (graph_view_id, 'graph')] + view_mode = "tree,graph" + if graph_report: + report_views = [(graph_view_id, 'graph'), + (tree_view_id, 'tree')] + view_mode = "graph,tree" + return { + 'name': _('Inventory Age Report'), + 'domain': [('data_id', '=', self.id)], + 'res_model': 'inventory.aging.data.report', + 'view_mode': view_mode, + 'type': 'ir.actions.act_window', + 'views': report_views + } + + def generate_data(self, data_values): + """Function for creating record in inventory aging data report model""" + return self.env['inventory.aging.data.report'].create({ + 'product_id': data_values.get('product_id'), + 'category_id': data_values.get('category_id'), + 'company_id': data_values.get('company_id'), + 'qty_available': data_values.get('qty_available'), + 'current_value': data_values.get('current_value'), + 'stock_percentage': data_values.get('stock_percentage'), + 'stock_value_percentage': data_values.get('stock_value_percentage'), + 'days_since_receipt': data_values.get('days_since_receipt'), + 'prev_qty_available': data_values.get('prev_qty_available'), + 'prev_value': data_values.get('prev_value'), + 'data_id': self.id, + }) diff --git a/inventory_advanced_reports/wizard/inventory_aging_report_views.xml b/inventory_advanced_reports/wizard/inventory_aging_report_views.xml new file mode 100644 index 000000000..251315d37 --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_aging_report_views.xml @@ -0,0 +1,47 @@ + + + + + inventory.aging.report.view.form + inventory.aging.report + +
+ + + + + + + + + + +
+
+
+
+
+
+ + + Inventory Aging Report + ir.actions.act_window + inventory.aging.report + form + + new + + + +
diff --git a/inventory_advanced_reports/wizard/inventory_fsn_data_report.py b/inventory_advanced_reports/wizard/inventory_fsn_data_report.py new file mode 100644 index 000000000..6cbb3b84e --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_fsn_data_report.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +from odoo import fields, models + + +class InventoryFSNDataReport(models.TransientModel): + """This model is for creating a wizard for viewing the report data""" + _name = "inventory.fsn.data.report" + _description = "Inventory FSN Data Report" + + product_id = fields.Many2one( + "product.product", string="Product", + help="Select the products you want to generate the report for") + category_id = fields.Many2one( + "product.category", string="Category", + help="Select the product categories you want to generate the report for") + company_id = fields.Many2one( + "res.company", string="Company", + help="Select the companies you want to generate the report for" ) + warehouse_id = fields.Many2one( + "stock.warehouse", + string="Warehouse", + help="Select the warehouse you want to generate the report for") + opening_stock = fields.Float( + string="Opening Stock", + help="Quantity of stock available at the beginning.") + closing_stock = fields.Float( + string="Closing Value", + help="Quantity of stock at the the time of closing.") + average_stock = fields.Float(string="Average Stock", + help="The average stock inventory.") + sales = fields.Float(string="Sales", + help="Total quantity or value of stock sold.") + turnover_ratio = fields.Float(string="Turnover Ratio", + help="The frequency at which stock is sold" + " and replaced during a specific period") + fsn_classification = fields.Char( + string="FSN Classification", + help="FSN classification of the stock, which categorizes items based on" + " their consumption or movement rate.") + data_id = fields.Many2one('inventory.fsn.report', + string="FSN Data", help="corresponding FSN data") diff --git a/inventory_advanced_reports/wizard/inventory_fsn_data_report_views.xml b/inventory_advanced_reports/wizard/inventory_fsn_data_report_views.xml new file mode 100644 index 000000000..cbce23c1d --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_fsn_data_report_views.xml @@ -0,0 +1,33 @@ + + + + + inventory.fsn.data.report.view.graph + inventory.fsn.data.report + + + + + + + + + + inventory.fsn.data.report.view.tree + inventory.fsn.data.report + + + + + + + + + + + + + + + + diff --git a/inventory_advanced_reports/wizard/inventory_fsn_report.py b/inventory_advanced_reports/wizard/inventory_fsn_report.py new file mode 100644 index 000000000..a373c8fbf --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_fsn_report.py @@ -0,0 +1,363 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +import io +import json +from odoo import _, fields, models +from odoo.exceptions import ValidationError + +try: + from odoo.tools.misc import xlsxwriter +except ImportError: + import xlsxwriter + + +class InventoryFsnReport(models.TransientModel): + """This model is for creating a wizard for inventory turnover report.""" + _name = 'inventory.fsn.report' + _description = 'Inventory FSN Report' + + start_date = fields.Date('Start Date', required=True, + help="Start date to analyse the report") + end_date = fields.Date('End Date', required=True, + help="End date to analyse the report") + warehouse_ids = fields.Many2many( + "stock.warehouse", string="Warehouses", + help="Select the warehouses to generate the report") + product_ids = fields.Many2many( + "product.product", string="Products", + help="Select the products you want to generate the report for") + category_ids = fields.Many2many( + "product.category", string="Product categories", + help="Select the product categories you want to generate the report for" + ) + company_ids = fields.Many2many( + "res.company", string="Company", + default=lambda self: self.env.company, + help="Select the companies you want to generate the report for") + fsn = fields.Selection([ + ('fast_moving', 'Fast Moving'), + ('slow_moving', 'Slow Moving'), + ('non_moving', 'Non Moving'), + ('all', 'All') + ], string='FSN Category', default="all", required=True, + help="Select the FSN Category for which to generate the report for") + + def get_report_data(self): + """Function for returning data for printing""" + fsn = dict(self._fields['fsn'].selection).get(self.fsn) + if self.start_date > self.end_date: + raise ValidationError( + "Start date cant be greater than end date") + start_date = self.start_date + end_date = self.end_date + filtered_product_stock = [] + query = """ + SELECT + product_id, + product_code_and_name, + category_id, + category_name, + company_id, + warehouse_id, + opening_stock, + closing_stock, + sales, + average_stock, + CASE + WHEN sales > 0 + THEN ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END AS turnover_ratio, + CASE + WHEN + CASE + WHEN sales > 0 + THEN ROUND((sales / NULLIF(average_stock, 0)), + 2) + ELSE 0 + END > 3 THEN 'Fast Moving' + WHEN + CASE + WHEN sales > 0 + THEN ROUND((sales / NULLIF(average_stock, 0)), + 2) + ELSE 0 + END >= 1 AND + CASE + WHEN sales > 0 + THEN ROUND((sales / NULLIF(average_stock, 0)), + 2) + ELSE 0 + END <= 3 THEN 'Slow Moving' + ELSE 'Non Moving' + END AS fsn_classification + FROM + (SELECT + pp.id AS product_id, + pt.categ_id AS category_id, + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', + pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END AS product_code_and_name, + pc.complete_name AS category_name, + company.id AS company_id, + sw.id AS warehouse_id, + (SUM(CASE WHEN sm.date <= %s + AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s + AND sld_src.usage = 'internal' THEN sm.product_uom_qty + ELSE 0 END)) AS opening_stock, + (SUM(CASE WHEN sm.date <= %s + AND sld_dest.usage = 'internal' THEN sm.product_uom_qty + ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s + AND sld_src.usage = 'internal' THEN sm.product_uom_qty + ELSE 0 END)) AS closing_stock, + SUM(CASE WHEN sm.date BETWEEN %s + AND %s AND sld_dest.usage = 'customer' + THEN sm.product_uom_qty ELSE 0 END) AS sales, + ((SUM(CASE WHEN sm.date <= %s + AND sld_dest.usage = 'internal' THEN sm.product_uom_qty + ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s + AND sld_src.usage = 'internal' THEN sm.product_uom_qty + ELSE 0 END))+ + (SUM(CASE WHEN sm.date <= %s + AND sld_dest.usage = 'internal' THEN sm.product_uom_qty + ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s + AND sld_src.usage = 'internal' THEN sm.product_uom_qty + ELSE 0 END)))/2 AS average_stock + FROM + stock_move sm + JOIN + product_product pp ON sm.product_id = pp.id + JOIN + product_template pt ON pp.product_tmpl_id = pt.id + JOIN + product_category pc ON pt.categ_id = pc.id + JOIN + res_company company ON company.id = sm.company_id + JOIN + stock_warehouse sw ON sw.company_id = company.id + LEFT JOIN + stock_location sld_dest + ON sm.location_dest_id = sld_dest.id + LEFT JOIN + stock_location sld_src ON sm.location_id = sld_src.id + WHERE + sm.state = 'done' + """ + params = [ + start_date, start_date, end_date, end_date, start_date, end_date, + start_date, start_date, end_date, end_date + ] + sub_queries = [] + sub_params = [] + param_count = 0 + if self.product_ids or self.category_ids: + query += " AND (" + if self.product_ids: + product_ids = [product_id.id for product_id in self.product_ids] + query += "pp.id = ANY(%s)" + params.append(product_ids) + param_count += 1 + if self.product_ids and self.category_ids: + query += " OR " + if self.category_ids: + category_ids = [category_id.id for category_id in self.category_ids] + query += "pt.categ_id IN %s" + params.append(tuple(category_ids)) + param_count += 1 + if self.product_ids or self.category_ids: + query += ")" + if self.company_ids: + query += f" AND company.id IN %s" + sub_params.append(tuple(self.company_ids.ids)) + param_count += 1 + if self.warehouse_ids: + query += f" AND sw.id IN %s" + sub_params.append(tuple(self.warehouse_ids.ids)) + param_count += 1 + if sub_queries: + query += " AND " + " AND ".join(sub_queries) + query += """ + GROUP BY pp.id, pt.categ_id,CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END, pc.complete_name, company.id, sw.id + ) AS subquery + """ + self.env.cr.execute(query, tuple(params + sub_params)) + result_data = self.env.cr.dictfetchall() + for fsn_data in result_data: + if fsn_data.get('fsn_classification') == str(fsn): + filtered_product_stock.append(fsn_data) + if fsn == 'All' and not result_data: + raise ValidationError("No corresponding data to print") + elif fsn != 'All' and filtered_product_stock == []: + raise ValidationError("No corresponding data to print") + data = { + 'data': result_data if fsn == 'All' else filtered_product_stock, + 'start_date': start_date, + 'end_date': end_date + } + return data + + def action_pdf(self): + """Function for printing the pdf""" + data = { + 'model_id': self.id, + 'product_ids': self.product_ids.ids, + 'category_ids': self.category_ids.ids, + 'company_ids': self.company_ids.ids, + 'warehouse_ids': self.warehouse_ids.ids, + 'start_date': self.start_date, + "end_date": self.end_date, + "fsn": dict(self._fields['fsn'].selection).get(self.fsn) + } + return ( + self.env.ref( + 'inventory_advanced_reports.report_inventory_fsn_action') + .report_action(None, data=data)) + + def action_excel(self): + """This function is for printing excel report""" + data = self.get_report_data() + return { + 'type': 'ir.actions.report', + 'data': {'model': 'inventory.fsn.report', + 'options': json.dumps( + data, default=fields.date_utils.json_default), + 'output_format': 'xlsx', + 'report_name': 'Excel Report', + }, + 'report_type': 'xlsx', + } + + def get_xlsx_report(self, data, response): + """Excel sheet format for printing the data""" + datas = data['data'] + start_date = data['start_date'] + end_date = data['end_date'] + output = io.BytesIO() + workbook = xlsxwriter.Workbook(output, {'in_memory': True}) + sheet = workbook.add_worksheet() + sheet.set_margins(0.5, 0.5, 0.5, 0.5) + cell_format = workbook.add_format( + {'font_size': '12px', 'align': 'left'}) + header_style = workbook.add_format( + {'font_name': 'Times', 'bold': True, 'left': 1, 'bottom': 1, + 'right': 1, 'top': 1, 'align': 'center'}) + text_style = workbook.add_format( + {'font_name': 'Times', 'left': 1, 'bottom': 1, 'right': 1, 'top': 1, + 'align': 'left'}) + head = workbook.add_format( + {'align': 'center', 'bold': True, 'font_size': '20px'}) + sheet.merge_range('B2:F3', 'Inventory FSN Report', head) + bold_format = workbook.add_format( + {'bold': True, 'font_size': '10px', 'align': 'left'}) + txt = workbook.add_format({'font_size': '10px', 'align': 'left'}) + if start_date and end_date: + sheet.write('A5', 'Start Date: ', bold_format) + sheet.write('B5', start_date, txt) + sheet.write('A6', 'End Date: ', bold_format) + sheet.write('B6', end_date, txt) + headers = ['Product', 'Category', 'Opening Stock', 'Closing Value', + 'Average Stock', 'Sales', 'Turnover Ratio', + 'FSN Classification'] + for col, header in enumerate(headers): + sheet.write(8, col, header, header_style) + sheet.set_column('A:A', 27, cell_format) + sheet.set_column('B:B', 24, cell_format) + sheet.set_column('C:D', 13, cell_format) + sheet.set_column('E:F', 13, cell_format) + sheet.set_column('G:H', 13, cell_format) + row = 9 + number = 1 + for val in datas: + sheet.write(row, 0, val['product_code_and_name'], text_style) + sheet.write(row, 1, val['category_name'], text_style) + sheet.write(row, 2, val['opening_stock'], text_style) + sheet.write(row, 3, val['closing_stock'], text_style) + sheet.write(row, 4, val['average_stock'], text_style) + sheet.write(row, 5, val['sales'], text_style) + sheet.write(row, 6, val['turnover_ratio'], text_style) + sheet.write(row, 7, val['fsn_classification'], text_style) + row += 1 + number += 1 + workbook.close() + output.seek(0) + response.stream.write(output.read()) + output.close() + + def display_report_views(self): + """Function for displaying graph and tree view of data""" + data = self.get_report_data() + for data_values in data.get('data'): + data_values['data_id'] = self.id + self.generate_data(data_values) + graph_view_id = self.env.ref( + 'inventory_advanced_reports.' + 'inventory_fsn_data_report_view_graph').id + tree_view_id = self.env.ref( + 'inventory_advanced_reports.' + 'inventory_fsn_data_report_view_tree').id + graph_report = self.env.context.get("graph_report", False) + report_views = [(tree_view_id, 'tree'), + (graph_view_id, 'graph')] + view_mode = "tree,graph" + if graph_report: + report_views = [(graph_view_id, 'graph'), + (tree_view_id, 'tree')] + view_mode = "graph,tree" + return { + 'name': _('Inventory FSN Report'), + 'domain': [('data_id', '=', self.id)], + 'res_model': 'inventory.fsn.data.report', + 'view_mode': view_mode, + 'type': 'ir.actions.act_window', + 'views': report_views + } + + def generate_data(self, data_values): + """Function for creating data in model inventory fsn data report + model""" + return self.env['inventory.fsn.data.report'].create({ + 'product_id': data_values.get('product_id'), + 'category_id': data_values.get('category_id'), + 'company_id': data_values.get('company_id'), + 'warehouse_id': data_values.get('warehouse_id'), + 'opening_stock': data_values.get('opening_stock'), + 'closing_stock': data_values.get('closing_stock'), + 'average_stock': data_values.get('average_stock'), + 'sales': data_values.get('sales'), + 'turnover_ratio': data_values.get('turnover_ratio'), + 'fsn_classification': data_values.get('fsn_classification'), + 'data_id': self.id, + }) diff --git a/inventory_advanced_reports/wizard/inventory_fsn_report_views.xml b/inventory_advanced_reports/wizard/inventory_fsn_report_views.xml new file mode 100644 index 000000000..134fe38bd --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_fsn_report_views.xml @@ -0,0 +1,71 @@ + + + + + inventory.fsn.report.view.form + inventory.fsn.report + +
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ + + Inventory FSN Report + ir.actions.act_window + inventory.fsn.report + form + + new + + + +
diff --git a/inventory_advanced_reports/wizard/inventory_fsn_xyz_data_report.py b/inventory_advanced_reports/wizard/inventory_fsn_xyz_data_report.py new file mode 100644 index 000000000..16f64bdde --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_fsn_xyz_data_report.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +from odoo import fields, models + + +class InventoryFsnXyzDataReport(models.TransientModel): + """This model is for creating a wizard for viewing the report data""" + _name = "inventory.fsn.xyz.data.report" + _description = "Inventory FSN-XYZ Data Report" + + product_id = fields.Many2one("product.product", + string="Product", help="Select the Product") + category_id = fields.Many2one("product.category", + string="Category", + help="Select the Product Category") + company_id = fields.Many2one("res.company", + string="Company", + help="Select the Company") + average_stock = fields.Float(string="Average Stock", + help="Average quantity of Stock") + sales = fields.Float(string="Sales", + help="Total quantity or value of stock sold") + turnover_ratio = fields.Float( + string="Turnover Ratio", + help="The frequency at which stock is sold and replaced during a" + " specific period.") + current_stock = fields.Float(string="Current Stock", + help="Quantity of Stock available currently") + stock_value = fields.Float(string="Stock Value", + help="Value of Stock available currently") + fsn_classification = fields.Char( + string="FSN Classification", + help="FSN classification of the stock, which categorizes items based on" + " their consumption or movement rate.") + xyz_classification = fields.Char( + string="XYZ Classification", + help="Categorizing inventory items based on their variability in" + " consumption.") + combined_classification = fields.Char( + string="FSN-XYZ Classification", + help="The classification merging FSN and XYZ categorizations." ) + data_id = fields.Many2one('inventory.fsn.xyz.report', + string="FSN-XYZ Data", + help="corresponding FSN-XYZ data") diff --git a/inventory_advanced_reports/wizard/inventory_fsn_xyz_data_report_views.xml b/inventory_advanced_reports/wizard/inventory_fsn_xyz_data_report_views.xml new file mode 100644 index 000000000..37e8740dd --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_fsn_xyz_data_report_views.xml @@ -0,0 +1,34 @@ + + + + + inventory.fsn.xyz.data.report.view.graph + inventory.fsn.xyz.data.report + + + + + + + + + + inventory.fsn.xyz.data.report.view.tree + inventory.fsn.xyz.data.report + + + + + + + + + + + + + + + + + diff --git a/inventory_advanced_reports/wizard/inventory_fsn_xyz_report.py b/inventory_advanced_reports/wizard/inventory_fsn_xyz_report.py new file mode 100644 index 000000000..bafee6e58 --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_fsn_xyz_report.py @@ -0,0 +1,431 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +import io +import json +from odoo import _, fields, models +from odoo.exceptions import ValidationError + +try: + from odoo.tools.misc import xlsxwriter +except ImportError: + import xlsxwriter + + +class InventoryFsnXyzReport(models.TransientModel): + """This model is for creating a wizard for inventory turnover report.""" + _name = 'inventory.fsn.xyz.report' + _description = 'Inventory FSN-XYZ Report' + + start_date = fields.Date('Start Date', + help="Start date to analyse the report", + required=True) + end_date = fields.Date('End Date', + help="End date to analyse the report", + required=True) + warehouse_ids = fields.Many2many( + "stock.warehouse", string="Warehouses", + help="Select the warehouses to generate the report") + product_ids = fields.Many2many( + "product.product", string="Products", + help="Select the products you want to generate the report for") + category_ids = fields.Many2many( + "product.category", string="Product categories", + help="Select the product categories you want to generate the report for" + ) + company_ids = fields.Many2many( + "res.company", string="Company", + default=lambda self: self.env.company, + help="Select the companies you want to generate the report for") + fsn = fields.Selection([ + ('fast_moving', 'Fast Moving'), + ('slow_moving', 'Slow Moving'), + ('non_moving', 'Non Moving'), + ('all', 'All') + ], string='FSN Category', default="all", required=True, + help="Select the FSN Category for which to generate the report for.") + xyz = fields.Selection( + [('x', 'X'), ('y', 'Y'), ('z', 'Z'), ('all', 'All')], + string="XYZ Classification", default='all', required=True, + help="Select the XYZ Category for which to generate the report for.") + def get_report_data(self): + """Function for returning datas for printing""" + fsn = dict(self._fields['fsn'].selection).get(self.fsn) + xyz = dict(self._fields['xyz'].selection).get(self.xyz) + start_date = self.start_date + end_date = self.end_date + filtered_product_stock = [] + query = """ + SELECT + product_id, + product_code_and_name, + category_id, + category_name, + company_id, + warehouse_id, + opening_stock, + closing_stock, + sales, + average_stock, + current_stock, + stock_value, + CASE + WHEN sales > 0 THEN + ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END AS turnover_ratio, + CASE + WHEN + CASE + WHEN sales > 0 THEN + ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END > 3 THEN 'Fast Moving' + WHEN + CASE + WHEN sales > 0 THEN + ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END >= 1 AND + CASE + WHEN sales > 0 THEN + ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END <= 3 THEN 'Slow Moving' + ELSE 'Non Moving' + END AS fsn_classification, + stock_percentage, + SUM(stock_percentage) + OVER (ORDER BY stock_value DESC) + AS cumulative_stock_percentage, + CASE + WHEN SUM(stock_percentage) OVER (ORDER BY stock_value DESC) < 70 + THEN 'X' + WHEN SUM(stock_percentage) + OVER (ORDER BY stock_value DESC) >= 70 AND + SUM(stock_percentage) OVER (ORDER BY stock_value DESC) <= 90 + THEN 'Y' + ELSE 'Z' + END AS xyz_classification, + CONCAT( + CASE + WHEN + CASE + WHEN sales > 0 THEN + ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END > 3 THEN 'F' + WHEN + CASE + WHEN sales > 0 THEN + ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END >= 1 AND + CASE + WHEN sales > 0 THEN + ROUND((sales / NULLIF(average_stock, 0)), 2) + ELSE 0 + END <= 3 THEN 'S' + ELSE 'N' + END, + CASE + WHEN SUM(stock_percentage) + OVER (ORDER BY stock_value DESC) < 70 THEN 'X' + WHEN SUM(stock_percentage) + OVER (ORDER BY stock_value DESC) >= 70 + AND SUM(stock_percentage) + OVER (ORDER BY stock_value DESC) <= 90 THEN 'Y' + ELSE 'Z' + END + ) AS combined_classification + FROM + (SELECT + pp.id AS product_id, + pt.categ_id AS category_id, + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', + pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END AS product_code_and_name, + pc.complete_name AS category_name, + company.id AS company_id, + sw.id AS warehouse_id, + SUM(svl.remaining_qty) AS current_stock, + SUM(svl.remaining_value) AS stock_value, + COALESCE(ROUND((SUM(svl.remaining_value) / + NULLIF(SUM(SUM(svl.remaining_value)) + OVER (), 0)) * 100, 2),0) AS stock_percentage, + (SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END)) AS opening_stock, + (SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END)) AS closing_stock, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_dest.usage = 'customer' + THEN sm.product_uom_qty ELSE 0 END) AS sales, + ((SUM(CASE WHEN sm.date <= %s + AND sld_dest.usage = 'internal' THEN sm.product_uom_qty + ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END))+ + (SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END)))/2 AS average_stock + FROM + stock_move sm + JOIN + product_product pp ON sm.product_id = pp.id + JOIN + product_template pt ON pp.product_tmpl_id = pt.id + JOIN + product_category pc ON pt.categ_id = pc.id + JOIN + res_company company ON company.id = sm.company_id + JOIN + stock_warehouse sw ON sw.company_id = company.id + JOIN + stock_valuation_layer svl ON svl.stock_move_id = sm.id + LEFT JOIN + stock_location sld_dest ON sm.location_dest_id = sld_dest.id + LEFT JOIN + stock_location sld_src ON sm.location_id = sld_src.id + WHERE + sm.state = 'done' + AND pp.active = TRUE + AND pt.active = TRUE + AND pt.type = 'product' + AND svl.remaining_value IS NOT NULL + """ + params = [ + start_date, start_date, end_date, end_date, start_date, end_date, + start_date, start_date, end_date, end_date + ] + sub_queries = [] + sub_params = [] + param_count = 0 + if self.product_ids or self.category_ids: + query += " AND (" + if self.product_ids: + product_ids = [product_id.id for product_id in self.product_ids] + query += "pp.id = ANY(%s)" + params.append(product_ids) + param_count += 1 + if self.product_ids and self.category_ids: + query += " OR " + if self.category_ids: + category_ids = [category_id.id for category_id in self.category_ids] + query += "pt.categ_id IN %s" + params.append(tuple(category_ids)) + param_count += 1 + if self.product_ids or self.category_ids: + query += ")" + if self.company_ids: + query += f" AND sm.company_id IN %s" # Specify the table alias + sub_params.append(tuple(self.company_ids.ids)) + param_count += 1 + if self.warehouse_ids: + query += f" AND sw.id IN %s" # Specify the table alias + sub_params.append(tuple(self.warehouse_ids.ids)) + param_count += 1 + if sub_queries: + query += " AND " + " AND ".join(sub_queries) + query += """ + GROUP BY + pp.id,pt.name, pt.categ_id,pc.complete_name, company.id, sw.id + ) AS subquery + ORDER BY stock_value DESC + """ + self.env.cr.execute(query, tuple(params + sub_params)) + result_data = self.env.cr.dictfetchall() + for fsn_data in result_data: + if ( + (fsn == 'All' and xyz == 'All') or + (fsn == 'All' and fsn_data.get('xyz_classification') == str( + xyz)) or + (xyz == 'All' and fsn_data.get('fsn_classification') == str( + fsn)) or + (fsn_data.get('fsn_classification') == str( + fsn) and fsn_data.get('xyz_classification') == str(xyz)) + ): + filtered_product_stock.append(fsn_data) + if (fsn == 'All' or xyz == 'All') and not result_data: + raise ValidationError("No corresponding data to print") + elif not filtered_product_stock: + raise ValidationError("No corresponding data to print") + data = { + 'data': filtered_product_stock, + 'start_date': start_date, + 'end_date': end_date + } + return data + + def action_pdf(self): + """Function for printing pdf report""" + data = { + 'model_id': self.id, + 'product_ids': self.product_ids.ids, + 'category_ids': self.category_ids.ids, + 'company_ids': self.company_ids.ids, + 'warehouse_ids': self.warehouse_ids.ids, + 'start_date': self.start_date, + "end_date": self.end_date, + "fsn": dict(self._fields['fsn'].selection).get(self.fsn), + "xyz": dict(self._fields['xyz'].selection).get(self.xyz) + } + return ( + self.env.ref( + 'inventory_advanced_reports.report_inventory_fsn_xyz_action') + .report_action(None, data=data)) + + def action_excel(self): + """This function is for printing excel report""" + data = self.get_report_data() + return { + 'type': 'ir.actions.report', + 'data': {'model': 'inventory.fsn.xyz.report', + 'options': json.dumps( + data, default=fields.date_utils.json_default), + 'output_format': 'xlsx', + 'report_name': 'Excel Report', + }, + 'report_type': 'xlsx', + } + + def get_xlsx_report(self, data, response): + """Excel sheet format for printing the data""" + datas = data['data'] + start_date = data['start_date'] + end_date = data['end_date'] + output = io.BytesIO() + workbook = xlsxwriter.Workbook(output, {'in_memory': True}) + sheet = workbook.add_worksheet() + sheet.set_margins(0.5, 0.5, 0.5, 0.5) + cell_format = workbook.add_format( + {'font_size': '12px', 'align': 'left'}) + header_style = workbook.add_format( + {'font_name': 'Times', 'bold': True, 'left': 1, 'bottom': 1, + 'right': 1, 'top': 1, 'align': 'center'}) + text_style = workbook.add_format( + {'font_name': 'Times', 'left': 1, 'bottom': 1, 'right': 1, 'top': 1, + 'align': 'left'}) + head = workbook.add_format( + {'align': 'center', 'bold': True, 'font_size': '20px'}) + sheet.merge_range('E2:I3', 'Inventory FSN-XYZ Report', head) + bold_format = workbook.add_format( + {'bold': True, 'font_size': '10px', 'align': 'left'}) + txt = workbook.add_format({'font_size': '10px', 'align': 'left'}) + if start_date and end_date: + sheet.write('A5', 'Start Date: ', bold_format) + sheet.write('B5', start_date, txt) + sheet.write('A6', 'End Date: ', bold_format) + sheet.write('B6', end_date, txt) + headers = ['Product', 'Category', 'Opening Stock', 'Closing Stock', + 'Average Stock', 'Sales', 'Turnover Ratio', 'Current Stock', + 'Stock Value', 'Stock Value(%)', 'Cumulative Value(%)', + 'FSN Classification', 'XYZ Classification', + 'FSN-XYZ Classification'] + for col, header in enumerate(headers): + sheet.write(8, col, header, header_style) + sheet.set_column('A:A', 27, cell_format) + sheet.set_column('B:B', 24, cell_format) + sheet.set_column('C:D', 13, cell_format) + sheet.set_column('E:F', 13, cell_format) + sheet.set_column('G:H', 15, cell_format) + sheet.set_column('I:J', 15, cell_format) + sheet.set_column('K:L', 17, cell_format) + sheet.set_column('M:N', 17, cell_format) + row = 9 + number = 1 + for val in datas: + sheet.write(row, 0, val['product_code_and_name'], text_style) + sheet.write(row, 1, val['category_name'], text_style) + sheet.write(row, 2, val['opening_stock'], text_style) + sheet.write(row, 3, val['closing_stock'], text_style) + sheet.write(row, 4, val['average_stock'], text_style) + sheet.write(row, 5, val['sales'], text_style) + sheet.write(row, 6, val['turnover_ratio'], text_style) + sheet.write(row, 7, val['current_stock'], text_style) + sheet.write(row, 8, val['stock_value'], text_style) + sheet.write(row, 9, val['stock_percentage'], text_style) + sheet.write(row, 10, val['cumulative_stock_percentage'], text_style) + sheet.write(row, 11, val['fsn_classification'], text_style) + sheet.write(row, 12, val['xyz_classification'], text_style) + sheet.write(row, 13, val['combined_classification'], text_style) + row += 1 + number += 1 + workbook.close() + output.seek(0) + response.stream.write(output.read()) + output.close() + + def display_report_views(self): + """Function for displaying graph and tree view of the data""" + data = self.get_report_data() + for data_values in data.get('data'): + data_values['data_id'] = self.id + self.generate_data(data_values) + graph_view_id = self.env.ref( + 'inventory_advanced_reports.' + 'inventory_fsn_xyz_data_report_view_graph').id + tree_view_id = self.env.ref( + 'inventory_advanced_reports.' + 'inventory_fsn_xyz_data_report_view_tree').id + graph_report = self.env.context.get("graph_report", False) + report_views = [(tree_view_id, 'tree'), + (graph_view_id, 'graph')] + view_mode = "tree,graph" + if graph_report: + report_views = [(graph_view_id, 'graph'), + (tree_view_id, 'tree')] + view_mode = "graph,tree" + return { + 'name': _('Inventory FSN-XYZ Report'), + 'domain': [('data_id', '=', self.id)], + 'res_model': 'inventory.fsn.xyz.data.report', + 'view_mode': view_mode, + 'type': 'ir.actions.act_window', + 'views': report_views + } + + def generate_data(self, data_values): + """Function for creating a record in model inventory fsn xyz data + 'report""" + return self.env['inventory.fsn.xyz.data.report'].create({ + 'product_id': data_values.get('product_id'), + 'category_id': data_values.get('category_id'), + 'company_id': data_values.get('company_id'), + 'average_stock': data_values.get('average_stock'), + 'sales': data_values.get('sales'), + 'turnover_ratio': data_values.get('turnover_ratio'), + 'current_stock': data_values.get('current_stock'), + 'stock_value': data_values.get('stock_value'), + 'fsn_classification': data_values.get('fsn_classification'), + 'xyz_classification': data_values.get('xyz_classification'), + 'combined_classification': data_values.get( + 'combined_classification'), + 'data_id': self.id, + }) diff --git a/inventory_advanced_reports/wizard/inventory_fsn_xyz_report_views.xml b/inventory_advanced_reports/wizard/inventory_fsn_xyz_report_views.xml new file mode 100644 index 000000000..6dc09d55a --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_fsn_xyz_report_views.xml @@ -0,0 +1,72 @@ + + + + + inventory.fsn.xyz.report.view.form + inventory.fsn.xyz.report + +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ + + Inventory FSN-XYZ Report + ir.actions.act_window + inventory.fsn.xyz.report + form + + new + + + +
diff --git a/inventory_advanced_reports/wizard/inventory_out_of_stock_data_report.py b/inventory_advanced_reports/wizard/inventory_out_of_stock_data_report.py new file mode 100644 index 000000000..999ad7051 --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_out_of_stock_data_report.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +from odoo import fields, models + + +class InventoryOutOfStockDataReport(models.TransientModel): + """This model is for creating a wizard for viewing the report data""" + _name = "inventory.out.of.stock.data.report" + _description = "Inventory Out Of Stock Data Report" + + product_id = fields.Many2one("product.product", + string="Product", + help="Select the Product.") + category_id = fields.Many2one("product.category", + string="Category", + help="Select the Product Category.") + company_id = fields.Many2one("res.company", string="Company", + help="Select the Company.") + warehouse_id = fields.Many2one("stock.warehouse", + string="Warehouse", + help="Select the Warehouse.") + virtual_stock = fields.Float(string="Forecasted QTY", + help="Forecasted quantity of stock") + sales = fields.Float(string="Sales", + help="Total quantity or value of stock sold") + ads = fields.Float(string="ADS", help="Average Daily Sales") + demanded_quantity = fields.Float(string="Demanded QTY", + help="Quantity demanded") + in_stock_days = fields.Float( + string="In Stock Days", + help="Number of days the inventory was in stock.") + out_of_stock_days = fields.Float( + string="Out Of Stock Days", + help="Number of days the inventory was unavailable or out of stock.") + out_of_stock_ratio = fields.Float( + string="Out Of Stock Ratio", + help="Proportion of out-of-stock days relative to the total" + " days in the period") + cost = fields.Float(string="Cost Price", help="Cost of the stock") + out_of_stock_qty = fields.Float(string="Out Of Stock QTY", + help="Total quantity of out of stock") + out_of_stock_qty_percentage = fields.Float( + string="Out Of Stock QTY(%)", + help="Percentage of out of stock quantity") + out_of_stock_value = fields.Float( + string="Out Of Stock Value(%)", + help="Total value of out of stock quantity") + turnover_ratio = fields.Float( + string="Turnover Ratio", + help="The frequency at which stock is sold and replaced during a" + " specific period.") + fsn_classification = fields.Char( + string="FSN Classification", + help="Classification which categorizes items based on their consumption" + " or movement rate.") + data_id = fields.Many2one('inventory.out.of.stock.report', + string="Out Of Stock Data", + help="corresponding FSN data") diff --git a/inventory_advanced_reports/wizard/inventory_out_of_stock_data_report_views.xml b/inventory_advanced_reports/wizard/inventory_out_of_stock_data_report_views.xml new file mode 100644 index 000000000..15b63e6ec --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_out_of_stock_data_report_views.xml @@ -0,0 +1,40 @@ + + + + + inventory.out.of.stock.data.report.view.graph + inventory.out.of.stock.data.report + + + + + + + + + + inventory.out.of.stock.data.report.view.tree + inventory.out.of.stock.data.report + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inventory_advanced_reports/wizard/inventory_out_of_stock_report.py b/inventory_advanced_reports/wizard/inventory_out_of_stock_report.py new file mode 100644 index 000000000..6d4f8d3e4 --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_out_of_stock_report.py @@ -0,0 +1,453 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +import io +import json +from odoo import _, fields, models +from odoo.exceptions import ValidationError + +try: + from odoo.tools.misc import xlsxwriter +except ImportError: + import xlsxwriter + + +class InventoryOutOfStockReport(models.TransientModel): + """This model is for creating a wizard for inventory turnover report.""" + _name = 'inventory.out.of.stock.report' + _description = 'Inventory Out of Stock Report' + + start_date = fields.Date('Start Date', required=True, + help="Start date to analyse the report") + end_date = fields.Date('End Date', + help="End date to analyse the report", required=True) + warehouse_ids = fields.Many2many( + "stock.warehouse", string="Warehouses", + help="Select the warehouses to generate the report") + product_ids = fields.Many2many( + "product.product", string="Products", + help="Select the products you want to generate the report for") + category_ids = fields.Many2many( + "product.category", string="Product categories", + help="Select the product categories you want to generate the report for" + ) + company_ids = fields.Many2many( + "res.company", string="Company", + default=lambda self: self.env.company, + help="Select the companies you want to generate the report for") + inventory_for_next_x_days = fields.Integer( + string="Inventory For Next X Days", + help="Select next number of days for the inventory") + + def get_report_data(self): + """Function for returning data to print""" + query = """ + SELECT + product_id, + product_code_and_name, + category_id, + category_name, + company_id, + current_stock, + warehouse_id, + incoming_quantity, + outgoing_quantity, + virtual_stock, + sales, + ads, + advance_stock_days, + ROUND(advance_stock_days * ads, 0) AS demanded_quantity, + ROUND(CASE + WHEN ads = 0 THEN virtual_stock / 0.001 + ELSE virtual_stock / ads + END,0) AS in_stock_days, + ROUND(CASE + WHEN ads = 0 THEN GREATEST(advance_stock_days - + ROUND(virtual_stock / 0.001, 2), 0) + ELSE GREATEST(advance_stock_days - + ROUND(virtual_stock / ads, 2), 0) + END ,0) AS out_of_stock_days, + ROUND( + CASE + WHEN advance_stock_days = 0 THEN 0 + ELSE + CASE + WHEN ads = 0 THEN GREATEST(advance_stock_days - + ROUND(virtual_stock / 0.001, 2), 0) + ELSE GREATEST(advance_stock_days - + ROUND(virtual_stock / ads, 2), 0) + END + END, 2 + ) AS out_of_stock_ratio, + ROUND( + CASE + WHEN ads = 0 THEN GREATEST(advance_stock_days - + ROUND(virtual_stock / 0.001, 2), 0) + ELSE GREATEST(advance_stock_days - + ROUND(virtual_stock / ads, 2), 0) + END * ads, 0 + ) AS out_of_stock_qty, + ROUND( + CASE + WHEN virtual_stock = 0 THEN 0 + ELSE sales / virtual_stock + END, 2 + ) AS turnover_ratio, + CASE + WHEN + CASE + WHEN sales > 0 THEN + ROUND((sales / NULLIF(virtual_stock, 0)), 2) + ELSE 0 + END > 3 THEN 'Fast Moving' + WHEN + CASE + WHEN sales > 0 THEN + ROUND((sales / NULLIF(virtual_stock, 0)), 2) + ELSE 0 + END >= 1 AND + CASE + WHEN sales > 0 THEN + ROUND((sales / NULLIF(virtual_stock, 0)), 2) + ELSE 0 + END <= 3 THEN 'Slow Moving' + ELSE 'Non Moving' + END AS fsn_classification + FROM( + SELECT + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', + pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END AS product_code_and_name, + company.id AS company_id, + company.name AS company_name, + sm.product_id AS product_id, + pc.id AS category_id, + pc.complete_name AS category_name, + sw.id AS warehouse_id, + SUM(CASE + WHEN sld_dest.usage = 'internal' AND sm.state + IN ('assigned', 'confirmed', 'waiting') + THEN sm.product_uom_qty + ELSE 0 + END) AS incoming_quantity, + SUM(CASE + WHEN sld_src.usage = 'internal' AND sm.state + IN ('assigned', 'confirmed', 'waiting') + THEN sm.product_uom_qty + ELSE 0 + END) AS outgoing_quantity, + SUM(CASE + WHEN sld_dest.usage = 'internal' AND sm.state = 'done' + THEN sm.product_uom_qty + ELSE 0 + END) - + SUM(CASE + WHEN sld_src.usage = 'internal' AND sm.state = 'done' + THEN sm.product_uom_qty + ELSE 0 + END) AS current_stock, + SUM(CASE + WHEN sld_dest.usage = 'internal' AND sm.state = 'done' + THEN sm.product_uom_qty + ELSE 0 + END) - + SUM(CASE + WHEN sld_src.usage = 'internal' AND sm.state = 'done' + THEN sm.product_uom_qty + ELSE 0 + END)+ + SUM(CASE + WHEN sld_dest.usage = 'internal' AND sm.state + IN ('assigned', 'confirmed', 'waiting') + THEN sm.product_uom_qty + ELSE 0 + END) - + SUM(CASE + WHEN sld_src.usage = 'internal' AND sm.state + IN ('assigned', 'confirmed', 'waiting') + THEN sm.product_uom_qty + ELSE 0 + END) AS virtual_stock, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_dest.usage = 'customer' + THEN sm.product_uom_qty ELSE 0 END) AS sales, + ROUND(SUM(CASE + WHEN sm.date BETWEEN %s AND %s + AND sld_src.usage = 'internal' AND sm.state = 'done' + THEN sm.product_uom_qty + ELSE 0 + END) / ((date %s - date %s)+1), 2) AS ads, + %s AS advance_stock_days + FROM stock_move sm + INNER JOIN product_product pp ON pp.id = sm.product_id + INNER JOIN product_template pt ON pt.id = pp.product_tmpl_id + INNER JOIN res_company company ON company.id = sm.company_id + INNER JOIN stock_warehouse sw ON sw.company_id = company.id + INNER JOIN product_category pc ON pc.id = pt.categ_id + LEFT JOIN ( + SELECT sm.id AS move_id, usage + FROM stock_location sld + INNER JOIN stock_move sm ON sld.id = sm.location_dest_id + ) sld_dest ON sm.id = sld_dest.move_id + LEFT JOIN ( + SELECT sm.id AS move_id, usage + FROM stock_location sld + INNER JOIN stock_move sm ON sld.id = sm.location_id + ) sld_src ON sm.id = sld_src.move_id + WHERE pp.active = TRUE + AND pt.active = TRUE + AND pt.type = 'product' + """ + params = [ + self.start_date, self.end_date, + self.start_date, self.end_date, + self.end_date, self.start_date, + self.inventory_for_next_x_days + ] + sub_queries = [] + sub_params = [] + param_count = 0 + if self.product_ids or self.category_ids: + query += " AND (" + if self.product_ids: + product_ids = [product_id.id for product_id in self.product_ids] + query += "pp.id = ANY(%s)" + params.append(product_ids) + param_count += 1 + if self.product_ids and self.category_ids: + query += " OR " + if self.category_ids: + category_ids = [category.id for category in self.category_ids] + params.append(category_ids) + query += "(pt.categ_id = ANY(%s))" + param_count += 1 + if self.product_ids or self.category_ids: + query += ")" + if self.company_ids: + company_ids = [company.id for company in self.company_ids] + query += " AND (sm.company_id = ANY(%s))" # Specify the table alias + sub_params.append(company_ids) + param_count += 1 + if self.warehouse_ids: + warehouse_ids = [warehouse.id for warehouse in self.warehouse_ids] + query += " AND (sw.id = ANY(%s))" # Specify the table alias + sub_params.append(warehouse_ids) + param_count += 1 + if sub_queries: + query += " AND " + " AND ".join(sub_queries) + query += """ GROUP BY pp.id, pt.name, pc.id, company.id, sm.product_id, + sw.id) AS sub_query """ + self.env.cr.execute(query, tuple(params + sub_params)) + result_data = self.env.cr.dictfetchall() + for data in result_data: + product_id = data.get('product_id') + out_of_stock_qty = data.get('out_of_stock_qty') + total_value = sum( + item.get('out_of_stock_qty', 0) for item in result_data) + if total_value: + out_of_stock_qty_percentage = \ + (out_of_stock_qty / total_value) * 100 + else: + out_of_stock_qty_percentage = 0.0 + data['out_of_stock_qty_percentage'] = round( + out_of_stock_qty_percentage, 2) + cost = self.env['product.product'].search([ + ('id', '=', product_id)]).standard_price + data['cost'] = cost + data['out_of_stock_value'] = out_of_stock_qty * cost + if result_data: + data = { + 'data': result_data, + 'start_date': self.start_date, + 'end_date': self.end_date, + 'inventory_for_next_x_days': self.inventory_for_next_x_days + } + return data + else: + raise ValidationError("No records found for the given criteria!") + + def action_pdf(self): + """Function for printing the pdf""" + data = { + 'model_id': self.id, + 'product_ids': self.product_ids.ids, + 'category_ids': self.category_ids.ids, + 'company_ids': self.company_ids.ids, + 'warehouse_ids': self.warehouse_ids.ids, + 'start_date': self.start_date, + "end_date": self.end_date, + "inventory_for_next_x_days": self.inventory_for_next_x_days + } + return ( + self.env.ref( + 'inventory_advanced_reports.' + 'report_inventory_out_of_stock_action') + .report_action(None, data=data)) + + def action_excel(self): + """This function is for printing excel report""" + data = self.get_report_data() + return { + 'type': 'ir.actions.report', + 'data': {'model': 'inventory.out.of.stock.report', + 'options': json.dumps( + data, default=fields.date_utils.json_default), + 'output_format': 'xlsx', + 'report_name': 'Excel Report', + }, + 'report_type': 'xlsx', + } + + def get_xlsx_report(self, data, response): + """Excel formats for the Excel report to print""" + datas = data['data'] + start_date = data['start_date'] + end_date = data['end_date'] + inventory_for_next_x_days = data['inventory_for_next_x_days'] + output = io.BytesIO() + workbook = xlsxwriter.Workbook(output, {'in_memory': True}) + sheet = workbook.add_worksheet() + sheet.set_margins(0.5, 0.5, 0.5, 0.5) + cell_format = workbook.add_format( + {'font_size': '12px', 'align': 'left'}) + header_style = workbook.add_format( + {'font_name': 'Times', 'bold': True, 'left': 1, 'bottom': 1, + 'right': 1, 'top': 1, 'align': 'center'}) + text_style = workbook.add_format( + {'font_name': 'Times', 'left': 1, 'bottom': 1, 'right': 1, 'top': 1, + 'align': 'left'}) + head = workbook.add_format( + {'align': 'center', 'bold': True, 'font_size': '20px'}) + sheet.merge_range('H2:L3', 'Inventory Out OF Stock Report', head) + bold_format = workbook.add_format( + {'bold': True, 'font_size': '10px', 'align': 'left'}) + txt = workbook.add_format({'font_size': '10px', 'align': 'left'}) + if start_date and end_date: + sheet.write('A5', 'Sales History From: ', bold_format) + sheet.write('B5', start_date, txt) + sheet.write('A6', 'Sales History Upto: ', bold_format) + sheet.write('B6', end_date, txt) + sheet.write('A7', 'Inventory Analysis For Next: ', bold_format) + sheet.write('B7', str(inventory_for_next_x_days) + ' days', txt) + headers = ['Product', 'Category', 'Current Stock', 'Incoming', + 'Outgoing', 'Virtual Stock', 'Sales', 'ADS', 'Demanded QTY', + 'In Stock Days', 'Out Of Stock Days', 'Out Of Stock Ratio', + 'Cost Price', 'Out Of Stock QTY', 'Out Of Stock QTY(%)', + 'Out Of Stock Value(%)', 'Turnover Ratio', + 'FSN Classification'] + for col, header in enumerate(headers): + sheet.write(8, col, header, header_style) + sheet.set_column('A:A', 27, cell_format) + sheet.set_column('B:B', 24, cell_format) + sheet.set_column('C:D', 10, cell_format) + sheet.set_column('E:F', 10, cell_format) + sheet.set_column('G:H', 10, cell_format) + sheet.set_column('I:J', 15, cell_format) + sheet.set_column('K:L', 15, cell_format) + sheet.set_column('M:N', 15, cell_format) + sheet.set_column('O:P', 17, cell_format) + sheet.set_column('Q:R', 15, cell_format) + sheet.set_column('S:T', 15, cell_format) + row = 9 + number = 1 + for val in datas: + sheet.write(row, 0, val['product_code_and_name'], text_style) + sheet.write(row, 1, val['category_name'], text_style) + sheet.write(row, 2, val['current_stock'], text_style) + sheet.write(row, 3, val['incoming_quantity'], text_style) + sheet.write(row, 4, val['outgoing_quantity'], text_style) + sheet.write(row, 5, val['virtual_stock'], text_style) + sheet.write(row, 6, val['sales'], text_style) + sheet.write(row, 7, val['ads'], text_style) + sheet.write(row, 8, val['demanded_quantity'], text_style) + sheet.write(row, 9, val['in_stock_days'], text_style) + sheet.write(row, 10, val['out_of_stock_days'], text_style) + sheet.write(row, 11, val['out_of_stock_ratio'], text_style) + sheet.write(row, 12, val['cost'], text_style) + sheet.write(row, 13, val['out_of_stock_qty'], text_style) + sheet.write(row, 14, val['out_of_stock_qty_percentage'], text_style) + sheet.write(row, 15, val['out_of_stock_value'], text_style) + sheet.write(row, 16, val['turnover_ratio'], text_style) + sheet.write(row, 17, val['fsn_classification'], text_style) + row += 1 + number += 1 + workbook.close() + output.seek(0) + response.stream.write(output.read()) + output.close() + + def display_report_views(self): + """Function for displaying the graph and tree view of data""" + data = self.get_report_data() + for data_values in data.get('data'): + data_values['data_id'] = self.id + self.generate_data(data_values) + graph_view_id = self.env.ref( + 'inventory_advanced_reports.' + 'inventory_out_of_stock_data_report_view_graph').id + tree_view_id = self.env.ref( + 'inventory_advanced_reports.' + 'inventory_out_of_stock_data_report_view_tree').id + graph_report = self.env.context.get("graph_report", False) + report_views = [(tree_view_id, 'tree'), + (graph_view_id, 'graph')] + view_mode = "tree,graph" + if graph_report: + report_views = [(graph_view_id, 'graph'), + (tree_view_id, 'tree')] + view_mode = "graph,tree" + return { + 'name': _('Inventory Out Of Stock Report'), + 'domain': [('data_id', '=', self.id)], + 'res_model': 'inventory.out.of.stock.data.report', + 'view_mode': view_mode, + 'type': 'ir.actions.act_window', + 'views': report_views + } + + def generate_data(self, data_values): + """Function for creating record in model inventory out of stock data + report""" + return self.env['inventory.out.of.stock.data.report'].create({ + 'product_id': data_values.get('product_id'), + 'category_id': data_values.get('category_id'), + 'company_id': data_values.get('company_id'), + 'warehouse_id': data_values.get('warehouse_id'), + 'virtual_stock': data_values.get('virtual_stock'), + 'sales': data_values.get('sales'), + 'ads': data_values.get('ads'), + 'demanded_quantity': data_values.get('demanded_quantity'), + 'in_stock_days': data_values.get('in_stock_days'), + 'out_of_stock_days': data_values.get('out_of_stock_days'), + 'out_of_stock_ratio': data_values.get('out_of_stock_ratio'), + 'cost': data_values.get('cost'), + 'out_of_stock_qty': data_values.get('out_of_stock_qty'), + 'out_of_stock_qty_percentage': data_values.get( + 'out_of_stock_qty_percentage'), + 'out_of_stock_value': data_values.get('out_of_stock_value'), + 'turnover_ratio': data_values.get('turnover_ratio'), + 'fsn_classification': data_values.get('fsn_classification'), + 'data_id': self.id, + }) diff --git a/inventory_advanced_reports/wizard/inventory_out_of_stock_report_views.xml b/inventory_advanced_reports/wizard/inventory_out_of_stock_report_views.xml new file mode 100644 index 000000000..397ba3001 --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_out_of_stock_report_views.xml @@ -0,0 +1,71 @@ + + + + + inventory.out.of.stock.report.view.form + inventory.out.of.stock.report + +
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ + + Inventory Out Of Stock Report + ir.actions.act_window + inventory.out.of.stock.report + form + + new + + + +
diff --git a/inventory_advanced_reports/wizard/inventory_over_stock_data_report.py b/inventory_advanced_reports/wizard/inventory_over_stock_data_report.py new file mode 100644 index 000000000..1993c5527 --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_over_stock_data_report.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +from odoo import fields, models + + +class InventoryOverStockDataReport(models.TransientModel): + """This model is for creating a wizard for viewing the report data""" + _name = "inventory.over.stock.data.report" + _description = "Inventory Over Stock Data Report" + + product_id = fields.Many2one("product.product", + string="Product", help="Select the Product") + category_id = fields.Many2one("product.category", + string="Category", + help="Select the Product Category") + company_id = fields.Many2one("res.company", + string="Company", help="Select the Company") + warehouse_id = fields.Many2one("stock.warehouse", + string="Warehouse", + help="Select the Warehouse") + virtual_stock = fields.Float(string="Forecasted QTY", + help="Forecasted quantity of stock") + sales = fields.Float(string="Sales", + help="Total quantity or value of stock sold") + ads = fields.Float(string="ADS", help="Average Daily Sales") + demanded_quantity = fields.Float(string="Demanded QTY", + help="Quantity Demanded") + in_stock_days = fields.Float(string="Coverage Days", + help="Number of days the inventory was in stock.") + over_stock_qty = fields.Float( + string="Over Stock QTY", + help="Quantity of stock that exceeds the optimal or desired stock level") + over_stock_qty_percentage = fields.Float( + string="Over Stock QTY(%)", + help="Percentage of quantity of stock that exceeds the optimal or" + " desired stock level") + over_stock_value = fields.Float( + string="Over Stock Value", + help="Value of stock that exceeds the optimal or desired stock level") + over_stock_value_percentage = fields.Float( + string="Over Stock Value(%)", + help="Percentage of value of stock that exceeds the optimal or " + "desired stock level") + turnover_ratio = fields.Float(string="Turnover Ratio", + help="The frequency at which stock is sold and" + " replaced during a specific period") + fsn_classification = fields.Char( + string="FSN Classification", + help="Classification which categorizes items based on their consumption" + " or movement rate.") + po_date = fields.Datetime(string="Last PO Date", + help="Date of last purchase") + po_qty = fields.Float(string="Last PO QTY", + help="Last purchased quantity") + po_price_total = fields.Float(string="Last PO Price", + help="Total Price of last purchase") + po_currency_id = fields.Many2one("res.currency", + string="Currency", + help='Currency of purchase') + po_partner_id = fields.Many2one("res.partner", + string="Partner", help="Partner of purchase") + data_id = fields.Many2one('inventory.over.stock.report', + string="Over Stock Data", + help="Corresponding stock data") diff --git a/inventory_advanced_reports/wizard/inventory_over_stock_data_report_views.xml b/inventory_advanced_reports/wizard/inventory_over_stock_data_report_views.xml new file mode 100644 index 000000000..fcbf3d914 --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_over_stock_data_report_views.xml @@ -0,0 +1,43 @@ + + + + + inventory.over.stock.data.report.view.graph + inventory.over.stock.data.report + + + + + + + + + + inventory.over.stock.data.report.view.tree + inventory.over.stock.data.report + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inventory_advanced_reports/wizard/inventory_over_stock_report.py b/inventory_advanced_reports/wizard/inventory_over_stock_report.py new file mode 100644 index 000000000..758bf531d --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_over_stock_report.py @@ -0,0 +1,495 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +import io +import json +from odoo import _, fields, models +from odoo.exceptions import ValidationError + +try: + from odoo.tools.misc import xlsxwriter +except ImportError: + import xlsxwriter + + +class InventoryOverStockReport(models.TransientModel): + """This model is for creating a wizard for inventory Over Stock report.""" + _name = 'inventory.over.stock.report' + _description = 'Inventory Over Stock Report' + + start_date = fields.Date('Start Date', required=True, + help="Start date to analyse the report") + end_date = fields.Date('End Date', required=True, + help="End date to analyse the report") + warehouse_ids = fields.Many2many( + "stock.warehouse", string="Warehouses", + help="Select the warehouses to generate the report") + product_ids = fields.Many2many( + "product.product", string="Products", + help="Select the products you want to generate the report for") + category_ids = fields.Many2many( + "product.category", string="Product categories", + help="Select the product categories you want to generate the report for" + ) + company_ids = fields.Many2many( + "res.company", string="Company", + default=lambda self: self.env.company, + help="Select the companies you want to generate the report for") + inventory_for_next_x_days = fields.Integer( + string="Inventory For Next X Days", + help="Select next number of days for the inventory") + + def get_report_data(self): + """Function for returning data to print""" + processed_product_ids = [] + filtered_result_data = [] + query = """ + SELECT + product_id, + product_code_and_name, + category_id, + category_name, + company_id, + current_stock, + warehouse_id, + incoming_quantity, + outgoing_quantity, + virtual_stock, + sales, + ads, + advance_stock_days, + ROUND(advance_stock_days * ads, 0) AS demanded_quantity, + ROUND(CASE + WHEN ads = 0 THEN virtual_stock / 0.001 + ELSE virtual_stock / ads + END,0) AS in_stock_days, + ROUND(virtual_stock-(ads*advance_stock_days),0) + AS over_stock_qty, + ROUND( + CASE + WHEN virtual_stock = 0 THEN 0 + ELSE sales / virtual_stock + END, 2 + ) AS turnover_ratio, + CASE + WHEN + CASE + WHEN sales > 0 THEN + ROUND((sales / NULLIF(virtual_stock, 0)), 2) + ELSE 0 + END > 3 THEN 'Fast Moving' + WHEN + CASE + WHEN sales > 0 THEN + ROUND((sales / NULLIF(virtual_stock, 0)), 2) + ELSE 0 + END >= 1 AND + CASE + WHEN sales > 0 THEN + ROUND((sales / NULLIF(virtual_stock, 0)), 2) + ELSE 0 + END <= 3 THEN 'Slow Moving' + ELSE 'Non Moving' + END AS fsn_classification + FROM( + SELECT + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', + pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END AS product_code_and_name, + company.id AS company_id, + company.name AS company_name, + sm.product_id AS product_id, + pc.id AS category_id, + pc.complete_name AS category_name, + sw.id AS warehouse_id, + SUM(CASE + WHEN sld_dest.usage = 'internal' AND sm.state + IN ('assigned', 'confirmed', 'waiting') + THEN sm.product_uom_qty + ELSE 0 + END) AS incoming_quantity, + SUM(CASE + WHEN sld_src.usage = 'internal' AND sm.state + IN ('assigned', 'confirmed', 'waiting') + THEN sm.product_uom_qty + ELSE 0 + END) AS outgoing_quantity, + SUM(CASE + WHEN sld_dest.usage = 'internal' AND sm.state = 'done' + THEN sm.product_uom_qty + ELSE 0 + END) - + SUM(CASE + WHEN sld_src.usage = 'internal' AND sm.state = 'done' + THEN sm.product_uom_qty + ELSE 0 + END) AS current_stock, + SUM(CASE + WHEN sld_dest.usage = 'internal' AND sm.state = 'done' + THEN sm.product_uom_qty + ELSE 0 + END) - + SUM(CASE + WHEN sld_src.usage = 'internal' AND sm.state = 'done' + THEN sm.product_uom_qty + ELSE 0 + END)+ + SUM(CASE + WHEN sld_dest.usage = 'internal' AND sm.state + IN ('assigned', 'confirmed', 'waiting') + THEN sm.product_uom_qty + ELSE 0 + END) - + SUM(CASE + WHEN sld_src.usage = 'internal' AND sm.state + IN ('assigned', 'confirmed', 'waiting') + THEN sm.product_uom_qty + ELSE 0 + END) AS virtual_stock, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_dest.usage = 'customer' + THEN sm.product_uom_qty ELSE 0 END) AS sales, + ROUND(SUM(CASE + WHEN sm.date BETWEEN %s AND %s AND sld_src.usage = 'internal' + AND sm.state = 'done' THEN sm.product_uom_qty + ELSE 0 + END) / ((date %s - date %s)+1), 2) AS ads,%s AS advance_stock_days + FROM stock_move sm + INNER JOIN product_product pp ON pp.id = sm.product_id + INNER JOIN product_template pt ON pt.id = pp.product_tmpl_id + INNER JOIN res_company company ON company.id = sm.company_id + INNER JOIN stock_warehouse sw ON sw.company_id = company.id + INNER JOIN product_category pc ON pc.id = pt.categ_id + LEFT JOIN ( + SELECT sm.id AS move_id, usage + FROM stock_location sld + INNER JOIN stock_move sm ON sld.id = sm.location_dest_id + ) sld_dest ON sm.id = sld_dest.move_id + LEFT JOIN ( + SELECT sm.id AS move_id, usage + FROM stock_location sld + INNER JOIN stock_move sm ON sld.id = sm.location_id + ) sld_src ON sm.id = sld_src.move_id + WHERE pp.active = TRUE + AND pt.active = TRUE + AND pt.type = 'product' + """ + params = [ + self.start_date, self.end_date, + self.start_date, self.end_date, + self.end_date, self.start_date, + self.inventory_for_next_x_days + ] + sub_queries = [] + sub_params = [] + param_count = 0 + if self.product_ids or self.category_ids: + query += " AND (" + if self.product_ids: + product_ids = [product_id.id for product_id in self.product_ids] + query += "pp.id = ANY(%s)" + params.append(product_ids) + param_count += 1 + if self.product_ids and self.category_ids: + query += " OR " + if self.category_ids: + category_ids = [category.id for category in self.category_ids] + params.append(category_ids) + query += "(pt.categ_id = ANY(%s))" + param_count += 1 + if self.product_ids or self.category_ids: + query += ")" + if self.company_ids: + company_ids = [company.id for company in self.company_ids] + query += " AND (sm.company_id = ANY(%s))" # Specify the table alias + sub_params.append(company_ids) + param_count += 1 + if self.warehouse_ids: + warehouse_ids = [warehouse.id for warehouse in self.warehouse_ids] + query += " AND (sw.id = ANY(%s))" # Specify the table alias + sub_params.append(warehouse_ids) + param_count += 1 + if sub_queries: + query += " AND " + " AND ".join(sub_queries) + query += """ GROUP BY pp.id, pt.name, pc.id, company.id, sm.product_id, + sw.id) AS sub_query """ + self.env.cr.execute(query, tuple(params + sub_params)) + result_data = self.env.cr.dictfetchall() + for data in result_data: + product_id = data.get('product_id') + if product_id not in processed_product_ids: + processed_product_ids.append( + product_id) + filtered_result_data.append(data) + for data in filtered_result_data: + over_stock_qty = data.get('over_stock_qty') + product_id = data.get('product_id') + total_qty = sum( + item.get('over_stock_qty', 0) for item in filtered_result_data) + if total_qty: + over_stock_qty_percentage = \ + (over_stock_qty / total_qty) * 100 + else: + over_stock_qty_percentage = 0.0 + data['over_stock_qty_percentage'] = round( + over_stock_qty_percentage, 2) + cost = self.env['product.product'].search([ + ('id', '=', product_id)]).standard_price + data['cost'] = cost + data['over_stock_value'] = over_stock_qty * cost + latest_po = '' + confirmed_po = self.env['purchase.order.line'].search([ + ('product_id', '=', product_id), + ('state', '=', 'purchase'), + ]) + for po in confirmed_po: + if latest_po: + if latest_po.date_approve < po.date_approve: + latest_po = po + else: + latest_po = po + data['po_qty'] = 0 + data['po_price_total'] = 0 + if latest_po: + po_date = fields.Datetime.from_string(latest_po.date_approve) + if self.start_date <= po_date.date() <= self.end_date: + data['po_qty'] += latest_po.product_qty + data['po_price_total'] += latest_po.price_total + data['po_date'] = po_date + data['po_currency'] = latest_po.currency_id.name + data['po_currency_id'] = latest_po.currency_id.id + data['po_partner'] = latest_po.partner_id.name + data['po_partner_id'] = latest_po.partner_id.id + else: + data['po_price_total'] = None + data['po_qty'] = None + data['po_currency'] = None + data['po_currency_id'] = None + data['po_partner'] = None + data['po_partner_id'] = None + data[ + 'po_date'] = None + else: + data['po_price_total'] = None + data['po_qty'] = None + data['po_date'] = None + data['po_partner'] = None + data['po_partner_id'] = None + data['po_currency'] = None + data['po_currency_id'] = None + total_value = sum( + item.get('over_stock_value', 0) for item in filtered_result_data) + for data in filtered_result_data: + over_stock_value = data.get('over_stock_value') + if total_value: + over_stock_value_percentage = \ + (over_stock_value / total_value) * 100 + else: + over_stock_value_percentage = 0.0 + data['over_stock_value_percentage'] = round( + over_stock_value_percentage, 2) + if filtered_result_data: + data = { + 'data': filtered_result_data, + 'start_date': self.start_date, + 'end_date': self.end_date, + 'inventory_for_next_x_days': self.inventory_for_next_x_days + } + return data + else: + raise ValidationError("No records found for the given criteria!") + + def action_pdf(self): + """Function for printing pdf report""" + data = { + 'model_id': self.id, + 'product_ids': self.product_ids.ids, + 'category_ids': self.category_ids.ids, + 'company_ids': self.company_ids.ids, + 'warehouse_ids': self.warehouse_ids.ids, + 'start_date': self.start_date, + "end_date": self.end_date, + "inventory_for_next_x_days": self.inventory_for_next_x_days + } + return ( + self.env.ref( + 'inventory_advanced_reports.' + 'report_inventory_over_stock_action') + .report_action(None, data=data)) + + def action_excel(self): + """This function is for printing excel report""" + data = self.get_report_data() + return { + 'type': 'ir.actions.report', + 'data': {'model': 'inventory.over.stock.report', + 'options': json.dumps + (data, default=fields.date_utils.json_default), + 'output_format': 'xlsx', + 'report_name': 'Excel Report', + }, + 'report_type': 'xlsx', + } + + def get_xlsx_report(self, data, response): + """Excel format to print the Excel report. """ + datas = data['data'] + start_date = data['start_date'] + end_date = data['end_date'] + inventory_for_next_x_days = data['inventory_for_next_x_days'] + output = io.BytesIO() + workbook = xlsxwriter.Workbook(output, {'in_memory': True}) + sheet = workbook.add_worksheet() + sheet.set_margins(0.5, 0.5, 0.5, 0.5) + cell_format = workbook.add_format( + {'font_size': '12px', 'align': 'left'}) + header_style = workbook.add_format( + {'font_name': 'Times', 'bold': True, 'left': 1, 'bottom': 1, + 'right': 1, 'top': 1, 'align': 'center'}) + text_style = workbook.add_format( + {'font_name': 'Times', 'left': 1, 'bottom': 1, 'right': 1, 'top': 1, + 'align': 'left'}) + head = workbook.add_format( + {'align': 'center', 'bold': True, 'font_size': '20px'}) + sheet.merge_range('I2:M3', 'Inventory Over Stock Report', head) + bold_format = workbook.add_format( + {'bold': True, 'font_size': '10px', 'align': 'left'}) + txt = workbook.add_format({'font_size': '10px', 'align': 'left'}) + if start_date and end_date: + sheet.write('A5', 'Sales History From: ', bold_format) + sheet.write('B5', start_date, txt) + sheet.write('A6', 'Sales History Upto: ', bold_format) + sheet.write('B6', end_date, txt) + sheet.write('A7', 'Inventory Analysis For Next: ', bold_format) + sheet.write('B7', str(inventory_for_next_x_days) + ' days', txt) + headers = ['Product', 'Category', 'Current Stock', 'Incoming', + 'Outgoing', 'Virtual Stock', 'Sales', 'ADS', 'Demanded QTY', + 'Coverage Days', 'Over Stock QTY', 'Over Stock QTY(%)', + 'Over Stock Value', 'Over Stock Value(%)', 'Turnover Ratio', + 'FSN Classification', 'Last PO Date', 'Last PO QTY', + 'Last PO Price', 'Currency', 'Partner'] + for col, header in enumerate(headers): + sheet.write(8, col, header, header_style) + sheet.set_column('A:A', 27, cell_format) + sheet.set_column('B:B', 24, cell_format) + sheet.set_column('C:D', 10, cell_format) + sheet.set_column('E:F', 10, cell_format) + sheet.set_column('G:H', 10, cell_format) + sheet.set_column('I:J', 15, cell_format) + sheet.set_column('K:L', 15, cell_format) + sheet.set_column('M:N', 15, cell_format) + sheet.set_column('O:P', 15, cell_format) + sheet.set_column('Q:Q', 15, cell_format) + sheet.set_column('R:R', 13, cell_format) + sheet.set_column('S:T', 13, cell_format) + sheet.set_column('U:V', 13, cell_format) + row = 9 + number = 1 + for val in datas: + sheet.write(row, 0, val['product_code_and_name'], text_style) + sheet.write(row, 1, val['category_name'], text_style) + sheet.write(row, 2, val['current_stock'], text_style) + sheet.write(row, 3, val['incoming_quantity'], text_style) + sheet.write(row, 4, val['outgoing_quantity'], text_style) + sheet.write(row, 5, val['virtual_stock'], text_style) + sheet.write(row, 6, val['sales'], text_style) + sheet.write(row, 7, val['ads'], text_style) + sheet.write(row, 8, val['demanded_quantity'], text_style) + sheet.write(row, 9, val['in_stock_days'], text_style) + sheet.write(row, 10, val['over_stock_qty'], text_style) + sheet.write(row, 11, val['over_stock_qty_percentage'], text_style) + sheet.write(row, 12, val['over_stock_value'], text_style) + sheet.write(row, 13, val['over_stock_value_percentage'], text_style) + sheet.write(row, 14, val['turnover_ratio'], text_style) + sheet.write(row, 15, val['fsn_classification'], text_style) + sheet.write(row, 16, val['po_date'], text_style) + sheet.write(row, 17, val['po_qty'], text_style) + sheet.write(row, 18, val['po_price_total'], text_style) + sheet.write(row, 19, val['po_currency'], text_style) + sheet.write(row, 20, val['po_partner'], text_style) + row += 1 + number += 1 + workbook.close() + output.seek(0) + response.stream.write(output.read()) + output.close() + + def display_report_views(self): + """Function for displaying the graph and tree view of the data""" + data = self.get_report_data() + for data_values in data.get('data'): + data_values['data_id'] = self.id + self.generate_data(data_values) + graph_view_id = self.env.ref( + 'inventory_advanced_reports.' + 'inventory_over_stock_data_report_view_graph').id + tree_view_id = self.env.ref( + 'inventory_advanced_reports.' + 'inventory_over_stock_data_report_view_tree').id + graph_report = self.env.context.get("graph_report", False) + report_views = [(tree_view_id, 'tree'), + (graph_view_id, 'graph')] + view_mode = "tree,graph" + if graph_report: + report_views = [(graph_view_id, 'graph'), + (tree_view_id, 'tree')] + view_mode = "graph,tree" + return { + 'name': _('Inventory Over Stock Report'), + 'domain': [('data_id', '=', self.id)], + 'res_model': 'inventory.over.stock.data.report', + 'view_mode': view_mode, + 'type': 'ir.actions.act_window', + 'views': report_views + } + + def generate_data(self, data_values): + """Function to create record in model inventory over stock data + report""" + return self.env['inventory.over.stock.data.report'].create({ + 'product_id': data_values.get('product_id'), + 'category_id': data_values.get('category_id'), + 'company_id': data_values.get('company_id'), + 'warehouse_id': data_values.get('warehouse_id'), + 'virtual_stock': data_values.get('virtual_stock'), + 'sales': data_values.get('sales'), + 'ads': data_values.get('ads'), + 'demanded_quantity': data_values.get('demanded_quantity'), + 'in_stock_days': data_values.get('in_stock_days'), + 'over_stock_qty': data_values.get('over_stock_qty'), + 'over_stock_qty_percentage': + data_values.get('over_stock_qty_percentage'), + 'over_stock_value': data_values.get('over_stock_value'), + 'over_stock_value_percentage': + data_values.get('over_stock_value_percentage'), + 'turnover_ratio': data_values.get('turnover_ratio'), + 'fsn_classification': data_values.get('fsn_classification'), + 'po_date': data_values.get('po_date'), + 'po_qty': data_values.get('po_qty'), + 'po_price_total': data_values.get('po_price_total'), + 'po_currency_id': data_values.get('po_currency_id'), + 'po_partner_id': data_values.get('po_partner_id'), + 'data_id': self.id, + }) diff --git a/inventory_advanced_reports/wizard/inventory_over_stock_report_views.xml b/inventory_advanced_reports/wizard/inventory_over_stock_report_views.xml new file mode 100644 index 000000000..6cd331142 --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_over_stock_report_views.xml @@ -0,0 +1,71 @@ + + + + + inventory.over.stock.report.view.form + inventory.over.stock.report + +
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ + + Inventory Over Stock Report + ir.actions.act_window + inventory.over.stock.report + form + + new + + + +
diff --git a/inventory_advanced_reports/wizard/inventory_stock_movement_report.py b/inventory_advanced_reports/wizard/inventory_stock_movement_report.py new file mode 100644 index 000000000..7af8c9f84 --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_stock_movement_report.py @@ -0,0 +1,331 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +import io +import json +from odoo import fields, models +from odoo.exceptions import ValidationError + +try: + from odoo.tools.misc import xlsxwriter +except ImportError: + import xlsxwriter + + +class InventoryStockMovementReport(models.TransientModel): + """This model is for creating a wizard for inventory Over Stock report.""" + _name = 'inventory.stock.movement.report' + _description = 'Inventory Stock Movement Report' + + start_date = fields.Date('Start Date', + default=lambda self: fields.Date.today(), + help="Start date to analyze the report") + end_date = fields.Date('End Date', + default=lambda self: fields.Date.today(), + help="End date to analyse the report") + warehouse_ids = fields.Many2many( + "stock.warehouse", string="Warehouses", + help="Select the warehouses to generate the report") + product_ids = fields.Many2many( + "product.product", string="Products", + help="Select the products you want to generate the report for") + category_ids = fields.Many2many( + "product.category", string="Product categories", + help="Select the product categories you want to generate the report for" + ) + company_ids = fields.Many2many( + "res.company", string="Company", + default=lambda self: self.env.company, + help="Select the companies you want to generate the report for") + report_up_to_certain_date = fields.Boolean( + string="Date upto", + help="Report should be generated up to this specific date") + up_to_certain_date = fields.Date( + string="Movements Upto", + help="Specifies the exact date up to which the inventory movements " + "should be considered") + + def get_report_data(self): + """Function for returning the values for printing""" + query = """ + SELECT + pp.id as product_id, + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END AS product_code_and_name, + pc.complete_name AS category_name, + company.name AS company_name, + """ + if self.report_up_to_certain_date: + query += """ + SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'inventory' + THEN sm.product_uom_qty ELSE 0 END) AS opening_stock, + (SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END)) AS closing_stock, + SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'customer' + THEN sm.product_uom_qty ELSE 0 END) AS sales, + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'customer' + THEN sm.product_uom_qty ELSE 0 END) AS sales_return, + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'supplier' + THEN sm.product_uom_qty ELSE 0 END) AS purchase, + SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'supplier' + THEN sm.product_uom_qty ELSE 0 END) AS purchase_return, + SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) AS internal_in, + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) AS internal_out, + SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'inventory' + THEN sm.product_uom_qty ELSE 0 END) AS adj_in, + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'inventory' + THEN sm.product_uom_qty ELSE 0 END) AS adj_out, + SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'production' + THEN sm.product_uom_qty ELSE 0 END) AS production_in, + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'production' + THEN sm.product_uom_qty ELSE 0 END) AS production_out, + SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'transit' + THEN sm.product_uom_qty ELSE 0 END) AS transit_in, + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'transit' + THEN sm.product_uom_qty ELSE 0 END) AS transit_out + """ + params = [self.up_to_certain_date] * 15 + else: + query += """ + (SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END)) AS opening_stock, + (SUM(CASE WHEN sm.date <= %s AND sld_dest.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END) - + SUM(CASE WHEN sm.date <= %s AND sld_src.usage = 'internal' + THEN sm.product_uom_qty ELSE 0 END)) AS closing_stock, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_dest.usage = 'customer' + THEN sm.product_uom_qty ELSE 0 END) AS sales, + SUM(CASE WHEN sm.date BETWEEN %s + AND %s AND sld_src.usage = 'customer' THEN sm.product_uom_qty + ELSE 0 END) AS sales_return, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_src.usage = 'supplier' THEN sm.product_uom_qty + ELSE 0 END) AS purchase, + SUM(CASE WHEN sm.date BETWEEN %s + AND %s AND sld_dest.usage = 'supplier' THEN sm.product_uom_qty + ELSE 0 END) AS purchase_return, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_dest.usage = 'internal' THEN sm.product_uom_qty + ELSE 0 END) AS internal_in, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_src.usage = 'internal' THEN sm.product_uom_qty + ELSE 0 END) AS internal_out, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_dest.usage = 'inventory' THEN sm.product_uom_qty + ELSE 0 END) AS adj_in, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_src.usage = 'inventory' THEN sm.product_uom_qty + ELSE 0 END) AS adj_out, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_dest.usage = 'production' THEN sm.product_uom_qty + ELSE 0 END) AS production_in, + SUM(CASE WHEN sm.date BETWEEN %s + AND %s AND sld_src.usage = 'production' THEN sm.product_uom_qty + ELSE 0 END) AS production_out, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_dest.usage = 'transit' THEN sm.product_uom_qty + ELSE 0 END) AS transit_in, + SUM(CASE WHEN sm.date BETWEEN %s AND %s + AND sld_src.usage = 'transit' THEN sm.product_uom_qty + ELSE 0 END) AS transit_out + """ + params = [self.start_date, self.start_date, self.end_date, + self.end_date, self.start_date, + self.end_date, self.start_date, self.end_date, + self.start_date, self.end_date, self.start_date, + self.end_date, self.start_date, self.end_date, + self.start_date, self.end_date, self.start_date, + self.end_date, self.start_date, self.end_date, + self.start_date, + self.end_date, self.start_date, self.end_date, + self.start_date, + self.end_date, self.start_date, self.end_date] + query += """ + FROM stock_move sm + INNER JOIN product_product pp ON pp.id = sm.product_id + INNER JOIN product_template pt ON pt.id = pp.product_tmpl_id + INNER JOIN res_company company ON company.id = sm.company_id + INNER JOIN stock_warehouse sw ON sw.company_id = company.id + INNER JOIN product_category pc ON pc.id = pt.categ_id + """ + query += """ + LEFT JOIN stock_location sld_dest + ON sm.location_dest_id = sld_dest.id + LEFT JOIN stock_location sld_src + ON sm.location_id = sld_src.id + WHERE + sm.state = 'done' + """ + sub_queries = [] + if self.product_ids: + product_ids = [product_id.id for product_id in self.product_ids] + sub_queries.append("pp.id = ANY(%s)") + params.append(product_ids) + if self.category_ids: + category_ids = [category.id for category in self.category_ids] + sub_queries.append("pt.categ_id = ANY(%s)") + params.append(category_ids) + if sub_queries: + query += " AND (" + " OR ".join(sub_queries) + ")" + if self.company_ids: + company_ids = [company.id for company in self.company_ids] + query += " AND sm.company_id = ANY(%s)" + params.append(company_ids) + if self.warehouse_ids: + warehouse_ids = [warehouse.id for warehouse in self.warehouse_ids] + query += " AND sw.id = ANY(%s)" + params.append(warehouse_ids) + query += """ + GROUP BY pp.id,pt.name,pc.complete_name,company.name + """ + self.env.cr.execute(query, params) + result_data = self.env.cr.dictfetchall() + if result_data: + data = { + 'data': result_data, + 'start_date': self.start_date, + 'end_date': self.end_date, + 'up_to_certain_date': self.up_to_certain_date + } + return data + else: + raise ValidationError("No records found for the given criteria!") + + def action_pdf(self): + """Function for printing the pdf report""" + data = { + 'model_id': self.id, + 'product_ids': self.product_ids.ids, + 'category_ids': self.category_ids.ids, + 'company_ids': self.company_ids.ids, + 'warehouse_ids': self.warehouse_ids.ids, + 'start_date': self.start_date, + "end_date": self.end_date, + "report_up_to_certain_date": self.report_up_to_certain_date, + "up_to_certain_date": self.up_to_certain_date + } + return ( + self.env.ref( + 'inventory_advanced_reports.' + 'report_inventory_stock_movement_action') + .report_action(None, data=data)) + + def action_excel(self): + """This function is for printing excel report""" + data = self.get_report_data() + return { + 'type': 'ir.actions.report', + 'data': {'model': 'inventory.stock.movement.report', + 'options': json.dumps + (data, default=fields.date_utils.json_default), + 'output_format': 'xlsx', + 'report_name': 'Excel Report', + }, + 'report_type': 'xlsx', + } + + def get_xlsx_report(self, data, response): + """Excel format to print the data in Excel sheet""" + datas = data['data'] + start_date = data['start_date'] + end_date = data['end_date'] + up_to_certain_date = data['up_to_certain_date'] + output = io.BytesIO() + workbook = xlsxwriter.Workbook(output, {'in_memory': True}) + sheet = workbook.add_worksheet() + sheet.set_margins(0.5, 0.5, 0.5, 0.5) + cell_format = workbook.add_format( + {'font_size': '12px', 'align': 'left'}) + header_style = workbook.add_format( + {'font_name': 'Times', 'bold': True, 'left': 1, 'bottom': 1, + 'right': 1, 'top': 1, 'align': 'center'}) + text_style = workbook.add_format( + {'font_name': 'Times', 'left': 1, 'bottom': 1, 'right': 1, 'top': 1, + 'align': 'left'}) + head = workbook.add_format( + {'align': 'center', 'bold': True, 'font_size': '20px'}) + bold_format = workbook.add_format( + {'bold': True, 'font_size': '10px', 'align': 'left'}) + txt = workbook.add_format({'font_size': '10px', 'align': 'left'}) + if start_date and end_date and not up_to_certain_date: + sheet.write('A6', 'From Date: ', bold_format) + sheet.write('B6', start_date, txt) + sheet.write('A7', 'To Date: ', bold_format) + sheet.write('B7', end_date, txt) + if up_to_certain_date: + sheet.write('A7', 'Stock Movements Up To: ', bold_format) + sheet.write('B7', up_to_certain_date, txt) + sheet.merge_range('E2:K3', 'Inventory Stock Movement Report', head) + headers = ['Company', 'Product', 'Category', 'Opening Stock', 'Sales', + 'Sales Return', 'Purchase', 'Purchase Return', 'Internal In', + 'Internal Out', 'Adjustment In', 'Adjustment Out', + 'Production In', 'Production Out', 'Transit In', + 'Transit Out', 'Closing Stock'] + for col, header in enumerate(headers): + sheet.write(8, col, header, header_style) + sheet.set_column('A:A', 23, cell_format) + sheet.set_column('B:B', 27, cell_format) + sheet.set_column('C:C', 25, cell_format) + sheet.set_column('D:D', 13, cell_format) + sheet.set_column('E:E', 13, cell_format) + sheet.set_column('F:G', 15, cell_format) + sheet.set_column('H:I', 15, cell_format) + sheet.set_column('J:K', 15, cell_format) + sheet.set_column('L:M', 15, cell_format) + sheet.set_column('N:O', 15, cell_format) + sheet.set_column('P:Q', 15, cell_format) + row = 9 + number = 1 + for val in datas: + sheet.write(row, 0, val['company_name'], text_style) + sheet.write(row, 1, val['product_code_and_name'], text_style) + sheet.write(row, 2, val['category_name'], text_style) + sheet.write(row, 3, val['opening_stock'], text_style) + sheet.write(row, 4, val['sales'], text_style) + sheet.write(row, 5, val['sales_return'], text_style) + sheet.write(row, 6, val['purchase'], text_style) + sheet.write(row, 7, val['purchase_return'], text_style) + sheet.write(row, 8, val['internal_in'], text_style) + sheet.write(row, 9, val['internal_out'], text_style) + sheet.write(row, 10, val['adj_in'], text_style) + sheet.write(row, 11, val['adj_out'], text_style) + sheet.write(row, 12, val['production_in'], text_style) + sheet.write(row, 13, val['production_out'], text_style) + sheet.write(row, 14, val['transit_in'], text_style) + sheet.write(row, 15, val['transit_out'], text_style) + sheet.write(row, 16, val['closing_stock'], text_style) + row += 1 + number += 1 + workbook.close() + output.seek(0) + response.stream.write(output.read()) + output.close() diff --git a/inventory_advanced_reports/wizard/inventory_stock_movement_report_views.xml b/inventory_advanced_reports/wizard/inventory_stock_movement_report_views.xml new file mode 100644 index 000000000..091ceea39 --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_stock_movement_report_views.xml @@ -0,0 +1,69 @@ + + + + + inventory.stock.movement.report.view.form + inventory.stock.movement.report + +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ + + Inventory Stock Movement Report + ir.actions.act_window + inventory.stock.movement.report + form + + new + + + +
diff --git a/inventory_advanced_reports/wizard/inventory_xyz_data_report.py b/inventory_advanced_reports/wizard/inventory_xyz_data_report.py new file mode 100644 index 000000000..ba38459e1 --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_xyz_data_report.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +from odoo import fields, models + + +class InventoryXyzDataReport(models.TransientModel): + """This model is for creating a wizard for viewing the report data""" + _name = "inventory.xyz.data.report" + _description = "Inventory XYZ Data Report" + + product_id = fields.Many2one("product.product", + string="Product", help="Select the Product.") + category_id = fields.Many2one("product.category", + string="Category", + help="Select the Product Category") + company_id = fields.Many2one("res.company", string="Company", + help="Select the Company") + current_stock = fields.Float(string="Current Stock", + help="Quantity of Stock available currently") + stock_value = fields.Float(string="Stock Value", + help="Total value of out of stock quantity") + stock_percentage = fields.Float(string="Stock Value(%)", + help="Percentage of current stock") + cumulative_stock_percentage = fields.Float( + string="CUMULATIVE STOCK( %)", + help="Cumulative percentage of stock over a specified period.") + xyz_classification = fields.Char( + string="XYZ CLASSIFICATION", + help="Categorizing inventory items based on their variability in" + " consumption.") + data_id = fields.Many2one('inventory.xyz.report', + string="XYZ Data", help="corresponding XYZ data") diff --git a/inventory_advanced_reports/wizard/inventory_xyz_data_report_views.xml b/inventory_advanced_reports/wizard/inventory_xyz_data_report_views.xml new file mode 100644 index 000000000..22a029702 --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_xyz_data_report_views.xml @@ -0,0 +1,32 @@ + + + + + inventory.xyz.data.report.view.graph + inventory.xyz.data.report + + + + + + + + + + + inventory.xyz.data.report.view.tree + inventory.xyz.data.report + + + + + + + + + + + + + + diff --git a/inventory_advanced_reports/wizard/inventory_xyz_report.py b/inventory_advanced_reports/wizard/inventory_xyz_report.py new file mode 100644 index 000000000..d6b58c88f --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_xyz_report.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Jumana Haseen (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +import io +import json +from odoo import _, fields, models +from odoo.exceptions import ValidationError + +try: + from odoo.tools.misc import xlsxwriter +except ImportError: + import xlsxwriter + + +class InventoryXyzReport(models.TransientModel): + """This model is for creating a wizard for inventory aging report""" + _name = "inventory.xyz.report" + _description = "Inventory XYZ Report" + + product_ids = fields.Many2many( + "product.product", string="Products", + help="Select the products you want to generate the report for") + category_ids = fields.Many2many( + "product.category", string="Product Categories", + help="Select the product categories you want to generate the report for" + ) + company_ids = fields.Many2many( + 'res.company', string="Company", + help="Select the companies you want to generate the report for" + ) + xyz = fields.Selection( + [('x', 'X'), ('y', 'Y'), ('z', 'Z'), ('all', 'All')], + string="XYZ Classification", default='all', required=True, + help="Categorizing inventory items based on their variability in" + " consumption.") + + def get_report_data(self): + """Function for returning data to print""" + xyz = dict(self._fields['xyz'].selection).get(self.xyz) + params = [] + param_count = 0 + query = """ + SELECT + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', + pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END AS product_code_and_name, + svl.company_id, + company.name AS company_name, + svl.product_id, + pt.categ_id AS category_id, + c.complete_name AS category_name, + SUM(svl.remaining_qty) AS current_stock, + SUM(svl.remaining_value) AS stock_value + FROM stock_valuation_layer svl + INNER JOIN res_company company ON company.id = svl.company_id + INNER JOIN product_product pp ON pp.id = svl.product_id + INNER JOIN product_template pt ON pt.id = pp.product_tmpl_id + INNER JOIN product_category c ON c.id = pt.categ_id + WHERE pp.active = TRUE + AND pt.active = TRUE + AND pt.type = 'product' + AND svl.remaining_value IS NOT NULL + """ + if self.company_ids: + company_ids = [company_id.id for company_id in self.company_ids] + query += f" AND (company.id IS NULL OR company.id = ANY(%s))" + params.append(company_ids) + param_count += 1 + if self.product_ids or self.category_ids: + query += " AND (" + if self.product_ids: + product_ids = [product_id.id for product_id in self.product_ids] + query += f"pp.id = ANY(%s)" + params.append(product_ids) + param_count += 1 + if self.product_ids and self.category_ids: + query += " OR " + if self.category_ids: + category_ids = [category_id.id for category_id in + self.category_ids] + query += f"c.id = ANY(%s)" + params.append(category_ids) + param_count += 1 + query += ")" + query += """ + GROUP BY + svl.company_id, + company.name, + svl.product_id, + CASE + WHEN pp.default_code IS NOT NULL + THEN CONCAT(pp.default_code, ' - ', pt.name->>'en_US') + ELSE + pt.name->>'en_US' + END, + pt.categ_id, + c.complete_name + ORDER BY SUM(svl.remaining_value) DESC; + """ + self.env.cr.execute(query, params) + result_data = self.env.cr.dictfetchall() + total_current_value = 0 + cumulative_stock = 0 + filtered_stock = [] + for row in result_data: + current_value = row.get('stock_value') + total_current_value += current_value + for value in result_data: + current_value = value.get('stock_value') + if total_current_value != 0 and current_value: + stock_percentage = (current_value / total_current_value) * 100 + else: + stock_percentage = 0.0 + value['stock_percentage'] = round(stock_percentage, 2) + cumulative_stock += value['stock_percentage'] + value['cumulative_stock_percentage'] = round(cumulative_stock, 2) + if cumulative_stock < 70: + xyz_classification = 'X' + elif 70 <= cumulative_stock <= 90: + xyz_classification = 'Y' + else: + xyz_classification = 'Z' + value['xyz_classification'] = xyz_classification + if result_data: + for xyz_class in result_data: + if xyz_class.get('xyz_classification') == str(xyz): + filtered_stock.append(xyz_class) + if xyz == 'All' and not result_data: + raise ValidationError("No corresponding data to print") + elif xyz != 'All' and filtered_stock == []: + raise ValidationError("No corresponding data to print") + data = { + 'data': result_data if xyz == 'All' else filtered_stock, + } + return data + else: + raise ValidationError("No records found for the given criteria!") + + def action_pdf(self): + """Function for printing the pdf report""" + data = { + 'model_id': self.id, + 'product_ids': self.product_ids.ids, + 'category_ids': self.category_ids.ids, + 'company_ids': self.company_ids.ids, + "xyz": dict(self._fields['xyz'].selection).get(self.xyz) + + } + return ( + self.env.ref( + 'inventory_advanced_reports.report_inventory_xyz_action') + .report_action(None, data=data)) + + def action_excel(self): + """This function is for printing excel report""" + data = self.get_report_data() + return { + 'type': 'ir.actions.report', + 'data': {'model': 'inventory.xyz.report', + 'options': json.dumps + (data, default=fields.date_utils.json_default), + 'output_format': 'xlsx', + 'report_name': 'Excel Report', + }, + 'report_type': 'xlsx', + } + + def get_xlsx_report(self, data, response): + """Excel formats for printing data in Excel sheets""" + datas = data['data'] + output = io.BytesIO() + workbook = xlsxwriter.Workbook(output, {'in_memory': True}) + sheet = workbook.add_worksheet() + sheet.set_margins(0.5, 0.5, 0.5, 0.5) + cell_format = workbook.add_format( + {'font_size': '12px', 'align': 'left'}) + header_style = workbook.add_format( + {'font_name': 'Times', 'bold': True, 'left': 1, 'bottom': 1, + 'right': 1, 'top': 1, 'align': 'center'}) + text_style = workbook.add_format( + {'font_name': 'Times', 'left': 1, 'bottom': 1, 'right': 1, 'top': 1, + 'align': 'left'}) + head = workbook.add_format( + {'align': 'center', 'bold': True, 'font_size': '20px'}) + sheet.merge_range('B2:E3', 'Inventory XYZ Report', head) + headers = ['Product', 'Category', 'Current Stock', 'Stock Value', + 'Cumulative Stock', 'XYZ Calculation'] + for col, header in enumerate(headers): + sheet.write(8, col, header, header_style) + sheet.set_column('A:B', 27, cell_format) + sheet.set_column('C:D', 15, cell_format) + sheet.set_column('E:F', 15, cell_format) + row = 9 + number = 1 + for val in datas: + sheet.write(row, 0, val['product_code_and_name'], text_style) + sheet.write(row, 1, val['category_name'], text_style) + sheet.write(row, 2, val['current_stock'], text_style) + sheet.write(row, 3, val['stock_value'], text_style) + sheet.write(row, 4, val['stock_percentage'], text_style) + sheet.write(row, 5, val['cumulative_stock_percentage'], text_style) + sheet.write(row, 5, val['xyz_classification'], text_style) + row += 1 + number += 1 + workbook.close() + output.seek(0) + response.stream.write(output.read()) + output.close() + + def display_report_views(self): + """Function for displaying graph and tree view of the data""" + data = self.get_report_data() + for data_values in data.get('data'): + data_values['data_id'] = self.id + self.generate_data(data_values) + graph_view_id = self.env.ref( + 'inventory_advanced_reports.' + 'inventory_xyz_data_report_view_graph').id + tree_view_id = self.env.ref( + 'inventory_advanced_reports.' + 'inventory_xyz_data_report_view_tree').id + graph_report = self.env.context.get("graph_report", False) + report_views = [(tree_view_id, 'tree'), + (graph_view_id, 'graph')] + view_mode = "tree,graph" + if graph_report: + report_views = [(graph_view_id, 'graph'), + (tree_view_id, 'tree')] + view_mode = "graph,tree" + return { + 'name': _('Inventory XYZ Report'), + 'domain': [('data_id', '=', self.id)], + 'res_model': 'inventory.xyz.data.report', + 'view_mode': view_mode, + 'type': 'ir.actions.act_window', + 'views': report_views + } + + def generate_data(self, data_values): + """Function for creating record in the model inventory cyz data + report""" + return self.env['inventory.xyz.data.report'].create({ + 'product_id': data_values.get('product_id'), + 'category_id': data_values.get('category_id'), + 'company_id': data_values.get('company_id'), + 'current_stock': data_values.get('current_stock'), + 'stock_value': data_values.get('stock_value'), + 'stock_percentage': data_values.get('stock_percentage'), + 'cumulative_stock_percentage': data_values.get( + 'cumulative_stock_percentage'), + 'xyz_classification': data_values.get('xyz_classification'), + 'data_id': self.id, + }) diff --git a/inventory_advanced_reports/wizard/inventory_xyz_report_views.xml b/inventory_advanced_reports/wizard/inventory_xyz_report_views.xml new file mode 100644 index 000000000..131cb0b90 --- /dev/null +++ b/inventory_advanced_reports/wizard/inventory_xyz_report_views.xml @@ -0,0 +1,48 @@ + + + + + inventory.xyz.report.view.form + inventory.xyz.report + +
+ + + + + + + + + + + +
+
+
+
+
+
+ + + Inventory XYZ Report + ir.actions.act_window + inventory.xyz.report + form + + new + + + +