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 exports Item Price records from ERPNext to Medusa price lists, keeping the storefront’s displayed prices in sync with the ERP. This page covers how prices are created and updated in Medusa, how amounts are converted, how customer-specific negotiated prices work, and how the price visibility threshold controls what customers see.

How price sync is triggered

A validate hook on the Item Price doctype calls create_medusa_price_list() every time an Item Price record is saved:
doc_events = {
    "Item Price": {
        "validate": "medusa_integration.api.create_medusa_price_list"
    },
}
Only standard (non-customer-specific) prices in the “Standard Selling” price list are synced automatically. Customer-specific prices are skipped at the hook level:
if self.price_list != "Standard Selling" or self.customer:
    print(f"Skipping {self.name} as it does not belong to common Standard Selling price list")
    return

Price amount conversion

Medusa stores prices in the smallest currency unit (e.g., fils for OMR, paise for INR). ERPNext stores prices as decimal values. The conversion multiplies by 1000:
item_price = int(item_price * 1000)
For example, an ERPNext price of 12.500 OMR becomes 12500 in the Medusa price list payload.
This multiplier is hardcoded as 1000. If your deployment uses a currency where the smallest unit conversion factor differs (e.g., USD/cents = 100), you will need to adjust this value in create_medusa_price_list().

Creating a price list in Medusa

When an Item Price record without a medusa_id is saved, create_medusa_price_list() calls POST /admin/price-lists and stores the returned IDs:
payload = {
    "name": web_item_name,
    "description": self.price_list,   # e.g. "Standard Selling"
    "type": "override",
    "customer_groups": [],
    "status": "active",
    "starts_at": starts_at,           # from valid_from
    "ends_at": ends_at,               # from valid_upto
    "prices": [
        {
            "amount": item_price,         # ERPNext rate × 1000
            "variant_id": medusa_variant_id,
            "currency_code": "omr",       # lowercased from self.currency
        }
    ],
}
After a successful response, two fields are written back to the Item Price record:
  • medusa_id — the ID of the Medusa price list object.
  • medusa_price_id — the ID of the individual price entry within the list.

Updating an existing price

When a price record already has a medusa_id (and get_doc_before_save() is available, indicating an actual edit), the function calls POST /admin/price-lists/{medusa_id} with only the price entry, using medusa_price_id to target the specific record:
payload = {
    "prices": [
        {
            "id": self.medusa_price_id,
            "amount": item_price,
            "variant_id": medusa_variant_id,
            "currency_code": self.currency.lower(),
        }
    ]
}

Bulk sync: sync_missing_prices_to_medusa()

This function finds all Website Items that have a Medusa product (medusa_id set) and a variant (medusa_variant_id set), but whose Standard Selling Item Price record does not yet have a medusa_price_id. It creates a price list in Medusa for each such item. It runs as part of the nightly cron at 0 1 * * *:
scheduler_events = {
    "cron": {
        "0 1 * * *": [
            "medusa_integration.api.sync_missing_prices_to_medusa"
        ]
    }
}
The function works in three steps:
1

Fetch synced Website Items

Retrieves all Website Items that have both medusa_id and medusa_variant_id set.
2

Find unsynced Item Prices

Queries Item Prices where price_list = 'Standard Selling', customer is empty, and medusa_price_id is not set. Builds a map of item_code → price_data.
3

Create missing price lists

For each Website Item without a synced price, calls POST /admin/price-lists and writes the medusa_price_id back to the Item Price record. Items with no price data default to 1000 (i.e., 1.000 OMR).

Storefront price endpoints

get_medusa_prices()

Returns standard and negotiated prices for a list of products, intended for the Medusa storefront cart or product detail pages. Parameters:
ParameterTypeDescription
itemslistArray of {medusa_product_id} or {medusa_variant_id} objects
price_liststringERPNext price list name (default: "Standard Selling")
customer_idstringMedusa customer ID; used to look up negotiated prices
draft_order_idstringOptional; if provided, also returns payment_url for the linked order
The function resolves each Medusa ID to an ERPNext item_code, fetches both the standard price and any customer-specific negotiated price, then applies the price_visibility_threshold before returning:
display_price = standard_price if standard_price < price_visibility_threshold else 0

result[medusa_product_id] = {
    "item_code": item_code,
    "standard_price": display_price or 0,
    "negotiated_price": negotiated_price or 0
}

fetch_standard_price()

Internal utility used across multiple endpoints. Returns both the standard list price and any customer-specific negotiated price for a set of items:
@frappe.whitelist()
def fetch_standard_price(items, price_list, party, quotation_to):
    items = json.loads(items)
    customer = None
    if quotation_to == 'Customer':
        customer = frappe.db.get_value("Customer", party)
    if quotation_to == 'Lead':
        customer = frappe.db.get_value("Customer", {"lead_name": party})

    result = {}
    for item in items:
        result[item['item_code']] = frappe.db.get_value(
            "Item Price",
            {"price_list": price_list, "item_code": item['item_code'], "selling": 1},
            "price_list_rate"
        )
        result[item['item_code'] + "-negotiated"] = frappe.db.get_value(
            "Item Price",
            {"price_list": price_list, "item_code": item['item_code'],
             "customer": customer, "selling": 1},
            "price_list_rate"
        )
    return result
The {item_code}-negotiated key in the result holds the customer-specific price. This allows callers to display negotiated prices to logged-in customers while showing the standard price to guests.

Price visibility threshold

The price_visibility_threshold is a field on the Homepage Landing singleton document (fetched as "Active Homepage Landing"). Any item whose standard price meets or exceeds this threshold has its displayed price set to 0, effectively hiding the price from the storefront.
price_visibility_threshold = frappe.db.get_value(
    "Homepage Landing",
    "Active Homepage Landing",
    "price_visibility_threshold"
) or 50

display_price = standard_price if standard_price < price_visibility_threshold else 0
This mechanism is used consistently in get_medusa_prices(), get_website_items(), get_website_image(), and the homepage item fetch functions.
Set price_visibility_threshold to a high value (e.g., 999999) to show all prices, or to 0 to hide all prices. The default fallback value when the field is empty is 50.

Standard vs. negotiated prices

Price typeERPNext sourceMedusa usage
Standard priceItem Price with price_list = "Standard Selling" and no customerShown to all storefront users; hidden if above threshold
Negotiated priceItem Price with price_list = "Standard Selling" and customer setShown only to the specific logged-in customer
Negotiated prices are not exported to Medusa as separate price lists. They are fetched at query time from ERPNext using the customer’s ERP name (resolved via medusa_id) and returned alongside the standard price in every price endpoint response.

Build docs developers (and LLMs) love