Skip to content

Events

The ERPNext Incentive Compensation Engine uses Frappe document events to connect ERPNext Sales Invoice activity with commission processing.

The event layer is intentionally small. It acts as the integration boundary between ERPNext documents and the commission engine rather than containing the commission calculation logic itself.

Event Flow

The primary event flow is:

ERPNext Sales Invoice
        │
        ├── Submit
        │     ↓
        │  Commission Event
        │     ↓
        │  Create Commission Ledger Entries
        │
        └── Cancel
              ↓
           Commission Event
              ↓
           Create Reversal Entries

The application currently uses Sales Invoice submission and cancellation as the source events for automatic commission processing.

Event Module

The event handlers are located in:

incentive_compensation/
└── incentive_compensation/
    └── commission_engine/
        └── commission_events.py

The module contains the event handlers that connect Frappe document lifecycle events to the commission engine.

The main handlers are:

on_sales_invoice_submit()
on_sales_invoice_cancel()

Sales Invoice Submission

When a Sales Invoice is submitted, the application starts commission processing.

The submission handler delegates to the ledger layer:

Sales Invoice Submitted
        ↓
on_sales_invoice_submit()
        ↓
create_ledger_entries_for_invoice()
        ↓
Commission Calculation
        ↓
Commission Ledger

The event handler itself does not determine the commission rate or perform the calculation.

That responsibility remains in the commission engine.

This keeps the event layer thin and makes the calculation logic reusable and easier to test.

Why Submission Triggers Commission Calculation

A submitted Sales Invoice represents a finalized sales transaction in ERPNext.

The commission engine uses this point in the document lifecycle to calculate and record the commission associated with the transaction.

The resulting Commission Ledger entry represents the commission calculated from that source transaction.

Conceptually:

Draft Sales Invoice
       ↓
Submitted Sales Invoice
       ↓
Commission Calculation
       ↓
Commission Ledger

The commission is therefore tied to the submitted transaction rather than an arbitrary document save.

Sales Invoice Cancellation

Sales Invoice cancellation is handled separately from submission.

When a Sales Invoice that has generated commissions is cancelled, the application creates corresponding reversal ledger entries.

The flow is:

Sales Invoice Cancelled
        ↓
on_sales_invoice_cancel()
        ↓
reverse_commissions_for_invoice()
        ↓
Reversal Ledger Entries

The original Commission Ledger entries are not modified or deleted.

Instead, the cancellation is represented as a separate reversal event.

Why Reversals Are Separate

The Commission Ledger is designed as a historical audit record.

Changing the original commission entry when an invoice is cancelled would destroy information about what was originally calculated.

Instead, the system preserves the original entry and records the cancellation separately:

Original Commission
       ₹500
         │
         │ Invoice cancelled
         ▼
Reversal
      -₹500

The reversal entry references the original commission through reversal_of.

This creates an auditable sequence of events.

Event-to-Engine Separation

The event layer and calculation engine have different responsibilities.

Event Layer

Determines:

  • when commission processing should happen
  • which source document caused the processing

Commission Engine

Determines:

  • which plan applies
  • which rule matches
  • who receives the commission
  • what the calculation base is
  • which calculation method applies
  • how much commission should be recorded

Conceptually:

Frappe Event
     │
     ▼
Commission Engine
     │
     ├── Resolve
     ├── Evaluate
     ├── Calculate
     └── Record

This separation prevents ERPNext-specific event handling from becoming tightly coupled to commission calculation logic.

Submit Event

The submit handler is intentionally simple:

def on_sales_invoice_submit(doc, method=None):
    create_ledger_entries_for_invoice(doc)

The doc argument is the submitted Sales Invoice.

The optional method argument is compatible with Frappe document event handler conventions.

The handler delegates the actual work to:

create_ledger_entries_for_invoice()

That function belongs to the ledger layer and is responsible for starting commission entry creation for the invoice.

Cancel Event

The cancellation handler follows the same pattern:

def on_sales_invoice_cancel(doc, method=None):
    reverse_commissions_for_invoice(doc)

The event handler delegates reversal processing to:

reverse_commissions_for_invoice()

The reversal logic therefore remains outside the event module.

Event Hooks

The event handlers need to be registered against the corresponding ERPNext document events.

Conceptually, the application connects:

Sales Invoice
    │
    ├── on_submit  → on_sales_invoice_submit
    │
    └── on_cancel  → on_sales_invoice_cancel

Frappe's document event system is what allows the custom application to react to ERPNext document lifecycle events without modifying ERPNext core code.

The application therefore extends ERPNext behavior through its own hooks.

Why Use Frappe Hooks

Using Frappe hooks provides several architectural advantages:

  • ERPNext core code remains unchanged.
  • Commission processing is isolated in the custom application.
  • The integration follows the Frappe application model.
  • The event handlers can be maintained independently from ERPNext.
  • The commission engine can evolve without modifying the Sales Invoice implementation.

The integration point is therefore:

ERPNext Core
     │
     │ Frappe Event
     ▼
Custom Application

Idempotency at the Event Boundary

Document events should not be treated as a guarantee that processing will only ever be attempted once.

The commission engine therefore protects ledger creation with a source key.

The important distinction is:

Event Handler
    ↓
May be invoked more than once
    ↓
Ledger layer checks source identity
    ↓
Duplicate commission is prevented

The event layer triggers processing, while the ledger layer ensures that the same calculation is not recorded repeatedly.

This makes idempotency a persistence concern rather than an assumption made by the event handler.

Source Key

Commission Ledger creation uses a source_key to identify a commission calculation.

Conceptually:

Source Transaction
       +
Calculation Context
       ↓
Source Key
       ↓
Commission Ledger

Before creating a new ledger entry, the ledger layer can determine whether the corresponding source calculation has already been recorded.

If it already exists, another copy should not be created.

Event Processing and Sales Team

A Sales Invoice may contain multiple Sales Team members.

The submit event passes the Sales Invoice into the commission processing pipeline.

The transaction builder then handles the Sales Team allocation.

For example:

Sales Invoice
    │
    ├── Item ₹20,000
    │
    └── Sales Team
          ├── Person A — 70%
          └── Person B — 30%

The event handler does not calculate these allocations itself.

Instead:

Sales Invoice Event
       ↓
Transaction Builder
       ↓
Allocated Commission Inputs
       ↓
Commission Engine
       ↓
Ledger Entries

This keeps transaction preparation separate from event handling.

Event Processing and Commission Plans

The Sales Invoice event does not directly select a Commission Plan.

Plan resolution occurs inside the commission engine.

The flow is:

Sales Invoice Submitted
        ↓
Event Handler
        ↓
Commission Processing
        ↓
Applicable Company / Date
        ↓
Commission Plan Resolution

This allows plan-selection rules to remain centralized in resolver.py.

Event Processing and Commission Rules

The same principle applies to Commission Rules.

The event handler does not know whether the rule is based on:

  • Item
  • Item Group
  • Sales Person
  • Customer
  • Customer Group
  • Territory

It simply starts commission processing.

The resolver and evaluator determine which rule applies.

Event Processing and Payees

Payee resolution also belongs to the commission engine.

The event layer does not need to know whether the commission recipient is an Employee or Sales Partner.

Instead:

Sales Invoice
     ↓
Sales Team / transaction context
     ↓
Payee Resolution
     ↓
Commission Payee

The resolved payee is then stored on the Commission Ledger entry.

Error Handling

Because the submit event starts commission processing, errors raised by the commission engine can affect the document operation that triggered the event.

This is important when developing or debugging the integration.

When investigating a failed Sales Invoice submission, check:

1. Sales Invoice data
2. Sales Team allocation
3. Applicable Commission Plan
4. Matching Commission Rule
5. Commission Payee
6. Calculation
7. Ledger creation

The event handler is usually only the entry point; the actual error may originate deeper in the commission engine.

Debugging Event Processing

When a submitted Sales Invoice does not produce a commission, inspect the pipeline in order.

1. Confirm the Invoice Was Submitted

The commission submit event is tied to Sales Invoice submission.

2. Check Sales Team

Confirm that the invoice has the expected Sales Person and contribution percentage.

3. Check the Plan

Verify that an active Commission Plan applies to the Company and transaction date.

4. Check the Rule

Verify that an enabled Commission Rule matches the transaction.

5. Check the Payee

Verify that the Sales Person can be resolved to the configured Commission Payee.

6. Check the Calculation

Verify the base amount, calculation method, rate, fixed amount, or tier configuration.

7. Check the Ledger

If calculation succeeds, verify that the Commission Ledger entry was created.

This follows the same order as the application's processing architecture.

Cancellation Debugging

If cancelling a Sales Invoice does not produce a reversal, inspect:

1. Was the original invoice commission recorded?
2. Does the original ledger entry reference the invoice?
3. Was the invoice actually cancelled?
4. Was reversal processing triggered?
5. Was a reversal already created?

The original ledger entry should remain unchanged.

The expected result is a separate reversal entry.

Event and Historical Integrity

Events are responsible for creating historical records at meaningful points in the ERPNext document lifecycle.

The submit event creates the commission record.

The cancellation event creates the corresponding reversal.

This produces a historical sequence such as:

Sales Invoice Submitted
        ↓
Commission Calculated
        ↓
Commission Ledger Created
        ↓
Sales Invoice Cancelled
        ↓
Commission Reversed

The history is therefore represented by separate records rather than destructive updates.

Testing Event Handlers

Event behavior should be tested at more than one level.

Unit-Level Tests

Test the underlying calculation and ledger functions independently.

Examples include:

  • commission calculation
  • rule matching
  • ledger creation
  • duplicate prevention
  • reversal creation

Integration Tests

Test that the Frappe document event actually invokes the intended commission processing.

For example:

Create Sales Invoice
       ↓
Submit
       ↓
Commission Ledger exists

And for cancellation:

Submit Sales Invoice
       ↓
Commission Ledger exists
       ↓
Cancel Invoice
       ↓
Reversal Ledger exists

End-to-End Tests

An end-to-end test should verify the full lifecycle from source transaction through statement and payout processing.

The event portion of the flow is:

Sales Invoice
       ↓
Submit
       ↓
Commission Ledger
       ↓
Statement
       ↓
Payout

and cancellation should verify:

Sales Invoice
       ↓
Submit
       ↓
Original Commission
       ↓
Cancel
       ↓
Reversal

Design Principles

The event architecture follows several principles.

Keep handlers thin

Event handlers should delegate work rather than implement business logic.

Keep ERPNext integration at the boundary

Sales Invoice-specific event handling belongs at the integration boundary, while commission logic belongs in the commission engine.

Do not modify ERPNext core

The application extends ERPNext through Frappe hooks rather than changing ERPNext source code.

Make processing idempotent

The event layer should be safe even when the same processing operation is attempted more than once.

Preserve history

Cancellation creates reversal records rather than rewriting the original commission calculation.

Centralize business rules

Plan, rule, payee, and calculation logic should remain in the resolver/evaluator/calculator layers instead of being duplicated in event handlers.

Development Entry Points

When working on event-driven commission processing, these are the most useful files to inspect:

File Responsibility
commission_engine/commission_events.py Sales Invoice event handlers
commission_engine/ledger.py Commission creation and reversal
commission_engine/transaction_builder.py Sales Invoice transaction preparation
commission_engine/resolver.py Plan, rule, tier, and payee resolution
commission_engine/evaluator.py Commission evaluation
commission_engine/calculator.py Mathematical calculation
hooks.py Application event registration

Next Step

Continue with Testing to understand the application's test structure, test layers, end-to-end testing, and how to run the test suite.