Notifications System
Furnishka Notifications System
This app sends WhatsApp, SMS, and Email messages to customers as their order moves through the Furnishka pipeline — placed, payment reminders, delivery scheduled, out for delivery, delivered, invoiced, returned, rescheduled. It works by extending Frappe's built-in Notification doctype so it can also speak WhatsApp and SMS (Email is native to Frappe already), and adding a handful of dispatchers for documents that represent more than one customer at a time.
How a Notification actually gets sent
Every send starts from a Notification record — the same doctype ERPNext
already uses for Email alerts, just with two more channel options added.
hooks.py overrides the doctype class:
override_doctype_class = {
"Notification": "furnishka_notifications.overrides.notification.CustomNotification"
}
CustomNotification.send(doc) looks at self.channel and routes to
send_whatsapp, send_sms, or send_email — anything else falls back to
Frappe's own behaviour.
A Notification's event field decides how it fires:
- Submit / Cancel / New / Days Before / Days After / Value Change — Frappe
core fires these automatically at the right point in the document
lifecycle. Nothing in this app has to do anything extra.
- Method — Frappe does not auto-fire these. They only send when
something in code calls trigger_notification(name, doc)
(overrides/dispatchers.py), which looks up the Notification, checks
enabled and condition, and calls .send(doc) directly. This is used
for anything that can't be expressed as a single doctype event — payment
reminder stages, and the per-Sales-Order sends inside a multi-customer
document.
Nothing exotic here — send_email() adds one thing on top of core Frappe:
a recipient-resolution fallback chain, because several trigger documents
(Payment Entry, for one) don't have a contact_email field of their own.
It tries, in order: email_recipient_field (a fieldname or Jinja
expression set on the Notification) → contact_person → customer →
Customer's Dynamic Link → Contact. Once an address is resolved, it hands
off to Frappe's own Notification.send() → frappe.sendmail(), which is
what actually queues the message — this is also what creates the standard
Communication and Email Queue records Frappe already gives you for
free.
- WhatsApp Template (this app's doctype) holds
template_name,campaign_name(must match an approved campaign in Combirds — the WhatsApp provider), and atemplate_parameterschild table. Each row resolves one{{1}},{{2}}, ... placeholder in the actual approved template, via one of: Document Field, Jinja Expression, or Print Format Link (renders a print format to PDF and returns a link). - A Notification with
channel = WhatsAppsetswhatsapp_template(link to the template above) andwhatsapp_phone_field(a fieldname or Jinja expression that resolves the recipient's number). send_whatsapp()resolves the phone number, then callssend_whatsapp_via_template()(api/whatsapp_api.py), which resolves every template parameter against the document and callssend_whatsapp_message(), which POSTs to Combirds using the credentials in WhatsApp Settings.- A few flows (the dispatchers, below) call
send_whatsapp_message()directly with hand-built params instead of going through a Notification record at all — used where the content can't be expressed as simple per-document fields (e.g. the delivery driver's name/phone for "Out for Delivery").
SMS
- SMS Template (this app's doctype) holds
template_id(the DLT-approved ID your SMS gateway requires),message_template(using either{#var#}sequential placeholders or named{variable_name}placeholders), and avariableschild table. Each variable row has avar_type: Document Field, Jinja Expression, Print Format Link, or S3 Attachment (latest file attached to the document), plus an optionalfallback_value. - A Notification with
channel = SMSsetscustom_sms_templateto the DLTtemplate_id— not a Link field, it's looked up at send-time viafrappe.db.get_value("SMS Template", {"template_id": ..., "active": 1}, "name"). send_sms()resolves the message viaSMSTemplate.get_resolved_message(doc), resolves recipients viasms_phone_fieldor the Notification'srecipientsrows, then callssend_sms()(api/sms_api.py), which GETs the gateway configured in Custom SMS Settings, once per recipient.
Documents with multiple customers — the dispatchers
Some triggers don't map to one customer per document. Pick List and
Sales Invoice (as used here) can carry several Sales Orders — and
therefore several customers — in one submit. A plain Notification only
sends once per document, so overrides/dispatchers.py iterates the
relevant rows and fires one WhatsApp/SMS/Email per Sales Order via
trigger_notification(). Wired in hooks.py:
doc_events = {
"Pick List": {"on_submit": "...dispatchers.send_delivery_schedule_confirmation"},
"Sales Invoice": {"on_submit": "...dispatchers.send_out_for_delivery"},
"Payment Entry": {"before_save/before_submit": "...utils.set_party_mobile"},
}
send_delivery_schedule_confirmation(Pick List submit) — a Pick List is generated per Sales Order off a Delivery Plan (against_sales_order), so the dispatcher groupsdoc.locationsbysales_order(handling a consolidated multi-SO pick list defensively) and, per SO, sends the delivery-scheduled WhatsApp (direct API call, templatedelivery_schedule_conf_v3, sent against the Pick List doc itself —against_sales_order/locationspatched in-memory to that SO's rows first, so "Order Number" resolves to the Sales Order and the item list is just this SO's picked items), SMS (SMS Out for Delivery), and Email (Email Delivery Schedule Confirmation) — the latter two against the Sales Order. The delivery date shown is the Pick List's linked Delivery Plan's owndelivery_date(the date picked when the plan was created), looked up viadoc.delivery_plan. Before the Email trigger,_apply_pick_list_batch()patchesso.delivery_date/so.itemsin-memory to that same date and this batch's items, so the Email (which resolves against the Sales Order, not the Pick List) can't show a different date/item list than the WhatsApp message the customer just got. "Order Link" points at the Sales Order's own print-out (Sales Order 1.1, same format the old v2 template used) rather than the Pick List's — viaget_sales_order_print_link(), a small helper that fetches the Sales Order doc explicitly, since the template otherwise resolves everything against the Pick List. This used to fire onDelivery Routing.on_submit(the driver-assignment step); moved to Pick List submit so the confirmation reflects the actual picked batch.send_out_for_delivery(Sales Invoice submit) — groups the invoice's items bysales_orderand sends one WhatsApp (direct API call, templateout_for_delivery_v2), SMS (SMS Out for Delivery), and Email (Email Out for Delivery) per order, using only the items on that invoice. Driver name/phone come from that Sales Order's Delivery Routing Order row (Maponomy-assigned), via_resolve_delivery_driver()/utils.get_delivery_executive(). Previously this was wired toDispatch Plan.on_submit— butDispatch Planisn't actually a submittable doctype, so that hook never fired. Moving it to Sales Invoice submit makes it a real, working trigger.
Message Log — one audit trail for all three channels
Furnishka Message Log (this app's doctype) records every outbound
send attempt — WhatsApp, SMS, and Email alike — whether it went through a
Notification record or a direct dispatcher call. Each row has: channel,
status (Success/Failed), recipient, reference_doctype/
reference_name, notification (which Notification fired it, if any),
template, message/payload, and response. Written via
utils.log_message(), called from send_whatsapp_message(), send_sms(),
and CustomNotification.send_email(). Email is logged here too, even
though Frappe's Communication/Email Queue already track it separately —
this gives one unified place to look instead of three different views.
Before this, WhatsApp and SMS had no database record of sends at all, only
text log files.
The order lifecycle, notification by notification
| Stage | Trigger | SMS | ||
|---|---|---|---|---|
| Order Placed | SO submit | Whatsapp Order Placed (order_place_v2) |
SMS Order Placed | Email Order Placed |
| Order Cancelled | SO cancel | Whatsapp Order Cancelled (cancelled_order_v2) |
SMS Order Cancelled | Email Order Cancelled |
| Payment reminder M4 (D-4+, unpaid) | daily 9am scheduler | Whatsapp Payment Collection M4 | — | Email Payment Collection M4 |
| Payment reminder M3 (D-4..D-2, unpaid) | scheduler | Whatsapp Payment Collection M3 | — | Email Payment Collection M3 |
| Payment reschedule M2 (still unpaid, delivery pushed) | scheduler | Whatsapp Payment Reschedule M2 | — | Email Payment Reschedule M2 |
| Payment cleared M1 | scheduler | Whatsapp Payment Delivery Confirmed M1 | — | Email Payment Delivery Confirmed M1 |
| Delivery scheduled | Pick List submit | direct API call, delivery_schedule_conf_v3 |
SMS Out for Delivery | Email Delivery Schedule Confirmation |
| Out for delivery | Sales Invoice submit | direct API call, out_for_delivery_v2 |
SMS Out for Delivery | Email Out for Delivery |
| Delivered | Delivery Note submit, not a return | Whatsapp Delivered (delivered_v2) |
SMS Delivered, SMS Delivered with Warranty | Email Delivered |
| Delivery rescheduled | Delivery Note submit, return + reason=Reschedule | Whatsapp Order Reschedule (order_reschedule_v2) |
— | Email Order Reschedule |
| Return initiated | Delivery Note submit, return, not reschedule | Whatsapp Order Return (order_return_v2) |
SMS Return Initiated | Email Order Return |
| Invoice generated | Sales Invoice submit | Whatsapp Invoice (invoice_v2) |
SMS Invoice | Email Invoice (PDF attached) |
| Warranty card | Sales Invoice submit | Whatsapp Warranty Card (warranty_card_v2) |
— | Email Warranty Card (PDF attached) |
| Payment received | Payment Entry submit | Whatsapp Payment Received (payment_received) |
SMS Payment Received | Email Payment Received |
All of the above are Notification records with module = "Furnishka
Notifications" — created and kept in sync by the patch files under
furnishka_notifications/patches/ (create_whatsapp_notifications.py,
create_sms_email_notifications.py, v1_3_1_create_payment_reminder_notifications.py,
v1_5_0_fix_delivery_schedule_v3_template_params.py).
The two rows marked "direct API call" bypass the Notification doctype
entirely (see the dispatchers section above).
Notifications that exist but aren't managed by this app
Some Notification records in the system were created by hand through the
UI rather than by a patch (module is blank or something else). A few
directly duplicate the ones above and would double-send if both got
enabled at once — worth checking before turning anything on:
Sale Order Created Message, Cancelled Order, Payment Received SMS,
Whatsapp Payment Received (name collision with the code-managed one),
Invoice Prepaid, Delivered/Out for Delivery (on Sales Invoice submit,
predating the current Delivery Note-based flow), Order Delay, and a set
of older Delivery Note return/refund SMS notifications (Refund Rejected,
Refund Initiated, Return Initiated, Refund Accepted, Return
Received, Return Pickup Done). There are also six orphaned
Whatsapp/Email Payment Collection D2/D3/D4 records left over from an
earlier version of the payment reminder flow — no code calls them anymore,
the scheduler only fires the M4/M3/M2/M1 stages now.
Operational status — check before you enable anything
- Every Notification record is disabled by default until turned on per-environment. Nothing sends on its own.
- The payment reminder scheduler (
scheduler_eventsinhooks.py, daily 9am cron →payment_scheduler.run_payment_reminder_scheduler) needs to be uncommented to run at all. - Because "Out for Delivery" previously never fired (the old
Dispatch Planhook was dead — that doctype isn't submittable), it's now wired to a document event that does reliably fire (Sales Invoice submit). Before enabling it in any environment, check the enabled state of WhatsApp Templateout_for_delivery_v2and NotificationsSMS Out for Delivery/Email Out for Delivery— if any are already on, real messages start going out the moment this ships, which wasn't happening before.
This page is generated from USER.md in the furnishka_notifications
app on every bench migrate — edit USER.md, not this page directly,
or the next migrate will overwrite your changes.