Proforma Invoice (PI) — Workflow & App Guide
App: ToolJet — Proforma Invoice Generator
Schema: saar_biotech
Related tables: tbl_order_details, tbl_client, tbl_product, tbl_client_address, tbl_proforma_drafts
Data model: see tbl_proforma_drafts in Data Dictionary
1. What This App Does
The Proforma Invoice app lets dispatch/admin staff generate a client-facing Proforma Invoice (PI) PDF for any order. It is opened by clicking an order row in the main orders table.
The app: - Pre-fills all client details, addresses, and line items from the database - Lets the user override names, addresses, GSTIN, DL numbers, transport info, and line-item details (batch, dates, boxes) - Auto-calculates GST (CGST/SGST for Himachal Pradesh clients, IGST for others) using a master HSN rates table - Shows Plate Charges as a transparent line item (displayed but excluded from the payable total) - Saves a draft so the user can come back without losing their edits - Generates and downloads a formatted PDF
2. Queries at a Glance
| Query | Type | Purpose |
|---|---|---|
fetch_pi_details |
SQL | Main data loader — fetches order, client, product, address, and existing draft |
pi_initialize_draft_items |
RunJS | Parses the SQL result, loads the saved draft (if any), or builds fresh line items |
fetch_hsn_code_rates |
SQL | Loads the master HSN code to GST rate lookup table |
pi_save_changes |
RunJS | Triggers the database save and then launches the PDF print dialog |
pi_save_changes_database |
SQL | UPSERT into tbl_proforma_drafts — saves all form fields and line items |
pi_upsert_address |
SQL | Saves a new/edited address to tbl_client_address |
split_pi_row |
RunJS | Splits one line-item row into two in the editable table |
download_pi_pdf |
RunJS | Saves draft first, then renders the HTML into a print-dialog PDF |
3. Data Load and Hydration Flow
When the user opens an order, the following sequence runs automatically:
User clicks order row
|
v
fetch_pi_details (SQL)
|
+-- Fetches from: tbl_order_details + tbl_client + tbl_product
| + tbl_client_address (aggregated as ALL_ADDRESSES JSON)
| + tbl_proforma_drafts (LEFT JOIN -- returns NULL if no draft exists)
|
v
pi_initialize_draft_items (RunJS)
|
+-- Does PI_DATA_JSON exist in the result?
| |
| +-- YES --> Load saved draft:
| | - Set variable: active_draft_data (all form field values)
| | - Set variable: draft_pi_items (saved line-item rows)
| | - STOP (draft fully restored, DB values ignored)
| |
| +-- NO --> Build fresh items:
| - Map each SQL row into a draft_pi_items object
| - Set BATCH_NO, MFG_DATE, EXP_DATE to empty strings
| - Set No_of_Boxes = 1 (default)
|
v
form_pi_settings reads active_draft_data to pre-fill all inputs
html1 renders the live PDF preview using draft_pi_items + form inputs
4. The fetch_pi_details SQL
SELECT
o."ORDER_NO_C", o."MARG_ORDER_NO_C", o."BRAND_C", o."PACKING_DIMENSIONS_C",
o."CARTON_PACKAGING_C", o."QUANTITY_I", o."RATE_I", o."MRP_I",
o."HSN_CODE_I", o."BATCH_NO_C", o."PLATE_CHARGES_B",
o."INVENTORY_CHARGES_B", o."CYLINDER_CHARGES_B", o."ENQ_PHONE_C",
c."CLIENT_NAME_C", c."GST_NO_C", c."DRUG_LICENSE_NO_C", c."CLIENT_ID_I",
p."PRODUCT_NAME_C",
(SELECT jsonb_agg(addr) FROM (
SELECT "ADDESS_ID_C" AS value,
"ADDRESS_TYPE_C" || ' - ' || "C_ADDRESS_DISPLAY" AS label,
"ADDRESS_TYPE_C" AS type,
REPLACE("C_ADDRESS_DISPLAY", CHR(10), '<br/>') AS raw_address,
"STATE_C" AS state
FROM saar_biotech.tbl_client_address
WHERE "CLIENT_ID_I" = o."CLIENT_ID_I"
) addr) AS "ALL_ADDRESSES",
pd."PI_DATA_JSON", -- NULL if no draft saved yet
pd."UPDATED_TS"
FROM saar_biotech.tbl_order_details o
LEFT JOIN saar_biotech.tbl_client c ON o."CLIENT_ID_I" = c."CLIENT_ID_I"
LEFT JOIN saar_biotech.tbl_product p ON p."PRODUCT_ID_I" = o."COMPOSITION_C"
LEFT JOIN saar_biotech.tbl_proforma_drafts pd ON pd."ORDER_NO_C" = o."ORDER_NO_C"
WHERE o."ORDER_NO_C" = '{{components.table1.selectedRow.ORDER_NO_C}}'
Key point: tbl_proforma_drafts is a LEFT JOIN. If no draft exists, PI_DATA_JSON is NULL and the initializer builds fresh items from tbl_order_details.
5. The HTML Renderer
The html1 component runs a JavaScript function that produces the live PDF preview.
5.1 Three-Tier Value Resolution
Every field resolves in priority order:
1. Live form input — what the user is currently typing in form_pi_settings
2. Saved draft — the last value stored in PI_DATA_JSON.selections
3. Database fallback — the original value from tbl_order_details or tbl_client
5.2 GST Routing
Determined from the Billing GSTIN in the form (not hardcoded from DB):
const isHimachalClient =
clientGst.startsWith('02') // State code 02 = Himachal Pradesh
|| billingState.includes('HIMACHAL')
|| billingState === 'HP';
| Client State | Tax Applied |
|---|---|
| Himachal Pradesh | CGST + SGST (rate split equally) |
| Any other state | IGST (full rate) |
Changing the Billing GSTIN in the form instantly recalculates all GST values in the preview.
5.3 Plate Charges
PLATE_CHARGES_Bis read from the database row (not from the form)- If
> 0, a PLATE CHARGES row is added to the invoice table - GST is looked up using the fixed HSN code
84425010 - Plate charges are included in
totalAssValueand flow into the Grand Total - The
QTYcell shows-— they are not counted in the physical goods quantity total
Business rule: Plate charges are shown to the client for transparency, but are considered already included in the Billing Amount for payment tracking. They are never manually entered — always read from the database.
5.4 Per-Row HSN Resolution
row.HSN_CODE_I || row.HSN_CODE || row.HSN_CODE_C || row.hsn_code || globalHsnCode
6. Form Settings Fields (form_pi_settings)
| Field | Pre-filled from | Also used for |
|---|---|---|
billing_client_name |
CLIENT_NAME_C |
Printed name on invoice |
delivery_client_name |
CLIENT_NAME_C |
Delivery section name |
billing_address |
Address dropdown | Selects which address block prints |
delivery_address |
Address dropdown | Selects delivery address block |
billing_gst |
GST_NO_C |
Drives CGST/IGST routing |
billing_dl |
DRUG_LICENSE_NO_C |
DL No. under billing section |
delivery_gst |
GST_NO_C |
Delivery section GSTIN |
delivery_dl |
DRUG_LICENSE_NO_C |
Delivery section DL No. |
enq_phone |
ENQ_PHONE_C |
Phone number on invoice |
po_no, po_date |
Empty | PO reference from client |
transport_name |
Empty | Transport company name |
transport_mode |
BY ROAD |
Dropdown |
gr_no |
Empty | GR/LR number |
hsn_code |
HSN_CODE_I |
Global fallback HSN code |
7. Save Flow (pi_save_changes_database)
The save query performs an UPSERT into tbl_proforma_drafts. One row per order — saving always overwrites the previous draft.
The order_items saved are built by merging the base variable with any live table cell edits:
variables.draft_pi_items.map((row, index) => ({
...row,
...(components.Table_DraftItems?.dataUpdates?.[index] || {})
}))
The three sections saved are:
selections— allform_pi_settingsfield values (names, addresses, GSTIN, DL, phone)dispatch_info— transport name/mode, GR No., PO No., PO Dateorder_items— complete snapshot of all line items including user edits (batch, dates, boxes, suffix)
For the full table schema, see Data Dictionary → tbl_proforma_drafts.
8. PDF Generation (download_pi_pdf)
User clicks Download PDF
|
v
await pi_save_changes_database.run() <- Save always happens first
|
v
Read html1.rawHTML <- Grab the rendered invoice HTML
|
v
Create a hidden iframe <- Invisible to user
Inject HTML + print CSS <- A4 Portrait, 8mm margins,
background colors forced on
|
v
Set document.title to <- Tricks the browser Save dialog
"PI {MargOrderNo} {ClientName}" into using the right filename
|
v
iframe.contentWindow.print() <- Opens native PDF dialog
|
v
Restore title, remove iframe <- Cleanup after 1 second
Filename format: PI SO-2024-001 ABC Pharma (spaces, no underscores, forbidden chars stripped)
9. Known Behaviours and Rules
| Behaviour | Reason |
|---|---|
| Save always overwrites the previous draft | ON CONFLICT DO UPDATE — one draft per order by design |
Plate charges QTY shows -, not added to physical total |
Plate charges are not physical goods |
| Editing Billing GSTIN to a non-HP code instantly re-routes all GST to IGST | isHimachalClient is computed live from the form, not from the DB value |
| Per-row HSN code always wins over the form selector | Product-level HSN is more specific than the order-level fallback |
| Download always saves first | Ensures the PDF always reflects the latest edits |
For the approval workflow that runs before PI generation, see Pre-Sales Order Approval.
For the tbl_proforma_drafts data model, see Data Dictionary.