Development
How it works
The hooks, document events, side panel wrapper and save method behind CIS Plus for ERPNext, and how data flows between an Address and its parties.
On this page
CIS Plus is a small app with no doctypes of its own. It adds custom fields, three Address document events, two form scripts and a list script, and four whitelisted methods. This page follows the data through them.
Hooks
erpnext_cis_plus/hooks.py registers:
doctype_jsfor Customer and Supplier:public/js/party_records.jsfollowed bycustomer.jsorsupplier.js. Frappe concatenates them into one doctype script, soparty_records.jsdefineserpnext_cis_plus.party_recordsonce and both form scripts call it.doctype_list_jsfor Customer:customer_list.js, which mergesget_coords_methodinto ERPNext's own list settings for Customer.doc_eventsfor Address:validate,before_saveandon_update, all inerpnext_cis_plus/erpnext_cis_plus/hooks/address.py.
There are no class overrides, scheduled jobs, install hooks or fixtures. The custom fields live in erpnext_cis_plus/erpnext_cis_plus/custom/ with sync_on_migrate, so Frappe syncs them on install and on every migrate.
Geocoding an Address
Frappe runs an Address's validate hooks before its before_save hooks, and checks mandatory fields after both.
validate:geolocate_address.needs_geocodingdecides whether to look the address up: not when the five geocoded fields (address_line1,city,state,pincode,country) are all blank; always when the Address is new or has no coordinates; otherwise only when one of those fields changed. The lookup waits for a rate limit slot (acquire_rate_limit_slot): a Redis key,erpnext_cis_plus:nominatim:last-request, set withNXand a one-second expiry, shared by every site on the bench. It polls every 0.1 seconds for up to five seconds. With no Redis connection it does not coordinate. Then onerequests.gettohttps://nominatim.openstreetmap.org/searchwithformat=json,limit=1andaddressdetails=1, an identifying User-Agent and a(5, 10)second timeout. The first result setslatitudeandlongitudeand fills blank fields;get_state_abbrevuses theuspackage to shorten a United States state. A failed request is logged (frappe.log_error, title Geolocation Error) and shown as an alert. A failure, an empty result or no slot callsclear_stale_coordinates, which drops coordinates only when they belong to the address as it was before this save, and setsdoc.flags.coordinates_cleared.before_save:set_default_country. When the Address still has no country anddoc.flags.default_countryis set, it takes that country. Onlysave_party_recordsets the flag, for a new Address from the side panel.before_save:generate_point. With coordinates, it writes the GeoJSONFeatureCollectionwith one point that the Geolocation fieldlocationshows. Without them it leaveslocationalone, so a hand-drawn point stays, unlesscoordinates_clearedsays this save dropped them.on_update:update_party_locations. Whenlatitude,longitudeorlocationchanged, it writes them withfrappe.db.set_value(without touchingmodified) to every Customer whosecustomer_primary_addressand every Supplier whosesupplier_primary_addressis this Address. A missing coordinate is written as 0, because the parties' Float columns are not nullable.
On the parties, latitude, longitude and location have fetch_from set to the primary address's fields, so saving the party, or changing its primary address in the form, also fetches them.
The side panel editor
Frappe v16's side panel (frappe/public/js/frappe/ui/side_panel.js, loaded as side_panel.bundle.js) only previews documents read-only and has no hook for a view of its own. party_records.js therefore wraps the prototype of frappe.ui.SidePanel once the bundle has loaded, the way avunu_erp gives the Email Queue a view there:
render_docdraws an editor only for the records in the script'seditorsmap, which holds the records opened from a party's cards with the form they belong to. Every other preview falls through to Frappe's ownrender_doc.set_headertitles a new record New Address or New Contact.closeclears theeditorsmap.
The editor is a frappe.ui.FieldGroup built from the doctype's meta. It skips layout-only and display-only fieldtypes, the Geolocation map and the links table; it leaves out fields whose permission level the user cannot read and makes read-only those they cannot write. Table fields get their child doctype's fields and the record's rows. On a new Address, country is made optional.
bind_cards runs after ERPNext's refresh has drawn the cards. It does nothing on a new party or when frappe.ui.split_view_enabled() is false (the same rule as Frappe's: on unless the user's report_split_view is off, never on mobile). For each of Address and Contact that the user can write, it binds delegated click and Enter handlers on the cards (ignoring clicks on links, buttons and the card menu), and, when the user can create the doctype, replaces the add buttons' handler with open_new_record.
open_new_record builds a new document with frappe.model.get_new_doc and puts it straight into the panel's previews, so the panel shows it without fetching it.
Saving a record
Save (or Ctrl+S in the panel) sends the whole edited record to party_utils.save_party_record (POST only), with the party's doctype and name. The method:
Accepts only an Address or Contact, of a Customer or Supplier, and checks that the user can read the party.
Drops the desk's bookkeeping keys (
__islocal,__unsavedand the like) and the placeholder names of new child rows.For a new record (no name, or
__islocal), links it to the party, marks it primary when the party has no primary record of its kind, and for an Address setsflags.default_countryto the party'scountry(Suppliers have one) or the System Settings country. For an existing record, refuses it unless a Dynamic Link already ties it to the party, and remembers the Contact's current phone numbers.Turns every Check value into 0 or 1, because a table outside a form sends an unticked default as
"0".On a Contact, writes each new number of
phone_nosin E.164 withphonenumbers, read in the country of the party's primary address or of System Settings.Inserts or saves the record with the user's own permissions. An existing record carries the
modifiedit was opened with, so Frappe refuses to overwrite a newer version, asfrappe.client.savedoes.Returns the saved record (with field-level read permissions applied), its readable permission levels, the party's address and contact cards (
get_address_display_list,get_contact_display_list) and the party's map point.
Back in the browser, save_record puts the saved record into the panel under its real name and redraws it, then after_save replaces the party's __onload card lists and map point in place, without marking the form changed. When a new record is the party's first of its kind, it sets the party's primary field and saves the party if it had no other unsaved changes; otherwise it redraws the cards with frappe.contacts.render_address_and_contact.
The default primary record
On onload, select_default_primary checks each of the party's primary address and contact fields. When one is empty it calls get_customer_records or get_supplier_records, which return the Addresses or Contacts linked to the party (oldest first) through frappe.get_list, after checking that the user can read the party, and sets the first one. The form is left changed.
The Customer map method
customer.get_coords returns a GeoJSON FeatureCollection of the customers the user may read that have coordinates, with a popup linking the Customer and its primary contact. customer_list.js names it as the list's get_coords_method, which on Frappe v16 is one of the conditions that make the Map view available (list_view_select.js); the latitude, longitude and Geolocation location custom fields meet the others, so the Address and Supplier lists offer it too. The view plots the list's own location values and does not call the method. The view itself does not open on v16; see Limitations.
The patch
patches/remove_inline_party_fields.py, in the post_model_sync section of patches.txt, deletes the custom fields earlier versions added to Customer and Supplier (copies of the primary address's and contact's fields, named customer_primary_address_*, customer_primary_contact_* and the Supplier equivalents) and the property setters that showed ERPNext's hidden Primary Address and Contact section. Customization sync only adds and updates, so the records are deleted by this patch. The columns stay in the tables.