Clean up customers, contacts, and addresses in ERPNext
Use the ERPNext CIS Plus app to geocode and validate addresses, edit a customer's primary contact and address in one form, and see customers on a map.
On this page
This document covers erpnext_cis_plus, Avunu's open-source ERPNext app for small businesses where each customer has one main contact and one main address. Use it when you want addresses checked and geocoded automatically, want to edit the primary contact and address without leaving the Customer form, or want to see your customers on a map.
ERPNext normally keeps contacts and addresses as separate records that you create from the Customer's dashboard. The app leaves that data model alone. It adds fields to the Customer form that read from and write back to the same Contact and Address records.
What the app does
Looks up every Address in OpenStreetMap's Nominatim service when it is saved, stores coordinates, and fills in missing postal code, state, county, city, and country.
Adds editable address and contact fields to the Customer form, backed by the real Address and Contact records.
Normalizes the primary contact's phone and mobile numbers to E.164 format.
Adds a map view of customers to the Customer list.
The same form behavior is also wired up for Suppliers. The map and geocoding apply to Addresses, so they cover any party that has one.
Install the app
The app requires ERPNext and pulls in two Python packages, us and phonenumbers, when you add it. From the bench directory:
bench get-app https://github.com/Avunu/erpnext_cis_plus
bench --site <SITE_NAME> install-app erpnext_cis_plusThe source was read against Frappe and ERPNext v16. Test on a staging site before installing on production, because the app adds custom fields to Address, Customer, and Supplier.
Edit the primary contact and address from the customer
Open a Customer. The "Primary Address and Contact" section now shows editable fields instead of only the two link fields.
| Group | Fields |
|---|---|
| Address | Address line 1 and 2, city, state, postal code, email, phone, fax |
| Contact | First name, last name, email, phone, mobile, department |
The standard "Primary Address" display text, "Mobile No", and "Email Id" fields on the Customer are hidden so the new fields are the single place to edit.
How the form behaves
When you open a Customer that has no primary address or contact selected, the form looks for Addresses and Contacts linked to that customer and selects the first one it finds. Save the customer to keep that choice.
When a primary address or contact is selected, the form loads its current values into the editable fields, so you always edit what is stored.
When you save an existing customer, the app does one of two things for each of the address and the contact:
If a primary record is selected, it copies any changed fields to that Address or Contact and saves it.
If none is selected but you typed something, it creates a new Address or Contact, links it to the customer, and marks it as primary.
Note
To create a new address from the customer form you must fill in Address Line 1. To create a new contact you must fill in First Name. Without them, the save stops with a message naming the missing field.
New customers are skipped on their first save
The app skips its validation and its address and contact sync when a Customer or Supplier is saved for the first time, so ERPNext's normal handling applies. This covers every new customer, including ones created from the quick entry dialog: ERPNext marks the document as new while it validates it, and the app checks that same flag. Details typed into the new address and contact fields on that first save stay on the Customer and are not turned into Address and Contact records until a later save. We read this from the code and did not test it on a live site. Create the customer first, then open it, add or check the address and contact details, and save again.
Phone numbers
On save, the primary contact's phone and mobile numbers are converted to E.164 (for example, +12025550123). Clean numbers matter if you plan to text customers: the messaging app matches inbound texts to contacts by that exact format. The app picks the country from the primary address, and falls back to the country in System Settings.
If a number cannot be parsed as valid, you get an alert and the value is saved as you typed it. It does not block the save, so scan for those alerts when you are cleaning up old data.
What the form does not remove
Clearing the email, phone, or mobile field on the Customer form does not delete that entry from the Contact. The sync only adds or promotes entries in the Contact's email and phone tables. To remove an old email or phone number, edit the Contact record directly.
If you type an email or number that is already on the Contact, the app makes that existing row primary instead of adding a duplicate. A value that is not on the Contact yet is added as a new primary row, and the old row stays.
Geocoding and address validation
Every time an Address is saved, whether from the Address form, the Customer form, an import, or a script, the app sends a search to the OpenStreetMap Nominatim service.
What it sends
The query is built from whichever of these fields are filled in: address line 1, city, state, postal code, and country, joined with commas. If none of them has a value, the lookup is skipped.
What it changes
When Nominatim returns a match, the app takes the first result and:
Always overwrites the Address's latitude and longitude.
Fills the postal code, country, state, and county only if they are blank.
Fills the city from the result's city, town, or village.
When it fills in the state for a United States address, uses the two-letter abbreviation (converted from the full name with the
uspackage). A state you typed yourself is not changed.
Then, before saving, it builds a GeoJSON point from the coordinates and stores it in the Address's "Map" field, which is what Frappe uses to draw the pin on the Address form.
Because blank fields are filled from the match, a short address with just a street and postal code often comes out complete after one save. Review the result, though. It is a best guess from a free service.
Note
The city rule is looser than the others. When Nominatim returns a town or village, the app can replace a city you already entered. Check the city on addresses in small places.
When it finds nothing
If Nominatim returns no result, the save continues and the app does not touch the coordinates. An Address with stale coordinates keeps them, and a new one stays without a pin. Fix the street or postal code and save again.
When the lookup fails
If the request errors (the service is unreachable, or returns an HTTP error), the app writes an entry to the Error Log and blocks the save with the message "Geolocation failed".
Warning
Because the lookup runs on every Address save, an outage or a blocked outbound connection to the Nominatim service stops Address edits, including edits made from the Customer form. The code sets no request timeout and has no off switch. If your server cannot reach the service, expect failed saves.
Usage policy
The app identifies itself to Nominatim with the user agent FrappeERP/1.0. It sends one request per Address save, with no caching, queueing, or throttling.
The README and code do not discuss rate limits, and we did not verify the current limits. The public service has its own usage policy, so read it before you run a bulk import or re-save thousands of addresses. For large batches, consider running the work in smaller groups, or ask us about pointing the app at a self-hosted Nominatim instance. The service URL is a constant in hooks/address.py, so that change is a code change.
See customers on a map
The app registers a map data method for the Customer list, so the list view switcher offers a Map view.
Open the Customer list.
Switch the view to Map.
Apply filters as usual. The map plots only the customers that match.
Each customer appears as a pin when its primary address has coordinates. Click a pin to see the customer name (linked to the form), the primary contact (linked to the Contact), and the primary address text. Customers whose primary address has no coordinates are left off the map.
The Customer's coordinates and map field are copied from its primary address, so a customer shows up on the map only after its address has been geocoded.
Tip
If a pin does not move after you change an address from the Customer form, save the customer once more. The customer copies coordinates from the address when it is validated, and the app writes address changes after that step. We did not test this timing, so treat it as a first thing to try.
A cleanup routine
Use this sequence to bring an existing customer list up to date.
Install the app and open the Customer list in Map view. Customers without pins have no geocoded primary address.
Open one of those customers. Confirm the right Address and Contact are selected as primary.
Fix the address line, city, state, and postal code on the Customer form, add the contact's name, email, and phone, and save.
Watch for the phone number alert and for a "Geolocation failed" message.
Return to the map and confirm the pin.
For customers with several addresses or contacts, edit the secondary ones on their own Address and Contact records. The Customer form edits only the primary pair.
Where the code lives
All paths are inside the app repository.
| Path | Purpose |
|---|---|
erpnext_cis_plus/hooks.py | Wires the scripts and document events together |
erpnext_cis_plus/erpnext_cis_plus/hooks/address.py | Nominatim lookup and GeoJSON point |
erpnext_cis_plus/erpnext_cis_plus/hooks/party_utils.py | Shared sync, phone normalization, and validation |
erpnext_cis_plus/erpnext_cis_plus/hooks/customer.py | Customer hooks and the map data method |
erpnext_cis_plus/erpnext_cis_plus/custom/ | Custom field and property definitions for Address, Customer, and Supplier |
erpnext_cis_plus/public/js/customer.js | Customer form layout and loading |
erpnext_cis_plus/public/js/customer_list.js | Registers the map data method |
Sources
This article is in the public domain (CC0 1.0), code samples included. Use it however helps you.