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 maps Medusa customers to ERPNext Lead and Customer records. New registrations create a Lead; the Lead is converted to a Customer when a purchase order is approved. This page covers how the lifecycle works, how records are linked via medusa_id, and how to manage addresses and manually link existing customers.

Registration flow: Lead creation

When a new customer signs up on the Medusa storefront, the sign_up() endpoint calls Medusa’s own signup API, then enqueues insert_lead() as a background job to create the ERPNext record:
frappe.enqueue(
    "medusa_integration.api.insert_lead",
    data={
        "id": return_data.get("customer_id"),
        "first_name": first_name,
        "last_name": last_name,
        "email": email,
        "mobile": mobile,
        "organization_name": organization_name,
        "t_c_acceptance": t_c_acceptance,
    },
)
insert_lead() creates an ERPNext Lead with the Medusa customer ID stored in medusa_id:
def insert_lead(data):
    lead = frappe.get_doc({
        "doctype": "Lead",
        "medusa_id": data.get("id"),
        "first_name": data.get("first_name"),
        "last_name": data.get("last_name"),
        "email_id": data.get("email"),
        "mobile_no": data.get("mobile"),
        "source": "Alfarsi Website",
        "status": "Lead",
        "company_name": data.get("organization_name"),
        "t_c_acceptance": data.get("t_c_acceptance"),
    })
    lead.insert(ignore_permissions=True, ignore_mandatory=True)

The medusa_id field

Both the Lead and Customer doctypes have a custom medusa_id field. This is the primary key used to look up ERPNext records from Medusa data. Lookups throughout the integration follow this pattern:
# Look up customer by Medusa ID
customer = frappe.db.get_value("Customer", {"medusa_id": medusa_id})

# Look up lead by Medusa ID
lead = frappe.get_value("Lead", {"medusa_id": medusa_id}, "name")
In create_quotation(), the integration first checks for a Customer record with the given medusa_id. If none is found, it falls back to searching Lead records:
customer_details = frappe.get_value(
    "Customer", {"medusa_id": medusa_id},
    ["name", "customer_name"], as_dict=True
)
if customer_details:
    party_name = customer_details.name
    quotation_to = "Customer"
else:
    lead = frappe.get_value("Lead", {"medusa_id": medusa_id}, "name")
    party_name = lead
    quotation_to = "Lead"

Lead to Customer conversion

When an order is approved, the integration can automatically promote a Lead to a Customer. This happens inside update_quotation() when create_so=true and the Quotation is currently linked to a Lead:
if quote.quotation_to == "Lead" and create_so == True:
    customer = get_mapped_doc("Lead", quote.party_name, {
        "Lead": {"doctype": "Customer"}
    })
    customer.customer_name = company_name
    customer.append("sales_team", {
        "sales_person": "Website Sales",
        "contribution": 100
    })
    customer.customer_group = "Ecommerce Customer"
    customer.insert(ignore_permissions=True, ignore_mandatory=True)

    quote.quotation_to = "Customer"
    quote.party_name = customer.name
    quote.save()
All ecommerce-originated customers are assigned to the “Ecommerce Customer” customer group and attributed to the “Website Sales” sales person at 100% contribution.
The converted Customer inherits the Lead’s medusa_id indirectly: the Lead’s fields are mapped across via get_mapped_doc. If the medusa_id field is not included in the mapping, you should link it manually using link_medusa_lead() (described below).

Manually linking a Customer to a Medusa Lead

For cases where an ERPNext Customer already exists and needs to be associated with a Medusa account, the link_medusa_lead() utility function is available:
@frappe.whitelist()
def link_medusa_lead(customer, lead):
    customer_doc = frappe.get_doc("Customer", customer)
    lead_doc = frappe.get_doc("Lead", lead)

    if not lead_doc.medusa_id:
        frappe.throw("Selected Lead does not have a Medusa ID")

    medusa_id = lead_doc.medusa_id

    # Prevent duplicate links
    exists = frappe.db.exists(
        "Customer", {"medusa_id": medusa_id, "name": ("!=", customer)}
    )
    if exists:
        frappe.throw(f"This Medusa Lead is already linked to another Customer: {exists}")

    customer_doc.medusa_id = medusa_id
    customer_doc.lead_name = lead
    customer_doc.save(ignore_permissions=True)
This function is whitelisted and can be called from a custom button in the Customer form (the integration ships public/js/customer.js which adds this button).
link_medusa_lead() will throw an error if the Medusa ID is already linked to a different Customer, preventing accidental duplicate links.

Address management

Billing addresses are created or updated during quotation creation. When create_quotation() receives a billing_address payload, it:
  1. Resolves the country code to an ERPNext Country name.
  2. If is_default=true, checks whether the customer already has a primary billing address. If one exists, it is updated in place; otherwise a new Address is created and marked as primary.
  3. If is_default=false, a new Address is always created.
  4. Links the Address to the Customer via a Dynamic Link.
  5. Sets quote.customer_address to the address name.
The update_address() endpoint allows subsequent address updates:
{
  "customer_id": "CUST-0001",
  "address_line1": "45 New Street",
  "city": "Muscat",
  "country": "Oman",
  "pincode": "112"
}
Fields accepted: address_line1, address_line2, city, state, country, pincode. The endpoint resolves the customer_id to its linked Address via Dynamic Link.

Fetching unlinked customers

The fetch_all_customers() endpoint returns all ERPNext Customers that do not yet have a medusa_id. This is used to find existing customers who need to be linked to Medusa accounts.
@frappe.whitelist(allow_guest=True)
def fetch_all_customers(name=None):
    base_query = """
        SELECT name, customer_name, email_id, mobile_no
        FROM `tabCustomer`
        WHERE medusa_id IS NULL
    """
    # Optional name filter
    if name:
        name_parts = name.split()
        conditions = " AND ".join([
            f"customer_name LIKE '%{part}%'" for part in name_parts
        ])
        base_query += f" AND ({conditions})"

    return frappe.db.sql(base_query, as_dict=True)
Pass name as a query parameter to filter results by customer name (supports multi-word searches).

Customer group and sales team

All customers created through the ecommerce flow are assigned:
FieldValue
customer_group"Ecommerce Customer"
sales_team[0].sales_person"Website Sales"
sales_team[0].contribution100
This allows ecommerce orders to be segregated from walk-in or manually created customers in reports.

Build docs developers (and LLMs) love