Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/aerele/medusa_integration/llms.txt

Use this file to discover all available pages before exploring further.

The ERPNext Medusa Integration extends standard ERPNext behaviour through Frappe’s doc_events system, custom JavaScript injections, and Python class overrides. This page documents every hook registered in hooks.py, the conditions under which each fires, and what it does. Understanding these hooks is essential when debugging data-flow issues or extending the integration with your own logic.

Document event hooks

The table below summarises all registered hooks. Detailed descriptions follow.
DoctypeEventHandler
Item Pricevalidatecreate_medusa_price_list
Website Itemvalidatewebsite_item_validate
Website Itemon_trashdelete_medusa_item
Quotationon_updateexport_quotation_on_update
Sales Orderbefore_insertvalidate_medusa_order_id
Sales Orderon_submitexport_sales_order_on_update
Sales Orderon_updateexport_sales_order_on_update
Sales Orderon_update_after_submitexport_sales_order_on_update
Sales Invoicebefore_insertset_ecommerce_details_from_sales_order
Sales Invoiceon_submitexport_sales_invoice_on_update
Delivery Notebefore_insertset_ecommerce_details_from_sales_order
Delivery Noteon_submitexport_delivery_note_on_update
Payment Entryafter_inserthandle_payment_entry
Payment Entryon_updatehandle_payment_entry
Payment Entryon_submithandle_payment_entry

Item Price

Fires every time an Item Price record is saved. The handler checks two conditions before pushing anything to Medusa:
  • The price_list field must equal "Standard Selling".
  • The customer field must be empty (standard price, not a customer-specific negotiated price).
If both conditions are met and the Item Price has no medusa_id yet, a new Medusa price list is created via a POST /admin/price-lists request and the returned IDs are written back to medusa_id and medusa_price_id on the record. If medusa_id is already set, the existing price list is updated in place.
Customer-specific Item Price records (where customer is set) are deliberately skipped. They are used internally for negotiated pricing and are served to the storefront through the quotation workflow rather than via Medusa price lists.

Website Item

Fires on every save of a Website Item. The handler branches on whether a medusa_id is already present:
  • No medusa_id: calls export_website_item(), which creates a new product in Medusa (POST /admin/products), creates a default product option, creates a default variant, and writes the returned medusa_id and medusa_variant_id back to the Website Item. It also creates an initial price list entry.
  • medusa_id present: calls update_website_item(), which sends a POST /admin/products/{medusa_id} request to update title, description, collection, status, specifications, and other metadata.
A custom_skip_update_hook flag on Website Item can be set to 1 to skip the update path for a single save cycle (used internally when updating reviews and wishlists to prevent re-triggering the sync).
Fires before a Website Item is deleted from ERPNext. Sends a delete request to Medusa to remove the corresponding product, ensuring the storefront does not display items that no longer exist in ERPNext.

Quotation

This hook fires on every update to a Quotation, but performs work only when both of the following conditions are true:
  • doc.workflow_state == "Ready for Customer Review"
  • doc.from_ecommerce == 1
When both conditions are met, the handler calls export_quotation(), which posts the full quotation — including line items, pricing, taxes, and totals — to the Medusa storefront endpoint POST /store/quotation-update. It then sends a notification email to the customer’s email address informing them their quote is ready for review.
If the customer or lead has no email_id set, the export to Medusa still succeeds but an error is logged to ERPNext Error Log under the title “Unable to send mail to website user”.

Sales Order

Runs before a new Sales Order is inserted. If the order has from_ecommerce = 1 but no medusa_order_id, the handler looks up the source Quotation (via items[0].prevdoc_docname) and copies the medusa_order_id from it. This ensures the Sales Order always carries the Medusa order reference even when it is created programmatically from a quotation.
All three events call the same handler, which checks doc.from_ecommerce == 1 before proceeding. When the condition is met, it calls export_sales_order(), which:
  1. Resolves the customer’s medusa_id.
  2. Queries any linked Sales Invoice to determine the current payment_status.
  3. Posts the order status and payment status to POST /store/order-update?order_id={medusa_order_id}.
Subscribing to all three events ensures Medusa is notified regardless of whether the status changes on save, on submit, or on a post-submit amendment.

Sales Invoice

Copies ecommerce-related custom fields from the source Sales Order to the new Sales Invoice before it is inserted. This ensures downstream handlers have the medusa_order_id and from_ecommerce flag available without needing to traverse document links.
When a Sales Invoice is submitted, the handler finds the linked Sales Order and calls export_sales_order() so that Medusa’s order record reflects the updated payment status. Medusa is not sent the invoice itself; instead, the Sales Order’s payment status (derived from the invoice’s status field) is what gets synchronised.

Delivery Note

Same behaviour as the Sales Invoice hook of the same name: copies medusa_order_id and from_ecommerce from the originating Sales Order into the new Delivery Note record.
On Delivery Note submission, the handler resolves the linked Sales Order via Delivery Note Item.against_sales_order and calls export_sales_order(), pushing the updated order status (which may now reflect “Completed” or a shipped state) to Medusa.

Payment Entry

All three events trigger the same handler. It queries Payment Entry Reference for any linked Sales Invoices. For each invoice that has a medusa_order_id, it calls export_sales_invoice_on_update(), which in turn calls export_sales_order(). The chain ensures that as soon as a payment is recorded — whether on creation, update, or submission — the corresponding Medusa order reflects the new payment status.

Custom JavaScript (doctype_js)

The integration injects custom JavaScript into two standard ERPNext forms:
DoctypeScript fileWhat it adds
Customerpublic/js/customer.jsAdditional toolbar button(s) for Medusa-specific actions, such as linking an existing Medusa Lead to the Customer record
Website Itempublic/js/website_item.jsAdditional toolbar button(s) for manual sync operations directly from the Website Item form

Doctype class overrides (override_doctype_class)

The integration replaces three standard ERPNext controller classes with custom subclasses:
DoctypeCustom classModule
Sales OrderCustomSalesOrdermedusa_integration.custom_sales_order
Sales InvoiceCustomSalesInvoicemedusa_integration.custom_sales_invoice
Delivery NoteCustomDeliveryNotemedusa_integration.custom_delivery_note
All three custom classes override the validate_selling_price() method. The original ERPNext implementation raises a validation error when the net rate of any item falls below its last purchase rate or valuation rate. The custom implementation adds one check at the very start of the method:
def validate_selling_price(self):
    if self.from_ecommerce:
        return
    # ... original validation logic ...
When from_ecommerce is truthy, validation is skipped entirely. This is necessary because ecommerce orders often carry negotiated or promotional rates that are legitimately lower than the standard valuation rate, and blocking submission would break the order fulfilment workflow.
This bypass applies only when from_ecommerce = 1. Regular sales orders, invoices, and delivery notes created inside ERPNext still go through the full selling-price validation.

Build docs developers (and LLMs) love