diff --git a/budget_control_report/README.rst b/budget_control_report/README.rst new file mode 100644 index 00000000..a4ed31e0 --- /dev/null +++ b/budget_control_report/README.rst @@ -0,0 +1,75 @@ +========================== +Base Budget Control Report +========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:4a9283ecc66152478f5215fcaf52e14b74666d0604f9dc8ebcc9e05a661941d8 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-ecosoft--odoo%2Fbudgeting-lightgray.png?logo=github + :target: https://github.com/ecosoft-odoo/budgeting/tree/15.0/budget_control_report + :alt: ecosoft-odoo/budgeting + +|badge1| |badge2| |badge3| + +This module is base budget report + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Ecosoft + +Contributors +~~~~~~~~~~~~ + +* `Ecosoft `__: + + * Saran Lim. + +Maintainers +~~~~~~~~~~~ + +.. |maintainer-Saran440| image:: https://github.com/Saran440.png?size=40px + :target: https://github.com/Saran440 + :alt: Saran440 + +Current maintainer: + +|maintainer-Saran440| + +This module is part of the `ecosoft-odoo/budgeting `_ project on GitHub. + +You are welcome to contribute. diff --git a/budget_control_report/__init__.py b/budget_control_report/__init__.py new file mode 100644 index 00000000..df501157 --- /dev/null +++ b/budget_control_report/__init__.py @@ -0,0 +1,6 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import wizard +from . import templates + +# from . import report diff --git a/budget_control_report/__manifest__.py b/budget_control_report/__manifest__.py new file mode 100644 index 00000000..683ef7cf --- /dev/null +++ b/budget_control_report/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright 2025 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Base Budget Control Report", + "version": "15.0.1.0.0", + "author": "Ecosoft, Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "https://github.com/ecosoft-odoo/budgeting", + "category": "Accounting", + "summary": "Report Budget Control", + "depends": [ + "budget_control", + "report_xlsx_helper", + ], + "data": [ + "security/budget_security.xml", + "security/ir.model.access.csv", + "data/report_action.xml", + "wizard/budget_report_view.xml", + "wizard/budget_consumption_report_view.xml", + ], + "installable": True, + "development_status": "Alpha", + "maintainers": ["Saran440"], +} diff --git a/budget_control_report/data/report_action.xml b/budget_control_report/data/report_action.xml new file mode 100644 index 00000000..b74ae6a3 --- /dev/null +++ b/budget_control_report/data/report_action.xml @@ -0,0 +1,23 @@ + + + + + Export Excel Budget + budget.report.wizard + ir.actions.report + budget.report.wizard.xlsx + xlsx + object._get_report_base_filename() + + + + + Export Excel Budget Consumption + budget.consumption.report.wizard + ir.actions.report + budget.consumption.xlsx + xlsx + object._get_report_base_filename() + + + diff --git a/budget_control_report/readme/CONTRIBUTORS.rst b/budget_control_report/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..66edcda9 --- /dev/null +++ b/budget_control_report/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Ecosoft `__: + + * Saran Lim. diff --git a/budget_control_report/readme/DESCRIPTION.rst b/budget_control_report/readme/DESCRIPTION.rst new file mode 100644 index 00000000..3e3ae5ba --- /dev/null +++ b/budget_control_report/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module is base budget report diff --git a/budget_control_report/security/budget_security.xml b/budget_control_report/security/budget_security.xml new file mode 100644 index 00000000..23918fa2 --- /dev/null +++ b/budget_control_report/security/budget_security.xml @@ -0,0 +1,11 @@ + + + + Allow Export Excel Budget + + + + diff --git a/budget_control_report/security/ir.model.access.csv b/budget_control_report/security/ir.model.access.csv new file mode 100644 index 00000000..7d803a1a --- /dev/null +++ b/budget_control_report/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_budget_report_wizard,access_budget_report_wizard,model_budget_report_wizard,,1,1,1,1 +access_budget_consumption_report_wizard,access_budget_consumption_report_wizard,model_budget_consumption_report_wizard,,1,1,1,1 diff --git a/budget_control_report/static/description/index.html b/budget_control_report/static/description/index.html new file mode 100644 index 00000000..f4dc33ab --- /dev/null +++ b/budget_control_report/static/description/index.html @@ -0,0 +1,430 @@ + + + + + +Base Budget Control Report + + + +
+

Base Budget Control Report

+ + +

Alpha License: AGPL-3 ecosoft-odoo/budgeting

+

This module is base budget report

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Ecosoft
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

Current maintainer:

+

Saran440

+

This module is part of the ecosoft-odoo/budgeting project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/budget_control_report/templates/__init__.py b/budget_control_report/templates/__init__.py new file mode 100644 index 00000000..c3e97cdd --- /dev/null +++ b/budget_control_report/templates/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import report_budget_export_xlsx +from . import report_budget_consumption_export_xlsx diff --git a/budget_control_report/templates/report_budget_consumption_export_xlsx.py b/budget_control_report/templates/report_budget_consumption_export_xlsx.py new file mode 100644 index 00000000..b4da3489 --- /dev/null +++ b/budget_control_report/templates/report_budget_consumption_export_xlsx.py @@ -0,0 +1,463 @@ +# Copyright 2021 Ecosoft Co., Ltd (https://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from psycopg2 import sql + +from odoo import models + +from odoo.addons.report_xlsx_helper.report.report_xlsx_format import ( + FORMATS, + XLS_HEADERS, +) + + +class BudgetConsumptionExportXslx(models.AbstractModel): + _name = "report.budget.consumption.xlsx" + _inherit = "report.report_xlsx.abstract" + _description = "Budget Export xlsx" + + def _get_export_budget_vals(self, obj): + return { + "0010_date": { + "header": {"value": "Date"}, + "data": { + "value": self._render("date"), + "format": FORMATS["format_tcell_date_left"], + }, + "width": 15, + }, + "0020_type": { + "header": {"value": "Type"}, + "data": { + "value": self._render("type"), + }, + "width": 10, + }, + "0030_analytic_period": { + "header": {"value": "Analytic Period"}, + "data": { + "value": self._render("analytic_period"), + }, + "width": 15, + }, + "0040_analytic_account_code": { + "header": {"value": "Analytic Code"}, + "data": { + "value": self._render("analytic_code"), + }, + "width": 10, + }, + "0050_analytic_account": { + "header": {"value": "Analytic Account"}, + "data": { + "value": self._render("analytic_account"), + }, + "width": 20, + }, + "0060_kpi": { + "header": {"value": "KPI"}, + "data": { + "value": self._render("kpi_name"), + }, + "width": 15, + }, + "0070_account_code": { + "header": {"value": "Account Code"}, + "data": { + "value": self._render("account_code"), + }, + "width": 15, + }, + "0080_account_name": { + "header": {"value": "Account Name"}, + "data": { + "value": self._render("account_name"), + }, + "width": 20, + }, + "0090_doc_name": { + "header": {"value": "Reference"}, + "data": { + "value": self._render("doc_name"), + }, + "width": 20, + }, + "0100_doc_ref_next": { + "header": {"value": "Uncommit From"}, + "data": { + "value": self._render("doc_ref_next"), + }, + "width": 15, + }, + "0110_amount": { + "header": {"value": "Amount Commit"}, + "data": { + "value": self._render("amount"), + "format": FORMATS["format_tcell_amount_right"], + }, + "width": 15, + }, + "0120_name": { + "header": {"value": "Description"}, + "data": { + "value": self._render("name"), + }, + "width": 25, + }, + "0130_budget_type": { + "header": {"value": "Budget Type"}, + "data": { + "value": self._render("budget_type"), + }, + "width": 15, + }, + "0140_project": { + "header": {"value": "Project"}, + "data": { + "value": self._render("project"), + }, + "width": 30, + }, + "0150_parent_project": { + "header": {"value": "Parent Project"}, + "data": { + "value": self._render("parent_project"), + }, + "width": 30, + }, + "0160_department": { + "header": {"value": "Department"}, + "data": { + "value": self._render("department"), + }, + "width": 30, + }, + "0170_note": { + "header": {"value": "Note"}, + "data": { + "value": self._render("note"), + }, + "width": 15, + }, + } + + def _get_ws_params(self, wb, data, obj): + export_budget_template = self._get_export_budget_vals(obj) + ws_params = { + "ws_name": "Budget Consumption Excel Report", + "generate_ws_method": "_export_budget_report", + "title": "Budget Consumption Report", + "wanted_list": [x for x in sorted(export_budget_template.keys())], + "col_specs": export_budget_template, + } + + return [ws_params] + + def _get_render_space(self, result): + # Find type + amount_type = [ + x["type"][1] + for x in self.env["budget.monitor.report"]._get_consumed_sources() + if x["type"][0] == result["amount_type"] + ] + # Find period on analytic + BudgetPeriod = self.env["budget.period"] + budget_period = BudgetPeriod.browse(result["budget_period_id"]) + # Find account + AccountAccount = self.env["account.account"] + account = AccountAccount.browse(result["account_id"]) + # Find analytic tag dimension + AnalyticTag = self.env["account.analytic.tag"] + budget_type = AnalyticTag.browse(result["x_dimension_budget_type"]) + return { + "date": result["date"] or "", + "type": amount_type[0] or "", + "analytic_period": budget_period.name or "", + "activity": result["activity_name"] or "", + "kpi_name": result["kpi_name"] or "", + "account_code": account.code or "", + "account_name": account.name or "", + "analytic_account": result["analytic_name"] or "", + "analytic_code": result["analytic_code"] or "", + "doc_name": result["doc_name"] or "", + "doc_ref_next": result["doc_ref_next"] or "", + "amount": result["debit"] or -1 * result["credit"] or 0, + "name": result["name"] or "", + "budget_type": budget_type.name or "", + "project": result["project_name"] or "", + "parent_project": result["parent_project_name"] or "", + "department": result["department_name"] or "", + "operating_unit": result["ou_name"] or "", + "fund": result["fund_name"] or "", + "fund_group": result["fund_group_name"] or "", + "note": result["note"] or "", + } + + def _get_header_data_list(self, obj): + amount_type = [ + x["type"][1] + for x in self.env["budget.monitor.report"]._get_consumed_sources() + if x["type"][0] == obj.amount_type + ] + return [ + ("Type", obj.amount_type and amount_type[0] or "-"), + ("Date From", obj.date_from and obj.date_from.strftime("%d/%m/%Y") or "-"), + ("Date To", obj.date_to and obj.date_to.strftime("%d/%m/%Y") or "-"), + ( + "Analytic", + obj.analytic_account_ids + and ", ".join(obj.analytic_account_ids.mapped("name")) + or "-", + ), + ] + + def _write_ws_header(self, row_pos, ws, data_list): + for data in data_list: + ws.write_row(row_pos, 0, [data[0]], FORMATS["format_theader_blue_left"]) + ws.merge_range(row_pos, 1, row_pos, 7, "") + ws.write_row(row_pos, 1, [data[1]]) + row_pos += 1 + return row_pos + 1 + + def _write_ws_lines(self, row_pos, ws, ws_params, results): + row_pos = self._write_line( + ws, + row_pos, + ws_params, + col_specs_section="header", + default_format=FORMATS["format_theader_blue_center"], + ) + ws.freeze_panes(row_pos, 0) + for result in results: + row_pos = self._write_line( + ws, + row_pos, + ws_params, + col_specs_section="data", + render_space=self._get_render_space(result), + default_format=FORMATS["format_tcell_left"], + ) + return row_pos + + def _write_ws_footer(self, row_pos, ws, results): + ws.merge_range(row_pos, 0, row_pos, 11, "") + ws.write_row(row_pos, 0, [""], FORMATS["format_theader_blue_right"]) + amount_total = sum( + result["debit"] or -1 * result["credit"] for result in results + ) + ws.write_row( + row_pos, + 12, + [amount_total], + FORMATS["format_theader_blue_amount_right"], + ) + ws.merge_range(row_pos, 13, row_pos, 20, "") + ws.write_row(row_pos, 13, [""], FORMATS["format_theader_blue_right"]) + return row_pos + + def _export_budget_report(self, workbook, ws, ws_params, data, obj): + ws.set_portrait() + ws.fit_to_pages(1, 0) + ws.set_header(XLS_HEADERS["xls_headers"]["standard"]) + ws.set_footer(XLS_HEADERS["xls_footers"]["standard"]) + self._set_column_width(ws, ws_params) + row_pos = 0 + header_data_list = self._get_header_data_list(obj) + results = self._get_query_budget_report(obj) + row_pos = self._write_ws_title(ws, row_pos, ws_params, merge_range=True) + row_pos = self._write_ws_header(row_pos, ws, header_data_list) + row_pos = self._write_ws_lines(row_pos, ws, ws_params, results) + row_pos = self._write_ws_footer(row_pos, ws, results) + return row_pos + + def _get_domain(self, obj): + domain = [] + self.env["account.analytic.tag"].search([]) + if obj.date_from: + domain.append("a.date >= '{}'".format(obj.date_from)) + if obj.date_to: + domain.append("a.date <= '{}'".format(obj.date_to)) + if obj.analytic_account_ids: + analytic = ( + len(obj.analytic_account_ids.ids) != 1 + and "in {}".format(tuple(obj.analytic_account_ids.ids)) + or "= {}".format(obj.analytic_account_ids.id) + ) + domain.append("a.analytic_account_id {}".format(analytic)) + return domain + + def _get_domain_where_query(self, obj): + domain = self._get_domain(obj) + # convert domain list to string + domain_str = domain and " and ".join(domain) or "" + domain_str = domain_str and "WHERE {}".format(domain_str) or "" + return domain_str + + def _get_doc_partner_table(self, budget_table): + """Expense, Advance, Clearing use employee field. + We need to change employee to partner + """ + doc_partner_table = "res_partner" + if budget_table in ("advance_budget_move", "expense_budget_move"): + doc_partner_table = "hr_employee" + return doc_partner_table + + def _get_doc_partner_field(self, budget_table): + doc_partner_field = "b.partner_id" + if budget_table in ("advance_budget_move", "expense_budget_move"): + doc_partner_field = "b.employee_id" + if budget_table == "purchase_request_budget_move": + doc_partner_field = "ru.partner_id" + if budget_table == "account_budget_move": + doc_partner_field = "aml.partner_id" + return doc_partner_field + + def _get_select_addon(self, budget_table): + """Addon field reference and next commit + - AV > CAV + - CAV or EX > Move + - PR > PO + - Actual (no next) + - CT > Move + - PO > Move + """ + select_addon = "" + if budget_table == "advance_budget_move": + select_addon = """ + exp.name, + CASE WHEN cav.number is not null THEN cav.number + ELSE am.name + END as doc_ref_next + """ + if budget_table == "expense_budget_move": + select_addon = "exp.name, am.name as doc_ref_next" + if budget_table == "purchase_request_budget_move": + select_addon = "prl.name, po.name as doc_ref_next" + if budget_table == "account_budget_move": + select_addon = "aml.name, null::char as doc_ref_next" + if budget_table == "contract_budget_move": + select_addon = "b.name, am.name as doc_ref_next" + if budget_table == "purchase_budget_move": + select_addon = "pol.name, am.name as doc_ref_next" + return select_addon + + def _get_table_addon(self, budget_table): + table_addon = "" + if budget_table == "advance_budget_move": + table_addon = """ + LEFT OUTER JOIN hr_expense exp on exp.id = a.expense_id + LEFT OUTER JOIN hr_expense ex_cav on ex_cav.id = a.clearing_id + LEFT OUTER JOIN hr_expense_sheet cav on cav.id = ex_cav.sheet_id + LEFT OUTER JOIN account_move_line aml on aml.id = a.move_line_id + LEFT OUTER JOIN account_move am on am.id = aml.move_id + """ + if budget_table == "expense_budget_move": + table_addon = """ + LEFT OUTER JOIN hr_expense exp on exp.id = a.expense_id + LEFT OUTER JOIN account_move_line aml on aml.id = a.move_line_id + LEFT OUTER JOIN account_move am on am.id = aml.move_id + """ + if budget_table == "purchase_request_budget_move": + table_addon = """ + LEFT OUTER JOIN res_users ru on ru.id = b.requested_by + LEFT OUTER JOIN purchase_request_line prl + on prl.id = a.purchase_request_line_id + LEFT OUTER JOIN purchase_order_line pol + on pol.id = a.purchase_line_id + LEFT OUTER JOIN purchase_order po + on po.id = pol.order_id + """ + if budget_table == "account_budget_move": + table_addon = ( + "LEFT OUTER JOIN account_move_line aml on aml.id = a.move_line_id" + ) + if budget_table == "contract_budget_move": + table_addon = """ + LEFT OUTER JOIN account_move am on am.id = a.move_id + """ + if budget_table == "purchase_budget_move": + table_addon = """ + LEFT OUTER JOIN purchase_order_line pol + on pol.id = a.purchase_line_id + LEFT OUTER JOIN account_move am on am.id = a.move_id + """ + return table_addon + + def _get_budget_consumed(self, consumed_sources, domain_str): + sql_select = {} + for source in consumed_sources: + amount_type = source["type"][0] # i.e., 8_actual + budget_table = source["budget_move"][0] # i.e., account_budget_move + doc_table = source["source_doc"][0] # i.e., account_move + doc_field = source["source_doc"][1] # i.e., move_id + doc_partner_table = self._get_doc_partner_table(budget_table) + doc_partner_field = self._get_doc_partner_field(budget_table) + select_addon = self._get_select_addon(budget_table) + table_addon = self._get_table_addon(budget_table) + + sql_select[ + amount_type + ] = """ + SELECT {}000000000 + a.id as id, '{}' as amount_type, a.reference as doc_name, + {}, a.debit, a.credit, c.name as partner, a.date, a.account_id, + aa.code as analytic_code, aa.name as analytic_name, + aa.budget_period_id as budget_period_id, + ba.name as activity_name, kpi.name as kpi_name, + pj.name as project_name, pj.parent_project_name, + d.name as department_name, ou.name as ou_name, + f.name as fund_name, fg.name as fund_group_name, + a.x_dimension_budget_type, + a.note + FROM {} a + LEFT OUTER JOIN {} b on b.id = a.{} + {} + LEFT OUTER JOIN {} c on c.id = {} + LEFT OUTER JOIN budget_activity ba on ba.id = a.activity_id + LEFT OUTER JOIN budget_kpi kpi + on kpi.id = ba.kpi_id + LEFT OUTER JOIN account_analytic_account aa + on aa.id = a.analytic_account_id + LEFT OUTER JOIN res_project pj on pj.id = aa.project_id + LEFT OUTER JOIN hr_department d on d.id = a.department_id + LEFT OUTER JOIN operating_unit ou on ou.id = a.operating_unit_id + LEFT OUTER JOIN budget_source_fund f on f.id = a.fund_id + LEFT OUTER JOIN budget_source_fund_group fg on fg.id = a.fund_group_id + {} + """.format( + amount_type[:1], + amount_type, + select_addon, + budget_table, + doc_table, + doc_field, + table_addon, + doc_partner_table, + doc_partner_field, + domain_str, + ) + return sql_select + + def _get_query_budget_report(self, obj): + domain_str = self._get_domain_where_query(obj) + consumed_sources = self.env["budget.monitor.report"]._get_consumed_sources() + if obj.amount_type: + consumed_sources = [ + x for x in consumed_sources if x["type"][0] == obj.amount_type + ] + + sql_select = self._get_budget_consumed(consumed_sources, domain_str) + query = [sql_select.get(x["type"][0]) for x in consumed_sources] + all_query = "UNION".join(query) + self.env.cr.execute( + sql.SQL( + """ + SELECT * + FROM ({}) consumption + ORDER BY consumption.amount_type, + consumption.doc_name, consumption.date + """.format( + all_query + ) + ) + ) + return self.env.cr.dictfetchall() diff --git a/budget_control_report/templates/report_budget_export_xlsx.py b/budget_control_report/templates/report_budget_export_xlsx.py new file mode 100644 index 00000000..efbf67c7 --- /dev/null +++ b/budget_control_report/templates/report_budget_export_xlsx.py @@ -0,0 +1,559 @@ +# Copyright 2021 Ecosoft Co., Ltd (https://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from psycopg2 import sql + +from odoo import models + +from odoo.addons.report_xlsx_helper.report.report_xlsx_format import ( + FORMATS, + XLS_HEADERS, +) + + +class BudgetReportExportXslx(models.AbstractModel): + _name = "report.budget.report.wizard.xlsx" + _inherit = "report.report_xlsx.abstract" + _description = "Budget Export xlsx" + + def _get_export_budget_vals(self, obj): + return { + "0010_budget_type": { + "header": {"value": "Budget Type"}, + "data": { + "value": self._render("budget_type"), + }, + "width": 18, + }, + "0020_analytic_budget_period": { + "header": {"value": "Analytic Budget Period"}, + "data": { + "value": self._render("analytic_budget_period"), + }, + "width": 25, + }, + "0030_ref_analytic_account": { + "header": {"value": "Analytic Code"}, + "data": { + "value": self._render("analytic_account_code"), + }, + "width": 15, + }, + "0040_analytic_account": { + "header": {"value": "Analytic Account"}, + "data": { + "value": self._render("analytic_account_name"), + }, + "width": 25, + }, + "0050_parent_project": { + "header": {"value": "Parent Project"}, + "data": { + "value": self._render("parent_project"), + }, + "width": 20, + }, + "0060_project_code": { + "header": {"value": "Project Code"}, + "data": { + "value": self._render("project_code"), + }, + "width": 15, + }, + "0070_project": { + "header": {"value": "Project"}, + "data": { + "value": self._render("project_name"), + }, + "width": 25, + }, + "0080_department_code": { + "header": {"value": "Department Code"}, + "data": { + "value": self._render("department_code"), + }, + "width": 15, + }, + "0090_department": { + "header": {"value": "Department"}, + "data": { + "value": self._render("department_name"), + }, + "width": 20, + }, + "0100_estimated_amount": { + "header": {"value": "Estimated Amount"}, + "data": { + "value": self._render("estimated_amount"), + "format": FORMATS["format_tcell_amount_right"], + }, + "width": 20, + }, + "0110_budget_amount": { + "header": {"value": "Budget Amount"}, + "data": { + "value": self._render("budget_amount"), + "format": FORMATS["format_tcell_amount_right"], + }, + "width": 15, + }, + "16_pr_commit": { + "header": {"value": "PR Commit"}, + "data": { + "value": self._render("pr_commit"), + "format": FORMATS["format_tcell_amount_right"], + }, + "width": 15, + }, + "17_po_commit": { + "header": {"value": "PO Commit"}, + "data": { + "value": self._render("po_commit"), + "format": FORMATS["format_tcell_amount_right"], + }, + "width": 15, + }, + "18_ct_commit": { + "header": {"value": "CT Commit"}, + "data": { + "value": self._render("ct_commit"), + "format": FORMATS["format_tcell_amount_right"], + }, + "width": 15, + }, + "19_av_commit": { + "header": {"value": "AV Commit"}, + "data": { + "value": self._render("av_commit"), + "format": FORMATS["format_tcell_amount_right"], + }, + "width": 15, + }, + "20_ex_commit": { + "header": {"value": "EX Commit"}, + "data": { + "value": self._render("ex_commit"), + "format": FORMATS["format_tcell_amount_right"], + }, + "width": 15, + }, + "21_actual": { + "header": {"value": "Actual"}, + "data": { + "value": self._render("actual_commit"), + "format": FORMATS["format_tcell_amount_right"], + }, + "width": 15, + }, + "22_total_amount": { + "header": {"value": "Total"}, + "data": { + "value": self._render("total_amount"), + "format": FORMATS["format_tcell_amount_right"], + }, + "width": 15, + }, + "23_total_available": { + "header": {"value": "Available"}, + "data": { + "value": self._render("total_available"), + "format": FORMATS["format_tcell_amount_right"], + }, + "width": 15, + }, + } + + def _get_ws_params(self, wb, data, obj): + export_budget_template = self._get_export_budget_vals(obj) + ws_params = { + "ws_name": "Budget Excel Report", + "generate_ws_method": "_export_budget_report", + "title": "Budget Report", + "wanted_list": [x for x in sorted(export_budget_template.keys())], + "col_specs": export_budget_template, + } + + return [ws_params] + + def _filter_commitment(self, obj, result): + obj_filter = obj.filtered( + lambda l: l.analytic_account_id.name == result["analytic_account_name"] + and l.fund_id.name == result["fund_name"] + and l.x_dimension_budget_type.id == result["x_dimension_budget_type"] + ) + return obj_filter + + def _get_domain_budget_move(self, obj): + dom = [] + if obj.date_from: + dom.append(("date", ">=", obj.date_from)) + if obj.date_to: + dom.append(("date", "<=", obj.date_to)) + return dom + + def _get_amount_consumtion(self, results, obj): + pr_total = ( + po_total + ) = ( + av_total + ) = ( + ct_total + ) = ( + ex_total + ) = ( + actual_total + ) = ( + total_amount + ) = total_available = total_budget_amount = total_estimated_amount = 0.0 + PurchaseRequestBudgetMove = self.env["purchase.request.budget.move"] + PurchaseBudgetMove = self.env["purchase.budget.move"] + ContractBudgetMove = self.env["contract.budget.move"] + AdvanceBudgetMove = self.env["advance.budget.move"] + ExpenseBudgetMove = self.env["expense.budget.move"] + ActualBudgetMove = self.env["account.budget.move"] + dom_budget_move = self._get_domain_budget_move(obj) + for result in results: + pr_budget_move = PurchaseRequestBudgetMove.search(dom_budget_move) + po_budget_move = PurchaseBudgetMove.search(dom_budget_move) + ct_budget_move = ContractBudgetMove.search(dom_budget_move) + av_budget_move = AdvanceBudgetMove.search(dom_budget_move) + ex_budget_move = ExpenseBudgetMove.search(dom_budget_move) + actual_budget_move = ActualBudgetMove.search(dom_budget_move) + pr_budget_move = self._filter_commitment(pr_budget_move, result) + pr_commit = sum(pr_budget_move.mapped("debit")) - sum( + pr_budget_move.mapped("credit") + ) + po_budget_move = self._filter_commitment(po_budget_move, result) + po_commit = sum(po_budget_move.mapped("debit")) - sum( + po_budget_move.mapped("credit") + ) + ct_budget_move = self._filter_commitment(ct_budget_move, result) + ct_commit = sum(ct_budget_move.mapped("debit")) - sum( + ct_budget_move.mapped("credit") + ) + av_budget_move = self._filter_commitment(av_budget_move, result) + av_commit = sum(av_budget_move.mapped("debit")) - sum( + av_budget_move.mapped("credit") + ) + ex_budget_move = self._filter_commitment(ex_budget_move, result) + ex_commit = sum(ex_budget_move.mapped("debit")) - sum( + ex_budget_move.mapped("credit") + ) + actual_budget_move = self._filter_commitment(actual_budget_move, result) + actual_commit = sum(actual_budget_move.mapped("debit")) - sum( + actual_budget_move.mapped("credit") + ) + consumtion_commit = ( + actual_commit + + pr_commit + + po_commit + + ct_commit + + av_commit + + ex_commit + ) + + pr_total += pr_commit + po_total += po_commit + ct_total += ct_commit + av_total += av_commit + ex_total += ex_commit + actual_total += actual_commit + total_amount += consumtion_commit + total_available += result["released_amount"] - consumtion_commit + total_budget_amount += result["released_amount"] + total_estimated_amount += result["estimated_amount"] + return ( + pr_total, + po_total, + ct_total, + av_total, + ex_total, + actual_total, + total_amount, + total_available, + total_budget_amount, + total_estimated_amount, + ) + + def _get_render_space(self, result, budget_moves): + # Find analytic tag dimension + AnalyticTag = self.env["account.analytic.tag"] + budget_type = AnalyticTag.browse(result["x_dimension_budget_type"]) + # Find period on analytic + BudgetPeriod = self.env["budget.period"] + analytic_budget_period = BudgetPeriod.browse( + result["analytic_budget_period_id"] + ) + # pr_budget_move = self.env["purchase.request.budget.move"].search( + # dom_budget_move + # ) + pr_budget_move = budget_moves["pr_budget_move"] + pr_budget_move = self._filter_commitment(pr_budget_move, result) + pr_commit = sum(pr_budget_move.mapped("debit")) - sum( + pr_budget_move.mapped("credit") + ) + + # po_budget_move = self.env["purchase.budget.move"].search(dom_budget_move) + po_budget_move = budget_moves["po_budget_move"] + po_budget_move = self._filter_commitment(po_budget_move, result) + po_commit = sum(po_budget_move.mapped("debit")) - sum( + po_budget_move.mapped("credit") + ) + # ct_budget_move = self.env["contract.budget.move"].search(dom_budget_move) + ct_budget_move = budget_moves["ct_budget_move"] + ct_budget_move = self._filter_commitment(ct_budget_move, result) + ct_commit = sum(ct_budget_move.mapped("debit")) - sum( + ct_budget_move.mapped("credit") + ) + # av_budget_move = self.env["advance.budget.move"].search(dom_budget_move) + av_budget_move = budget_moves["av_budget_move"] + av_budget_move = self._filter_commitment(av_budget_move, result) + av_commit = sum(av_budget_move.mapped("debit")) - sum( + av_budget_move.mapped("credit") + ) + + # ex_budget_move = self.env["expense.budget.move"].search(dom_budget_move) + ex_budget_move = budget_moves["ex_budget_move"] + ex_budget_move = self._filter_commitment(ex_budget_move, result) + ex_commit = sum(ex_budget_move.mapped("debit")) - sum( + ex_budget_move.mapped("credit") + ) + + # actual_budget_move = self.env["account.budget.move"].search(dom_budget_move) + actual_budget_move = budget_moves["actual_budget_move"] + actual_budget_move = self._filter_commitment(actual_budget_move, result) + actual_commit = sum(actual_budget_move.mapped("debit")) - sum( + actual_budget_move.mapped("credit") + ) + + total_amount = ( + actual_commit + pr_commit + po_commit + ct_commit + av_commit + ex_commit + ) + total_available = result["released_amount"] - total_amount + + # NOTE: Move it out of loop for performance + analytic_account_id = self.env["account.analytic.account"].browse( + result["analytic_id"] + ) + return { + "budget_type": budget_type.name, + "analytic_budget_period": analytic_budget_period.name, + "analytic_account_code": result["analytic_account_code"], + "analytic_account_name": result["analytic_account_name"], + "operating_unit_code": analytic_account_id.operating_unit_ids.code, + "operating_unit_name": analytic_account_id.operating_unit_ids.name, + "parent_project": result["parent_project_name"], + "project_code": result["project_code"], + "project_name": result["project_name"], + "department_code": result["department_code"], + "department_name": result["department_name"], + "fund_name": result["fund_name"], + "fund_group_name": result["fund_group_name"], + "estimated_amount": result["estimated_amount"], + "budget_amount": result["released_amount"], + "actual_commit": actual_commit, + "pr_commit": pr_commit, + "po_commit": po_commit, + "ct_commit": ct_commit, + "av_commit": av_commit, + "ex_commit": ex_commit, + "total_amount": total_amount, + "total_available": total_available, + } + + def _get_header_data_list(self, obj): + return [ + ("Budget Period", obj.budget_period_id.name or "-"), + ("Date From", obj.date_from and obj.date_from.strftime("%d/%m/%Y") or "-"), + ("Date To", obj.date_to and obj.date_to.strftime("%d/%m/%Y") or "-"), + # ( + # "Budget Type", + # obj.budget_type_ids and ", ".join(obj.budget_type_ids.mapped("name")) or "-", + # ), + ("Fund", obj.fund_ids and ", ".join(obj.fund_ids.mapped("name")) or "-"), + ( + "Analytic", + obj.analytic_account_ids + and ", ".join(obj.analytic_account_ids.mapped("name")) + or "-", + ), + ] + + def _write_ws_header(self, row_pos, ws, data_list): + for data in data_list: + ws.write_row(row_pos, 0, [data[0]], FORMATS["format_theader_blue_left"]) + ws.merge_range(row_pos, 1, row_pos, 7, "") + ws.write_row(row_pos, 1, [data[1]]) + row_pos += 1 + return row_pos + 1 + + def _write_ws_lines(self, row_pos, ws, ws_params, results, obj): + row_pos = self._write_line( + ws, + row_pos, + ws_params, + col_specs_section="header", + default_format=FORMATS["format_theader_blue_center"], + ) + ws.freeze_panes(row_pos, 0) + dom_budget_move = self._get_domain_budget_move(obj) + pr_budget_move = self.env["purchase.request.budget.move"].search( + dom_budget_move + ) + po_budget_move = self.env["purchase.budget.move"].search(dom_budget_move) + ct_budget_move = self.env["contract.budget.move"].search(dom_budget_move) + av_budget_move = self.env["advance.budget.move"].search(dom_budget_move) + ex_budget_move = self.env["expense.budget.move"].search(dom_budget_move) + actual_budget_move = self.env["account.budget.move"].search(dom_budget_move) + + budget_moves = { + "pr_budget_move": pr_budget_move, + "po_budget_move": po_budget_move, + "ct_budget_move": ct_budget_move, + "av_budget_move": av_budget_move, + "ex_budget_move": ex_budget_move, + "actual_budget_move": actual_budget_move, + } + + for result in results: + row_pos = self._write_line( + ws, + row_pos, + ws_params, + col_specs_section="data", + render_space=self._get_render_space(result, budget_moves), + default_format=FORMATS["format_tcell_left"], + ) + return row_pos + + def _write_ws_footer(self, row_pos, ws, results, obj): + ( + pr_total, + po_total, + ct_total, + av_total, + ex_total, + actual_total, + total_amount, + total_available, + total_budget_amount, + total_estimated_amount, + ) = self._get_amount_consumtion(results, obj) + ws.merge_range(row_pos, 0, row_pos, 12, "") + ws.write_row(row_pos, 0, [""], FORMATS["format_theader_blue_right"]) + ws.write_row( + row_pos, + 13, + [ + total_estimated_amount, + total_budget_amount, + pr_total, + po_total, + ct_total, + av_total, + ex_total, + actual_total, + total_amount, + total_available, + ], + FORMATS["format_theader_blue_amount_right"], + ) + return row_pos + + def _export_budget_report(self, workbook, ws, ws_params, data, obj): + ws.set_portrait() + ws.fit_to_pages(1, 0) + ws.set_header(XLS_HEADERS["xls_headers"]["standard"]) + ws.set_footer(XLS_HEADERS["xls_footers"]["standard"]) + self._set_column_width(ws, ws_params) + row_pos = 0 + header_data_list = self._get_header_data_list(obj) + results = self._get_query_budget_report(obj) + row_pos = self._write_ws_title(ws, row_pos, ws_params, merge_range=True) + row_pos = self._write_ws_header(row_pos, ws, header_data_list) + row_pos = self._write_ws_lines(row_pos, ws, ws_params, results, obj) + row_pos = self._write_ws_footer(row_pos, ws, results, obj) + return row_pos + + def _get_budget_period(self, date): + return self.env["budget.period"].search( + [("bm_date_from", "<=", date), ("bm_date_to", ">=", date)] + ) + + def _get_domain_where_query(self, obj): + domain = [] + self.env["account.analytic.tag"].search([]) + if obj.budget_period_id: + domain.append("bal.budget_period_id = {}".format(obj.budget_period_id.id)) + period_from = self._get_budget_period(obj.date_from) + period_to = self._get_budget_period(obj.date_to) + periods = period_from + period_to + if periods: + if len(tuple(periods.ids)) > 1: + domain.append("bal.budget_period_id in {}".format(tuple(periods.ids))) + else: + domain.append("bal.budget_period_id = {}".format(periods.id)) + # if obj.budget_type_ids: + # budget_type = ( + # len(obj.budget_type_ids.ids) != 1 + # and "in {}".format(tuple(obj.budget_type_ids.ids)) + # or "= {}".format(obj.budget_type_ids.id) + # ) + # domain.append("bal.x_dimension_budget_type {}".format(budget_type)) + if obj.analytic_account_ids: + analytic = ( + len(obj.analytic_account_ids.ids) != 1 + and "in {}".format(tuple(obj.analytic_account_ids.ids)) + or "= {}".format(obj.analytic_account_ids.id) + ) + domain.append("bal.analytic_account_id {}".format(analytic)) + if obj.fund_ids: + fund = ( + len(obj.fund_ids.ids) != 1 + and "in {}".format(tuple(obj.fund_ids.ids)) + or "= {}".format(obj.fund_ids.id) + ) + domain.append("bal.fund_id {}".format(fund)) + # convert domain list to string + domain_str = domain and " and ".join(domain) or "" + domain_str = domain_str and "WHERE {}".format(domain_str) or "" + return domain_str + + def _get_query_budget_report(self, obj): + domain_str = self._get_domain_where_query(obj) + self.env.cr.execute( + sql.SQL( + """ + SELECT bal.x_dimension_budget_type, + CASE WHEN bal.released_amount is not null + THEN bal.released_amount + ELSE 0 + END as released_amount, + bal.estimated_amount as estimated_amount, + aa.id as analytic_id, + aa.budget_period_id as analytic_budget_period_id, + aa.code as analytic_account_code, + aa.name as analytic_account_name, + sf.name as fund_name, + sf_group.name as fund_group_name, + rp.parent_project_name, + rp.code as project_code, + rp.name as project_name, + hr.name as department_name, + hr.code as department_code + FROM budget_allocation_line bal + LEFT JOIN budget_allocation ba on ba.id = bal.budget_allocation_id + LEFT JOIN account_analytic_account aa on aa.id = bal.analytic_account_id + LEFT JOIN res_project rp on rp.id = aa.project_id + LEFT JOIN hr_department hr on hr.id = aa.department_id + LEFT JOIN budget_source_fund sf on sf.id = bal.fund_id + LEFT JOIN budget_source_fund_group sf_group on sf_group.id = sf.fund_group_id + LEFT JOIN budget_period bp on bp.id = bal.budget_period_id + {} + ORDER BY bal.x_dimension_budget_type + """.format( + domain_str + ) + ) + ) + return self.env.cr.dictfetchall() diff --git a/budget_control_report/wizard/__init__.py b/budget_control_report/wizard/__init__.py new file mode 100644 index 00000000..5da343bc --- /dev/null +++ b/budget_control_report/wizard/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import budget_report_wizard +from . import budget_consumption_report diff --git a/budget_control_report/wizard/budget_consumption_report.py b/budget_control_report/wizard/budget_consumption_report.py new file mode 100644 index 00000000..b6abdfed --- /dev/null +++ b/budget_control_report/wizard/budget_consumption_report.py @@ -0,0 +1,30 @@ +# Copyright 2025 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class BudgetConsumptionReportWizard(models.TransientModel): + _name = "budget.consumption.report.wizard" + _inherit = "budget.report.wizard" + _description = "Budget Consumption Report Wizard" + + amount_type = fields.Selection( + selection=lambda self: self.env[ + "budget.monitor.report" + ]._get_budget_amount_type(), + string="Type", + ) + + def _get_report_base_filename(self): + if self.report_type == "consumption": + period = "{} - ".format(self.budget_period_id.name) + return "{}BudgetConsumptionReport".format( + self.budget_period_id and period or "" + ) + return super()._get_report_base_filename() + + def _get_view_report(self): + if self.report_type == "consumption": + return "budget_control_report.action_export_budget_consumption_xlsx" + return super()._get_view_report() diff --git a/budget_control_report/wizard/budget_consumption_report_view.xml b/budget_control_report/wizard/budget_consumption_report_view.xml new file mode 100644 index 00000000..57e3bd65 --- /dev/null +++ b/budget_control_report/wizard/budget_consumption_report_view.xml @@ -0,0 +1,35 @@ + + + + budget.consumption.report.wizard.form + budget.consumption.report.wizard + + + primary + + + + + + + + Budget Consumption Report (.xlsx) + budget.consumption.report.wizard + form + {'default_report_type': 'consumption', 'access_sudo': 1} + new + + + diff --git a/budget_control_report/wizard/budget_report_view.xml b/budget_control_report/wizard/budget_report_view.xml new file mode 100644 index 00000000..1fff5940 --- /dev/null +++ b/budget_control_report/wizard/budget_report_view.xml @@ -0,0 +1,62 @@ + + + + budget.report.wizard.form + budget.report.wizard + +
+ + + + + + + + + + + + + + + +
+
+
+
+
+ + Budget Report (.xlsx) + budget.report.wizard + form + {'access_sudo': 1} + new + + + + + +
diff --git a/budget_control_report/wizard/budget_report_wizard.py b/budget_control_report/wizard/budget_report_wizard.py new file mode 100644 index 00000000..03c93db5 --- /dev/null +++ b/budget_control_report/wizard/budget_report_wizard.py @@ -0,0 +1,42 @@ +# Copyright 2025 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class BudgetReportWizard(models.TransientModel): + _name = "budget.report.wizard" + _description = "Budget Report Wizard" + + budget_period_id = fields.Many2one( + comodel_name="budget.period", + ) + report_type = fields.Selection( + selection=[ + ("budget", "Budget"), + ("consumption", "Consumption"), + ], + default="budget", + ) + date_from = fields.Date() + date_to = fields.Date() + analytic_account_ids = fields.Many2many( + comodel_name="account.analytic.account", + ) + + @api.onchange("budget_period_id") + def onchange_budget_period_id(self): + self.date_from = self.budget_period_id.bm_date_from + self.date_to = self.budget_period_id.bm_date_to + + def _get_report_base_filename(self): + period = "{} - ".format(self.budget_period_id.name) + return "{}BudgetReport".format(self.budget_period_id and period or "") + + def _get_view_report(self): + """Hooks this function to add action report""" + return "budget_control_report.action_export_budget_xlsx" + + def run_report_excel(self): + view_report = self._get_view_report() + return self.env.ref(view_report).sudo().report_action(self, config=False) diff --git a/setup/budget_control_report/odoo/addons/budget_control_report b/setup/budget_control_report/odoo/addons/budget_control_report new file mode 120000 index 00000000..be4a48b8 --- /dev/null +++ b/setup/budget_control_report/odoo/addons/budget_control_report @@ -0,0 +1 @@ +../../../../budget_control_report \ No newline at end of file diff --git a/setup/budget_control_report/setup.py b/setup/budget_control_report/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/budget_control_report/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)