diff --git a/.copier-answers.yml b/.copier-answers.yml index f6097ada..f1f1d5d0 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -13,12 +13,9 @@ odoo_version: 14.0 org_name: Odoo Community Association (OCA) org_slug: OCA rebel_module_groups: [] -repo_description: 'TODO: add repo description.' -repo_name: account-budgeting +repo_description: You'll find modules that manage budgeting. +repo_name: Odoo Budgeting repo_slug: account-budgeting repo_website: https://github.com/OCA/account-budgeting travis_apt_packages: [] travis_apt_sources: [] -use_pyproject_toml: false -use_ruff: false - diff --git a/README.md b/README.md index c12a275e..ff1ebf11 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ -# account-budgeting +# Odoo Budgeting -TODO: add repo description. +You'll find modules that manage budgeting. diff --git a/account_budget_oca_analytic_tag/README.rst b/account_budget_oca_analytic_tag/README.rst new file mode 100644 index 00000000..68a40518 --- /dev/null +++ b/account_budget_oca_analytic_tag/README.rst @@ -0,0 +1,24 @@ +.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 + +=============================== +OSI Account Budget Analytic Tag +=============================== + +This module allows you to select an analytic tag on the budget line and report the consumed budget using that tag. + +Usage +===== + +No special usage instructions + +Credits +======= + +* Maxime Chambreuil + +Contributors +------------ + +* Open Source Integrators diff --git a/account_budget_oca_analytic_tag/__init__.py b/account_budget_oca_analytic_tag/__init__.py new file mode 100644 index 00000000..7d768b54 --- /dev/null +++ b/account_budget_oca_analytic_tag/__init__.py @@ -0,0 +1,3 @@ +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import models diff --git a/account_budget_oca_analytic_tag/__manifest__.py b/account_budget_oca_analytic_tag/__manifest__.py new file mode 100644 index 00000000..38c38a29 --- /dev/null +++ b/account_budget_oca_analytic_tag/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright (c) 2021 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "OSI Account Budget OCA Analytic Tag", + "version": "14.0.1.0.0", + "license": "AGPL-3", + "summary": "This module allows you to select an analytic tag on the budget" + " line and report the consumed budget using that tag.", + "author": "Open Source Integrators, Odoo Community Association (OCA)", + "maintainer": "Open Source Integrators", + "website": "https://github.com/OCA/account-budgeting", + "category": "Accounting", + "depends": ["account_budget_oca"], + "data": [ + "views/account_budget_views.xml", + ], + "installable": True, + "maintainers": ["max3903"], +} diff --git a/account_budget_oca_analytic_tag/i18n/account_budget_analytic_tag.pot b/account_budget_oca_analytic_tag/i18n/account_budget_analytic_tag.pot new file mode 100644 index 00000000..e1a526ba --- /dev/null +++ b/account_budget_oca_analytic_tag/i18n/account_budget_analytic_tag.pot @@ -0,0 +1,49 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_budget_analytic_tag +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0+e-20211202\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-12-23 19:57+0000\n" +"PO-Revision-Date: 2021-12-23 19:57+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: account_budget_analytic_tag +#: model:ir.model.fields,field_description:account_budget_analytic_tag.field_crossovered_budget_lines__analytic_tag_id +msgid "Analytic Tag" +msgstr "" + +#. module: account_budget_analytic_tag +#: model:ir.model,name:account_budget_analytic_tag.model_crossovered_budget_lines +msgid "Budget Line" +msgstr "" + +#. module: account_budget_analytic_tag +#: model:ir.model.fields,field_description:account_budget_analytic_tag.field_crossovered_budget_lines__display_name +msgid "Display Name" +msgstr "" + +#. module: account_budget_analytic_tag +#: model:ir.model.fields,field_description:account_budget_analytic_tag.field_crossovered_budget_lines__id +msgid "ID" +msgstr "" + +#. module: account_budget_analytic_tag +#: model:ir.model.fields,field_description:account_budget_analytic_tag.field_crossovered_budget_lines____last_update +msgid "Last Modified on" +msgstr "" + +#. module: account_budget_analytic_tag +#: code:addons/account_budget_analytic_tag/models/account_budget.py:0 +#, python-format +msgid "" +"You have to enter at least a budgetary position or analytic account or " +"analytic tag on a budget line." +msgstr "" diff --git a/account_budget_oca_analytic_tag/i18n/es.po b/account_budget_oca_analytic_tag/i18n/es.po new file mode 100644 index 00000000..8d1c03a0 --- /dev/null +++ b/account_budget_oca_analytic_tag/i18n/es.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_budget_analytic_tag +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0+e-20211202\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-12-23 19:57+0000\n" +"PO-Revision-Date: 2021-12-23 19:57+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: account_budget_analytic_tag +#: model:ir.model.fields,field_description:account_budget_analytic_tag.field_crossovered_budget_lines__analytic_tag_id +msgid "Analytic Tag" +msgstr "Etiqueta analítica" + +#. module: account_budget_analytic_tag +#: model:ir.model,name:account_budget_analytic_tag.model_crossovered_budget_lines +msgid "Budget Line" +msgstr "Línea de presupuesto" + +#. module: account_budget_analytic_tag +#: model:ir.model.fields,field_description:account_budget_analytic_tag.field_crossovered_budget_lines__display_name +msgid "Display Name" +msgstr "Nombre" + +#. module: account_budget_analytic_tag +#: model:ir.model.fields,field_description:account_budget_analytic_tag.field_crossovered_budget_lines__id +msgid "ID" +msgstr "" + +#. module: account_budget_analytic_tag +#: model:ir.model.fields,field_description:account_budget_analytic_tag.field_crossovered_budget_lines____last_update +msgid "Last Modified on" +msgstr "Ultima modificación el" + +#. module: account_budget_analytic_tag +#: code:addons/account_budget_analytic_tag/models/account_budget.py:0 +#, python-format +msgid "" +"You have to enter at least a budgetary position or analytic account or " +"analytic tag on a budget line." +msgstr "" +"Deben entrar por lo menos una posicion presupuestarial, cuenta analítica o etiqueta " +"analítica en una línea de presupuesto." diff --git a/account_budget_oca_analytic_tag/models/__init__.py b/account_budget_oca_analytic_tag/models/__init__.py new file mode 100644 index 00000000..4eb3bdd9 --- /dev/null +++ b/account_budget_oca_analytic_tag/models/__init__.py @@ -0,0 +1,3 @@ +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import account_budget diff --git a/account_budget_oca_analytic_tag/models/account_budget.py b/account_budget_oca_analytic_tag/models/account_budget.py new file mode 100644 index 00000000..d5d85397 --- /dev/null +++ b/account_budget_oca_analytic_tag/models/account_budget.py @@ -0,0 +1,73 @@ +# Copyright (c) 2021 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class CrossoveredBudgetLines(models.Model): + _inherit = "crossovered.budget.lines" + + analytic_tag_id = fields.Many2one("account.analytic.tag", string="Analytic Tag") + + def _compute_practical_amount(self): + """Overwrite this method for to count practical_amount based on + analytic_tag_id on move line.""" + for line in self: + acc_ids = line.general_budget_id.account_ids.ids + date_to = line.date_to + date_from = line.date_from + if line.analytic_account_id.id: + self.env.cr.execute( + """ + SELECT SUM(amount) + FROM account_analytic_line + WHERE account_id=%s + AND (date between %s + AND %s) + AND general_account_id=ANY(%s)""", + (line.analytic_account_id.id, date_from, date_to, acc_ids), + ) + result = self.env.cr.fetchone()[0] or 0.0 + + elif line.general_budget_id.account_ids: + self.env.cr.execute( + """ + SELECT SUM(credit) - SUM(debit) + FROM account_move_line aml + LEFT JOIN account_move am ON aml.move_id = am.id + LEFT JOIN + account_analytic_tag_account_move_line_rel aat + ON aat.account_move_line_id = aml.id + WHERE state=%s + AND account_id in %s + AND (aml.date between %s + AND %s) + AND aat.account_analytic_tag_id=%s""", + ( + "posted", + tuple(line.general_budget_id.account_ids.ids), + date_from, + date_to, + line.analytic_tag_id.id or 0.0, + ), + ) + result = self.env.cr.fetchone()[0] + else: + result = 0.0 + line.practical_amount = result + + @api.constrains("general_budget_id", "analytic_account_id", "analytic_tag_id") + def _must_have_analytical_or_budgetary_or_both(self): + for record in self: + if ( + not record.analytic_account_id + and not record.general_budget_id + and not record.analytic_tag_id + ): + raise ValidationError( + _( + "You have to enter at least a budgetary position or analytic account" + " or analytic tag on a budget line." + ) + ) diff --git a/account_budget_oca_analytic_tag/static/description/icon.png b/account_budget_oca_analytic_tag/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/account_budget_oca_analytic_tag/static/description/icon.png differ diff --git a/account_budget_oca_analytic_tag/tests/__init__.py b/account_budget_oca_analytic_tag/tests/__init__.py new file mode 100644 index 00000000..847859fb --- /dev/null +++ b/account_budget_oca_analytic_tag/tests/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Open Source Integrators (https://www.opensourceintegrators.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import test_account_budget_oca_analytic_tag diff --git a/account_budget_oca_analytic_tag/tests/test_account_budget_oca_analytic_tag.py b/account_budget_oca_analytic_tag/tests/test_account_budget_oca_analytic_tag.py new file mode 100644 index 00000000..1d459a28 --- /dev/null +++ b/account_budget_oca_analytic_tag/tests/test_account_budget_oca_analytic_tag.py @@ -0,0 +1,63 @@ +# Copyright (C) 2022 Open Source Integrators (https://www.opensourceintegrators.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests import common + + +class TestBudgetAnalyticTag(common.TransactionCase): + def setUp(self): + super(TestBudgetAnalyticTag, self).setUp() + + self.budget_position_obj = self.env["account.budget.post"] + self.exp_account_type = self.env.ref("account.data_account_type_expenses") + self.company_id = self.env.ref("base.main_company") + self.analytic_account_id = self.env.ref("analytic.analytic_agrolait") + self.user_id = self.env.ref("base.user_admin") + self.partner_id = self.env.ref("base.res_partner_2") + self.product_id = self.env.ref("product.product_product_8") + self.budget_position_id = self.test_create_budget_position() + + def test_create_budget_position(self): + account_ids = ( + self.env["account.account"] + .search( + [ + ("company_id", "=", self.company_id.id), + ("user_type_id", "=", self.exp_account_type.id), + ] + ) + .ids + ) + return self.budget_position_obj.create( + { + "name": "Budget Position", + "company_id": self.company_id.id, + "account_ids": [(6, 0, account_ids)], + } + ) + + def test_budget_creation(self): + budget_id = self.env["crossovered.budget"].create( + { + "name": "Budget 2022", + "creating_user_id": self.user_id.id, + "company_id": self.company_id.id, + "date_from": "2022-01-01", + "date_to": "2022-06-30", + "crossovered_budget_line_ids": [ + ( + 0, + 0, + { + "general_budget_id": self.budget_position_id.id, + "analytic_account_id": self.analytic_account_id.id, + "date_from": "2022-01-01", + "date_to": "2022-06-30", + "planned_amount": 10000.00, + }, + ) + ], + } + ) + budget_id.action_budget_confirm() + budget_id.action_budget_validate() diff --git a/account_budget_oca_analytic_tag/views/account_budget_views.xml b/account_budget_oca_analytic_tag/views/account_budget_views.xml new file mode 100644 index 00000000..1530789e --- /dev/null +++ b/account_budget_oca_analytic_tag/views/account_budget_views.xml @@ -0,0 +1,41 @@ + + + + crossovered.budget.view.form.inherit.analytic_tag + crossovered.budget + + + + 0 + + + 0 + + + + + + + + + + + diff --git a/purchase_budget_oca_validation/README.rst b/purchase_budget_oca_validation/README.rst new file mode 100644 index 00000000..3b8e2251 --- /dev/null +++ b/purchase_budget_oca_validation/README.rst @@ -0,0 +1,71 @@ +========================== +Purchase Budget Validation +========================== + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 + +|badge1| |badge2| + +This module checks the purchase order lines amount against their related budgets and +prevents you from going over budget. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +* Create a budget with either a budgetary position, an analytic account or analytic tag +* Confirm it + +Usage +===== + +* Create a purchase order and select a product, analytic account or tag that matches one of your budget lines. +* Check for message in the chatter if you are over a budget. + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Credits +======= + +Authors +~~~~~~~ + +* Open Source Integrators +* Serpent Consulting Services + +Contributors +~~~~~~~~~~~~ + +* Maxime Chambreuil +* Hardik Suthar + +Other credits +~~~~~~~~~~~~~ + +The development of this module has been financially supported by: + +* Casai + +Maintainers +~~~~~~~~~~~ + +.. |maintainer-max3903| image:: https://github.com/max3903.png?size=40px + :target: https://github.com/max3903 + :alt: max3903 + +|maintainer-max3903| diff --git a/purchase_budget_oca_validation/__init__.py b/purchase_budget_oca_validation/__init__.py new file mode 100644 index 00000000..253a08ea --- /dev/null +++ b/purchase_budget_oca_validation/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2022 Open Source Integrators +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from . import models diff --git a/purchase_budget_oca_validation/__manifest__.py b/purchase_budget_oca_validation/__manifest__.py new file mode 100644 index 00000000..5559a15f --- /dev/null +++ b/purchase_budget_oca_validation/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright (c) 2022 Open Source Integrators +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +{ + "name": "Purchase Budget OCA Validation", + "version": "14.0.1.0.0", + "author": "Open Source Integrators, Odoo Community Association (OCA)", + "summary": "Check purchase orders against budgets", + "license": "AGPL-3", + "category": "Purchase", + "maintainer": "Open Source Integrators", + "website": "https://github.com/OCA/account-budgeting", + "depends": ["purchase", "account_budget_oca_analytic_tag"], + "installable": True, + "maintainers": ["max3903"], + "development_status": "Beta", + "data": [ + "views/crossovered_budget_lines_view.xml", + "views/purchase_order_view.xml", + ], +} diff --git a/purchase_budget_oca_validation/models/__init__.py b/purchase_budget_oca_validation/models/__init__.py new file mode 100644 index 00000000..a60f7980 --- /dev/null +++ b/purchase_budget_oca_validation/models/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) 2022 Open Source Integrators +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from . import crossovered_budget +from . import crossovered_budget_lines +from . import purchase_order +from . import purchase_order_line diff --git a/purchase_budget_oca_validation/models/crossovered_budget.py b/purchase_budget_oca_validation/models/crossovered_budget.py new file mode 100644 index 00000000..19f72c13 --- /dev/null +++ b/purchase_budget_oca_validation/models/crossovered_budget.py @@ -0,0 +1,11 @@ +# Copyright (c) 2022 Open Source Integrators +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from odoo import fields, models + + +class CrossoveredBudget(models.Model): + _inherit = "crossovered.budget" + + amount_include_tax = fields.Boolean( + "Include Taxes in Amount", default=False, copy=False + ) diff --git a/purchase_budget_oca_validation/models/crossovered_budget_lines.py b/purchase_budget_oca_validation/models/crossovered_budget_lines.py new file mode 100644 index 00000000..0ef35d17 --- /dev/null +++ b/purchase_budget_oca_validation/models/crossovered_budget_lines.py @@ -0,0 +1,138 @@ +# Copyright (c) 2022 Open Source Integrators +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from odoo import fields, models + + +class CrossoveredBudgetLines(models.Model): + _inherit = "crossovered.budget.lines" + + committed_amount = fields.Float( + compute="_compute_committ_uncommitt_amount", string="Committed Amount" + ) + uncommitted_amount = fields.Float( + compute="_compute_committ_uncommitt_amount", string="Uncommitted Amount" + ) + over_budget = fields.Boolean(compute="_compute_over_budget", string="Over Budget") + + def _compute_committ_uncommitt_amount(self): + purchase_line_obj = self.env["purchase.order.line"] + company_currency = self.company_id.currency_id + for budget_line in self: + commit_domain = [ + ("date_order", ">=", budget_line.date_from), + ("date_order", "<=", budget_line.date_to), + ("order_id.state", "in", ("purchase", "done")), + "|", + ("account_analytic_id", "=", budget_line.analytic_account_id.id), + ("analytic_tag_ids", "in", budget_line.analytic_tag_id.ids), + "|", + ( + "product_id.property_account_expense_id", + "in", + budget_line.general_budget_id.account_ids.ids, + ), + ( + "product_id.categ_id.property_account_expense_categ_id", + "in", + budget_line.general_budget_id.account_ids.ids, + ), + ] + po_lines_commimt = purchase_line_obj.search(commit_domain) + if budget_line.crossovered_budget_id.amount_include_tax: + budget_line.committed_amount = -sum( + [ + line.currency_id._convert( + line.price_unit, + company_currency, + self.company_id, + line.date_order, + round=False, + ) + * (line.product_qty - line.qty_invoiced) + + line.currency_id._convert( + line.price_tax, + company_currency, + self.company_id, + line.date_order, + round=False, + ) + for line in po_lines_commimt + ] + ) + else: + budget_line.committed_amount = -sum( + [ + line.currency_id._convert( + line.price_unit, + company_currency, + self.company_id, + line.date_order, + round=False, + ) + * (line.product_qty - line.qty_invoiced) + for line in po_lines_commimt + ] + ) + uncommit_domain = [ + ("date_order", ">=", budget_line.date_from), + ("date_order", "<=", budget_line.date_to), + ("order_id.state", "not in", ("purchase", "done", "cancel")), + "|", + ("account_analytic_id", "=", budget_line.analytic_account_id.id), + ("analytic_tag_ids", "in", budget_line.analytic_tag_id.ids), + "|", + ( + "product_id.property_account_expense_id", + "in", + budget_line.general_budget_id.account_ids.ids, + ), + ( + "product_id.categ_id.property_account_expense_categ_id", + "in", + budget_line.general_budget_id.account_ids.ids, + ), + ] + po_lines_uncommit = purchase_line_obj.search(uncommit_domain) + if budget_line.crossovered_budget_id.amount_include_tax: + budget_line.uncommitted_amount = -sum( + [ + line.currency_id._convert( + line.price_subtotal, + company_currency, + self.company_id, + line.date_order, + round=False, + ) + + line.currency_id._convert( + line.price_tax, + company_currency, + self.company_id, + line.date_order, + round=False, + ) + for line in po_lines_uncommit + ] + ) + else: + budget_line.uncommitted_amount = -sum( + [ + line.currency_id._convert( + line.price_subtotal, + company_currency, + self.company_id, + line.date_order, + round=False, + ) + for line in po_lines_uncommit + ] + ) + + def _compute_over_budget(self): + for rec in self: + rec.over_budget = False + if ( + abs(rec.practical_amount) + + abs(rec.committed_amount) + + abs(rec.uncommitted_amount) + ) > abs(rec.planned_amount): + rec.over_budget = True diff --git a/purchase_budget_oca_validation/models/purchase_order.py b/purchase_budget_oca_validation/models/purchase_order.py new file mode 100644 index 00000000..858727be --- /dev/null +++ b/purchase_budget_oca_validation/models/purchase_order.py @@ -0,0 +1,74 @@ +# Copyright (c) 2022 Open Source Integrators +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class PurchaseOrder(models.Model): + _inherit = "purchase.order" + + over_budget = fields.Boolean( + compute="_compute_over_budget_po", string="Over Budget" + ) + + def _compute_over_budget_po(self): + for rec in self: + rec.over_budget = rec.check_budget(notify=False) + + def check_budget(self, notify=False): + message = "" + users = self.env["res.users"] + over_budget = False + for po_line in self.order_line: + budget_lines = po_line._get_budget_lines() + for b_line in budget_lines: + if b_line.over_budget: + users += b_line.crossovered_budget_id.creating_user_id + message += _( + "This order is over budget %s / %s (%s):" + "
%s + %s + %s = %s > %s.
" + % ( + b_line.crossovered_budget_id.name, + b_line.general_budget_id.name, + b_line.crossovered_budget_id.creating_user_id.name, + abs(b_line.uncommitted_amount), + abs(b_line.committed_amount), + abs(b_line.practical_amount), + abs(b_line.practical_amount) + + abs(b_line.committed_amount) + + abs(b_line.uncommitted_amount), + abs(b_line.planned_amount), + ) + ) + if message: + over_budget = True + if notify: + self.message_subscribe(partner_ids=users.partner_id.ids) + self.message_post(body=message) + return over_budget + + def button_confirm(self): + for rec in self: + if rec.over_budget: + raise ValidationError( + _( + "You cannot confirm the purchase order " + "as one budget would go over the planned amount. " + "Please check the chatter for more details." + ) + ) + else: + super(PurchaseOrder, rec).button_confirm() + + @api.model + def create(self, vals): + res = super().create(vals) + res.check_budget(notify=True) + return res + + def write(self, vals): + res = super().write(vals) + for po in self: + po.check_budget(notify=True) + return res diff --git a/purchase_budget_oca_validation/models/purchase_order_line.py b/purchase_budget_oca_validation/models/purchase_order_line.py new file mode 100644 index 00000000..e4f7d172 --- /dev/null +++ b/purchase_budget_oca_validation/models/purchase_order_line.py @@ -0,0 +1,30 @@ +# Copyright (c) 2022 Open Source Integrators +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from odoo import models + + +class PurchaseOrderLine(models.Model): + _inherit = "purchase.order.line" + + def _get_budget_lines(self): + return self.env["crossovered.budget.lines"].search( + [ + ("date_from", "<=", self.date_order), + ("date_to", ">=", self.date_order), + ("crossovered_budget_id.state", "=", "validate"), + "|", + ("analytic_account_id", "=", self.account_analytic_id.id), + ("analytic_tag_id", "in", self.analytic_tag_ids.ids), + "|", + ( + "general_budget_id.account_ids", + "in", + self.product_id.property_account_expense_id.ids, + ), + ( + "general_budget_id.account_ids", + "in", + self.product_id.categ_id.property_account_expense_categ_id.ids, + ), + ] + ) diff --git a/purchase_budget_oca_validation/static/description/icon.png b/purchase_budget_oca_validation/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/purchase_budget_oca_validation/static/description/icon.png differ diff --git a/purchase_budget_oca_validation/tests/__init__.py b/purchase_budget_oca_validation/tests/__init__.py new file mode 100644 index 00000000..b4c186c8 --- /dev/null +++ b/purchase_budget_oca_validation/tests/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2022 Open Source Integrators (https://www.opensourceintegrators.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import test_purchase_budget_validation diff --git a/purchase_budget_oca_validation/tests/test_purchase_budget_validation.py b/purchase_budget_oca_validation/tests/test_purchase_budget_validation.py new file mode 100644 index 00000000..99230422 --- /dev/null +++ b/purchase_budget_oca_validation/tests/test_purchase_budget_validation.py @@ -0,0 +1,104 @@ +# Copyright (C) 2022 Open Source Integrators (https://www.opensourceintegrators.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.exceptions import ValidationError +from odoo.tests import common + + +class TestBudgetValidation(common.TransactionCase): + def setUp(self): + super(TestBudgetValidation, self).setUp() + + self.budget_position_obj = self.env["account.budget.post"] + self.exp_account_type = self.env.ref("account.data_account_type_expenses") + self.company_id = self.env.ref("base.main_company") + self.analytic_account_id = self.env.ref("analytic.analytic_agrolait") + self.user_id = self.env.ref("base.user_admin") + self.partner_id = self.env.ref("base.res_partner_2") + self.product_id = self.env.ref("product.product_product_8") + self.budget_position_id = self.test_create_budget_position() + + def test_create_budget_position(self): + account_ids = ( + self.env["account.account"] + .search( + [ + ("company_id", "=", self.company_id.id), + ("user_type_id", "=", self.exp_account_type.id), + ] + ) + .ids + ) + return self.budget_position_obj.create( + { + "name": "Budget Position", + "company_id": self.company_id.id, + "account_ids": [(6, 0, account_ids)], + } + ) + + def test_budget_creation(self): + budget_id = self.env["crossovered.budget"].create( + { + "name": "Budget 2022", + "creating_user_id": self.user_id.id, + "company_id": self.company_id.id, + "date_from": "2022-01-01", + "date_to": "2022-06-30", + "amount_include_tax": True, + "crossovered_budget_line_ids": [ + ( + 0, + 0, + { + "general_budget_id": self.budget_position_id.id, + "analytic_account_id": self.analytic_account_id.id, + "date_from": "2022-01-01", + "date_to": "2022-06-30", + "planned_amount": 10000.00, + }, + ) + ], + } + ) + budget_id.action_budget_confirm() + budget_id.action_budget_validate() + + def test_purchase_order_budget(self): + po_id = self.env["purchase.order"].create( + { + "partner_id": self.partner_id.id, + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product_id.id, + "account_analytic_id": self.analytic_account_id.id, + "product_qty": 5, + "price_unit": 1000, + }, + ) + ], + } + ) + self.assertRaises(ValidationError, po_id.button_confirm()) + po_id_new = po_id.copy() + po_id_new.order_line.unlink() + po_id_new.write( + { + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product_id.id, + "account_analytic_id": self.analytic_account_id.id, + "product_qty": 5, + "price_unit": 1500, + }, + ) + ] + } + ) + self.assertRaises(ValidationError, po_id_new.button_confirm()) diff --git a/purchase_budget_oca_validation/views/crossovered_budget_lines_view.xml b/purchase_budget_oca_validation/views/crossovered_budget_lines_view.xml new file mode 100644 index 00000000..70f9c71b --- /dev/null +++ b/purchase_budget_oca_validation/views/crossovered_budget_lines_view.xml @@ -0,0 +1,31 @@ + + + account.budget.lines.form.inherit + crossovered.budget + + + + + + + + + + + + + + + + + + diff --git a/purchase_budget_oca_validation/views/purchase_order_view.xml b/purchase_budget_oca_validation/views/purchase_order_view.xml new file mode 100644 index 00000000..0966d362 --- /dev/null +++ b/purchase_budget_oca_validation/views/purchase_order_view.xml @@ -0,0 +1,12 @@ + + + purchase.order.form.inherit.budget + purchase.order + + + + + + + + diff --git a/setup/account_budget_oca_analytic_tag/odoo/addons/account_budget_oca_analytic_tag b/setup/account_budget_oca_analytic_tag/odoo/addons/account_budget_oca_analytic_tag new file mode 120000 index 00000000..014f2182 --- /dev/null +++ b/setup/account_budget_oca_analytic_tag/odoo/addons/account_budget_oca_analytic_tag @@ -0,0 +1 @@ +../../../../account_budget_oca_analytic_tag \ No newline at end of file diff --git a/setup/account_budget_oca_analytic_tag/setup.py b/setup/account_budget_oca_analytic_tag/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/account_budget_oca_analytic_tag/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/purchase_budget_oca_validation/odoo/addons/purchase_budget_oca_validation b/setup/purchase_budget_oca_validation/odoo/addons/purchase_budget_oca_validation new file mode 120000 index 00000000..4b5be9dd --- /dev/null +++ b/setup/purchase_budget_oca_validation/odoo/addons/purchase_budget_oca_validation @@ -0,0 +1 @@ +../../../../purchase_budget_oca_validation \ No newline at end of file diff --git a/setup/purchase_budget_oca_validation/setup.py b/setup/purchase_budget_oca_validation/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/purchase_budget_oca_validation/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)