forked from OCA/l10n-usa
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
454 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg | ||
:alt: License: AGPL-3 | ||
|
||
======================= | ||
Counting House ACH Base | ||
======================= | ||
|
||
Add fields to Bank, Partner and Company required for ACH transactions in USA. | ||
|
||
Installation | ||
============ | ||
|
||
This module depends on : | ||
|
||
* stdnum | ||
|
||
|
||
Usage | ||
===== | ||
|
||
Add `routing_number` on Bank records. | ||
|
||
Add Legal ID on Partner and Company records. | ||
|
||
Add Mandate URL field to Company record. Use in email templates to provide customer with an easy | ||
way to access your Mandate Authorization form to streamline ACH authorizations. | ||
|
||
Known issues / Roadmap | ||
====================== | ||
|
||
* Add support for EFT 1464 byte payment files required in Canada | ||
|
||
Bug Tracker | ||
=========== | ||
|
||
Bugs are tracked on `GitHub Issues | ||
<https://github.com/thinkwelltwd/countinghouse>`_. 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import models |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# Copyright 2018 Thinkwell Designs <[email protected]> | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). | ||
|
||
{ | ||
'name': 'Localizations for North American Banking & Financials', | ||
'summary': 'Add fields required for North American Banking & Financials', | ||
'version': '11.0.1.0.0', | ||
'license': 'AGPL-3', | ||
'author': 'Thinkwell Designs', | ||
'website': 'https://github.com/thinkwelltwd/countinghouse', | ||
'category': 'Banking addons', | ||
'depends': [ | ||
'account_payment_order', | ||
'account_banking_mandate', | ||
], | ||
'data': [ | ||
'views/account_banking_mandate.xml', | ||
'views/account_invoice.xml', | ||
'views/res_bank.xml', | ||
'views/res_company.xml', | ||
'views/res_partner.xml', | ||
], | ||
'installable': True, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from . import account_banking_mandate | ||
from . import account_invoice | ||
from . import account_payment_order | ||
from . import base | ||
from . import res_bank | ||
from . import res_company | ||
from . import res_partner |
48 changes: 48 additions & 0 deletions
48
account_banking_ach_base/models/account_banking_mandate.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
from odoo import api, fields, models | ||
from odoo.exceptions import UserError | ||
|
||
|
||
class AccountBankingMandate(models.Model): | ||
_inherit = 'account.banking.mandate' | ||
|
||
delay_days = fields.Integer(string='Delay Days', required=True, default=10, | ||
help='Number of days to wait after invoice date before ' | ||
'including an invoice in Payment Order for processing.', | ||
) | ||
|
||
def validate(self): | ||
for mandate in self: | ||
if not mandate.delay_days: | ||
raise UserError('Delay days must be specified, and greater than 0.') | ||
|
||
super(AccountBankingMandate, self).validate() | ||
|
||
def set_payment_modes_on_partner(self): | ||
""" | ||
Set the payment modes on the Partner if they don't already exist. | ||
""" | ||
payment_modes = {} | ||
|
||
if self.partner_id.customer and not self.partner_id.customer_payment_mode_id: | ||
customer_mode = self.env['account.payment.mode'].search([ | ||
('payment_type', '=', 'inbound'), | ||
('company_id', '=', self.company_id.id), | ||
], limit=1) | ||
if customer_mode: | ||
payment_modes['customer_payment_mode_id'] = customer_mode.id | ||
if self.partner_id.supplier and not self.partner_id.supplier_payment_mode_id: | ||
supplier_mode = self.env['account.payment.mode'].search([ | ||
('payment_type', '=', 'outbound'), | ||
('company_id', '=', self.company_id.id), | ||
], limit=1) | ||
if supplier_mode: | ||
payment_modes['supplier_payment_mode_id'] = supplier_mode.id | ||
|
||
if payment_modes: | ||
self.partner_id.write(payment_modes) | ||
|
||
@api.model | ||
def create(self, vals): | ||
mandate = super(AccountBankingMandate, self).create(vals) | ||
self.set_payment_modes_on_partner() | ||
return mandate |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from datetime import date, timedelta | ||
from odoo import api, fields, models | ||
from odoo.exceptions import UserError | ||
|
||
|
||
class AccountInvoice(models.Model): | ||
_inherit = 'account.invoice' | ||
|
||
@api.multi | ||
def create_account_payment_line(self): | ||
today = date.today() | ||
|
||
for invoice in self: | ||
mandate = invoice.mandate_id | ||
if not mandate: | ||
continue | ||
|
||
invoice_date = fields.Date.from_string(invoice.date_invoice) | ||
delay_expired = invoice_date + timedelta(days=mandate.delay_days) | ||
|
||
if today < delay_expired: | ||
raise UserError( | ||
'To satisfy payment mandate, cannot add invoice %s to Debit Order until %s!' % | ||
(invoice.number, delay_expired.strftime('%Y-%m-%d')) | ||
) | ||
|
||
return super(AccountInvoice, self).create_account_payment_line() |
138 changes: 138 additions & 0 deletions
138
account_banking_ach_base/models/account_payment_order.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
from odoo import api, models, fields, _ | ||
from odoo.exceptions import UserError | ||
from string import ascii_uppercase | ||
from ach.builder import AchFile | ||
|
||
|
||
CREDIT_AUTOMATED_RETURN = '21' | ||
CREDIT_AUTOMATED_DEPOSIT = '22' | ||
CREDIT_PRENOTE_DNE_ENR = '23' | ||
CREDIT_ZERO_DOLLAR_ENTRY_WITH_ADDENDA = '24' | ||
|
||
DEBIT_AUTOMATED_RETURN = '26' | ||
DEBIT_AUTOMATED_PAYMENT = '27' | ||
DEBIT_PRENOTE_DNE_ENR = '28' | ||
DEBIT_ZERO_DOLLAR_ENTRY_WITH_ADDENDA = '29' | ||
|
||
|
||
class AccountPaymentOrder(models.Model): | ||
_inherit = 'account.payment.order' | ||
|
||
def get_file_id_mod(self): | ||
""" | ||
ACH file_id_mod should be 'A' for the first of the day, | ||
'B' for the second and so on. | ||
""" | ||
ach_transactions_today = self.env['account.payment.order'].search_count([ | ||
('create_date', '>=', fields.Date.today()), | ||
('company_partner_bank_id', '=', self.company_partner_bank_id.id), | ||
('state', 'in', ['generated', 'uploaded']), | ||
('payment_mode_id.payment_method_id.code', 'in', ['ACH-In', 'ACH-Out']), | ||
]) | ||
|
||
return ascii_uppercase[ach_transactions_today] | ||
|
||
def ach_settings(self): | ||
bank = self.company_partner_bank_id.bank_id | ||
routing_number = bank.routing_number | ||
legal_id_number = self.company_id.legal_id_number | ||
|
||
if not legal_id_number: | ||
raise UserError( | ||
'%s does not have an EIN / SSN / BN assigned!' % self.company_id.name | ||
) | ||
|
||
if not routing_number: | ||
raise UserError( | ||
'%s does not have a Routing Number assigned!' % bank.name | ||
) | ||
|
||
return { | ||
'immediate_dest': self.company_partner_bank_id.acc_number, | ||
'immediate_org': routing_number, | ||
'immediate_dest_name': bank.name, | ||
'immediate_org_name': self.company_id.name, | ||
'company_id': legal_id_number, | ||
} | ||
|
||
def validate_banking(self, line): | ||
if not line.partner_bank_id.bank_id: | ||
raise UserError( | ||
_('%s account number has no Bank assigned' % line.partner_bank_id.acc_number) | ||
) | ||
|
||
if not line.partner_bank_id.bank_id.routing_number: | ||
raise UserError( | ||
_('%s has no routing number specified' % line.partner_bank_id.bank_id.name) | ||
) | ||
|
||
def validate_mandates(self, line): | ||
"""Ensure that mandates are correctly set""" | ||
if not line.mandate_id: | ||
raise UserError( | ||
_("Missing ACH Direct Debit mandate on the " | ||
"bank payment line with partner '%s' " | ||
"(reference '%s').") | ||
% (line.partner_id.name, line.name)) | ||
if line.mandate_id.state != 'valid': | ||
raise Warning( | ||
_("The ACH Direct Debit mandate with reference '%s' " | ||
"for partner '%s' has expired.") | ||
% (line.mandate_id.unique_mandate_reference, | ||
line.mandate_id.partner_id.name)) | ||
if line.mandate_id.type == 'oneoff': | ||
if line.mandate_id.last_debit_date: | ||
raise Warning( | ||
_("The mandate with reference '%s' for partner " | ||
"'%s' has type set to 'One-Off' and it has a " | ||
"last debit date set to '%s', so we can't use " | ||
"it.") | ||
% (line.mandate_id.unique_mandate_reference, | ||
line.mandate_id.partner_id.name, | ||
line.mandate_id.last_debit_date)) | ||
|
||
def get_transaction_type(self, amount): | ||
if not amount: | ||
return DEBIT_ZERO_DOLLAR_ENTRY_WITH_ADDENDA if self.payment_type == 'inbound' \ | ||
else CREDIT_ZERO_DOLLAR_ENTRY_WITH_ADDENDA | ||
|
||
return DEBIT_AUTOMATED_PAYMENT if self.payment_type == 'inbound' \ | ||
else CREDIT_AUTOMATED_DEPOSIT | ||
|
||
@api.multi | ||
def generate_ach_file(self): | ||
self.ensure_one() | ||
|
||
inbound_payment = self.payment_type == 'inbound' | ||
|
||
file_mod = self.get_file_id_mod() | ||
ach_file = AchFile(file_id_mod=file_mod, settings=self.ach_settings()) | ||
filename = '{today}_{bank}_{file_mod}.txt'.format( | ||
today=fields.Date.today(), bank=self.company_partner_bank_id.id, file_mod=file_mod, | ||
) | ||
|
||
entries = [] | ||
|
||
for line in self.bank_line_ids: | ||
|
||
if inbound_payment: | ||
self.validate_mandates(line) | ||
|
||
self.validate_banking(line) | ||
|
||
amount = line.amount_currency | ||
entries.append({ | ||
'type': self.get_transaction_type(amount=amount), | ||
'routing_number': line.partner_bank_id.bank_id.routing_number, | ||
'account_number': line.partner_bank_id.acc_number, | ||
'amount': str(amount), | ||
'name': line.partner_id.name, | ||
'addenda': [{ | ||
'payment_related_info': line.communication, | ||
}], | ||
}) | ||
|
||
credits = self.payment_type == 'outbound' | ||
ach_file.add_batch('PPD', entries, credits=credits, debits=inbound_payment) | ||
|
||
return ach_file.render_to_string(), filename |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
from odoo import api, models, fields | ||
from odoo.exceptions import UserError | ||
from stdnum.us import ssn, ein | ||
from stdnum.ca import bn | ||
|
||
|
||
class LegalIDNumber(models.AbstractModel): | ||
""" | ||
Odoo's VAT field validation prevents it from being used | ||
for EIN / GST / SSN, etc. | ||
Use generic ID and apply validation depending on the Country field. | ||
""" | ||
_name = 'countinghouse.legal_id_number' | ||
|
||
legal_id_number = fields.Char( | ||
string='Legal ID', | ||
required=False, | ||
help='''For US entities, enter valid EIN or Social Security Number. | ||
Canadian entities, enter Canadian Business Number. | ||
''' | ||
) | ||
|
||
@api.constrains('legal_id_number') | ||
def validate_legal_id_number(self): | ||
if not self.legal_id_number: | ||
return | ||
|
||
valid = False | ||
|
||
for v in (ssn, ein, bn): | ||
try: | ||
v.validate(self.legal_id_number) | ||
valid = True | ||
break | ||
except Exception: | ||
continue | ||
|
||
if not valid: | ||
raise UserError( | ||
'%s is not a valid EIN / SSN / Canadian Business Number' % self.legal_id_number | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from odoo import api, models, fields | ||
from odoo.exceptions import ValidationError | ||
from stdnum.us import rtn | ||
|
||
|
||
class ResBank(models.Model): | ||
_inherit = 'res.bank' | ||
|
||
routing_number = fields.Char(string='Routing Number', required=False) | ||
|
||
@api.constrains('routing_number') | ||
def validate_routing_number(self): | ||
if not self.routing_number or not self.country: | ||
return | ||
|
||
country_code = self.country.code | ||
|
||
if country_code == 'US': | ||
try: | ||
rtn.validate(self.routing_number) | ||
except Exception: | ||
raise ValidationError( | ||
'%s is not a valid US routing number!' % self.routing_number | ||
) | ||
|
||
elif country_code == 'CA': | ||
if len(self.routing_number) != 8 or not not self.routing_number.is_digit(): | ||
raise ValidationError( | ||
'%s is not a valid Canadian routing number!' % self.routing_number | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
from odoo import fields, models | ||
|
||
|
||
class ResCompany(models.Model): | ||
_name = 'res.company' | ||
_inherit = ['countinghouse.legal_id_number', 'res.company'] | ||
|
||
mandate_url = fields.Char(string='Mandate URL', required=False, | ||
help='Full URL to download ACH Mandate / Authorization form. Useful ' | ||
'to include in email templates for customer to access and ' | ||
'complete the Mandate form.' | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from odoo import models | ||
|
||
|
||
class ResPartner(models.Model): | ||
_name = 'res.partner' | ||
_inherit = ['countinghouse.legal_id_number', 'res.partner'] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
stdnum | ||
python-ach |
Oops, something went wrong.