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.

Email

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_personcustomer → 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

  • WhatsApp Template (this app's doctype) holds template_name, campaign_name (must match an approved campaign in Combirds — the WhatsApp provider), and a template_parameters child 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 = WhatsApp sets whatsapp_template (link to the template above) and whatsapp_phone_field (a fieldname or Jinja expression that resolves the recipient's number).
  • send_whatsapp() resolves the phone number, then calls send_whatsapp_via_template() (api/whatsapp_api.py), which resolves every template parameter against the document and calls send_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 a variables child table. Each variable row has a var_type: Document Field, Jinja Expression, Print Format Link, or S3 Attachment (latest file attached to the document), plus an optional fallback_value.
  • A Notification with channel = SMS sets custom_sms_template to the DLT template_id — not a Link field, it's looked up at send-time via frappe.db.get_value("SMS Template", {"template_id": ..., "active": 1}, "name").
  • send_sms() resolves the message via SMSTemplate.get_resolved_message(doc), resolves recipients via sms_phone_field or the Notification's recipients rows, then calls send_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 groups doc.locations by sales_order (handling a consolidated multi-SO pick list defensively) and, per SO, sends the delivery-scheduled WhatsApp (direct API call, template delivery_schedule_conf_v3, sent against the Pick List doc itself — against_sales_order/locations patched 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 own delivery_date (the date picked when the plan was created), looked up via doc.delivery_plan. Before the Email trigger, _apply_pick_list_batch() patches so.delivery_date/so.items in-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 — via get_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 on Delivery 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 by sales_order and sends one WhatsApp (direct API call, template out_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 to Dispatch Plan.on_submit — but Dispatch Plan isn'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 WhatsApp SMS Email
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_events in hooks.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 Plan hook 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 Template out_for_delivery_v2 and Notifications SMS 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.

Discard
Save
This page has been updated since your last edit. Your draft may contain outdated content. Load Latest Version

On this page

Review Changes ← Back to Content
Message Status Space Raised By Last update on