Norman King a08a02ad4b feat(statements): top up open late-fee invoices instead of stacking new ones
Statement generation raised a fresh LPF invoice every month, so a customer
who never paid accumulated a pile of small invoices, each carrying the flat
dunning_fee again. Interest was also recomputed from each invoice's due date
every run, re-billing periods already charged for.

Now a customer gets one fee invoice per collections episode:

- While an earlier fee invoice still carries a balance, the next run amends
  it and appends the new period's interest as a further line item, rather
  than creating a second invoice.
- Payment Entries allocated to a partly paid fee invoice are unlinked by the
  cancellation and re-applied to the amended invoice via
  reconcile_against_document (the primitive Payment Reconciliation uses), so
  the outstanding amount and Payment Ledger stay correct.
- Original posting and due dates are carried over. Re-dating to today would
  reset the invoice to Current in the statement's aging buckets and hide how
  long the balance has been owed.
- Interest accrues from the last run, tracked by a new
  custom_late_fee_billed_upto field on Sales Invoice, so no period is billed
  twice. Fee invoices predating the field fall back to their posting date,
  which is when they were billed, so no migration patch is needed.
- The flat dunning_fee is charged once, when a fee invoice is first raised,
  not again on every top-up.
- Unpaid fee invoices are in the interest base on the same terms as any
  other overdue receivable, so interest compounds onto the fee balance.

Amending means cancelling, which is only reversible for links we can
restore. If the open fee invoice has a Journal Entry or credit note applied,
a negative payment allocation, or a posting date in a frozen period, it is
left alone, the charge goes on a new invoice, and the reason is recorded on
the customer's timeline.

Verified against nsi.local with two rolled-back integration probes covering
the amend + re-link path (including two consecutive amendments) and the
blocked-amend fallback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 06:15:55 -04:00
2026-04-27 10:22:58 -04:00

NS App ERPNext Custom Integrations

Overview

NS App is a custom ERPNext application that:

Adding a manual payment form using Collect.js Processing secure card payments through NMI Optionally saving payment methods to the NMI Customer Vault Storing the returned vault ID on the ERPNext Customer for future autopay use

This part of the app is designed to integrate directly into the Sales Invoice workflow.

Features Secure card entry using NMI Collect.js (tokenization) Manual payment dialog inside ERPNext Optional “Subscribe to Autopay” checkbox Conditional vault creation in NMI Storage of customer_vault_id in ERPNext Customer Configurable enable/disable of autopay via site_config.json

Requirements ERPNext / Frappe Bench environment NMI gateway account Collect.js enabled in NMI Access to modify site_config.json

Installation

  1. Get the app cd ~/frappe-bench bench get-app git@git.nsinnovations.net:nsinnovations/ns_erpnext_app.git --skip-assets
  2. Install on site bench --site "site name" install-app ns_app
  3. Build and restart bench build bench restart

Configuration

Edit site configuration: nano ~/frappe-bench/sites/site-name/site_config.json

Add:

{ "nmi_security_key": "your_nmi_security_key", "enable_autopay_signup": 0 } Notes nmi_security_key is required for all payment and vault operations enable_autopay_signup controls whether the checkbox actually triggers vault creation Frontend Behavior

The payment dialog includes:

Cardholder Name Billing ZIP Secure card fields via Collect.js “Subscribe to Autopay” checkbox

If the checkbox is selected and autopay is enabled in config, the backend will attempt to create a vault entry.

Backend Flow: Payment Collect.js generates a payment token Token is sent to backend API Backend sends transaction to NMI Payment is processed Autopay (Optional)

If enabled and selected:

Backend sends customer_vault=add_customer request to NMI Uses the same payment token Parses response for customer_vault_id Saves ID to ERPNext Customer Example Vault Request vault_data = { "security_key": frappe.conf.get("nmi_security_key"), "customer_vault": "add_customer", "payment_token": token, "first_name": first_name, "last_name": last_name, "zip": billing_zip } Error Handling

Vault creation is wrapped in a try/except block to prevent payment failures:

try: if int(save_autopay or 0): # vault logic pass except Exception: frappe.log_error(frappe.get_traceback(), "Autopay Vault Error") Testing Strategy

Before enabling in production:

Set in site_config.json:

"enable_autopay_signup": 0 Verify: Payments process correctly No vault entries are created

Enable autopay:

"enable_autopay_signup": 1 Test: Checkbox unchecked → no vault Checkbox checked → vault created and saved Deployment Notes Do not expose autopay functionality until fully tested Keep enable_autopay_signup disabled in production until ready Always run: bench build bench restart

Troubleshooting:

  1. App not found

Ensure app is in apps.txt:

nano ~/frappe-bench/sites/apps.txt

Add:

ns_app

  1. ModuleNotFoundError

Ensure app is installed in environment:

./env/bin/pip install -e apps/ns_app Asset build errors

Skip during install:

bench get-app --skip-assets

Then fix frontend paths before running bench build.

Vault not saving

Check logs:

frappe.log_error(response.text, "Vault Response")

Verify:

customer_vault_id is returned Security key is correct File Structure apps/ns_app/ ├── ns_app/ │ ├── api/ │ │ └── payments.py │ ├── public/ │ │ └── js/ │ └── hooks.py ├── setup.py ├── pyproject.toml └── MANIFEST.in

Security Notes: Never store raw card data Always use Collect.js tokenization Keep nmi_security_key private Use HTTPS for all requests

Description
No description provided
Readme 300 KiB
Languages
JavaScript 36.9%
HTML 35.7%
Python 19.9%
Jinja 7.5%