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 manages the complete order lifecycle across both platforms. When a customer creates a cart in Medusa, ERPNext documents are created and updated at each stage — from initial quotation through to payment collection — with status pushed back to Medusa so the storefront stays current.

Order flow overview

1

Customer creates cart in Medusa

Medusa calls POST /api/method/medusa_integration.api.create_quotation with the customer ID, items, and billing address. ERPNext creates a Quotation linked to the customer’s Lead or Customer record.
2

ERP team reviews and prices the quotation

The ERP team sets item rates on the Quotation and moves it to “Ready for Customer Review” workflow state. This triggers export_quotation_on_update(), which pushes the priced quote back to Medusa via POST /store/quotation-update.
3

Customer approves in Medusa

Medusa calls update_quotation() or update_quotation_new() with approval: "Approved". ERPNext submits the Quotation and, if create_so=true, immediately creates and submits a Sales Order plus a Payment Request.
4

Invoice and delivery

When a Sales Invoice is submitted, export_sales_invoice_on_update() triggers a status push to Medusa. When a Delivery Note is submitted, export_delivery_note_on_update() does the same.
5

Payment received

When a Payment Entry is linked to a Sales Invoice that has a medusa_order_id, handle_payment_entry() re-triggers the order status sync so Medusa reflects the updated payment status.

Document event hooks

All order-related hooks are registered in hooks.py:
doc_events = {
    "Quotation": {
        "on_update": "medusa_integration.api.export_quotation_on_update"
    },
    "Sales Order": {
        "on_submit":              "medusa_integration.api.export_sales_order_on_update",
        "on_update":              "medusa_integration.api.export_sales_order_on_update",
        "on_update_after_submit": "medusa_integration.api.export_sales_order_on_update",
        "before_insert":          "medusa_integration.api.validate_medusa_order_id",
    },
    "Sales Invoice": {
        "on_submit":     "medusa_integration.api.export_sales_invoice_on_update",
        "before_insert": "medusa_integration.api.set_ecommerce_details_from_sales_order"
    },
    "Delivery Note": {
        "on_submit":     "medusa_integration.api.export_delivery_note_on_update",
        "before_insert": "medusa_integration.api.set_ecommerce_details_from_sales_order"
    },
    "Payment Entry": {
        "after_insert": "medusa_integration.api.handle_payment_entry",
        "on_update":    "medusa_integration.api.handle_payment_entry",
        "on_submit":    "medusa_integration.api.handle_payment_entry"
    },
}

create_quotation()

Called by Medusa when a customer cart needs an ERP-side quotation. The endpoint resolves the Medusa customer_id to either an ERPNext Customer or Lead, then creates a Quotation with those items. Request body:
{
  "customer_id": "cus_01ABC",
  "draft_order_id": "dord_01XYZ",
  "quotation_id": "quot_01DEF",
  "create_so": false,
  "items": [
    { "variant_id": "variant_01AAA", "quantity": 2 }
  ],
  "billing_address": {
    "address_1": "123 Main St",
    "city": "Muscat",
    "country_code": "om",
    "postal_code": "100",
    "is_default": true
  }
}
Key behaviours:
  • Items are resolved from medusa_variant_id on the Website Item record.
  • Item tax templates are applied automatically from the Item’s tax configuration.
  • Standard and negotiated prices are fetched via fetch_standard_price() and stored on the Quotation items.
  • If billing_address is provided, an ERPNext Address record is created or updated and linked to the customer.
  • If create_so=true and the party is a Lead, a Customer record is created from the Lead first (assigned to the “Ecommerce Customer” group), and the Quotation is re-linked to the new Customer.
Response:
{
  "message": "Quotation created successfully",
  "quotationId": "QTN-0001"
}

update_quotation()

Handles approval or rejection of a quotation. Called by Medusa after a customer acts on a priced quote.
Sets workflow_state to "Approved", submits the Quotation, and stores the Medusa order ID.If create_so=true, it also:
  1. Creates and submits a Sales Order from the Quotation.
  2. Creates a Payment Request linked to the Sales Order.
  3. Returns the payment URL so Medusa can redirect the customer.
{
  "approval": "Approved",
  "quotation_id": "quot_01DEF",
  "order_id": "order_01GHI",
  "create_so": true,
  "is_courier_required": true,
  "location_and_contact_no": "Ruwi - 99912345"
}

update_quotation_new()

A newer update path that replaces the item list on the Quotation entirely before approving. This is used when the Medusa backend sends back a modified set of approved and unapproved items with explicit rates. In addition to replacing items, it also:
  • Populates unapproved_items with items that were rejected.
  • Populates custom_increased_items with items whose quantities changed.
  • Adds a delivery charge if the billing city is not Muscat (delivery charge amount sourced from Homepage Landing).
  • Calls export_quotation() immediately after saving to push the updated quote back to Medusa.

export_quotation()

Pushes a priced Quotation’s line items, totals, taxes, and delivery details back to Medusa. Called automatically via export_quotation_on_update() when the Quotation moves to “Ready for Customer Review”. Endpoint called: POST /store/quotation-update?quot_id={medusa_quotation_id} Payload structure:
{
  "customer_id": "cus_01ABC",
  "draft_order_id": "dord_01XYZ",
  "erp_status": "Price received",
  "erp_items": [
    {
      "item": "variant_01AAA",
      "item_code": "SG-M-001",
      "price": 12.5,
      "quantity": 2,
      "uom": "Box",
      "amount": 25.0,
      "item_tax_template": "Standard Tax - AFMS"
    }
  ],
  "erp_unaccepted_items": [],
  "erp_total_quantity": 2,
  "erp_total": 25.0,
  "erp_net_total": 25.0,
  "erp_tax": [
    { "account_head": "VAT - AFMS", "tax_rate": 5.0, "tax_amount": 1.25 }
  ],
  "erp_total_taxes": 1.25,
  "erp_grand_total": 26.25,
  "erp_rounding_adjustments": 0,
  "erp_discount_on": "Grand Total",
  "erp_discount_percentage": 0,
  "erp_discount_amount": 0,
  "tax_breakup": {}
}
An email notification is also sent to the customer after the export succeeds.

export_sales_order()

Pushes Sales Order status and payment status to Medusa. Called on every submit, update, and post-submit update of an ecommerce Sales Order (where from_ecommerce=1). Endpoint called: POST /store/order-update?order_id={medusa_order_id}
payload = {
    "customer_id": customer_id,
    "order_status": "Pending" if sales_order.status == "Draft" else sales_order.status,
    "payment_status": payment_status,  # from linked Sales Invoice
    "discount_amount": sales_order.discount_amount,
    "net_total": sales_order.net_total,
    "grand_total": sales_order.grand_total,
}
payment_status is derived by looking up any submitted Sales Invoice linked to the order and using its status field.

export_sales_invoice_on_update()

Fires when a Sales Invoice is submitted (on_submit). It finds the linked Sales Order, verifies it has a medusa_order_id, and calls export_sales_order() to push the updated payment status.

export_delivery_note_on_update()

Fires when a Delivery Note is submitted. It finds the linked Sales Order from the Delivery Note items and calls export_sales_order(), updating Medusa with the new order status (which will reflect partial or full delivery).

handle_payment_entry()

Fires on after_insert, on_update, and on_submit of a Payment Entry. It finds all Sales Invoices referenced in the Payment Entry, checks whether each invoice has a medusa_order_id, and if so calls export_sales_invoice_on_update() to push the updated payment status.
def handle_payment_entry(doc, method):
    linked_invoices = frappe.db.sql("""
        SELECT DISTINCT per.reference_name
        FROM `tabPayment Entry Reference` per
        WHERE per.parent = %s AND per.reference_doctype = 'Sales Invoice'
    """, (doc.name,), as_dict=True)

    for invoice in linked_invoices:
        sales_invoice = frappe.get_doc("Sales Invoice", invoice.reference_name)
        if sales_invoice.medusa_order_id:
            export_sales_invoice_on_update(sales_invoice, method)

Selling price validation bypass

ERPNext’s standard SalesOrder.validate_selling_price() rejects orders where item rates fall below the last purchase or valuation rate. Since Medusa-sourced orders may carry negotiated or pre-approved rates, the integration overrides this with CustomSalesOrder:
class CustomSalesOrder(SalesOrder):
    def validate_selling_price(self):
        if self.from_ecommerce:
            return  # skip validation entirely for ecommerce orders
        # ... standard validation continues for non-ecommerce orders
The from_ecommerce flag is set to 1 on Quotations created via create_quotation() and propagated automatically to Sales Invoices and Delivery Notes via set_ecommerce_details_from_sales_order().
validate_medusa_order_id() runs before_insert on Sales Orders. If the Sales Order comes from an ecommerce Quotation but medusa_order_id is not yet set, it looks up the Quotation linked in the first item’s prevdoc_docname and copies the ID over automatically.

Build docs developers (and LLMs) love