diff --git a/budget_control/models/budget_period.py b/budget_control/models/budget_period.py index 6d9105dc..ebde2247 100644 --- a/budget_control/models/budget_period.py +++ b/budget_control/models/budget_period.py @@ -260,6 +260,8 @@ def check_budget_precommit(self, doclines, doc_type="account"): budget_move = line.with_context(force_commit=True).commit_budget() if budget_move: budget_moves.append(budget_move) + # Update database, so we can check budget with query + budget_move.flush_model() # Check Budget self.env["budget.period"].check_budget(doclines, doc_type=doc_type) # Remove commits diff --git a/budget_control/views/account_move_views.xml b/budget_control/views/account_move_views.xml index 910b0370..b71ccb1e 100644 --- a/budget_control/views/account_move_views.xml +++ b/budget_control/views/account_move_views.xml @@ -46,6 +46,8 @@ expr="//field[@name='invoice_line_ids']/tree/field[@name='analytic_distribution']" position="after" > + + @@ -77,6 +80,8 @@ expr="//page[@id='aml_tab']/field[@name='line_ids']/tree/field[@name='analytic_distribution']" position="after" > + + diff --git a/budget_control_expense/README.rst b/budget_control_expense/README.rst new file mode 100644 index 00000000..0bca057c --- /dev/null +++ b/budget_control_expense/README.rst @@ -0,0 +1,82 @@ +========================= +Budget Control on Expense +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:706da40b770c239865c0e98f26db39077822f0f37c7c3c33a5c580b3df043c93 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/16.0/budget_control_expense + :alt: ecosoft-odoo/budgeting + +|badge1| |badge2| |badge3| + +This module will create budget commitment for expense (to be used as alternate actual source in mis_builder) + +When expense report is approved, hr_expense.budget.commit is created, and when +journal entry is posted, reversed hr_expense.budget.commit is created. + +A new tab "Budget Commitment" is created on expense report for budget user to keep track of the committed budget. + +.. 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 +~~~~~~~~~~~~ + +* Kitti Upariphutthiphong +* Saran Lim. + +Maintainers +~~~~~~~~~~~ + +.. |maintainer-kittiu| image:: https://github.com/kittiu.png?size=40px + :target: https://github.com/kittiu + :alt: kittiu +.. |maintainer-ru3ix-bbb| image:: https://github.com/ru3ix-bbb.png?size=40px + :target: https://github.com/ru3ix-bbb + :alt: ru3ix-bbb + +Current maintainers: + +|maintainer-kittiu| |maintainer-ru3ix-bbb| + +This module is part of the `ecosoft-odoo/budgeting `_ project on GitHub. + +You are welcome to contribute. diff --git a/budget_control_expense/__init__.py b/budget_control_expense/__init__.py new file mode 100644 index 00000000..1bfac206 --- /dev/null +++ b/budget_control_expense/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models +from . import report +from . import wizard diff --git a/budget_control_expense/__manifest__.py b/budget_control_expense/__manifest__.py new file mode 100644 index 00000000..78d5293f --- /dev/null +++ b/budget_control_expense/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2021 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Budget Control on Expense", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "Ecosoft, Odoo Community Association (OCA)", + "website": "https://github.com/ecosoft-odoo/budgeting", + "depends": ["budget_control", "hr_expense"], + "data": [ + "security/ir.model.access.csv", + "views/expense_budget_move.xml", + "views/budget_period_view.xml", + "views/hr_expense_view.xml", + "views/budget_control_view.xml", + "views/budget_commit_forward_view.xml", + ], + "installable": True, + "maintainers": ["kittiu", "ru3ix-bbb"], + "development_status": "Alpha", +} diff --git a/budget_control_expense/models/__init__.py b/budget_control_expense/models/__init__.py new file mode 100644 index 00000000..3eb0b116 --- /dev/null +++ b/budget_control_expense/models/__init__.py @@ -0,0 +1,10 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import account_budget_move +from . import expense_budget_move +from . import budget_period +from . import account_move +from . import account_move_line +from . import hr_expense +from . import budget_control +from . import budget_commit_forward diff --git a/budget_control_expense/models/account_budget_move.py b/budget_control_expense/models/account_budget_move.py new file mode 100644 index 00000000..b912d138 --- /dev/null +++ b/budget_control_expense/models/account_budget_move.py @@ -0,0 +1,19 @@ +# Copyright 2020 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class AccountBudgetMove(models.Model): + _inherit = "account.budget.move" + + @api.depends("move_id") + def _compute_source_document(self): + res = super()._compute_source_document() + for rec in self.filtered("move_line_id.expense_id.sheet_id"): + if hasattr(rec.move_line_id.expense_id.sheet_id, "number"): + display_name = rec.move_line_id.expense_id.sheet_id.number + else: + display_name = rec.move_line_id.expense_id.sheet_id.display_name + rec.source_document = rec.source_document or display_name + return res diff --git a/budget_control_expense/models/account_move.py b/budget_control_expense/models/account_move.py new file mode 100644 index 00000000..a653d65a --- /dev/null +++ b/budget_control_expense/models/account_move.py @@ -0,0 +1,15 @@ +# Copyright 2020 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class AccountMove(models.Model): + _inherit = "account.move" + + def write(self, vals): + """Uncommit budget for source expense document.""" + res = super().write(vals) + if vals.get("state") in ("draft", "posted", "cancel"): + self.mapped("line_ids.expense_id").recompute_budget_move() + return res diff --git a/budget_control_expense/models/account_move_line.py b/budget_control_expense/models/account_move_line.py new file mode 100644 index 00000000..2f44d8a7 --- /dev/null +++ b/budget_control_expense/models/account_move_line.py @@ -0,0 +1,46 @@ +# Copyright 2020 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + def _init_docline_budget_vals(self, budget_vals, analytic_id): + self.ensure_one() + res = super()._init_docline_budget_vals(budget_vals, analytic_id) + expense = self.expense_id + if expense: # case expense (support with include tax) + budget_vals["amount_currency"] = ( + (expense.quantity * expense.unit_amount) + if expense.product_has_cost + else expense.total_amount + ) + return res + + def uncommit_expense_budget(self): + """Uncommit the budget for related expenses when the vendor bill is in a valid state.""" + Expense = self.env["hr.expense"] + for ml in self: + inv_state = ml.move_id.state + if not ml.move_id.expense_sheet_id: + continue + if inv_state == "posted": + expense = ml.expense_id.filtered("amount_commit") + # Because this is not invoice, we need to compare account + if not expense: + continue + # Also test for future advance extension, never uncommit for advance + if hasattr(expense, "advance") and expense["advance"]: + continue + expense.commit_budget( + reverse=True, + move_line_id=ml.id, + date=ml.date_commit, + analytic_distribution=expense.fwd_analytic_distribution or False, + ) + else: # Cancel or draft, not commitment line + self.env[Expense._budget_model()].search( + [("move_line_id", "=", ml.id)] + ).unlink() diff --git a/budget_control_expense/models/budget_commit_forward.py b/budget_control_expense/models/budget_commit_forward.py new file mode 100644 index 00000000..7c92972d --- /dev/null +++ b/budget_control_expense/models/budget_commit_forward.py @@ -0,0 +1,53 @@ +# Copyright 2020 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class BudgetCommitForward(models.Model): + _inherit = "budget.commit.forward" + + expense = fields.Boolean( + default=True, + help="If checked, click review budget commitment will pull expense commitment", + ) + forward_expense_ids = fields.One2many( + comodel_name="budget.commit.forward.line", + inverse_name="forward_id", + string="Expenses", + domain=[("res_model", "=", "hr.expense")], + ) + + def _get_budget_docline_model(self): + res = super()._get_budget_docline_model() + if self.expense: + res.append("hr.expense") + return res + + def _get_document_number(self, doc): + if doc._name == "hr.expense": + return f"{doc.sheet_id._name},{doc.sheet_id.id}" + return super()._get_document_number(doc) + + def _get_base_domain_extension(self, res_model): + """For module extension""" + if res_model == "hr.expense": + return " AND a.state != 'cancel'" + return super()._get_base_domain_extension(res_model) + + +class BudgetCommitForwardLine(models.Model): + _inherit = "budget.commit.forward.line" + + res_model = fields.Selection( + selection_add=[("hr.expense", "Expense")], + ondelete={"hr.expense": "cascade"}, + ) + document_id = fields.Reference( + selection_add=[("hr.expense", "Expense")], + ondelete={"hr.expense": "cascade"}, + ) + document_number = fields.Reference( + selection_add=[("hr.expense.sheet", "Expense Sheet")], + ondelete={"hr.expense.sheet": "cascade"}, + ) diff --git a/budget_control_expense/models/budget_control.py b/budget_control_expense/models/budget_control.py new file mode 100644 index 00000000..56a5fb3c --- /dev/null +++ b/budget_control_expense/models/budget_control.py @@ -0,0 +1,14 @@ +# Copyright 2021 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class BudgetControl(models.Model): + _inherit = "budget.control" + + amount_expense = fields.Monetary( + string="Expense", + compute="_compute_budget_info", + help="Sum of expense amount", + ) diff --git a/budget_control_expense/models/budget_period.py b/budget_control_expense/models/budget_period.py new file mode 100644 index 00000000..b1ab18b9 --- /dev/null +++ b/budget_control_expense/models/budget_period.py @@ -0,0 +1,38 @@ +# Copyright 2020 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class BudgetPeriod(models.Model): + _inherit = "budget.period" + + expense = fields.Boolean( + string="On Expense", + compute="_compute_control_expense", + store=True, + readonly=False, + help="Control budget on expense approved", + ) + + def _budget_info_query(self): + query = super()._budget_info_query() + query["info_cols"]["amount_expense"] = ("5_ex_commit", True) + return query + + @api.depends("control_budget") + def _compute_control_expense(self): + for rec in self: + rec.expense = rec.control_budget + + @api.model + def _get_eligible_budget_period(self, date=False, doc_type=False): + budget_period = super()._get_eligible_budget_period(date, doc_type) + # Get period control budget. + # if doctype is expense, check special control too. + if doc_type == "expense": + return budget_period.filtered( + lambda l: (l.control_budget and l.expense) + or (not l.control_budget and l.expense) + ) + return budget_period diff --git a/budget_control_expense/models/expense_budget_move.py b/budget_control_expense/models/expense_budget_move.py new file mode 100644 index 00000000..db3b6d98 --- /dev/null +++ b/budget_control_expense/models/expense_budget_move.py @@ -0,0 +1,42 @@ +# Copyright 2020 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class ExpenseBudgetMove(models.Model): + _name = "expense.budget.move" + _inherit = ["base.budget.move"] + _description = "Expense Budget Moves" + + expense_id = fields.Many2one( + comodel_name="hr.expense", + readonly=True, + index=True, + help="Commit budget for this expense_id", + ) + sheet_id = fields.Many2one( + comodel_name="hr.expense.sheet", + related="expense_id.sheet_id", + readonly=True, + store=True, + index=True, + ) + move_id = fields.Many2one( + comodel_name="account.move", + related="move_line_id.move_id", + store=True, + ) + move_line_id = fields.Many2one( + comodel_name="account.move.line", + readonly=True, + index=True, + help="Uncommit budget from this move_line_id", + ) + + @api.depends("sheet_id") + def _compute_reference(self): + for rec in self: + rec.reference = ( + rec.reference if rec.reference else rec.sheet_id.display_name + ) diff --git a/budget_control_expense/models/hr_expense.py b/budget_control_expense/models/hr_expense.py new file mode 100644 index 00000000..d30fa771 --- /dev/null +++ b/budget_control_expense/models/hr_expense.py @@ -0,0 +1,113 @@ +# Copyright 2020 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class HRExpenseSheet(models.Model): + _inherit = "hr.expense.sheet" + _docline_rel = "expense_line_ids" + _docline_type = "expense" + + budget_move_ids = fields.One2many( + comodel_name="expense.budget.move", + inverse_name="sheet_id", + ) + + @api.constrains("expense_line_ids") + def recompute_budget_move(self): + self.mapped("expense_line_ids").recompute_budget_move() + + def close_budget_move(self): + self.mapped("expense_line_ids").close_budget_move() + + def write(self, vals): + """ + Uncommit budget when the state is "approve" or cancel/draft the document. + When the document is cancelled or drafted, delete all budget commitments. + """ + res = super().write(vals) + if vals.get("state") in ("approve", "cancel", "draft"): + doclines = self.mapped("expense_line_ids") + if vals.get("state") in ("cancel", "draft"): + doclines.write({"date_commit": False}) + doclines.recompute_budget_move() + return res + + def approve_expense_sheets(self): + res = super().approve_expense_sheets() + BudgetPeriod = self.env["budget.period"] + for doc in self: + BudgetPeriod.check_budget(doc.expense_line_ids, doc_type="expense") + return res + + def action_submit_sheet(self): + res = super().action_submit_sheet() + BudgetPeriod = self.env["budget.period"] + for doc in self: + BudgetPeriod.check_budget_precommit( + doc.expense_line_ids, doc_type="expense" + ) + return res + + def action_sheet_move_create(self): + res = super().action_sheet_move_create() + BudgetPeriod = self.env["budget.period"] + for doc in self: + BudgetPeriod.check_budget(doc.account_move_id.line_ids) + return res + + +class HRExpense(models.Model): + _name = "hr.expense" + _inherit = ["hr.expense", "budget.docline.mixin"] + _budget_date_commit_fields = ["sheet_id.write_date"] + _budget_move_model = "expense.budget.move" + _doc_rel = "sheet_id" + + budget_move_ids = fields.One2many( + comodel_name="expense.budget.move", + inverse_name="expense_id", + ) + + def recompute_budget_move(self): + for expense in self: + # Make sure that date_commit not recompute + ex_date_commit = expense.date_commit or self.env.context.get( + "force_date_commit", False + ) + expense[self._budget_field()].unlink() + expense.with_context(force_date_commit=ex_date_commit).commit_budget() + move_lines = expense.sheet_id.account_move_id.line_ids + # credit will not over debit (auto adjust) + expense.forward_commit() + move_lines.uncommit_expense_budget() + + def _init_docline_budget_vals(self, budget_vals, analytic_id): + self.ensure_one() + if not budget_vals.get("amount_currency", False): + percent_analytic = self[self._budget_analytic_field].get(str(analytic_id)) + total_amount = ( + (self.quantity * self.unit_amount) + if self.product_has_cost + else self.total_amount + ) + budget_vals["amount_currency"] = total_amount * percent_analytic / 100 + budget_vals["tax_ids"] = self.tax_ids.ids + # Document specific vals + budget_vals.update({"expense_id": self.id}) + return super()._init_docline_budget_vals(budget_vals, analytic_id) + + def _valid_commit_state(self): + return self.state in ["approved", "done"] + + def _prepare_move_line_vals(self): + ml_vals = super()._prepare_move_line_vals() + if ml_vals.get("analytic_distribution") and self.fwd_analytic_distribution: + ml_vals["analytic_distribution"] = self.fwd_analytic_distribution + return ml_vals + + def _get_included_tax(self): + if self._name == "hr.expense": + return self.env.company.budget_include_tax_expense + return super()._get_included_tax() diff --git a/budget_control_expense/readme/CONTRIBUTORS.rst b/budget_control_expense/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..9cf80039 --- /dev/null +++ b/budget_control_expense/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Kitti Upariphutthiphong +* Saran Lim. diff --git a/budget_control_expense/readme/DESCRIPTION.rst b/budget_control_expense/readme/DESCRIPTION.rst new file mode 100644 index 00000000..3b4b1af4 --- /dev/null +++ b/budget_control_expense/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ +This module will create budget commitment for expense (to be used as alternate actual source in mis_builder) + +When expense report is approved, hr_expense.budget.commit is created, and when +journal entry is posted, reversed hr_expense.budget.commit is created. + +A new tab "Budget Commitment" is created on expense report for budget user to keep track of the committed budget. diff --git a/budget_control_expense/report/__init__.py b/budget_control_expense/report/__init__.py new file mode 100644 index 00000000..9ec4b2a8 --- /dev/null +++ b/budget_control_expense/report/__init__.py @@ -0,0 +1,2 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import budget_monitor_report diff --git a/budget_control_expense/report/budget_monitor_report.py b/budget_control_expense/report/budget_monitor_report.py new file mode 100644 index 00000000..53b43732 --- /dev/null +++ b/budget_control_expense/report/budget_monitor_report.py @@ -0,0 +1,30 @@ +# Copyright 2020 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class BudgetMonitorReport(models.Model): + _inherit = "budget.monitor.report" + + def _get_consumed_sources(self): + return super()._get_consumed_sources() + [ + { + "model": ("hr.expense", "Expense"), + "type": ("5_ex_commit", "EX Commit"), + "budget_move": ("expense_budget_move", "expense_id"), + "source_doc": ("hr_expense_sheet", "sheet_id"), + } + ] + + def _where_expense(self): + return "" + + def _get_sql(self): + select_ex_query = self._select_statement("5_ex_commit") + key_select_list = sorted(select_ex_query.keys()) + select_ex = ", ".join(select_ex_query[x] for x in key_select_list) + return super()._get_sql() + "union (select {} {} {})".format( + select_ex, + self._from_statement("5_ex_commit"), + self._where_expense(), + ) diff --git a/budget_control_expense/security/ir.model.access.csv b/budget_control_expense/security/ir.model.access.csv new file mode 100644 index 00000000..fab59322 --- /dev/null +++ b/budget_control_expense/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_expense_budget_move_user,access_expense_budget_move_user,model_expense_budget_move,,1,1,1,1 diff --git a/budget_control_expense/static/description/icon.png b/budget_control_expense/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/budget_control_expense/static/description/icon.png differ diff --git a/budget_control_expense/static/description/index.html b/budget_control_expense/static/description/index.html new file mode 100644 index 00000000..b340840b --- /dev/null +++ b/budget_control_expense/static/description/index.html @@ -0,0 +1,428 @@ + + + + + +Budget Control on Expense + + + +
+

Budget Control on Expense

+ + +

Alpha License: AGPL-3 ecosoft-odoo/budgeting

+

This module will create budget commitment for expense (to be used as alternate actual source in mis_builder)

+

When expense report is approved, hr_expense.budget.commit is created, and when +journal entry is posted, reversed hr_expense.budget.commit is created.

+

A new tab “Budget Commitment” is created on expense report for budget user to keep track of the committed budget.

+
+

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 maintainers:

+

kittiu ru3ix-bbb

+

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

+

You are welcome to contribute.

+
+
+
+ + diff --git a/budget_control_expense/tests/__init__.py b/budget_control_expense/tests/__init__.py new file mode 100644 index 00000000..f4f62346 --- /dev/null +++ b/budget_control_expense/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_budget_expense diff --git a/budget_control_expense/tests/test_budget_expense.py b/budget_control_expense/tests/test_budget_expense.py new file mode 100644 index 00000000..2752d9cc --- /dev/null +++ b/budget_control_expense/tests/test_budget_expense.py @@ -0,0 +1,251 @@ +# Copyright 2020 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from freezegun import freeze_time + +from odoo import Command +from odoo.exceptions import UserError +from odoo.tests import tagged +from odoo.tests.common import Form + +from odoo.addons.budget_control.tests.common import BudgetControlCommon + + +@tagged("post_install", "-at_install") +class TestBudgetControlExpense(BudgetControlCommon): + @classmethod + @freeze_time("2001-02-01") + def setUpClass(cls): + super().setUpClass() + # Create sample ready to use Budget Control + cls.budget_control = cls.BudgetControl.create( + { + "name": "CostCenter1/%s" % cls.year, + "template_id": cls.budget_period.template_id.id, + "budget_period_id": cls.budget_period.id, + "analytic_account_id": cls.costcenter1.id, + "plan_date_range_type_id": cls.date_range_type.id, + "template_line_ids": [ + cls.template_line1.id, + cls.template_line2.id, + cls.template_line3.id, + ], + } + ) + # Test item created for 3 kpi x 4 quarters = 12 budget items + cls.budget_control.prepare_budget_control_matrix() + assert len(cls.budget_control.line_ids) == 12 + # Assign budget.control amount: KPI1 = 100, KPI2=800, Total=300 + cls.budget_control.line_ids.filtered(lambda x: x.kpi_id == cls.kpi1)[:1].write( + {"amount": 100} + ) + cls.budget_control.line_ids.filtered(lambda x: x.kpi_id == cls.kpi2)[:1].write( + {"amount": 200} + ) + # cls.budget_control.flush_model() # Need to flush data into table, so it can be sql + # cls.budget_control.allocated_amount = 300 + # cls.budget_control.action_done() + + @freeze_time("2001-02-01") + def _create_expense_sheet(self, ex_lines): + Expense = self.env["hr.expense"] + view_id = "hr_expense.hr_expense_view_form" + expense_ids = [] + user = self.env.ref("base.user_admin") + for ex_line in ex_lines: + with Form(Expense, view=view_id) as ex: + ex.employee_id = user.employee_id + ex.product_id = ex_line["product_id"] + ex.total_amount = ex_line["price_unit"] * ex_line["product_qty"] + ex.analytic_distribution = ex_line["analytic_distribution"] + expense = ex.save() + expense_ids.append(expense.id) + expense_sheet = self.env["hr.expense.sheet"].create( + { + "name": "Test Expense", + "employee_id": user.employee_id.id, + "expense_line_ids": [Command.set(expense_ids)], + } + ) + return expense_sheet + + @freeze_time("2001-02-01") + def test_01_budget_expense(self): + """ + On Expense Sheet + (1) Test case, no budget check -> OK + (2) Check Budget with analytic_kpi -> Error amount exceed on kpi1 + (3) Check Budget with analytic -> OK + (2) Check Budget with analytic -> Error amount exceed + """ + # Allocate and Done + self.budget_control.allocated_amount = 300 + self.budget_control.action_done() + # KPI1 = 100, KPI2 = 200, Total = 300 + self.assertEqual(300, self.budget_control.amount_budget) + # Prepare Expense Sheet + analytic_distribution = {str(self.costcenter1.id): 100} + expense = self._create_expense_sheet( + [ + { + "product_id": self.product1, # KPI1 = 101 -> error + "product_qty": 1, + "price_unit": 101, + "analytic_distribution": analytic_distribution, + }, + { + "product_id": self.product2, # KPI2 = 198 + "product_qty": 2, + "price_unit": 99, + "analytic_distribution": analytic_distribution, + }, + ] + ) + # (1) No budget check first + self.budget_period.control_budget = False + self.budget_period.control_level = "analytic_kpi" + # force date commit, as freeze_time not work for write_date + expense = expense.with_context( + force_date_commit=expense.expense_line_ids[:1].date + ) + expense.action_submit_sheet() # No budget check no error + # (2) Check Budget with analytic_kpi -> Error + expense.reset_expense_sheets() + self.budget_period.control_budget = True # Set to check budget + # kpi 1 (kpi1) & CostCenter1, will result in $ -1.00 + with self.assertRaises(UserError): + expense.action_submit_sheet() + # (3) Check Budget with analytic -> OK + self.budget_period.control_level = "analytic" + expense.action_submit_sheet() + expense.approve_expense_sheets() + self.assertEqual(self.budget_control.amount_balance, 1) + expense.reset_expense_sheets() + self.assertEqual(self.budget_control.amount_balance, 300) + # (4) Amount exceed -> Error + expense.expense_line_ids.write({"total_amount": 301}) + # CostCenter1, will result in $ -1.00 + with self.assertRaises(UserError): + expense.action_submit_sheet() + + @freeze_time("2001-02-01") + def test_02_budget_expense_to_journal_posting(self): + """Expense to Journal Posting, commit and uncommit""" + # Allocate and Done + self.budget_control.allocated_amount = 300 + self.budget_control.action_done() + + # KPI1 = 100, KPI2 = 200, Total = 300 + self.assertEqual(300, self.budget_control.amount_budget) + # Prepare Expense on kpi1 with qty 3 and unit_price 10 + analytic_distribution = {str(self.costcenter1.id): 100} + expense = self._create_expense_sheet( + [ + { + "product_id": self.product1, # KPI1 + "product_qty": 3, + "price_unit": 10, + "analytic_distribution": analytic_distribution, + }, + ] + ) + self.budget_period.control_budget = True + self.budget_period.control_level = "analytic" + expense = expense.with_context( + force_date_commit=expense.expense_line_ids[:1].date + ) + expense.action_submit_sheet() + expense.approve_expense_sheets() + # Expense = 30, JE Actual = 0, Balance = 270 + self.assertEqual(self.budget_control.amount_expense, 30) + self.assertEqual(self.budget_control.amount_actual, 0) + self.assertEqual(self.budget_control.amount_balance, 270) + # Create and post invoice + expense.action_sheet_move_create() + move = expense.account_move_id + self.assertEqual(move.state, "posted") + # EX Commit = 0, JE Actual = 30, Balance = 270 + # Update budget info + self.budget_control._compute_budget_info() + self.assertEqual(self.budget_control.amount_expense, 0) + self.assertEqual(self.budget_control.amount_actual, 30) + self.assertEqual(self.budget_control.amount_balance, 270) + # Set to draft + move.button_draft() + # Update budget info + self.budget_control._compute_budget_info() + self.assertEqual(self.budget_control.amount_expense, 30) + self.assertEqual(self.budget_control.amount_actual, 0) + self.assertEqual(self.budget_control.amount_balance, 270) + # Cancel journal entry, expense no change + move.button_cancel() + # Update budget info + self.budget_control._compute_budget_info() + self.assertEqual(self.budget_control.amount_expense, 30) + self.assertEqual(self.budget_control.amount_actual, 0) + self.assertEqual(self.budget_control.amount_balance, 270) + + @freeze_time("2001-02-01") + def test_03_budget_recompute_and_close_budget_move(self): + """EX to JE + - Test recompute on both EX and JE + - Test close on both EX and JE""" + # Allocate and Done + self.budget_control.allocated_amount = 300 + self.budget_control.action_done() + # KPI1 = 100, KPI2 = 200, Total = 300 + self.assertEqual(300, self.budget_control.amount_budget) + # Prepare Expense on kpi1 with qty 3 and unit_price 10 + analytic_distribution = {str(self.costcenter1.id): 100} + expense = self._create_expense_sheet( + [ + { + "product_id": self.product1, + "product_qty": 2, + "price_unit": 15, + "analytic_distribution": analytic_distribution, + }, + { + "product_id": self.product2, + "product_qty": 4, + "price_unit": 10, + "analytic_distribution": analytic_distribution, + }, + ] + ) + self.budget_period.control_budget = True + self.budget_period.control_level = "analytic" + expense = expense.with_context( + force_date_commit=expense.expense_line_ids[:1].date + ) + expense.action_submit_sheet() + expense.approve_expense_sheets() + # Expense = 70, JE Actual = 0 + self.assertEqual(self.budget_control.amount_expense, 70) + self.assertEqual(self.budget_control.amount_actual, 0) + # Create and post invoice + expense.action_sheet_move_create() + move = expense.account_move_id + self.assertEqual(move.state, "posted") + # EX Commit = 0, JE Actual = 70 + self.budget_control._compute_budget_info() + self.assertEqual(self.budget_control.amount_expense, 0) + self.assertEqual(self.budget_control.amount_actual, 70) + # Recompute + expense.recompute_budget_move() + self.budget_control._compute_budget_info() + self.assertEqual(self.budget_control.amount_expense, 0) + self.assertEqual(self.budget_control.amount_actual, 70) + move.recompute_budget_move() + self.budget_control._compute_budget_info() + self.assertEqual(self.budget_control.amount_expense, 0) + self.assertEqual(self.budget_control.amount_actual, 70) + # Close + expense.close_budget_move() + self.budget_control._compute_budget_info() + self.assertEqual(self.budget_control.amount_expense, 0) + self.assertEqual(self.budget_control.amount_actual, 70) + move.close_budget_move() + self.budget_control._compute_budget_info() + self.assertEqual(self.budget_control.amount_expense, 0) + self.assertEqual(self.budget_control.amount_actual, 0) diff --git a/budget_control_expense/views/budget_commit_forward_view.xml b/budget_control_expense/views/budget_commit_forward_view.xml new file mode 100644 index 00000000..b2353aa2 --- /dev/null +++ b/budget_control_expense/views/budget_commit_forward_view.xml @@ -0,0 +1,30 @@ + + + + view.budget.commit.forward.form + budget.commit.forward + + 40 + +
+
+ +
+
+ + + + + +
+
+
diff --git a/budget_control_expense/views/budget_control_view.xml b/budget_control_expense/views/budget_control_view.xml new file mode 100644 index 00000000..aef49593 --- /dev/null +++ b/budget_control_expense/views/budget_control_view.xml @@ -0,0 +1,26 @@ + + + + budget.control.view.tree + budget.control + + + + + + + + + budget.control.view.form + budget.control + + + + + + + + diff --git a/budget_control_expense/views/budget_period_view.xml b/budget_control_expense/views/budget_period_view.xml new file mode 100644 index 00000000..489b5b06 --- /dev/null +++ b/budget_control_expense/views/budget_period_view.xml @@ -0,0 +1,13 @@ + + + + budget.period.view.form + budget.period + + + + + + + + diff --git a/budget_control_expense/views/expense_budget_move.xml b/budget_control_expense/views/expense_budget_move.xml new file mode 100644 index 00000000..db6ba23a --- /dev/null +++ b/budget_control_expense/views/expense_budget_move.xml @@ -0,0 +1,45 @@ + + + + + view.expense.budget.move.form + expense.budget.move + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
diff --git a/budget_control_expense/views/hr_expense_view.xml b/budget_control_expense/views/hr_expense_view.xml new file mode 100644 index 00000000..e6982925 --- /dev/null +++ b/budget_control_expense/views/hr_expense_view.xml @@ -0,0 +1,103 @@ + + + + view.hr.expense.sheet.form + hr.expense.sheet + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + +
+
+
+
+ + + hr.expense.view.form + hr.expense + + + + + + + +
diff --git a/budget_control_expense/wizard/__init__.py b/budget_control_expense/wizard/__init__.py new file mode 100644 index 00000000..09e22cb0 --- /dev/null +++ b/budget_control_expense/wizard/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import account_payment_register diff --git a/budget_control_expense/wizard/account_payment_register.py b/budget_control_expense/wizard/account_payment_register.py new file mode 100644 index 00000000..bb542aa1 --- /dev/null +++ b/budget_control_expense/wizard/account_payment_register.py @@ -0,0 +1,14 @@ +# Copyright 2021 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class AccountPaymentRegister(models.TransientModel): + _inherit = "account.payment.register" + + def _init_payments(self, to_process, edit_mode=False): + """Make sure that move in payment must not affect budget""" + payments = super()._init_payments(to_process, edit_mode) + payments.mapped("move_id").write({"not_affect_budget": True}) + return payments diff --git a/setup/budget_control_expense/odoo/addons/budget_control_expense b/setup/budget_control_expense/odoo/addons/budget_control_expense new file mode 120000 index 00000000..eb531075 --- /dev/null +++ b/setup/budget_control_expense/odoo/addons/budget_control_expense @@ -0,0 +1 @@ +../../../../budget_control_expense \ No newline at end of file diff --git a/setup/budget_control_expense/setup.py b/setup/budget_control_expense/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/budget_control_expense/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)