Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[17.0][ADD] osi_nacha_80_byte. #1101

Merged
merged 1 commit into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions osi_nacha_80_byte/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3

========
Overview
========

* The Nacha format needs to be updated to match CIBC format and specifications.


=======
Credits
=======

* Open Source Integrators <http://www.opensourceintegrators.com>


Contributors
------------

* Chanakya Soni <[email protected]>
3 changes: 3 additions & 0 deletions osi_nacha_80_byte/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Copyright (C) 2024, Open Source Integrators
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from . import models
21 changes: 21 additions & 0 deletions osi_nacha_80_byte/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright (C) 2024, Open Source Integrators
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
{
"name": "OSI NACHA 80byte",
"countries": ["us"],
"summary": """Export payments as NACHA 80byte""",
"category": "Accounting",
"version": "17.0.1.0.0",
"license": "LGPL-3",
"author": "Open Source Integrators",
"maintainer": "Open Source Integrators",
"website": "https://github.com/ursais/osi-addons",
"depends": [
"l10n_us_payment_nacha",
],
"data": [
"data/ir_sequence.xml",
"views/account_journal_views.xml",
],
"installable": True,
}
9 changes: 9 additions & 0 deletions osi_nacha_80_byte/data/ir_sequence.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="ir_sequence_file_number" model="ir.sequence">
<field name="name">Nacha File Number</field>
<field name="code">nacha.file.sequence</field>
<field name="padding">4</field>
<field name="company_id" eval="False" />
</record>
</odoo>
5 changes: 5 additions & 0 deletions osi_nacha_80_byte/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copyright (C) 2024, Open Source Integrators
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

from . import account_journal
from . import account_batch_payment
167 changes: 167 additions & 0 deletions osi_nacha_80_byte/models/account_batch_payment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Copyright (C) 2024, Open Source Integrators
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

from odoo import _, fields, models
from odoo.exceptions import ValidationError


class AccountBatchPayment(models.Model):
_inherit = "account.batch.payment"

def _generate_nacha_batch_trailer_record(self):
entry = []
amount = self._get_total_cents(self.payment_ids)
entry.append("7460")
entry.append("{:06d}".format(len(self.payment_ids)))
entry.append("{:010d}".format(0))
entry.append("{:20.20}".format(""))
entry.append("{:012d}".format(amount))
entry.append("{:28.28}".format(""))
return "".join(entry)

def _generate_nacha_trailer(self):
entry = []
entry.append("9")
entry.append("{:06d}".format(1))
entry.append("{:06d}".format(len(self.payment_ids)))
entry.append("{:67.67}".format(""))
return "".join(entry)

def _generate_nacha_entry_detail(self, payment_nr, payment, is_offset):
if self.journal_id.nacha_file_type == "80_byte":
amount = sum(payment.mapped("amount"))
amount = self._get_total_cents(payment)
entry = []
if len(payment) > 1:
payment = payment[0]
self._validate_bank_for_nacha(payment)

reference = "{}{:011d}".format("CK", payment.id)
entry.append("6C")
entry.append("{:1.1}".format(""))
entry.append(
"{:4.4}".format(str(payment.partner_bank_id.bank_id.bic or ""))
)
entry.append("{:<5.5}".format(payment.partner_bank_id.aba_routing))
entry.append("{:<12.12}".format(payment.partner_bank_id.acc_number))
entry.append("{:5.5}".format(""))

entry.append("{:010d}".format(amount))
entry.append("{:<13.13}".format(reference))
entry.append(
"{}{:<21.21}".format(" ", payment.partner_bank_id.acc_holder_name)
)
entry.append("{:6.6}".format(""))
return "".join(entry)
else:
return super()._generate_nacha_entry_detail(payment_nr, payment, is_offset)

def _generate_nacha_batch_header_record(self, date, batch_nr):
if self.journal_id.nacha_file_type == "80_byte":
batch = []
batch.append("5")
batch.append("{:46.46}".format(""))
batch.append("{:3.3}".format("460"))
batch.append("{:10.10}".format(""))
batch.append("{:6.6}".format(self.date.strftime("%y%m%d")))
batch.append("{:14.14}".format(""))
return "".join(batch)
else:
return super()._generate_nacha_batch_header_record(date, batch_nr)

def _generate_nacha_header(self):
if self.journal_id.nacha_file_type == "80_byte":
# Ensure all mandatory fields are filled
if not self.journal_id.nacha_immediate_destination:
raise ValidationError(
_("Destination Data Center (Immediate Destination) is not filled.")
)
if not self.journal_id.nacha_immediate_origin:
raise ValidationError(
_("Originator Number (Immediate Origin) is not filled.")
)
if not self.journal_id.bank_account_id.bank_id.bic:
raise ValidationError(_("Institution Number (BIC) is not filled."))
if not self.journal_id.bank_account_id.aba_routing:
raise ValidationError(
_("Branch Transit Number (ABA Routing) is not filled.")
)
if not self.journal_id.bank_account_id.acc_number:
raise ValidationError(_("Account Number is not filled."))
if not self.journal_id.nacha_company_identification:
raise ValidationError(_("Originator's Short Name is not filled."))
if not self.currency_id.name:
raise ValidationError(_("Currency Indicator is not filled."))

# Prepare fields for the header
sequence = self.env["ir.sequence"].next_by_code("nacha.file.sequence")
if sequence == "10000":
sequence = "0001"
sequence_id = self.env.ref(
"osi_nacha_80_byte.ir_sequence_file_number",
)
sequence_id.number_next_actual = 2
destination = "{:>5.5}".format(self.journal_id.nacha_immediate_destination)
space1 = "{:>5.5}".format("")
origin = "{:>10.10}".format(self.journal_id.nacha_immediate_origin)
now_in_client_tz = fields.Datetime.context_timestamp(
self, fields.Datetime.now()
)
file_creation_date = "{:6.6}".format(
now_in_client_tz.strftime("%y%m%d")
) # File Creation Date
file_creation_number = str(sequence).zfill(4)
institution_number = "{:>4.4}".format(
self.journal_id.bank_account_id.bank_id.bic
)
branch_transit_number = "{:>5.5}".format(
self.journal_id.bank_account_id.aba_routing
)
account_number = self.journal_id.bank_account_id.acc_number.ljust(11)
space2 = "{:>2.2}".format("")
originator_name = self.journal_id.nacha_company_identification.ljust(15)
currency_indicator = self.currency_id.name.ljust(3)
space3 = "{:>4.4}".format("")

# Combine fields into an 80-character file header
header = (
"1 "
+ destination
+ space1
+ origin
+ file_creation_date
+ file_creation_number
+ " "
+ institution_number
+ branch_transit_number
+ account_number
+ space2
+ originator_name
+ " "
+ currency_indicator
+ space3
).ljust(80)
return header
else:
return super()._generate_nacha_header()

def _generate_nacha_file(self):
if self.journal_id.nacha_file_type == "80_byte":
entries = []
header = self._generate_nacha_header()
batch_nr = 0

entries.append(
self._generate_nacha_batch_header_record(self.date, batch_nr)
)
for date, payments in sorted(
self.payment_ids.grouped("partner_bank_id").items()
):
entries.append(
self._generate_nacha_entry_detail(0, payments, is_offset=False)
)
entries.append(self._generate_nacha_batch_trailer_record())
entries.append(self._generate_nacha_trailer())
return "\r\n".join([header] + entries)
else:
return super()._generate_nacha_file()
14 changes: 14 additions & 0 deletions osi_nacha_80_byte/models/account_journal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright (C) 2024, Open Source Integrators
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

from odoo import fields, models


class AccountJournal(models.Model):
_inherit = "account.journal"

nacha_file_type = fields.Selection(
[("80_byte", "80 Byte")],
default="80_byte",
string="File Type",
)
19 changes: 19 additions & 0 deletions osi_nacha_80_byte/views/account_journal_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<odoo>
<record id="view_account_journal_form_inherit_osi_nacha_80" model="ir.ui.view">
<field name="name">account.journal.form.inherit.osi_nacha_80</field>
<field name="model">account.journal</field>
<field name="priority">100</field>
<field name="inherit_id" ref="account.view_account_journal_form" />
<field name="arch" type="xml">
<xpath
expr="//field[@name='outbound_payment_method_line_ids']/tree/field[@name='payment_method_id']"
position="attributes"
>
<attribute name="domain">[]</attribute>
</xpath>
<field name="nacha_immediate_destination" position="before">
<field name="nacha_file_type" />
</field>
</field>
</record>
</odoo>
Loading