diff --git a/.copier-answers.yml b/.copier-answers.yml index 073df2b..b04cad8 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Do NOT update manually; changes here will be overwritten by Copier -_commit: edb24fc +_commit: b7bb2ff _src_path: gh:open-synergy/ssi-addons-repo-template dependency_installation_mode: OCA generate_requirements_txt: true @@ -7,7 +7,7 @@ include_aeroo_report: false include_wkhtmltopdf: false odoo_version: 14.0 rebel_module_groups: [] -repo_description: null +repo_description: ssi-financial-accounting repo_name: ssi-financial-accounting repo_slug: ssi-financial-accounting travis_apt_packages: [] diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 2e8eee3..1d27e1d 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -11,27 +11,13 @@ on: jobs: pre-commit: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - with: - python-version: "3.9.7" - name: Get python version run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV - uses: actions/cache@v1 with: path: ~/.cache/pre-commit key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} - - name: Install pre-commit - run: pip install pre-commit - - name: Run pre-commit - run: pre-commit run --all-files --show-diff-on-failure --color=always - - name: Check that all files generated by pre-commit are in git - run: | - newfiles="$(git ls-files --others --exclude-from=.gitignore)" - if [ "$newfiles" != "" ] ; then - echo "Please check-in the following files:" - echo "$newfiles" - exit 1 - fi diff --git a/.github/workflows/pre-commit.yml.rej b/.github/workflows/pre-commit.yml.rej new file mode 100644 index 0000000..74c606e --- /dev/null +++ b/.github/workflows/pre-commit.yml.rej @@ -0,0 +1,24 @@ +diff a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml (rejected hunks) +@@ -13,3 +18,20 @@ jobs: + with: +- python-version: "3.9.7" +- - uses: pre-commit/action@v2.0.0 ++ python-version: "3.9.7" ++ - name: Get python version ++ run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV ++ - uses: actions/cache@v1 ++ with: ++ path: ~/.cache/pre-commit ++ key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} ++ - name: Install pre-commit ++ run: pip install pre-commit ++ - name: Run pre-commit ++ run: pre-commit run --all-files --show-diff-on-failure --color=always ++ - name: Check that all files generated by pre-commit are in git ++ run: | ++ newfiles="$(git ls-files --others --exclude-from=.gitignore)" ++ if [ "$newfiles" != "" ] ; then ++ echo "Please check-in the following files:" ++ echo "$newfiles" ++ exit 1 ++ fi diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index adade37..923010c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -112,7 +112,7 @@ repos: - requirements.txt - --header - "# generated from manifests external_dependencies" - - repo: https://gitlab.com/PyCQA/flake8 + - repo: https://github.com/PyCQA/flake8 rev: 3.9.2 hooks: - id: flake8 diff --git a/README.md b/README.md index 1ad8d5a..cc7e039 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ # ssi-financial-accounting -None +ssi-financial-accounting diff --git a/ssi_account_receivable_penalty/README.rst b/ssi_account_receivable_penalty/README.rst new file mode 100644 index 0000000..6077d2c --- /dev/null +++ b/ssi_account_receivable_penalty/README.rst @@ -0,0 +1,51 @@ +.. 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 + +========================== +Account Receivable Penalty +========================== + + +Installation +============ + +To install this module, you need to: + +1. Clone the branch 14.0 of the repository +2. Add the path to this repository in your configuration (addons-path) +3. Update the module list +4. Go to menu *Apps -> Apps -> Main Apps* +5. Search For *Account Receivable Penalty* +6. Install the module + +Roadmap +======= + + +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 +======= + +Contributors +------------ + +* Michael Viriyananda + +Maintainer +---------- + +.. image:: https://simetri-sinergi.id/logo.png + :alt: PT. Simetri Sinergi Indonesia + :target: https://simetri-sinergi.id.com + +This module is maintained by the PT. Simetri Sinergi Indonesia. diff --git a/ssi_account_receivable_penalty/__init__.py b/ssi_account_receivable_penalty/__init__.py new file mode 100644 index 0000000..0e3bc94 --- /dev/null +++ b/ssi_account_receivable_penalty/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2022 OpenSynergy Indonesia +# Copyright 2022 PT. Simetri Sinergi Indonesia +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from . import ( + models, +) diff --git a/ssi_account_receivable_penalty/__manifest__.py b/ssi_account_receivable_penalty/__manifest__.py new file mode 100644 index 0000000..ee0f20a --- /dev/null +++ b/ssi_account_receivable_penalty/__manifest__.py @@ -0,0 +1,40 @@ +# Copyright 2022 OpenSynergy Indonesia +# Copyright 2022 PT. Simetri Sinergi Indonesia +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +{ + "name": "Account Receivable Penalty", + "version": "14.0.1.0.0", + "category": "Accounting", + "website": "https://simetri-sinergi.id", + "author": "PT. Simetri Sinergi Indonesia, OpenSynergy Indonesia", + "license": "LGPL-3", + "installable": True, + "depends": [ + "ssi_account_move_line_day_overdue", + "ssi_master_data_mixin", + "ssi_transaction_open_mixin", + "ssi_transaction_confirm_mixin", + "ssi_transaction_done_mixin", + "ssi_transaction_cancel_mixin", + "ssi_company_currency_mixin", + "base_automation", + ], + "data": [ + "security/ir_module_category_data.xml", + "security/res_group_data.xml", + "security/ir.model.access.csv", + "security/ir_rule_data.xml", + "data/ir_sequence_data.xml", + "data/sequence_template_data.xml", + "data/receivable_penalty_policy_data.xml", + "data/receivable_penalty_comp_policy_data.xml", + "data/approval_template_data.xml", + "data/ir_actions_server_data.xml", + "data/base_automation_data.xml", + "views/account_receivable_penalty_type_views.xml", + "views/account_receivable_penalty_computation_views.xml", + "views/account_receivable_penalty_views.xml", + ], + "demo": [], + "images": [], +} diff --git a/ssi_account_receivable_penalty/data/approval_template_data.xml b/ssi_account_receivable_penalty/data/approval_template_data.xml new file mode 100644 index 0000000..63e416e --- /dev/null +++ b/ssi_account_receivable_penalty/data/approval_template_data.xml @@ -0,0 +1,49 @@ + + + + + + + Standard + + + use_python + result = True + + + + + use_group + + + + + + Standard + + + use_python + result = True + + + + + use_group + + + + diff --git a/ssi_account_receivable_penalty/data/base_automation_data.xml b/ssi_account_receivable_penalty/data/base_automation_data.xml new file mode 100644 index 0000000..74cd421 --- /dev/null +++ b/ssi_account_receivable_penalty/data/base_automation_data.xml @@ -0,0 +1,21 @@ + + + + + + on_write + [('reconcile','=',False)] + [('reconcile','=',True)] + + + + + on_write + [('reconcile','=',True)] + [('reconcile','=',False)] + + + + diff --git a/ssi_account_receivable_penalty/data/ir_actions_server_data.xml b/ssi_account_receivable_penalty/data/ir_actions_server_data.xml new file mode 100644 index 0000000..7a9fdc9 --- /dev/null +++ b/ssi_account_receivable_penalty/data/ir_actions_server_data.xml @@ -0,0 +1,20 @@ + + + + + Action Open + + code + records.action_open() + + + + Action Done + + code + records.action_done() + + + diff --git a/ssi_account_receivable_penalty/data/ir_sequence_data.xml b/ssi_account_receivable_penalty/data/ir_sequence_data.xml new file mode 100644 index 0000000..80ed4a7 --- /dev/null +++ b/ssi_account_receivable_penalty/data/ir_sequence_data.xml @@ -0,0 +1,28 @@ + + + + + + Default + account.receivable_penalty + ARP/%(range_year)s/ + + + + + + + + Default + account.receivable_penalty_computation + ARPC/%(range_year)s/ + + + + + + + + diff --git a/ssi_account_receivable_penalty/data/receivable_penalty_comp_policy_data.xml b/ssi_account_receivable_penalty/data/receivable_penalty_comp_policy_data.xml new file mode 100644 index 0000000..5f118f5 --- /dev/null +++ b/ssi_account_receivable_penalty/data/receivable_penalty_comp_policy_data.xml @@ -0,0 +1,191 @@ + + + + + + + Standard + + + + result=True + + + + + + + + + + use_group + + + + + + + + + + + + + result = False +if env.user.id in document.active_approver_user_ids.ids: + result = True + + + + + + + + + + + result = False +if env.user.id in document.active_approver_user_ids.ids: + result = True + + + + + + + + + + + use_group + + + result = False +if not document.approval_template_id: + result = True + + + + + + + + + + use_group + + + + + + + + + + + + use_group + + + + + + + + + + + + + use_group + + + + + + + + + + + + use_group + + + + + diff --git a/ssi_account_receivable_penalty/data/receivable_penalty_policy_data.xml b/ssi_account_receivable_penalty/data/receivable_penalty_policy_data.xml new file mode 100644 index 0000000..0998217 --- /dev/null +++ b/ssi_account_receivable_penalty/data/receivable_penalty_policy_data.xml @@ -0,0 +1,191 @@ + + + + + + + Standard + + + + result=True + + + + + + + + + + use_group + + + + + + + + + + + + + result = False +if env.user.id in document.active_approver_user_ids.ids: + result = True + + + + + + + + + + + result = False +if env.user.id in document.active_approver_user_ids.ids: + result = True + + + + + + + + + + + use_group + + + result = False +if not document.approval_template_id: + result = True + + + + + + + + + + use_group + + + + + + + + + + + + use_group + + + + + + + + + + + + + use_group + + + + + + + + + + + + use_group + + + + + diff --git a/ssi_account_receivable_penalty/data/sequence_template_data.xml b/ssi_account_receivable_penalty/data/sequence_template_data.xml new file mode 100644 index 0000000..6d3f314 --- /dev/null +++ b/ssi_account_receivable_penalty/data/sequence_template_data.xml @@ -0,0 +1,54 @@ + + + + + + Standard + + + / + + + use_python + result=True + + use_sequence + + + + + Standard + + + / + + + use_python + result=True + + use_sequence + + + + + diff --git a/ssi_account_receivable_penalty/models/__init__.py b/ssi_account_receivable_penalty/models/__init__.py new file mode 100644 index 0000000..b295864 --- /dev/null +++ b/ssi_account_receivable_penalty/models/__init__.py @@ -0,0 +1,9 @@ +# Copyright 2022 OpenSynergy Indonesia +# Copyright 2022 PT. Simetri Sinergi Indonesia +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from . import ( + account_receivable_penalty_type, + account_receivable_penalty_computation, + account_receivable_penalty_tax, + account_receivable_penalty, +) diff --git a/ssi_account_receivable_penalty/models/account_receivable_penalty.py b/ssi_account_receivable_penalty/models/account_receivable_penalty.py new file mode 100644 index 0000000..ad69aae --- /dev/null +++ b/ssi_account_receivable_penalty/models/account_receivable_penalty.py @@ -0,0 +1,542 @@ +# Copyright 2022 OpenSynergy Indonesia +# Copyright 2022 PT. Simetri Sinergi Indonesia +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). +from odoo import api, fields, models + +from odoo.addons.ssi_decorator import ssi_decorator + + +class AccountReceivablePenalty(models.Model): + _name = "account.receivable_penalty" + _inherit = [ + "mixin.transaction_confirm", + "mixin.transaction_open", + "mixin.transaction_done", + "mixin.transaction_cancel", + "mixin.company_currency", + ] + _description = "Account Receivable Penalty" + + # Multiple Approval Attribute + _approval_from_state = "draft" + _approval_to_state = "open" + _approval_state = "confirm" + _after_approved_method = "action_open" + + # Attributes related to add element on view automatically + _automatically_insert_view_element = True + _automatically_insert_done_policy_fields = False + _automatically_insert_done_button = False + + _statusbar_visible_label = "draft,confirm,open,done" + _policy_field_order = [ + "confirm_ok", + "approve_ok", + "reject_ok", + "restart_approval_ok", + "cancel_ok", + "restart_ok", + "open_ok", + "done_ok", + "manual_number_ok", + ] + _header_button_order = [ + "action_confirm", + "action_approve_approval", + "action_reject_approval", + "action_done", + "%(ssi_transaction_cancel_mixin.base_select_cancel_reason_action)d", + "action_restart", + ] + + # Attributes related to add element on search view automatically + _state_filter_order = [ + "dom_draft", + "dom_confirm", + "dom_reject", + "dom_open", + "dom_done", + "dom_cancel", + ] + + # Sequence attribute + _create_sequence_state = "open" + + # FIELD + type_id = fields.Many2one( + string="Type", + comodel_name="account.receivable_penalty_type", + required=True, + readonly=True, + ondelete="restrict", + states={ + "draft": [ + ("readonly", False), + ], + }, + ) + date = fields.Date( + string="Date", + required=True, + readonly=True, + states={ + "draft": [ + ("readonly", False), + ], + }, + ) + + @api.depends( + "type_id", + ) + def _compute_allowed_base_move_line_ids(self): + obj_account_move_line = self.env["account.move.line"] + for document in self: + result = [] + criteria = [ + ("account_id.reconcile", "!=", False), + ("reconciled", "=", False), + ("account_id", "in", document.type_id.account_ids.ids), + ("debit", ">", 0), + ] + move_line_ids = obj_account_move_line.search(criteria) + if move_line_ids: + result = move_line_ids.ids + document.allowed_base_move_line_ids = result + + allowed_base_move_line_ids = fields.Many2many( + string="Allowed Base Move Line", + comodel_name="account.move.line", + compute="_compute_allowed_base_move_line_ids", + store=False, + ) + base_move_line_id = fields.Many2one( + string="Base Move Line", + comodel_name="account.move.line", + required=True, + readonly=True, + states={ + "draft": [ + ("readonly", False), + ], + }, + ) + journal_id = fields.Many2one( + string="Journal", + comodel_name="account.journal", + required=True, + readonly=True, + states={ + "draft": [ + ("readonly", False), + ], + }, + ondelete="restrict", + ) + receivable_account_id = fields.Many2one( + string="Receivable Account", + comodel_name="account.account", + required=True, + readonly=True, + states={ + "draft": [ + ("readonly", False), + ], + }, + ondelete="restrict", + ) + income_account_id = fields.Many2one( + string="Income Account", + comodel_name="account.account", + required=True, + readonly=True, + states={ + "draft": [ + ("readonly", False), + ], + }, + ondelete="restrict", + ) + receivable_move_line_id = fields.Many2one( + string="Receivable Move Line", + comodel_name="account.move.line", + readonly=True, + ondelete="set null", + copy=False, + ) + move_id = fields.Many2one( + string="# Move", + comodel_name="account.move", + readonly=True, + ) + + @api.depends( + "receivable_move_line_id", + "receivable_move_line_id.matched_debit_ids", + "receivable_move_line_id.matched_credit_ids", + ) + def _compute_reconcile(self): + for record in self: + result = False + if record.receivable_move_line_id.reconciled: + result = True + record.reconcile = result + + reconcile = fields.Boolean( + string="Reconcile", + compute="_compute_reconcile", + store=True, + ) + + @api.depends( + "type_id", + "base_move_line_id", + ) + def _compute_allowed_computation_ids(self): + obj_computation = self.env["account.receivable_penalty_computation"] + for document in self: + result = [] + criteria = [ + ("penalty_id", "=", False), + ("base_move_line_id", "=", document.base_move_line_id.id), + ("state", "=", "done"), + ] + computation_ids = obj_computation.search(criteria) + if computation_ids: + result = computation_ids.ids + document.allowed_computation_ids = result + + allowed_computation_ids = fields.Many2many( + string="Allowed Computations", + comodel_name="account.receivable_penalty_computation", + compute="_compute_allowed_computation_ids", + store=False, + ) + computation_ids = fields.One2many( + string="Computations", + comodel_name="account.receivable_penalty_computation", + inverse_name="penalty_id", + readonly=True, + states={ + "draft": [ + ("readonly", False), + ], + }, + copy=True, + ) + tax_ids = fields.Many2many( + string="Taxes", + comodel_name="account.tax", + relation="rel_receivable_penalty_2_tax", + column1="penalty_id", + column2="tax_id", + readonly=True, + states={ + "draft": [ + ("readonly", False), + ], + }, + copy=True, + ) + penalty_tax_ids = fields.One2many( + string="Penalty Taxes", + comodel_name="account.receivable_penalty_tax", + inverse_name="penalty_id", + readonly=True, + states={ + "draft": [ + ("readonly", False), + ], + }, + copy=True, + ) + + @api.depends( + "computation_ids", + "computation_ids.penalty_amount", + "penalty_tax_ids", + "penalty_tax_ids.tax_amount", + ) + def _compute_amount(self): + for document in self: + amount_untaxed = amount_tax = 0.0 + for computation in document.computation_ids: + amount_untaxed += computation.penalty_amount + + for tax in document.penalty_tax_ids: + amount_tax += tax.tax_amount + + document.amount_untaxed = amount_untaxed + document.amount_tax = amount_tax + document.amount_total = amount_untaxed + amount_tax + + amount_untaxed = fields.Monetary( + string="Amount Untaxed", + compute="_compute_amount", + store=True, + currency_field="company_currency_id", + ) + amount_tax = fields.Monetary( + string="Amount Tax", + compute="_compute_amount", + store=True, + currency_field="company_currency_id", + ) + amount_total = fields.Monetary( + string="Amount Total", + compute="_compute_amount", + store=True, + currency_field="company_currency_id", + ) + + @api.depends( + "amount_total", + "state", + "receivable_move_line_id", + "receivable_move_line_id.reconciled", + "receivable_move_line_id.amount_residual", + "receivable_move_line_id.amount_residual_currency", + ) + def _compute_residual(self): + for document in self: + paid = 0.0 + residual = document.amount_total + currency = document.company_currency_id + if document.receivable_move_line_id: + move_line = document.receivable_move_line_id + if not currency: + residual = -1.0 * move_line.amount_residual + else: + residual = -1.0 * move_line.amount_residual_currency + paid = document.amount_total - residual + document.amount_paid = paid + document.amount_residual = residual + + amount_paid = fields.Monetary( + string="Amount Paid", + compute="_compute_residual", + store=True, + currency_field="company_currency_id", + ) + amount_residual = fields.Monetary( + string="Amount Residual", + compute="_compute_residual", + store=True, + currency_field="company_currency_id", + ) + state = fields.Selection( + string="State", + default="draft", + required=True, + readonly=True, + selection=[ + ("draft", "Draft"), + ("confirm", "Waiting for Approval"), + ("open", "In Progress"), + ("done", "Done"), + ("cancel", "Cancelled"), + ("reject", "Rejected"), + ], + ) + + @api.model + def _get_policy_field(self): + res = super(AccountReceivablePenalty, self)._get_policy_field() + policy_field = [ + "confirm_ok", + "approve_ok", + "open_ok", + "done_ok", + "cancel_ok", + "reject_ok", + "restart_ok", + "restart_approval_ok", + "manual_number_ok", + ] + res += policy_field + return res + + @api.onchange( + "type_id", + ) + def onchange_journal_id(self): + if self.type_id: + self.journal_id = self.type_id.journal_id.id + + @api.onchange( + "type_id", + ) + def onchange_receivable_account_id(self): + if self.type_id: + self.receivable_account_id = self.type_id.receivable_account_id.id + + @api.onchange( + "type_id", + ) + def onchange_income_account_id(self): + if self.type_id: + self.income_account_id = self.type_id.income_account_id.id + + def _populate(self): + self.ensure_one() + self.allowed_computation_ids.write( + { + "penalty_id": self.id, + } + ) + + def action_populate(self): + for record in self: + record._populate() + + def action_clear_computation(self): + for record in self: + record._clear_computation() + + def _clear_computation(self): + self.ensure_one() + self.computation_ids.write({"penalty_id": False}) + + def action_compute_tax(self): + for record in self: + record._recompute_tax() + + def _recompute_tax(self): + self.ensure_one() + taxes_grouped = self.get_taxes_values() + self.penalty_tax_ids.unlink() + tax_lines = [] + for tax in taxes_grouped.values(): + tax_lines.append((0, 0, tax)) + self.write({"penalty_tax_ids": tax_lines}) + + def _prepare_tax_line_vals(self, tax): + vals = { + "penalty_id": self.id, + "tax_id": tax["id"], + "tax_amount": tax["amount"], + "base_amount": tax["base"], + "account_id": tax["account_id"], + } + return vals + + def get_taxes_values(self): + self.ensure_one() + obj_penalty_tax = self.env["account.receivable_penalty_tax"] + tax_grouped = {} + round_curr = self.company_currency_id.round + for computation in self.computation_ids: + for taxes_id in self.tax_ids: + price_unit = computation.penalty_amount + taxes = taxes_id.compute_all( + price_unit=price_unit, currency=self.company_currency_id, quantity=1 + )["taxes"] + for tax in taxes: + val = self._prepare_tax_line_vals(tax) + key = obj_penalty_tax.browse(tax["id"]).get_grouping_key(val) + + if key not in tax_grouped: + tax_grouped[key] = val + tax_grouped[key]["base_amount"] = round_curr(val["base_amount"]) + else: + tax_grouped[key]["tax_amount"] += val["tax_amount"] + tax_grouped[key]["base_amount"] += round_curr( + val["base_amount"] + ) + return tax_grouped + + def _prepare_account_move_data(self): + self.ensure_one() + return { + "date": self.date, + "name": self.name, + "journal_id": self.journal_id.id, + "ref": self.name, + } + + def _get_receivable_amount(self, currency): + self.ensure_one() + debit = credit = amount = amount_currency = 0.0 + move_date = self.date + + amount_currency = self.amount_total + amount = currency.with_context(date=move_date).compute( + amount_currency, + self.company_currency_id, + ) + + if amount < 0.0: + debit = abs(amount) + else: + credit = abs(amount) + + return debit, credit, amount_currency + + def _prepare_receivable_aml_data(self): + self.ensure_one() + debit, credit, amount_currency = self._get_receivable_amount( + self.company_currency_id + ) + data = { + "name": self.name, + "move_id": self.move_id.id, + "partner_id": self.base_move_line_id.partner_id.id, + "account_id": self.receivable_account_id.id, + "debit": debit, + "credit": credit, + "currency_id": self.company_currency_id.id, + "amount_currency": amount_currency, + } + return data + + def _create_receivable_aml(self): + self.ensure_one() + obj_account_move_line = self.env["account.move.line"] + aml = obj_account_move_line.with_context(check_move_validity=False).create( + self._prepare_receivable_aml_data() + ) + self.write( + { + "receivable_move_line_id": aml.id, + } + ) + + def _create_computation_aml(self): + self.ensure_one() + for computation in self.computation_ids: + computation._create_aml() + + def _create_tax_aml(self): + self.ensure_one() + for tax in self.penalty_tax_ids: + tax._create_aml() + + @ssi_decorator.post_open_action() + def _create_accounting_entry(self): + self.ensure_one() + move = ( + self.env["account.move"] + .with_context(check_move_validity=False) + .create(self._prepare_account_move_data()) + ) + self.write( + { + "move_id": move.id, + } + ) + self._create_receivable_aml() + self._create_computation_aml() + self._create_tax_aml() + self.move_id.action_post() + + @ssi_decorator.post_cancel_action() + def _delete_accounting_entry(self): + self.ensure_one() + if self.move_id: + move = self.move_id + self.write( + { + "move_id": False, + } + ) + move.unlink() diff --git a/ssi_account_receivable_penalty/models/account_receivable_penalty_computation.py b/ssi_account_receivable_penalty/models/account_receivable_penalty_computation.py new file mode 100644 index 0000000..f9ffdb7 --- /dev/null +++ b/ssi_account_receivable_penalty/models/account_receivable_penalty_computation.py @@ -0,0 +1,205 @@ +# Copyright 2022 OpenSynergy Indonesia +# Copyright 2022 PT. Simetri Sinergi Indonesia +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). +from odoo import api, fields, models + + +class AccountReceivablePenaltyComputation(models.Model): + _name = "account.receivable_penalty_computation" + _inherit = [ + "mixin.transaction_confirm", + "mixin.transaction_done", + "mixin.transaction_cancel", + "mixin.company_currency", + ] + _description = "Account Receivable Penalty Computation" + + # Multiple Approval Attribute + _approval_from_state = "draft" + _approval_to_state = "done" + _approval_state = "confirm" + _after_approved_method = "action_done" + + # Attributes related to add element on view automatically + _automatically_insert_view_element = True + _automatically_insert_done_policy_fields = False + _automatically_insert_done_button = False + + _statusbar_visible_label = "draft,confirm,done" + _policy_field_order = [ + "confirm_ok", + "approve_ok", + "reject_ok", + "restart_approval_ok", + "cancel_ok", + "restart_ok", + "done_ok", + "manual_number_ok", + ] + _header_button_order = [ + "action_confirm", + "action_approve_approval", + "action_reject_approval", + "%(ssi_transaction_cancel_mixin.base_select_cancel_reason_action)d", + "action_restart", + ] + + # Attributes related to add element on search view automatically + _state_filter_order = [ + "dom_draft", + "dom_confirm", + "dom_reject", + "dom_done", + "dom_cancel", + ] + + # Sequence attribute + _create_sequence_state = "done" + + # FIELD + penalty_id = fields.Many2one( + string="Penalty", + comodel_name="account.receivable_penalty", + required=False, + ondelete="cascade", + ) + base_move_line_id = fields.Many2one( + string="Base Move Line", + comodel_name="account.move.line", + required=True, + readonly=True, + states={ + "draft": [ + ("readonly", False), + ], + }, + ) + type_id = fields.Many2one( + string="Type", + comodel_name="account.receivable_penalty_type", + required=True, + readonly=True, + ondelete="restrict", + states={ + "draft": [ + ("readonly", False), + ], + }, + ) + date = fields.Date( + string="Date", + required=True, + readonly=True, + states={ + "draft": [ + ("readonly", False), + ], + }, + ) + base_amount = fields.Monetary( + string="Base Amount", + currency_field="company_currency_id", + ) + penalty_amount = fields.Monetary( + string="Penalty Amount", + currency_field="company_currency_id", + ) + account_move_line_id = fields.Many2one( + string="# Move Line", + comodel_name="account.move.line", + readonly=True, + copy=False, + ) + state = fields.Selection( + string="State", + default="draft", + required=True, + readonly=True, + selection=[ + ("draft", "Draft"), + ("confirm", "Waiting for Approval"), + ("done", "Done"), + ("cancel", "Cancelled"), + ("reject", "Rejected"), + ], + ) + + @api.model + def _get_policy_field(self): + res = super(AccountReceivablePenaltyComputation, self)._get_policy_field() + policy_field = [ + "confirm_ok", + "approve_ok", + "done_ok", + "cancel_ok", + "reject_ok", + "restart_ok", + "restart_approval_ok", + "manual_number_ok", + ] + res += policy_field + return res + + @api.onchange( + "type_id", + "base_move_line_id", + ) + def onchange_base_amount(self): + self.base_amount = 0.0 + + @api.onchange( + "type_id", + "base_move_line_id", + ) + def onchange_penalty_amount(self): + self.penalty_amount = 0.0 + + def _create_aml(self): + self.ensure_one() + obj_account_move_line = self.env["account.move.line"] + aml = obj_account_move_line.with_context(check_move_validity=False).create( + self._prepare_aml_data() + ) + self.write( + { + "account_move_line_id": aml.id, + } + ) + + def _prepare_aml_data(self): + self.ensure_one() + penalty = self.penalty_id + debit, credit, amount_currency = self._get_aml_amount( + penalty.company_currency_id + ) + return { + "move_id": penalty.move_id.id, + "name": self.name, + "partner_id": penalty.base_move_line_id.partner_id.id, + "account_id": penalty.income_account_id.id, + "quantity": 1.0, + "price_unit": self.penalty_amount, + "debit": debit, + "credit": credit, + "currency_id": penalty.company_currency_id.id, + "amount_currency": amount_currency, + } + + def _get_aml_amount(self, currency): + self.ensure_one() + debit = credit = amount = amount_currency = 0.0 + penalty = self.penalty_id + move_date = penalty.date + + amount_currency = self.penalty_amount + amount = currency.with_context(date=move_date).compute( + amount_currency, + penalty.company_currency_id, + ) + + if amount > 0.0: + debit = abs(amount) + else: + credit = abs(amount) + + return debit, credit, amount_currency diff --git a/ssi_account_receivable_penalty/models/account_receivable_penalty_tax.py b/ssi_account_receivable_penalty/models/account_receivable_penalty_tax.py new file mode 100644 index 0000000..78d2c60 --- /dev/null +++ b/ssi_account_receivable_penalty/models/account_receivable_penalty_tax.py @@ -0,0 +1,94 @@ +# Copyright 2022 OpenSynergy Indonesia +# Copyright 2022 PT. Simetri Sinergi Indonesia +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). +from odoo import fields, models + + +class AccountReceivablePenaltyTax(models.Model): + _name = "account.receivable_penalty_tax" + _description = "Account Receivable Penalty Tax" + + # FIELD + penalty_id = fields.Many2one( + string="Penalty", + comodel_name="account.receivable_penalty", + required=True, + ondelete="cascade", + ) + tax_id = fields.Many2one( + string="Tax", + comodel_name="account.tax", + required=False, + ondelete="restrict", + ) + account_id = fields.Many2one( + string="Account", + comodel_name="account.account", + required=False, + ondelete="restrict", + ) + base_amount = fields.Float( + string="Base Amount", + required=True, + ) + tax_amount = fields.Float( + string="Tax Amount", + required=True, + ) + account_move_line_id = fields.Many2one( + string="Journal Item", + comodel_name="account.move.line", + readonly=True, + copy=False, + ) + + def get_grouping_key(self, value): + return str(value["tax_id"]) + "-" + str(value["account_id"]) + + def _create_aml(self): + self.ensure_one() + obj_account_move_line = self.env["account.move.line"] + aml = obj_account_move_line.with_context(check_move_validity=False).create( + self._prepare_aml_data() + ) + self.write( + { + "account_move_line_id": aml.id, + } + ) + + def _prepare_aml_data(self): + self.ensure_one() + penalty = self.penalty_id + debit, credit, amount_currency = self._get_aml_amount( + penalty.company_currency_id + ) + return { + "move_id": penalty.move_id.id, + "name": self.tax_id.name, + "partner_id": penalty.base_move_line_id.partner_id.id, + "account_id": self.account_id.id, + "debit": debit, + "credit": credit, + "currency_id": penalty.company_currency_id.id, + "amount_currency": amount_currency, + } + + def _get_aml_amount(self, currency): + self.ensure_one() + debit = credit = amount = amount_currency = 0.0 + penalty = self.penalty_id + move_date = penalty.date + + amount_currency = self.tax_amount + amount = currency.with_context(date=move_date).compute( + amount_currency, + penalty.company_currency_id, + ) + + if amount > 0.0: + debit = abs(amount) + else: + credit = abs(amount) + + return debit, credit, amount_currency diff --git a/ssi_account_receivable_penalty/models/account_receivable_penalty_type.py b/ssi_account_receivable_penalty/models/account_receivable_penalty_type.py new file mode 100644 index 0000000..b5f926c --- /dev/null +++ b/ssi_account_receivable_penalty/models/account_receivable_penalty_type.py @@ -0,0 +1,149 @@ +# Copyright 2022 OpenSynergy Indonesia +# Copyright 2022 PT. Simetri Sinergi Indonesia +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import _, fields, models +from odoo.exceptions import Warning as UserError +from odoo.tools.safe_eval import safe_eval + + +class AccountReceivablePenaltyType(models.Model): + _name = "account.receivable_penalty_type" + _inherit = ["mixin.master_data"] + _description = "Account Receivable Penalty Type" + + name = fields.Char( + string="Penalty Type", + ) + journal_id = fields.Many2one( + string="Journal", + comodel_name="account.journal", + required=True, + ondelete="restrict", + ) + receivable_account_id = fields.Many2one( + string="Receivable Account", + comodel_name="account.account", + required=True, + ondelete="restrict", + ) + income_account_id = fields.Many2one( + string="Income Account", + comodel_name="account.account", + required=True, + ondelete="restrict", + ) + base_amount_python = fields.Text( + string="Base Amount Python", + default="""# Available variables: +# - env: Odoo Environment on which the action is triggered. +# - document: Recordset of move lines. +# - result: Return result. +result = 0.0""", + copy=True, + ) + penalty_amount_python = fields.Text( + string="Penalty Amount Python", + default="""# Available variables: +# - env: Odoo Environment on which the action is triggered. +# - document: Recordset of move lines. +# - result: Return result. +result = 0.0""", + copy=True, + ) + condition_python = fields.Text( + string="Condition Python", + default="""# Available variables: +# - env: Odoo Environment on which the action is triggered. +# - document: Recordset of move lines. +# - result: Return result. +result = True""", + copy=True, + ) + cron_id = fields.Many2one( + string="Cron", + comodel_name="ir.cron", + readonly=True, + copy=False, + ) + account_ids = fields.Many2many( + string="Accounts", + comodel_name="account.account", + relation="rel_account_2_receivable_penalty_type", + column1="type_id", + column2="account_id", + ) + + def _get_policy_localdict(self, move_line): + self.ensure_one() + return { + "env": self.env, + "document": move_line, + } + + def _evaluate_python(self, move_line, python_code): + self.ensure_one() + res = False + localdict = self._get_policy_localdict(move_line) + try: + safe_eval(python_code, localdict, mode="exec", nocopy=True) + res = localdict["result"] + except Exception as error: + raise UserError(_("Error evaluating conditions.\n %s") % error) + return res + + def _prepare_cron_data(self): + self.ensure_one() + return { + "name": self.name, + "model_id": self.env.ref( + "ssi_account_receivable_penalty.model_account_receivable_penalty_type" + ).id, + "interval_number": 1, + "interval_type": "hours", + "state": "code", + "code": "model.browse(%s).cron_create_penalty_computation()" % self.id, + "active": True, + } + + def _prepare_computation_data(self, move_line): + self.ensure_one() + return { + "base_move_line_id": move_line.id, + "type_id": self.id, + "date": fields.Date.context_today(self), + "base_amount": self._evaluate_python(move_line, self.base_amount_python), + "penalty_amount": self._evaluate_python( + move_line, self.penalty_amount_python + ), + "state": "draft", + } + + def action_create_cron(self): + for document in self: + obj_ir_cron = self.env["ir.cron"] + cron_id = obj_ir_cron.create(document._prepare_cron_data()) + document.cron_id = cron_id + + def action_delete_cron(self): + for document in self: + document.cron_id.unlink() + + def cron_create_penalty_computation(self): + obj_account_move_line = self.env["account.move.line"] + obj_penalty_computation = self.env["account.receivable_penalty_computation"] + for document in self: + criteria = [ + ("account_id.reconcile", "!=", False), + ("reconciled", "=", False), + ("account_id", "in", document.account_ids.ids), + ("debit", ">", 0), + ] + move_line_ids = obj_account_move_line.search(criteria) + if move_line_ids: + for move_line in move_line_ids: + _check = self._evaluate_python(move_line, self.condition_python) + if _check: + obj_penalty_computation.create( + self._prepare_computation_data(move_line) + ) diff --git a/ssi_account_receivable_penalty/security/ir.model.access.csv b/ssi_account_receivable_penalty/security/ir.model.access.csv new file mode 100644 index 0000000..0a535e4 --- /dev/null +++ b/ssi_account_receivable_penalty/security/ir.model.access.csv @@ -0,0 +1,12 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_account_receivable_penalty_type_configurator,account.receivable_penalty_type - configurator,model_account_receivable_penalty_type,receivable_penalty_type_group,1,1,1,1 +access_account_receivable_penalty_type_all,account.receivable_penalty_type - all user,model_account_receivable_penalty_type,,1,0,0,0 +access_account_receivable_penalty_computation_all,account.receivable_penalty_computation - all,model_account_receivable_penalty_computation,,1,0,0,0 +access_account_receivable_penalty_computation_viewer,account.receivable_penalty_computation - viewer,model_account_receivable_penalty_computation,receivable_penalty_viewer_group,1,0,0,0 +access_account_receivable_penalty_computation_user,account.receivable_penalty_computation - user,model_account_receivable_penalty_computation,receivable_penalty_user_group,1,1,1,1 +access_account_receivable_penalty_all,account.receivable_penalty - all,model_account_receivable_penalty,,1,0,0,0 +access_account_receivable_penalty_viewer,account.receivable_penalty - viewer,model_account_receivable_penalty,receivable_penalty_comp_viewer_group,1,0,0,0 +access_account_receivable_penalty_user,account.receivable_penalty - user,model_account_receivable_penalty,receivable_penalty_comp_user_group,1,1,1,1 +access_account_receivable_penalty_tax_all,account.receivable_penalty_tax - all,model_account_receivable_penalty_tax,,1,0,0,0 +access_account_receivable_penalty_tax_viewer,account.receivable_penalty_tax - viewer,model_account_receivable_penalty_tax,receivable_penalty_comp_viewer_group,1,0,0,0 +access_account_receivable_penalty_tax_user,account.receivable_penalty_tax - user,model_account_receivable_penalty_tax,receivable_penalty_comp_user_group,1,1,1,1 diff --git a/ssi_account_receivable_penalty/security/ir_module_category_data.xml b/ssi_account_receivable_penalty/security/ir_module_category_data.xml new file mode 100644 index 0000000..e81e590 --- /dev/null +++ b/ssi_account_receivable_penalty/security/ir_module_category_data.xml @@ -0,0 +1,47 @@ + + + + + + Account Receivable Penalty + + + + Account Receivable Penalty Computation + + + + + + Account Receivable Penalty + + + + Account Receivable Penalty Computation + + + + diff --git a/ssi_account_receivable_penalty/security/ir_rule_data.xml b/ssi_account_receivable_penalty/security/ir_rule_data.xml new file mode 100644 index 0000000..a55ce50 --- /dev/null +++ b/ssi_account_receivable_penalty/security/ir_rule_data.xml @@ -0,0 +1,118 @@ + + + + + + Account Receivable Penalty - Internal Users + + + [ + ('user_id','=',user.id), + ] + + + + + + + + Account Receivable Penalty - Responsible to company data + + + [('company_id','=',user.company_id.id)] + + + + + + + + Account Receivable Penalty - Responsible to company and all child companies data + + + [('company_id','in',user.company_ids.ids)] + + + + + + + + Account Receivable Penalty - All + + + [(1,'=',1)] + + + + + + + + + Account Receivable Penalty Computation- Internal Users + + + [ + ('user_id','=',user.id), + ] + + + + + + + + Account Receivable Penalty Computation - Responsible to company data + + + [('company_id','=',user.company_id.id)] + + + + + + + + Account Receivable Penalty Computation - Responsible to company and all child companies data + + + [('company_id','in',user.company_ids.ids)] + + + + + + + + Account Receivable Penalty Computation - All + + + [(1,'=',1)] + + + + + + diff --git a/ssi_account_receivable_penalty/security/res_group_data.xml b/ssi_account_receivable_penalty/security/res_group_data.xml new file mode 100644 index 0000000..5fa9ec5 --- /dev/null +++ b/ssi_account_receivable_penalty/security/res_group_data.xml @@ -0,0 +1,145 @@ + + + + + + Account Receivable Penalty Type + + + + + + Viewer + + + + + User + + + + + + Validator + + + + + + + + Viewer + + + + + User + + + + + + Validator + + + + + + + + Company + + + + + Company and All Child Companies + + + + + + All + + + + + + + Company + + + + + Company and All Child Companies + + + + + + All + + + + diff --git a/ssi_account_receivable_penalty/static/description/icon.png b/ssi_account_receivable_penalty/static/description/icon.png new file mode 100644 index 0000000..4c8e112 Binary files /dev/null and b/ssi_account_receivable_penalty/static/description/icon.png differ diff --git a/ssi_account_receivable_penalty/views/account_receivable_penalty_computation_views.xml b/ssi_account_receivable_penalty/views/account_receivable_penalty_computation_views.xml new file mode 100644 index 0000000..423eaa5 --- /dev/null +++ b/ssi_account_receivable_penalty/views/account_receivable_penalty_computation_views.xml @@ -0,0 +1,111 @@ + + + + + account.receivable_penalty_computation - search + account.receivable_penalty_computation + + primary + + + + + + + + + + + + + + + + + + account.receivable_penalty_computation - tree + account.receivable_penalty_computation + primary + + + + + + + + + + + + + + + + account.receivable_penalty_computation - tree 2 + account.receivable_penalty_computation + + + + + + + + + + + + + account.receivable_penalty_computation - form + account.receivable_penalty_computation + primary + + + + + + + + + + + + + + + + + + + Receivable Penalty Computations + ir.actions.act_window + account.receivable_penalty_computation + tree,form + + + + + diff --git a/ssi_account_receivable_penalty/views/account_receivable_penalty_type_views.xml b/ssi_account_receivable_penalty/views/account_receivable_penalty_type_views.xml new file mode 100644 index 0000000..6bbc05c --- /dev/null +++ b/ssi_account_receivable_penalty/views/account_receivable_penalty_type_views.xml @@ -0,0 +1,119 @@ + + + + + account.receivable_penalty_type - search + account.receivable_penalty_type + + primary + + + + + + + account.receivable_penalty_type - tree + account.receivable_penalty_type + + primary + + + + + + + account.receivable_penalty_type - form + account.receivable_penalty_type + + primary + + + + + + + + + + + + + + + + + + + + + + + + + + + +