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 keeps your product catalog in sync by pushing Website Item data from ERPNext to Medusa automatically. This page explains the full sync lifecycle: which fields are mapped, how product variants and collections are created, how to trigger a bulk export, and how to skip updates when needed.

How sync is triggered

Every time a Website Item is saved, the website_item_validate hook fires. It checks whether the item already has a medusa_id:
  • If medusa_id is empty → calls export_website_item() to create a new Medusa product.
  • If medusa_id is set → calls update_website_item() to push changes to the existing Medusa product.
This hook is registered in hooks.py:
doc_events = {
    "Website Item": {
        "validate": "medusa_integration.api.website_item_validate",
        "on_trash": "medusa_integration.api.delete_medusa_item",
    },
}

Field mapping

The following ERPNext fields are mapped to Medusa product fields when calling POST /admin/products:
ERPNext fieldMedusa fieldNotes
web_item_nametitleDisplay name on storefront
item_codeitem_codeCustom field on Medusa product
web_long_descriptiondescriptionHTML stripped via strip_html() + html.unescape()
short_descriptionshort_descriptionPlain text
rankingrankingNumeric sort order
publishedstatusTrue"published", False"draft"
brandbrand_nameBrand name string
country_of_origin (from Item)origin_countryResolved to ISO country code (uppercase)
website_specificationsspecificationsArray of {label, description} objects; HTML stripped
item_group → collectioncollection_idItem Group is exported as a Medusa collection first
stock_uommetadata.UOMStored in product metadata

Example payload

{
  "title": "Surgical Gloves - Size M",
  "item_code": "SG-M-001",
  "discountable": false,
  "is_giftcard": false,
  "collection_id": "pcol_01HXYZ123ABC",
  "short_description": "Sterile surgical gloves, size medium",
  "description": "High-quality sterile surgical gloves suitable for all procedures.",
  "ranking": 10,
  "status": "published",
  "brand_name": "MedGlove",
  "origin_country": "DE",
  "metadata": { "UOM": "Box" },
  "specifications": [
    { "label": "Material", "description": "Latex-free nitrile" },
    { "label": "Sterility", "description": "Sterile, individually packed" }
  ]
}

Product variants

Each Website Item gets exactly one Medusa variant — a “Default” variant. Variants are created immediately after the product is created, via create_medusa_variant(). The variant is configured with:
  • manage_inventory: true — Medusa tracks stock levels for this variant.
  • allow_backorder — set to true if the Website Item has on_backorder checked, false otherwise.
  • inventory_quantity: 0 — initial quantity; stock is synced separately by the scheduled task.
def create_medusa_variant(product_id, backorder=False, country_code=None):
    option_id = create_medusa_option(product_id)
    payload = {
        "title": "Default",
        "inventory_quantity": 0,
        "manage_inventory": True,
        "allow_backorder": True if backorder else False,
        "options": [{"option_id": option_id, "value": "Default"}],
    }
    # POST /admin/products/{product_id}/variants
The returned variant ID is stored back on the Website Item as medusa_variant_id. This field is used throughout the integration (order creation, inventory sync, price lists) to reference the correct Medusa variant.

Item Groups as Medusa collections

Before a product can be exported, its Item Group must exist in Medusa as a collection. The export_item_group() function handles this:
def export_item_group(self):
    if get_url()[1] and not self.medusa_id:
        payload = {
            "title": self.name,
            "metadata": {
                "parent_item_group": self.parent_item_group,
                "is_group": self.is_group,
            },
        }
        # POST /admin/collections
        self.db_set("medusa_id", response.get("collection").get("id"))
export_website_item() calls export_item_group() automatically if item_group.medusa_id is empty. The collection ID is then included in the product payload as collection_id.

Bulk export

There are two ways to export all Website Items that do not yet have a medusa_id.
export_all_website_item() iterates over all published Website Items without a medusa_id and exports them one by one. Errors are caught per item and logged.
def export_all_website_item():
    records = frappe.get_all(
        "Website Item",
        filters={"published": 1, "medusa_id": ["is", "not set"]},
        pluck="name"
    )
    for name in records:
        doc = frappe.get_doc("Website Item", name)
        export_website_item(doc, method="")

Skipping updates

Some operations (such as adding a review or updating a wishlist) save the Website Item internally without intending to push changes to Medusa. To prevent a spurious update, these operations set the custom_skip_update_hook flag to 1 before saving:
frappe.db.set_value("Website Item", website_item.name, "custom_skip_update_hook", 1)
website_item.save(ignore_permissions=True)
update_website_item() checks this flag at the start of every call:
def update_website_item(self, method):
    if self.custom_skip_update_hook:
        frappe.db.set_value("Website Item", self.name, "custom_skip_update_hook", 0)
        return
When the flag is set, the function resets it and returns immediately — no request is sent to Medusa.
Always reset custom_skip_update_hook back to 0 after a programmatic save, or the next genuine update to the item will also be skipped. The integration handles this automatically inside update_website_item(), but external code that sets the flag must ensure the save path runs through the hook.

Deleting products

When a Website Item is trashed, the on_trash hook calls delete_medusa_item(), which sends a DELETE /admin/products/{medusa_id} request to remove the product from Medusa.

Scheduled tasks

ScheduleFunctionPurpose
0 1 * * * (daily at 1 AM)export_items_and_images_custom()Bulk export any unsynced published items in 4 parallel batches
To re-export a specific item manually, clear its medusa_id field and save the record. The website_item_validate hook will treat it as a new item and create a fresh Medusa product.

Build docs developers (and LLMs) love