diff --git a/account_banking_ach_discount/README.rst b/account_banking_ach_discount/README.rst new file mode 100644 index 00000000..9044807f --- /dev/null +++ b/account_banking_ach_discount/README.rst @@ -0,0 +1,136 @@ +============================== +Discount on ACH batch payments +============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:49de557183014de2a8dba5fce829d24e0fd04061b538c048a3553c6f835b02b9 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-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-OCA%2Fl10n--usa-lightgray.png?logo=github + :target: https://github.com/OCA/l10n-usa/tree/16.0/account_banking_ach_discount + :alt: OCA/l10n-usa +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/l10n-usa-16-0/l10n-usa-16-0-account_banking_ach_discount + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/l10n-usa&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module will add support for discount in ACH and batch ACH workflow. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Payment Terms +~~~~~~~~~~~~~ + +* Go to *Accounting > Configuration > Payment Terms* +* Create or select a payment term +* Activate the discounts options +* On a line, set the discount percentage and number of days + +Payment Modes +~~~~~~~~~~~~~ + +* Go to *Accounting > Configuration > Payment Modes* +* Create or select a payment mode +* Link it to an ACH payment method + +Vendors +~~~~~~~ + +* Go *Contacts* or *Accounting > Vendors > Vendors* +* Create or select a vendor +* On the Sales and Purchase tab, set the supplier payment mode +* On the Accounting tab, set their bank information (account number, bank, routing number) + +Usage +===== + +* Go to *Accounting > Customers > Invoices* or *Accounting > Vendors > Bills* +* Select or create various records in the state posted with ACH and discounts +* In the Action menu, click on Batch Payments +* Review the payment information provided by default +* Click on Make Payments +* Review the payment order, confirm it and generate the ACH file +* Go to your bank's website to upload the file +* Come back to Odoo and confirm the upload to the bank was successful + +Changelog +========= + +12.0.1.0.0 +~~~~~~~~~~ + +- ACH Payment, Automatic Discount and Batch Payment Partial Pay Integration + +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 +~~~~~~~ + +* Open Source Integrators + +Contributors +~~~~~~~~~~~~ + +* Open Source Integrators + + * Bhavesh Odedra + * Maxime Chambreuil + +* ForgeFlow + * Jasmin Solanki + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-bodedra| image:: https://github.com/bodedra.png?size=40px + :target: https://github.com/bodedra + :alt: bodedra + +Current `maintainer `__: + +|maintainer-bodedra| + +This module is part of the `OCA/l10n-usa `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_banking_ach_discount/__init__.py b/account_banking_ach_discount/__init__.py new file mode 100644 index 00000000..a5b63e4a --- /dev/null +++ b/account_banking_ach_discount/__init__.py @@ -0,0 +1,5 @@ +# Copyright (C) 2019 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models +from . import wizard diff --git a/account_banking_ach_discount/__manifest__.py b/account_banking_ach_discount/__manifest__.py new file mode 100644 index 00000000..4de311b3 --- /dev/null +++ b/account_banking_ach_discount/__manifest__.py @@ -0,0 +1,24 @@ +# Copyright (C) 2019 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Discount on ACH batch payments", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "Open Source Integrators, Odoo Community Association (OCA)", + "category": "Accounting", + "maintainer": "Odoo Community Association (OCA)", + "website": "https://github.com/OCA/l10n-usa", + "development_status": "Beta", + "maintainers": ["bodedra"], + "depends": [ + "account_payment_term_discount", + "account_payment_batch_process", + "account_payment_order", + "account_banking_ach_credit_transfer", + "account_banking_ach_direct_debit", + ], + "data": [ + "views/account_payment_view.xml", + ], +} diff --git a/account_banking_ach_discount/i18n/account_banking_ach_discount.pot b/account_banking_ach_discount/i18n/account_banking_ach_discount.pot new file mode 100644 index 00000000..7eca063e --- /dev/null +++ b/account_banking_ach_discount/i18n/account_banking_ach_discount.pot @@ -0,0 +1,148 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_banking_ach_discount +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \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_banking_ach_discount +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment_line__writeoff_account_id +msgid "Account" +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment_line__payment_difference_handling +msgid "Action" +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model,name:account_banking_ach_discount.model_bank_payment_line +msgid "Bank Payment Lines" +msgstr "" + +#. module: account_banking_ach_discount +#: model_terms:ir.ui.view,arch_db:account_banking_ach_discount.account_payment_line_discount_amount_form +#: model_terms:ir.ui.view,arch_db:account_banking_ach_discount.account_payment_line_discount_amount_tree +msgid "Discount Account" +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment_line__discount_amount +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_bank_payment_line__discount_amount +msgid "Discount Amount" +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_move__display_name +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_move_line__display_name +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment__display_name +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment_line__display_name +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment_order__display_name +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment_register__display_name +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_bank_payment_line__display_name +msgid "Display Name" +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_move__id +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_move_line__id +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment__id +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment_line__id +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment_order__id +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment_register__id +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_bank_payment_line__id +msgid "ID" +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model,name:account_banking_ach_discount.model_account_move +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment_line__move_id +msgid "Journal Entry" +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model,name:account_banking_ach_discount.model_account_move_line +msgid "Journal Item" +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model.fields.selection,name:account_banking_ach_discount.selection__account_payment_line__payment_difference_handling__open +msgid "Keep open" +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_move____last_update +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_move_line____last_update +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment____last_update +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment_line____last_update +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment_order____last_update +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment_register____last_update +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_bank_payment_line____last_update +msgid "Last Modified on" +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model.fields.selection,name:account_banking_ach_discount.selection__account_payment_line__payment_difference_handling__reconcile +msgid "Mark invoice as fully paid" +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment_line__note +msgid "Note" +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment_line__payment_difference +msgid "Payment Difference" +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model,name:account_banking_ach_discount.model_account_payment_line +msgid "Payment Lines" +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model,name:account_banking_ach_discount.model_account_payment_order +msgid "Payment Order" +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model,name:account_banking_ach_discount.model_account_payment +msgid "Payments" +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment_line__reason_code +msgid "Reason Code" +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model,name:account_banking_ach_discount.model_account_payment_register +msgid "Register Payment" +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model.fields,help:account_banking_ach_discount.field_account_payment_line__move_id +msgid "The move of this entry line." +msgstr "" + +#. module: account_banking_ach_discount +#: code:addons/account_banking_ach_discount/models/account_payment.py:0 +#, python-format +msgid "" +"This method should only be called to process a single invoice's payment." +msgstr "" + +#. module: account_banking_ach_discount +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_account_payment_line__total_amount +#: model:ir.model.fields,field_description:account_banking_ach_discount.field_bank_payment_line__total_amount +msgid "Total Amount" +msgstr "" diff --git a/account_banking_ach_discount/models/__init__.py b/account_banking_ach_discount/models/__init__.py new file mode 100644 index 00000000..b8e1ebba --- /dev/null +++ b/account_banking_ach_discount/models/__init__.py @@ -0,0 +1,6 @@ +# Copyright (C) 2019 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import account_payment +from . import account_move_line +from . import account_move +from . import account_payment_line diff --git a/account_banking_ach_discount/models/account_move.py b/account_banking_ach_discount/models/account_move.py new file mode 100644 index 00000000..08662374 --- /dev/null +++ b/account_banking_ach_discount/models/account_move.py @@ -0,0 +1,48 @@ +# Copyright (C) 2019 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class AccountMove(models.Model): + _inherit = "account.move" + + def _compute_payments_widget_reconciled_info(self): + res = super(AccountMove, self)._compute_payments_widget_reconciled_info() + for move in self: + if move.invoice_payments_widget: + inv_number = move.name + flag = False + payment_lines = set() + for line in move.line_ids: + payment_lines.update( + line.mapped("matched_credit_ids.credit_move_id.id") + ) + payment_lines.update( + line.mapped("matched_debit_ids.debit_move_id.id") + ) + payment_move_line_ids = ( + self.env["account.move.line"].browse(list(payment_lines)).sorted() + ) + + for mvl in payment_move_line_ids: + for item in move.invoice_payments_widget["content"]: + # get payment line + if mvl.payment_id.id == item["account_payment_id"]: + for pay_li in mvl.payment_id.line_ids.filtered( + lambda line: not line.reconciled + ): + # Get related payment line ref + if inv_number in pay_li.name: + item["amount"] = abs(pay_li.amount_currency) + # for non-ach payment + # Deduct the discount only for the related payment. + # Discount is applied on the last payment (i.e. fully reconciled). + if ( + mvl.payment_id + and mvl.move_id.id == move.id + and item["account_payment_id"] == mvl.payment_id.id + ): + if mvl.full_reconcile_id and not flag: + item["amount"] = item["amount"] - move.discount_taken + flag = True + return res diff --git a/account_banking_ach_discount/models/account_move_line.py b/account_banking_ach_discount/models/account_move_line.py new file mode 100644 index 00000000..5d1b5f87 --- /dev/null +++ b/account_banking_ach_discount/models/account_move_line.py @@ -0,0 +1,43 @@ +# Copyright (C) 2019 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + def _prepare_payment_line_vals(self, payment_order): + vals = super(AccountMoveLine, self)._prepare_payment_line_vals(payment_order) + invoice = self.move_id + amount_currency = vals.get("amount_currency") + # No discount for open invoices + if ( + ( + "payment_line_state" in self._context + and self._context.get("payment_line_state") != "open" + ) + or self._context.get("is_new_order") + or self._context.get("is_update_order") + ): + if ( + invoice + and invoice.invoice_payment_term_id + and invoice.invoice_payment_term_id.is_discount + and invoice.invoice_payment_term_id.line_ids + ): + discount_information = ( + invoice.invoice_payment_term_id._check_payment_term_discount( + invoice, + self._context.get("payment_date") or invoice.invoice_date, + ) + ) + discount_amt = discount_information[0] + vals.update( + { + "discount_amount": discount_amt, + "amount_currency": amount_currency - discount_amt, + "writeoff_account_id": discount_information[1], + } + ) + return vals diff --git a/account_banking_ach_discount/models/account_payment.py b/account_banking_ach_discount/models/account_payment.py new file mode 100644 index 00000000..b301be10 --- /dev/null +++ b/account_banking_ach_discount/models/account_payment.py @@ -0,0 +1,53 @@ +# Copyright (C) 2019 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, models +from odoo.exceptions import UserError + + +class AccountPayment(models.Model): + _inherit = "account.payment" + + def action_validate_invoice_payment(self): + # Check if Invoices have ACH IN/OUT payment method, to avoid any + # conflict + valid = False + if self.filtered(lambda p: p.payment_method_id.code in ("ACH-In", "ACH-Out")): + valid = True + if any(len(record.invoice_ids) != 1 for record in self): + # For multiple invoices, there is account.register.payments wizard + raise UserError( + _( + "This method should only be called to process a " + "single invoice's payment." + ) + ) + if valid: + for payment in self: + payment_method = payment.payment_method_id + if payment_method: + if payment_method.code in ("ACH-In", "ACH-Out"): + # Update invoice with Payment mode + if not payment.reconciled_invoice_ids.payment_mode_id: + payment_mode_id = self.env["account.payment.mode"].search( + [ + ("payment_type", "=", payment.payment_type), + ("payment_method_id", "=", payment_method.id), + ("payment_order_ok", "=", True), + ], + limit=1, + ) + if payment_mode_id: + payment.reconciled_invoice_ids.write( + {"payment_mode_id": payment_mode_id.id} + ) + payment.reconciled_invoice_ids.move_id.line_ids.write( + {"payment_mode_id": payment_mode_id.id} + ) + action = ( + payment.reconciled_invoice_ids.create_account_payment_line() + ) + payment.unlink() + return action + res = super(AccountPayment, self).action_validate_invoice_payment() + return res diff --git a/account_banking_ach_discount/models/account_payment_line.py b/account_banking_ach_discount/models/account_payment_line.py new file mode 100644 index 00000000..53ed7008 --- /dev/null +++ b/account_banking_ach_discount/models/account_payment_line.py @@ -0,0 +1,97 @@ +# Copyright (C) 2019 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 UserError +from odoo.tools.float_utils import float_is_zero + + +class AccountPaymentLine(models.Model): + _inherit = "account.payment.line" + + discount_amount = fields.Monetary(currency_field="currency_id") + total_amount = fields.Monetary( + compute="_compute_total_amount", currency_field="currency_id" + ) + payment_difference_handling = fields.Selection( + [("open", "Keep open"), ("reconcile", "Mark invoice as fully paid")], + default="reconcile", + string="Action", + copy=False, + ) + writeoff_account_id = fields.Many2one( + "account.account", + string="Account", + domain=[("deprecated", "!=", True)], + copy=False, + ) + reason_code = fields.Many2one("payment.adjustment.reason") + note = fields.Text() + payment_difference = fields.Float() + move_id = fields.Many2one( + "account.move", related="move_line_id.move_id", store=True + ) + + @api.onchange("discount_amount") + def _onchange_discount_amount(self): + if self.discount_amount: + self.amount_currency = self.amount_currency - self.discount_amount + + @api.depends("amount_currency", "discount_amount") + def _compute_total_amount(self): + for line in self: + line.total_amount = line.amount_currency + line.payment_difference + + @api.model + def same_fields_payment_line_and_bank_payment_line(self): + res = super( + AccountPaymentLine, self + ).same_fields_payment_line_and_bank_payment_line() + res.update( + { + "payment_difference_handling", + "writeoff_account_id", + "reason_code", + "move_id", + } + ) + return res + + @api.model + def _get_payment_line_grouping_fields(self): + """This list of fields is used o compute the grouping hashcode.""" + fields = super()._get_payment_line_grouping_fields() + fields.append("writeoff_account_id") + return fields + + def _prepare_account_payment_vals(self): + values = super()._prepare_account_payment_vals() + note = "" + total_payment_difference = 0.0 + for rec in self: + payment_difference = rec.payment_difference + if rec.reason_code: + note = rec.reason_code.display_name + ": " + if rec.note: + note += rec.note + if rec.payment_type == "outbound": + payment_difference *= -1 + total_payment_difference += payment_difference + if not float_is_zero( + total_payment_difference, precision_digits=self.currency_id.decimal_places + ): + if not self.writeoff_account_id: + raise UserError( + _( + "A payment difference was found, but you " + "need to indicicate a corresponding " + "writeoff account" + ) + ) + write_off_line_vals = { + "account_id": self[:1].writeoff_account_id.id, + "name": note, + "amount_currency": total_payment_difference, + "balance": total_payment_difference, + } + values["write_off_line_vals"] = [write_off_line_vals] + return values diff --git a/account_banking_ach_discount/readme/CONFIGURE.rst b/account_banking_ach_discount/readme/CONFIGURE.rst new file mode 100644 index 00000000..ac6179e5 --- /dev/null +++ b/account_banking_ach_discount/readme/CONFIGURE.rst @@ -0,0 +1,22 @@ +Payment Terms +~~~~~~~~~~~~~ + +* Go to *Accounting > Configuration > Payment Terms* +* Create or select a payment term +* Activate the discounts options +* On a line, set the discount percentage and number of days + +Payment Modes +~~~~~~~~~~~~~ + +* Go to *Accounting > Configuration > Payment Modes* +* Create or select a payment mode +* Link it to an ACH payment method + +Vendors +~~~~~~~ + +* Go *Contacts* or *Accounting > Vendors > Vendors* +* Create or select a vendor +* On the Sales and Purchase tab, set the supplier payment mode +* On the Accounting tab, set their bank information (account number, bank, routing number) diff --git a/account_banking_ach_discount/readme/CONTRIBUTORS.rst b/account_banking_ach_discount/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..958bc325 --- /dev/null +++ b/account_banking_ach_discount/readme/CONTRIBUTORS.rst @@ -0,0 +1,7 @@ +* Open Source Integrators + + * Bhavesh Odedra + * Maxime Chambreuil + +* ForgeFlow + * Jasmin Solanki diff --git a/account_banking_ach_discount/readme/DESCRIPTION.rst b/account_banking_ach_discount/readme/DESCRIPTION.rst new file mode 100644 index 00000000..c4609831 --- /dev/null +++ b/account_banking_ach_discount/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module will add support for discount in ACH and batch ACH workflow. diff --git a/account_banking_ach_discount/readme/HISTORY.rst b/account_banking_ach_discount/readme/HISTORY.rst new file mode 100644 index 00000000..3e246dc4 --- /dev/null +++ b/account_banking_ach_discount/readme/HISTORY.rst @@ -0,0 +1,4 @@ +12.0.1.0.0 +~~~~~~~~~~ + +- ACH Payment, Automatic Discount and Batch Payment Partial Pay Integration diff --git a/account_banking_ach_discount/readme/USAGE.rst b/account_banking_ach_discount/readme/USAGE.rst new file mode 100644 index 00000000..05146861 --- /dev/null +++ b/account_banking_ach_discount/readme/USAGE.rst @@ -0,0 +1,8 @@ +* Go to *Accounting > Customers > Invoices* or *Accounting > Vendors > Bills* +* Select or create various records in the state posted with ACH and discounts +* In the Action menu, click on Batch Payments +* Review the payment information provided by default +* Click on Make Payments +* Review the payment order, confirm it and generate the ACH file +* Go to your bank's website to upload the file +* Come back to Odoo and confirm the upload to the bank was successful diff --git a/account_banking_ach_discount/static/description/icon.png b/account_banking_ach_discount/static/description/icon.png new file mode 100644 index 00000000..84791119 Binary files /dev/null and b/account_banking_ach_discount/static/description/icon.png differ diff --git a/account_banking_ach_discount/static/description/index.html b/account_banking_ach_discount/static/description/index.html new file mode 100644 index 00000000..adb657f5 --- /dev/null +++ b/account_banking_ach_discount/static/description/index.html @@ -0,0 +1,490 @@ + + + + + +Discount on ACH batch payments + + + +
+

Discount on ACH batch payments

+ + +

Beta License: AGPL-3 OCA/l10n-usa Translate me on Weblate Try me on Runboat

+

This module will add support for discount in ACH and batch ACH workflow.

+

Table of contents

+ +
+

Configuration

+
+

Payment Terms

+
    +
  • Go to Accounting > Configuration > Payment Terms
  • +
  • Create or select a payment term
  • +
  • Activate the discounts options
  • +
  • On a line, set the discount percentage and number of days
  • +
+
+
+

Payment Modes

+
    +
  • Go to Accounting > Configuration > Payment Modes
  • +
  • Create or select a payment mode
  • +
  • Link it to an ACH payment method
  • +
+
+
+

Vendors

+
    +
  • Go Contacts or Accounting > Vendors > Vendors
  • +
  • Create or select a vendor
  • +
  • On the Sales and Purchase tab, set the supplier payment mode
  • +
  • On the Accounting tab, set their bank information (account number, bank, routing number)
  • +
+
+
+
+

Usage

+
    +
  • Go to Accounting > Customers > Invoices or Accounting > Vendors > Bills
  • +
  • Select or create various records in the state posted with ACH and discounts
  • +
  • In the Action menu, click on Batch Payments
  • +
  • Review the payment information provided by default
  • +
  • Click on Make Payments
  • +
  • Review the payment order, confirm it and generate the ACH file
  • +
  • Go to your bank’s website to upload the file
  • +
  • Come back to Odoo and confirm the upload to the bank was successful
  • +
+
+
+

Changelog

+
+

12.0.1.0.0

+
    +
  • ACH Payment, Automatic Discount and Batch Payment Partial Pay Integration
  • +
+
+
+
+

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

+
    +
  • Open Source Integrators
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

bodedra

+

This module is part of the OCA/l10n-usa project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/account_banking_ach_discount/tests/__init__.py b/account_banking_ach_discount/tests/__init__.py new file mode 100644 index 00000000..bc307d7a --- /dev/null +++ b/account_banking_ach_discount/tests/__init__.py @@ -0,0 +1 @@ +from . import test_account_banking_ach_discount diff --git a/account_banking_ach_discount/tests/test_account_banking_ach_discount.py b/account_banking_ach_discount/tests/test_account_banking_ach_discount.py new file mode 100644 index 00000000..2aec385b --- /dev/null +++ b/account_banking_ach_discount/tests/test_account_banking_ach_discount.py @@ -0,0 +1,179 @@ +# Copyright (C) 2024, ForgeFlow S.A. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from datetime import datetime, timedelta + +from odoo.tests.common import Form, TransactionCase + + +class TestPayment(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.payment_method_model = cls.env["account.payment.method"] + cls.payment_term_model = cls.env["account.payment.term"] + cls.account_invoice_model = cls.env["account.move"] + cls.account_model = cls.env["account.account"] + Journal = cls.env["account.journal"] + + cls.journal_sale = Journal.search([("type", "=", "sale")], limit=1) + cls.income_account = cls.account_model.search( + [ + ( + "account_type", + "=", + "income_other", + ) + ], + limit=1, + ) + cls.partner = cls.env["res.partner"].create({"name": "Partner 1"}) + cls.company = cls.env.ref("base.main_company") + cls.company.partner_id = cls.partner.id + cls.company.legal_id_number = "12-3456789" + cls.payment_method_line = cls.env["account.payment.method.line"].search( + [("code", "=", "ACH-In")], limit=1 + ) + cls.ach_payment_method_01 = cls.payment_method_model.search( + [("code", "=", "ACH-In")], limit=1 + ) + cls.acme_bank = cls.env["res.bank"].create( + { + "name": "ACME Bank", + "bic": "GEBABEBB03B", + "city": "Charleroi", + "routing_number": "021000021", + "country": cls.env.ref("base.be").id, + } + ) + bank_account = cls.env["res.partner.bank"].create( + { + "acc_number": "0023032234211123", + "partner_id": cls.partner.id, + "bank_id": cls.acme_bank.id, + "company_id": cls.company.id, + } + ) + cls.mandate = cls.env["account.banking.mandate"].create( + { + "partner_bank_id": bank_account.id, + "signature_date": "2024-01-01", + "company_id": cls.company.id, + "delay_days": 1, + } + ) + cls.mandate.validate() + cls.journal_c1 = cls.env["account.journal"].create( + { + "name": "Journal 1", + "code": "J1", + "type": "bank", + "company_id": cls.company.id, + "bank_account_id": bank_account.id, + } + ) + cls.inbound_mode = cls.env.ref("account_payment_mode.payment_mode_inbound_dd1") + cls.journal = cls.env["account.journal"].search( + [("type", "=", "bank"), ("company_id", "=", cls.env.user.company_id.id)], + limit=1, + ) + cls.payment_mode_c1 = cls.env["account.payment.mode"].create( + { + "name": "ACH Direct Debit", + "bank_account_link": "variable", + "payment_method_id": cls.ach_payment_method_01.id, + "company_id": cls.company.id, + "fixed_journal_id": cls.journal_c1.id, + "variable_journal_ids": [(6, 0, [cls.journal_c1.id])], + } + ) + + # Create account for invoice discount + cls.account_discount = cls.account_model.create( + dict( + code="custaccdiscount", + name="Discount Expenses", + account_type="expense", + reconcile=True, + ) + ) + + # Create Payment term + cls.payment_term = cls.payment_term_model.create( + dict( + name="5%10 NET30", + is_discount=True, + note="5% discount if payment done within 10 days, otherwise net", + line_ids=[ + ( + 0, + 0, + { + "value": "balance", + "discount_percentage": 5.0, + "discount_days": 10, + "discount_expense_account_id": cls.account_discount.id, + "days": 30, + }, + ) + ], + ) + ) + + def test_account_payment_order_ach_discount(self): + + # Create customer invoice + self.customer_invoice = self.account_invoice_model.create( + dict( + name="Test Customer Invoice", + move_type="out_invoice", + invoice_date=datetime.today() - timedelta(days=1), + invoice_payment_term_id=self.payment_term.id, + journal_id=self.journal_sale.id, + partner_id=self.partner.id, + ) + ) + # Prepare invoice line values + self.customer_invoice.write( + { + "invoice_line_ids": [ + ( + 0, + 0, + { + "product_id": self.env.ref("product.product_product_5").id, + "quantity": 10.0, + "account_id": self.income_account.id, + "name": "product test 5", + "price_unit": 100.00, + }, + ) + ] + } + ) + # Validate customer invoice + self.customer_invoice.action_post() + + with Form( + self.env["account.payment.register"].with_context( + active_ids=self.customer_invoice.ids, + batch=True, + active_model="account.move", + ) + ) as register_wizard_form: + register_wizard_form.payment_method_line_id = self.payment_method_line + register_wizard = register_wizard_form.save() + # check 5% discount applied on payment + self.assertEqual( + register_wizard.payment_difference, + self.customer_invoice.amount_total * 0.05, + ) + payment_order_dict = register_wizard.make_payments() + payment_order = self.env["account.payment.order"].browse( + payment_order_dict["res_id"] + ) + payment_order.journal_id = self.journal_c1.id + payment_order.draft2open() + payment_order.payment_line_ids.payment_ids.mandate_id = self.mandate + payment_order.generate_payment_file() + payment_order.generated2uploaded() diff --git a/account_banking_ach_discount/views/account_payment_view.xml b/account_banking_ach_discount/views/account_payment_view.xml new file mode 100644 index 00000000..922a1468 --- /dev/null +++ b/account_banking_ach_discount/views/account_payment_view.xml @@ -0,0 +1,38 @@ + + + + + account.payment.line.form + account.payment.line + + + + + + + + + + + + account.payment.line.tree + account.payment.line + + + + + + + + + + + + + diff --git a/account_banking_ach_discount/wizard/__init__.py b/account_banking_ach_discount/wizard/__init__.py new file mode 100644 index 00000000..17731038 --- /dev/null +++ b/account_banking_ach_discount/wizard/__init__.py @@ -0,0 +1,3 @@ +# Copyright (C) 2019 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import account_payment_register diff --git a/account_banking_ach_discount/wizard/account_payment_register.py b/account_banking_ach_discount/wizard/account_payment_register.py new file mode 100644 index 00000000..370f1729 --- /dev/null +++ b/account_banking_ach_discount/wizard/account_payment_register.py @@ -0,0 +1,66 @@ +# Copyright (C) 2019 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models + + +class AccountPaymentRegister(models.TransientModel): + _inherit = "account.payment.register" + + def make_payments(self): + if self.payment_method_code and self.payment_method_code in ( + "ACH-In", + "ACH-Out", + ): + action = False + payment_mode = self.env["account.payment.mode"].search( + [ + ("payment_type", "=", self.payment_type), + ( + "payment_method_id", + "=", + self.payment_method_line_id.payment_method_id.id, + ), + ("payment_order_ok", "=", True), + ], + limit=1, + ) + payment_line_pool = self.env["account.payment.line"] + # Update invoice with Payment mode + if payment_mode: + for line in self.invoice_payments: + invoice_id = line.invoice_id + # updated discount logic + discount = invoice_id.discount_taken + # discount should not be consider for open invoices + if line.payment_difference_handling != "open": + discount = invoice_id.discount_taken + line.payment_difference + invoice_id.write( + { + "payment_mode_id": payment_mode.id, + "discount_taken": discount, + } + ) + invoice_id.line_ids.write({"payment_mode_id": payment_mode.id}) + action = invoice_id.with_context( + payment_date=self.payment_date, + payment_line_state=line.payment_difference_handling, + ).create_account_payment_line() + # Find related ACH transaction line + domain = [("move_id", "=", invoice_id.id), ("state", "=", "draft")] + ach_lines = payment_line_pool.search(domain) + if ach_lines: + ach_lines.write( + { + "payment_difference_handling": line.payment_difference_handling, + "writeoff_account_id": line.writeoff_account_id.id, + "reason_code": line.reason_code.id, + "note": line.note, + "communication": "Payment of invoice %s" + % line.invoice_id.name, + "communication_type": "normal", + "amount_currency": line.amount, + "payment_difference": line.payment_difference, + } + ) + return action + return super().make_payments() diff --git a/setup/account_banking_ach_discount/odoo/addons/account_banking_ach_discount b/setup/account_banking_ach_discount/odoo/addons/account_banking_ach_discount new file mode 120000 index 00000000..cd5ad99d --- /dev/null +++ b/setup/account_banking_ach_discount/odoo/addons/account_banking_ach_discount @@ -0,0 +1 @@ +../../../../account_banking_ach_discount \ No newline at end of file diff --git a/setup/account_banking_ach_discount/setup.py b/setup/account_banking_ach_discount/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/account_banking_ach_discount/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)