diff --git a/budget_activity/README.rst b/budget_activity/README.rst new file mode 100644 index 00000000..11615649 --- /dev/null +++ b/budget_activity/README.rst @@ -0,0 +1,97 @@ +=============== +Budget Activity +=============== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a5a8909d4b508145ac8b4f7811bc684d06e2e7e126e227c8e349ee255d4c9c7b + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_activity + :alt: ecosoft-odoo/budgeting + +|badge1| |badge2| |badge3| + +This module helps the system accurately record the account code for budget recording. +It includes an "Activity" linking the two, where the activity serves as a match between what the user understands and the account code. +The user can select from a list of activity that have been created in the system, +and the system will then match the selected activity to the corresponding account, +so the user does not need to understand the account themselves. + +.. 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: + +Usage +===== + +To used this module you have to configued Budget Activity first. + +#. Go to Budgeting > Activity > Budget Activity +#. Create new activity, select a KPI (if you want to group it) and match the activity to an account. +#. Add a `Keyword` if you need to search for other words to show this activity. For example, the activity name is `Activity1` and the keywords are `Ticket` and `Transportation`. In the Activity field, the user can search for the name `Ticket` to see the activity `Activity1`. +#. Go to Budgeting > Configurations > Budget Template +#. Create new template or use old template (if you have). +#. When you select a KPI, it will automatically select the corresponding activity and account. If you haven't set up an activity in the KPI, you can select the activity and it will automatically select the corresponding account. + + +In the usage window, you will see a new field called "Activity" where the user can select from a list of options. +The system will then match the selected activity to the corresponding account that has been set up. + +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 `__: + + * Kitti Upariphutthiphong + * Saran Lim. + * Pimolnat Suntian + +Maintainers +~~~~~~~~~~~ + +.. |maintainer-kittiu| image:: https://github.com/kittiu.png?size=40px + :target: https://github.com/kittiu + :alt: kittiu + +Current maintainer: + +|maintainer-kittiu| + +This module is part of the `ecosoft-odoo/budgeting `_ project on GitHub. + +You are welcome to contribute. diff --git a/budget_activity/__init__.py b/budget_activity/__init__.py new file mode 100644 index 00000000..37e105d0 --- /dev/null +++ b/budget_activity/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models +from . import report diff --git a/budget_activity/__manifest__.py b/budget_activity/__manifest__.py new file mode 100644 index 00000000..f4cd94ae --- /dev/null +++ b/budget_activity/__manifest__.py @@ -0,0 +1,28 @@ +# Copyright 2021 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Budget Activity", + "version": "16.0.1.0.0", + "category": "Accounting", + "license": "AGPL-3", + "author": "Ecosoft, Odoo Community Association (OCA)", + "website": "https://github.com/ecosoft-odoo/budgeting", + "depends": ["budget_control"], + "data": [ + "security/budget_activity_security.xml", + "security/ir.model.access.csv", + "report/budget_monitor_report_view.xml", + "views/account_move_views.xml", + "views/analytic_line_views.xml", + "views/budget_activity_view.xml", + "views/budget_kpi_view.xml", + "views/budget_template_view.xml", + "views/budget_menuitem.xml", + "views/budget_move_adjustment_view.xml", + ], + "demo": ["demo/budget_activity_demo.xml"], + "installable": True, + "maintainers": ["kittiu"], + "development_status": "Alpha", +} diff --git a/budget_activity/demo/budget_activity_demo.xml b/budget_activity/demo/budget_activity_demo.xml new file mode 100644 index 00000000..ab8d07a0 --- /dev/null +++ b/budget_activity/demo/budget_activity_demo.xml @@ -0,0 +1,48 @@ + + + + + Expense + + + + + Purchase of Equipments + + + + + Rent + + + + + + + + + + + + + + + diff --git a/budget_activity/models/__init__.py b/budget_activity/models/__init__.py new file mode 100644 index 00000000..8cdeab55 --- /dev/null +++ b/budget_activity/models/__init__.py @@ -0,0 +1,11 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import res_company +from . import account_move +from . import budget_activity +from . import account_analytic_line +from . import base_budget_move +from . import budget_subkpi +from . import budget_kpi_template +from . import budget_period +from . import budget_move_adjustment diff --git a/budget_activity/models/account_analytic_line.py b/budget_activity/models/account_analytic_line.py new file mode 100644 index 00000000..f25f9b0f --- /dev/null +++ b/budget_activity/models/account_analytic_line.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 AccountAnalyticLine(models.Model): + _inherit = "account.analytic.line" + + activity_id = fields.Many2one( + comodel_name="budget.activity", + string="Activity", + index=True, + ) diff --git a/budget_activity/models/account_move.py b/budget_activity/models/account_move.py new file mode 100644 index 00000000..376db577 --- /dev/null +++ b/budget_activity/models/account_move.py @@ -0,0 +1,54 @@ +# Copyright 2021 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 AccountMoveLine(models.Model): + _inherit = "account.move.line" + + activity_id = fields.Many2one( + comodel_name="budget.activity", + string="Activity", + index=True, + ) + + def _is_realtime_inventory_product(self): + # Case non-realtime inventory product + # activity's account takes priority over product's account + if self.product_id.type != "product": + return False + if not hasattr(self.product_id.categ_id, "property_valuation"): + return False + return self.product_id.categ_id.property_valuation == "real_time" + + def _compute_account_id(self): + """In case of Realtime Inventory Product, + activity's account takes priority""" + input_lines = self.filtered( + lambda line: ( + line._is_realtime_inventory_product() + and line.move_id.company_id.anglo_saxon_accounting + ) + ).with_prefetch( + ["move_id", "move_id.company_id", "move_id.journal_id.company_id"] + ) + for line in input_lines: + line = line.with_company(line.move_id.journal_id.company_id) + if line.activity_id: + line.account_id = line.activity_id.account_id + return super(AccountMoveLine, self - input_lines)._compute_account_id() + + def _prepare_analytic_lines(self): + """Add activity_id to analytic line""" + analytic_line_vals = super()._prepare_analytic_lines() + self.ensure_one() + if self.activity_id: # Check if activity_id is set + for analytic_line in analytic_line_vals: + analytic_line["activity_id"] = self.activity_id.id + return analytic_line_vals + + @api.onchange("activity_id") + def _onchange_activity_id(self): + if self.activity_id: + self.account_id = self.activity_id.account_id diff --git a/budget_activity/models/base_budget_move.py b/budget_activity/models/base_budget_move.py new file mode 100644 index 00000000..cc9dc6ae --- /dev/null +++ b/budget_activity/models/base_budget_move.py @@ -0,0 +1,74 @@ +# Copyright 2021 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 +from odoo.exceptions import UserError + + +class BaseBudgetMove(models.AbstractModel): + _inherit = "base.budget.move" + + activity_id = fields.Many2one( + comodel_name="budget.activity", + string="Activity", + index=True, + ) + account_id = fields.Many2one( + compute="_compute_activity_account", + store=True, + help="When activity is selected, always use activity's account to " + "enusre that the KPI budgeting which rely on account is valid. " + "Because in some case, i.e., perpetual inventory, " + "account in account.move.line can be different with " + "account in account.budget.move.", + ) + + @api.depends("activity_id") + def _compute_activity_account(self): + budget_control_key = self.env.company.budget_control_key + if budget_control_key == "activity_id": + for rec in self: + rec.account_id = ( + rec.activity_id.account_id if rec.activity_id else rec.account_id + ) + + @api.constrains("activity_id", "account_id") + def _check_activity_account(self): + budget_control_key = self.env.company.budget_control_key + if budget_control_key == "activity_id": + for rec in self.filtered("activity_id"): + if rec.account_id != rec.activity_id.account_id: + raise UserError( + _("Account not equal to Activity's Account: %s") + % rec.activity_id.account_id.display_name + ) + + +class BudgetDoclineMixinBase(models.AbstractModel): + _inherit = "budget.docline.mixin.base" + + activity_id = fields.Many2one( + comodel_name="budget.activity", + string="Activity", + domain=lambda self: self._domain_activity(), + index=True, + ) + + def _domain_activity(self): + return [] + + +class BudgetDoclineMixin(models.AbstractModel): + _inherit = "budget.docline.mixin" + + def _update_budget_commitment(self, budget_vals, analytic, reverse=False): + budget_vals = super()._update_budget_commitment( + budget_vals, analytic, reverse=reverse + ) + budget_vals["activity_id"] = self.activity_id.id + # For case object without account_id (PR/PO), normally account is from + # product, it should now changed to follow activity. + # But if account_id is part of object (INV), use whatever is passed-in. + if "account_id" not in self: + budget_vals["account_id"] = self["activity_id"].account_id.id + return budget_vals diff --git a/budget_activity/models/budget_activity.py b/budget_activity/models/budget_activity.py new file mode 100644 index 00000000..4850d48a --- /dev/null +++ b/budget_activity/models/budget_activity.py @@ -0,0 +1,104 @@ +# Copyright 2021 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 +from odoo.exceptions import UserError + + +class BudgetActivity(models.Model): + _name = "budget.activity" + _description = "Budget Activity" + + name = fields.Char( + required=True, + ) + kpi_id = fields.Many2one( + comodel_name="budget.kpi", + index=True, + ) + active = fields.Boolean(default=True) + account_id = fields.Many2one( + comodel_name="account.account", + string="Account", + domain=[("deprecated", "=", False)], + readonly=False, + index=True, + ) + company_id = fields.Many2one( + comodel_name="res.company", + string="Company", + default=lambda self: self.env.company, + required=True, + ) + keyword_ids = fields.Many2many( + comodel_name="budget.activity.keyword", + relation="budget_activity_keyword_rel", + column1="budget_activity_id", + column2="budget_activity_keyword_id", + string="Keywords", + help="Optional keyword you may want to assign for search", + ) + + _sql_constraints = [ + ("name_uniq", "UNIQUE(name)", "Name must be unique!"), + ] + + @api.model + def name_search(self, name, args=None, operator="ilike", limit=100): + args = args or [] + domain = [] + if name: + domain = [ + "|", + "|", + ("name", operator, name), + ("keyword_ids", operator, name), + ("account_id", operator, name), + ] + activitys = self.search(domain + args, limit=limit) + return activitys.name_get() + + @api.onchange("account_id") + def onchange_account_id(self): + """Not allow edit account after use it in commit budgeting""" + BudgetPeriod = self.env["budget.period"] + MonitorReport = self.env["budget.monitor.report"] + query = BudgetPeriod._budget_info_query() + + domain = [("activity", "=", self._origin.name)] + dataset_all = MonitorReport.read_group( + domain=domain, + fields=query["fields"], + groupby=query["groupby"], + lazy=False, + ) + if dataset_all and self.account_id: + raise UserError( + _( + "You cannot change the account because it is already used in a commit." + ) + ) + + +class BudgetActivityKeyword(models.Model): + _name = "budget.activity.keyword" + _description = "Search budget activity with keyword" + + name = fields.Char(required=True) + company_id = fields.Many2one( + comodel_name="res.company", + string="Company", + default=lambda self: self.env.company, + required=True, + ) + active = fields.Boolean( + default=True, + help="Set active to false to hide the Budget Activity keyword without removing it.", + ) + _sql_constraints = [ + ( + "name_company_unique", + "unique(name, company_id)", + "This keyword is already used!", + ) + ] diff --git a/budget_activity/models/budget_kpi_template.py b/budget_activity/models/budget_kpi_template.py new file mode 100644 index 00000000..30481f82 --- /dev/null +++ b/budget_activity/models/budget_kpi_template.py @@ -0,0 +1,25 @@ +# Copyright 2022 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 BudgetKPI(models.Model): + _inherit = "budget.template.line" + + activity_ids = fields.Many2many( + comodel_name="budget.activity", + relation="budget_kpi_activity_rel", + column1="budget_kpi_id", + column2="activity_id", + ondelete="restrict", + required=True, + ) + + @api.onchange("kpi_id") + def _onchange_kpi_id(self): + self.activity_ids = self.kpi_id.activity_ids.ids + + @api.onchange("activity_ids") + def _onchange_account_ids(self): + self.account_ids = self.activity_ids.mapped("account_id").ids diff --git a/budget_activity/models/budget_move_adjustment.py b/budget_activity/models/budget_move_adjustment.py new file mode 100644 index 00000000..3101acf2 --- /dev/null +++ b/budget_activity/models/budget_move_adjustment.py @@ -0,0 +1,27 @@ +# Copyright 2021 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 BudgetMoveAdjustmentItem(models.Model): + _inherit = "budget.move.adjustment.item" + + activity_id = fields.Many2one( + comodel_name="budget.activity", + string="Activity", + index=True, + ) + + _sql_constraints = [ + ( + "amount_positive", + "CHECK(amount >= 0)", + "The adjusted budget must be positive.", + ) + ] + + @api.onchange("activity_id") + def _onchange_activity_id(self): + if self.activity_id: + self.account_id = self.activity_id.account_id diff --git a/budget_activity/models/budget_period.py b/budget_activity/models/budget_period.py new file mode 100644 index 00000000..3bacbfed --- /dev/null +++ b/budget_activity/models/budget_period.py @@ -0,0 +1,24 @@ +# 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 BudgetPeriod(models.Model): + _inherit = "budget.period" + + def _get_control_key_obj(self, control_key, control_id): + if control_key == "activity_id": + control = self.env["budget.activity"].browse(control_id) + control_name = "activity" + return control, control_name + return super()._get_control_key_obj(control_key, control_id) + + def _get_filter_template_line(self, all_template_lines, control): + budget_control_key = self.env.company.budget_control_key + control_id = control[budget_control_key] + if budget_control_key == "activity_id": + return all_template_lines.filtered( + lambda l: control_id in l.activity_ids.ids + ) + return super()._get_filter_template_line(all_template_lines, control) diff --git a/budget_activity/models/budget_subkpi.py b/budget_activity/models/budget_subkpi.py new file mode 100644 index 00000000..5f4dd18a --- /dev/null +++ b/budget_activity/models/budget_subkpi.py @@ -0,0 +1,14 @@ +# Copyright 2022 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 BudgetKPITemplate(models.Model): + _inherit = "budget.kpi" + + activity_ids = fields.One2many( + comodel_name="budget.activity", + inverse_name="kpi_id", + readonly=True, + ) diff --git a/budget_activity/models/res_company.py b/budget_activity/models/res_company.py new file mode 100644 index 00000000..bb37dd3f --- /dev/null +++ b/budget_activity/models/res_company.py @@ -0,0 +1,13 @@ +# 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 ResCompany(models.Model): + _inherit = "res.company" + + budget_control_key = fields.Selection( + selection_add=[("activity_id", "Activity")], + ondelete={"activity_id": "cascade"}, + ) diff --git a/budget_activity/readme/CONTRIBUTORS.rst b/budget_activity/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..92e451a5 --- /dev/null +++ b/budget_activity/readme/CONTRIBUTORS.rst @@ -0,0 +1,5 @@ +* `Ecosoft `__: + + * Kitti Upariphutthiphong + * Saran Lim. + * Pimolnat Suntian diff --git a/budget_activity/readme/DESCRIPTION.rst b/budget_activity/readme/DESCRIPTION.rst new file mode 100644 index 00000000..b44bc6e8 --- /dev/null +++ b/budget_activity/readme/DESCRIPTION.rst @@ -0,0 +1,5 @@ +This module helps the system accurately record the account code for budget recording. +It includes an "Activity" linking the two, where the activity serves as a match between what the user understands and the account code. +The user can select from a list of activity that have been created in the system, +and the system will then match the selected activity to the corresponding account, +so the user does not need to understand the account themselves. diff --git a/budget_activity/readme/USAGE.rst b/budget_activity/readme/USAGE.rst new file mode 100644 index 00000000..675de521 --- /dev/null +++ b/budget_activity/readme/USAGE.rst @@ -0,0 +1,12 @@ +To used this module you have to configued Budget Activity first. + +#. Go to Budgeting > Activity > Budget Activity +#. Create new activity, select a KPI (if you want to group it) and match the activity to an account. +#. Add a `Keyword` if you need to search for other words to show this activity. For example, the activity name is `Activity1` and the keywords are `Ticket` and `Transportation`. In the Activity field, the user can search for the name `Ticket` to see the activity `Activity1`. +#. Go to Budgeting > Configurations > Budget Template +#. Create new template or use old template (if you have). +#. When you select a KPI, it will automatically select the corresponding activity and account. If you haven't set up an activity in the KPI, you can select the activity and it will automatically select the corresponding account. + + +In the usage window, you will see a new field called "Activity" where the user can select from a list of options. +The system will then match the selected activity to the corresponding account that has been set up. diff --git a/budget_activity/report/__init__.py b/budget_activity/report/__init__.py new file mode 100644 index 00000000..16216c42 --- /dev/null +++ b/budget_activity/report/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import budget_monitor_report diff --git a/budget_activity/report/budget_monitor_report.py b/budget_activity/report/budget_monitor_report.py new file mode 100644 index 00000000..57733d08 --- /dev/null +++ b/budget_activity/report/budget_monitor_report.py @@ -0,0 +1,32 @@ +# 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 BudgetMonitorReport(models.Model): + _inherit = "budget.monitor.report" + + activity = fields.Char() + + # Budget + def _select_budget(self): + select_budget_query = super()._select_budget() + # Budget can't find activity + select_budget_query[20] = "null::char as activity" + return select_budget_query + + # All consumed + def _select_statement(self, amount_type): + select_statement = super()._select_statement(amount_type) + select_statement[20] = "ba.name as activity" + return select_statement + + def _from_statement(self, amount_type): + from_statment = super()._from_statement(amount_type) + from_statment = "\n".join( + [ + from_statment, + "left outer join budget_activity ba on a.activity_id = ba.id ", + ] + ) + return from_statment diff --git a/budget_activity/report/budget_monitor_report_view.xml b/budget_activity/report/budget_monitor_report_view.xml new file mode 100644 index 00000000..139c87ff --- /dev/null +++ b/budget_activity/report/budget_monitor_report_view.xml @@ -0,0 +1,33 @@ + + + + budget.monitor.report.tree + budget.monitor.report + + + + + + + + + budget.monitor.report.search + budget.monitor.report + + + + + + + + diff --git a/budget_activity/security/budget_activity_security.xml b/budget_activity/security/budget_activity_security.xml new file mode 100644 index 00000000..3d873324 --- /dev/null +++ b/budget_activity/security/budget_activity_security.xml @@ -0,0 +1,11 @@ + + + + Budget Activity multi company rule + + + ['|',('company_id','=',False),('company_id', 'in', company_ids)] + + diff --git a/budget_activity/security/ir.model.access.csv b/budget_activity/security/ir.model.access.csv new file mode 100644 index 00000000..5f5dde59 --- /dev/null +++ b/budget_activity/security/ir.model.access.csv @@ -0,0 +1,5 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +access_budget_activity_manager,access_budget_activity_manager,model_budget_activity,budget_control.group_budget_control_manager,1,1,1,1 +access_budget_activity_user,access_budget_activity_user,model_budget_activity,budget_control.group_budget_control_user,1,0,0,0 +access_budget_activity_keyword_manager,access_budget_activity_keyword_manager,model_budget_activity_keyword,budget_control.group_budget_control_manager,1,1,1,1 +access_budget_activity_keyword_user,access_budget_activity_keyword_user,model_budget_activity_keyword,budget_control.group_budget_control_user,1,0,0,0 diff --git a/budget_activity/static/description/icon.png b/budget_activity/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/budget_activity/static/description/icon.png differ diff --git a/budget_activity/static/description/index.html b/budget_activity/static/description/index.html new file mode 100644 index 00000000..3917995b --- /dev/null +++ b/budget_activity/static/description/index.html @@ -0,0 +1,451 @@ + + + + + +Budget Activity + + + +
+

Budget Activity

+ + +

Alpha License: AGPL-3 ecosoft-odoo/budgeting

+

This module helps the system accurately record the account code for budget recording. +It includes an “Activity” linking the two, where the activity serves as a match between what the user understands and the account code. +The user can select from a list of activity that have been created in the system, +and the system will then match the selected activity to the corresponding account, +so the user does not need to understand the account themselves.

+
+

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

+ +
+

Usage

+

To used this module you have to configued Budget Activity first.

+
    +
  1. Go to Budgeting > Activity > Budget Activity
  2. +
  3. Create new activity, select a KPI (if you want to group it) and match the activity to an account.
  4. +
  5. Add a Keyword if you need to search for other words to show this activity. For example, the activity name is Activity1 and the keywords are Ticket and Transportation. In the Activity field, the user can search for the name Ticket to see the activity Activity1.
  6. +
  7. Go to Budgeting > Configurations > Budget Template
  8. +
  9. Create new template or use old template (if you have).
  10. +
  11. When you select a KPI, it will automatically select the corresponding activity and account. If you haven’t set up an activity in the KPI, you can select the activity and it will automatically select the corresponding account.
  12. +
+

In the usage window, you will see a new field called “Activity” where the user can select from a list of options. +The system will then match the selected activity to the corresponding account that has been set up.

+
+
+

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:

+

kittiu

+

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

+

You are welcome to contribute.

+
+
+
+ + diff --git a/budget_activity/tests/__init__.py b/budget_activity/tests/__init__.py new file mode 100644 index 00000000..dd391c72 --- /dev/null +++ b/budget_activity/tests/__init__.py @@ -0,0 +1,2 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import test_budget_activity diff --git a/budget_activity/tests/test_budget_activity.py b/budget_activity/tests/test_budget_activity.py new file mode 100644 index 00000000..fef89e06 --- /dev/null +++ b/budget_activity/tests/test_budget_activity.py @@ -0,0 +1,188 @@ +# Copyright 2020 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime + +from freezegun import freeze_time + +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 TestBudgetActivity(BudgetControlCommon): + @classmethod + @freeze_time("2001-02-01") + def setUpClass(cls): + super().setUpClass() + BudgetActivity = cls.env["budget.activity"] # Create sample activity + cls.env.company.budget_control_key = "activity_id" # Control by activity + cls.activity1 = BudgetActivity.create( + { + "name": "Activity 1", + "kpi_id": cls.kpi1.id, + "account_id": cls.account_kpi1.id, + } + ) + cls.activity2 = BudgetActivity.create( + { + "name": "Activity 2", + "kpi_id": cls.kpi2.id, + "account_id": cls.account_kpi2.id, + } + ) + cls.activity3 = BudgetActivity.create( + { + "name": "Activity 3", + "kpi_id": cls.kpi3.id, + "account_id": cls.account_kpi3.id, + } + ) + # Add activity on template line + with Form(cls.template_line1) as line: + line.kpi_id = cls.kpi1 + with Form(cls.template_line2) as line: + line.kpi_id = cls.kpi2 + with Form(cls.template_line3) as line: + line.kpi_id = cls.kpi3 + # 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, + "allocated_amount": 2400.0, + "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 = 100x4=400, KPI2=800, KPI3=1,200 + cls.budget_control.line_ids.filtered(lambda x: x.kpi_id == cls.kpi1).write( + {"amount": 100} + ) + cls.budget_control.line_ids.filtered(lambda x: x.kpi_id == cls.kpi2).write( + {"amount": 200} + ) + cls.budget_control.line_ids.filtered(lambda x: x.kpi_id == cls.kpi3).write( + {"amount": 300} + ) + + def _create_simple_bill_activity(self, analytic_distribution, activity, amount): + with Form(self.Move.with_context(default_move_type="in_invoice")) as inv: + inv.partner_id = self.vendor + inv.invoice_date = datetime.today() + with inv.invoice_line_ids.new() as line: + line.quantity = 1 + line.price_unit = amount + line.analytic_distribution = analytic_distribution + line.activity_id = activity + invoice = inv.save() + + return invoice + + @freeze_time("2001-02-01") + def test_01_budget_activity_account(self): + """ + On vendor bill, + - If no activity, budget follows product's account + - If activity is selected, account follows activity's regardless of product + - User can always change account code afterwards + - Posting invoice, will create budget move with activity + """ + # Control budget + self.budget_period.control_budget = True + self.budget_control.action_done() + price_unit = 10.0 + + analytic_distribution = {str(self.costcenter1.id): 100.0} + invoice = self._create_simple_bill_activity( + analytic_distribution, self.activity1, price_unit + ) + # Check account is set to account in activity + invoice.invoice_line_ids[0]._onchange_activity_id() + self.assertEqual( + self.activity1.account_id, invoice.invoice_line_ids[0].account_id + ) + # Change to product2, account should not change. + with Form(invoice) as invoice_form: + with invoice_form.invoice_line_ids.edit(0) as line_form: + line_form.product_id = self.product2 + line_form.price_unit = price_unit # Change product, amount will reset + invoice_form.save() + invoice.invoice_line_ids[0]._onchange_activity_id() + self.assertEqual( + self.activity1.account_id, invoice.invoice_line_ids[0].account_id + ) + + # Invoice line is not set up as following, + self.assertEqual( + self.activity1.account_id, invoice.invoice_line_ids[0].account_id + ) + self.assertEqual(self.product2, invoice.invoice_line_ids[0].product_id) + self.assertEqual(self.activity1, invoice.invoice_line_ids[0].activity_id) + # Change activity on template line for test no activity in template line + with Form(self.template_line1) as line: + line.kpi_id = self.kpi2 + with self.assertRaises(UserError): + invoice.action_post() + # Change activity on template line for test multi activity in template line + with Form(self.template_line1) as line: + line.kpi_id = self.kpi1 + with Form(self.template_line2) as line: + line.kpi_id = self.kpi1 + with self.assertRaises(UserError): + invoice.action_post() + # Change back to basic + with Form(self.template_line2) as line: + line.kpi_id = self.kpi2 + # Reset state and set account = account in activity + invoice.invoice_line_ids[0].account_id = self.activity1.account_id + # All values will be passed to budget move + invoice.action_post() + self.assertEqual(self.account_kpi1, invoice.budget_move_ids[0].account_id) + self.assertEqual(self.product2, invoice.budget_move_ids[0].product_id) + self.assertEqual(self.activity1, invoice.budget_move_ids[0].activity_id) + # Check budget move must account equal accuont in activity + with self.assertRaises(UserError): + invoice.budget_move_ids[0].account_id = self.account_kpi3.id + + @freeze_time("2001-02-01") + def test_02_budget_adjustment_activity(self): + """ + On budget adjustment, + - If no activity, budget follows product's account + - If activity is selected, account follows activity's regardless of product + - User can always change account code afterwards + """ + self.assertEqual(self.budget_control.amount_balance, 2400.0) + budget_adjust = self.BudgetAdjust.create( + { + "date_commit": "2001-02-01", + } + ) + with Form(budget_adjust.adjust_item_ids) as line: + line.adjust_id = budget_adjust + line.adjust_type = "consume" + line.product_id = self.product1 + line.analytic_distribution = {self.costcenter1.id: 100} + line.amount = 100.0 + adjust_line = line.save() + self.assertEqual(adjust_line.account_id, self.account_kpi1) + # Change to activity2, account should change to account_kpi2 + with Form(adjust_line) as line: + line.activity_id = self.activity2 + self.assertEqual(adjust_line.account_id, self.activity2.account_id) + # balance in budget control must be 'Decrease' + budget_adjust.action_adjust() + self.assertEqual(self.budget_control.amount_balance, 2300.0) diff --git a/budget_activity/views/account_move_views.xml b/budget_activity/views/account_move_views.xml new file mode 100644 index 00000000..7f44cd57 --- /dev/null +++ b/budget_activity/views/account_move_views.xml @@ -0,0 +1,28 @@ + + + + account.move.form + account.move + + + + + + + + + + + diff --git a/budget_activity/views/analytic_line_views.xml b/budget_activity/views/analytic_line_views.xml new file mode 100644 index 00000000..2f9336f6 --- /dev/null +++ b/budget_activity/views/analytic_line_views.xml @@ -0,0 +1,43 @@ + + + + account.analytic.line.tree + account.analytic.line + + + + + + + + + + account.analytic.line.select + account.analytic.line + + + + + + + + + + + + + + account.analytic.line.form + account.analytic.line + + + + + + + + diff --git a/budget_activity/views/budget_activity_view.xml b/budget_activity/views/budget_activity_view.xml new file mode 100644 index 00000000..7f2b2ad3 --- /dev/null +++ b/budget_activity/views/budget_activity_view.xml @@ -0,0 +1,162 @@ + + + + + budget.activity.keyword.view.tree + budget.activity.keyword + + + + + + + + + + budget.activity.keyword.view.search + budget.activity.keyword + + + + + + + + + + + + + budget.activity.view.form + budget.activity.keyword + +
+ + + + + + + + + + + + +
+
+
+ + Budget Activity Keywords + + budget.activity.keyword + tree,form + + + + budget.activity.view.tree + budget.activity + + + + + + + + + + + view.budget.activity.filter + budget.activity + + + + + + + + + + + + + + + + budget.activity.view.form + budget.activity + +
+ + +
+
+ + + + + + + + + + +
+
+
+
+ + Budget Activity + + budget.activity + tree,form + +
diff --git a/budget_activity/views/budget_kpi_view.xml b/budget_activity/views/budget_kpi_view.xml new file mode 100644 index 00000000..55dd4de6 --- /dev/null +++ b/budget_activity/views/budget_kpi_view.xml @@ -0,0 +1,17 @@ + + + + budget.kpi.view.form + budget.kpi + + + + + + + + + + + + diff --git a/budget_activity/views/budget_menuitem.xml b/budget_activity/views/budget_menuitem.xml new file mode 100644 index 00000000..9060bb64 --- /dev/null +++ b/budget_activity/views/budget_menuitem.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/budget_activity/views/budget_move_adjustment_view.xml b/budget_activity/views/budget_move_adjustment_view.xml new file mode 100644 index 00000000..0c01ac21 --- /dev/null +++ b/budget_activity/views/budget_move_adjustment_view.xml @@ -0,0 +1,25 @@ + + + + view.budget.move.adjustment.form + budget.move.adjustment + + + + + + + + + + + diff --git a/budget_activity/views/budget_template_view.xml b/budget_activity/views/budget_template_view.xml new file mode 100644 index 00000000..ee2b329b --- /dev/null +++ b/budget_activity/views/budget_template_view.xml @@ -0,0 +1,44 @@ + + + + budget.template.view.form + budget.template + + + + + + + 1 + 1 + + +
  • Activity: Replaces binding with an account, where 1 activity = 1 account and each activity is associated with a KPI.
  • +
    + + Hierarchy: +
    +                            KPI 1                   KPI 2
    +                            /  \                   /   \
    +                          /      \               /       \
    +                        /          \           /           \
    +                    Activity1   Activity2   Activity3   Activity4
    +                        |            \      /               |
    +                        |              \  /                 |
    +                    Account1          Account2          Account3
    +                
    + So, Activity must not be repeated in order for users to be able to select them, + and the system will correctly check the budget. +
    + +
    +
    +
    diff --git a/setup/budget_activity/odoo/addons/budget_activity b/setup/budget_activity/odoo/addons/budget_activity new file mode 120000 index 00000000..2ce89a3e --- /dev/null +++ b/setup/budget_activity/odoo/addons/budget_activity @@ -0,0 +1 @@ +../../../../budget_activity \ No newline at end of file diff --git a/setup/budget_activity/setup.py b/setup/budget_activity/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/budget_activity/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)