Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions SETUP_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,18 @@ If you want to quickly test Commera with demo products, you have two options:
bench --site your-site-name execute commera.install_summer_demo.install_summer_demo
```

This seeds the Summer storefront in **SAR**. Pass `--kwargs '{"currency": "INR"}'` for a rupee store;
the supported currencies are the keys of `CURRENCY_PROFILES` in `commera/install_summer_demo.py`.
This seeds the Summer storefront in the **company's own currency**, so finish the setup wizard first:
without a company, a default currency and a chart of accounts the seeder stops and says so.
SAR, INR and IDR get hand-picked local prices (`CURRENCY_PROFILES` in `commera/install_summer_demo.py`);
any other currency is converted from the dollar catalogue at the USD exchange rate, which needs a
Currency Exchange record (or ERPNext's live rate lookup) to exist.
The seeder is idempotent — run it as often as you like.

Currency is a parameter, not a constant: one run puts the system default, the company, every account
under it, all three price lists and every Item Price into the same currency, and restates the demo
catalogue, the shipping rule, the COD charge and the hero-slide copy at that currency's scale. A store
whose company and price list disagree cannot submit a single Sales Invoice, which is what makes this
the setting to get right before anything else.
The company, its accounts and its currency are never changed. What the seeder does align is the system
default, all three price lists and every Item Price, and it restates the demo catalogue, the shipping
rule, the COD charge and the hero-slide copy at that currency's scale. A store whose company and price
list disagree cannot submit a single Sales Invoice, which is what makes this the setting to get right
before anything else.

#### Rebuilding a demo site from nothing

Expand All @@ -82,7 +85,7 @@ bench new-site $SITE --admin-password admin
bench --site $SITE install-app erpnext commera
bench --site $SITE execute frappe.desk.page.setup_wizard.setup_wizard.setup_complete --kwargs '{"args": {"language": "en", "country": "Saudi Arabia", "timezone": "Asia/Riyadh", "currency": "SAR", "company_name": "Lifestyle Demo", "company_abbr": "LSD", "chart_of_accounts": "Standard", "fy_start_date": "2026-01-01", "fy_end_date": "2026-12-31", "full_name": "Administrator", "email": "admin@example.com", "password": "admin"}}'
bench --site $SITE migrate
bench --site $SITE execute commera.install_summer_demo.install_summer_demo --kwargs '{"currency": "SAR"}'
bench --site $SITE execute commera.install_summer_demo.install_summer_demo
```

Two things about that order are load-bearing:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,27 +234,19 @@ def sync_item_group_mapping_to_ecommerce_items(self):

@frappe.whitelist()
def install_demo_data(self):
"""Seed the Summer demo storefront in the store's own currency.
"""Seed the Summer demo storefront in the company's own currency.
Not install_demo_data: its price lists are hardcoded USD, which blocks every Sales Invoice on a non-USD company.
"""
from commera.install_summer_demo import (
CURRENCY_PROFILES,
DEFAULT_CURRENCY,
get_company_currency,
install_summer_demo,
)
from commera.install_summer_demo import get_currency_profile, get_demo_company, install_summer_demo

# A store already trading in a supported currency keeps it; anything else gets the default.
currency = get_company_currency()
if currency not in CURRENCY_PROFILES:
currency = DEFAULT_CURRENCY
# Refused here rather than in the job, so the merchant sees why nothing was seeded.
get_currency_profile(frappe.db.get_value("Company", get_demo_company(), "default_currency"))

frappe.enqueue(
install_summer_demo,
queue="long",
timeout=3000,
enqueue_after_commit=True,
currency=currency,
)

return "Demo data installation has been queued. This may take a few minutes. Check the background jobs for progress."
Expand Down
135 changes: 93 additions & 42 deletions commera/install_summer_demo.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""One-shot seeder for a Summer storefront demo site, in whatever currency it is handed.
"""One-shot seeder for a Summer storefront demo site, in the currency of the company it seeds.
create_price_lists is skipped: it hardcodes USD, which blocks Sales Invoices on a non-USD company."""

import math

import frappe
from erpnext.setup.utils import get_exchange_rate
from frappe.permissions import add_permission, update_permission_property
from frappe.utils.data import flt

Expand All @@ -25,14 +28,10 @@
COD_CHARGE_ACCOUNT = "Cash on Delivery Charges"
SHOPPER_ROLE = "Customer"

DEFAULT_CURRENCY = "SAR"

# FASHION_PRODUCTS carries dollar-scale numbers, so price_multiplier restates them in the site's own
# currency. Rates are recomputed from it on every run - scaling in place would compound on a re-run.
# FASHION_PRODUCTS prices are in dollars; unlisted currencies fall back to DOLLAR_PROFILE.
# Rates are recomputed on every run - scaling in place would compound on a re-run.
CURRENCY_PROFILES = {
"SAR": {
"country": "Saudi Arabia",
"symbol": "ر.س",
"price_multiplier": 3.75,
"price_rounding": 10,
"free_shipping_above": 299,
Expand All @@ -42,8 +41,6 @@
"contact_phone": "+966 55 123 4567",
},
"INR": {
"country": "India",
"symbol": "₹",
"price_multiplier": 50,
"price_rounding": 100,
"free_shipping_above": 999,
Expand All @@ -52,6 +49,22 @@
"cod_charge_applicable_below": 999,
"contact_phone": "+91 98765 43210",
},
"IDR": {
"price_multiplier": 16000,
"price_rounding": 1000,
"free_shipping_above": 300000,
"flat_shipping_charge": 15000,
"cod_charge": 5000,
"cod_charge_applicable_below": 300000,
"contact_phone": "+62 812 3456 7890",
},
}

DOLLAR_PROFILE = {
"free_shipping_above": 80,
"flat_shipping_charge": 7,
"cod_charge": 4,
"cod_charge_applicable_below": 80,
}

DEFAULT_HOMEPAGE_IMAGES = "/assets/commera/images/homepage/demo"
Expand Down Expand Up @@ -106,11 +119,13 @@
)


def install_summer_demo(currency=DEFAULT_CURRENCY):
"""Seed a demo storefront end to end in `currency`. Safe to re-run."""
def install_summer_demo():
"""Seed a demo storefront end to end in the company's own currency. Safe to re-run."""
company = get_demo_company()
currency, country = frappe.db.get_value("Company", company, ["default_currency", "country"])
profile = get_currency_profile(currency)

configure_site_defaults(currency, profile)
configure_site_defaults(currency, country)
align_store_currency(currency)
save_sale_price_list(currency)
create_item_attributes()
Expand Down Expand Up @@ -143,33 +158,62 @@ def install_summer_demo(currency=DEFAULT_CURRENCY):


def get_currency_profile(currency):
profile = CURRENCY_PROFILES.get(currency)
if not profile:
symbol = frappe.db.get_value("Currency", currency, "symbol") or currency
if currency in CURRENCY_PROFILES:
return {**CURRENCY_PROFILES[currency], "symbol": symbol}

rate = 1 if currency == "USD" else flt(get_exchange_rate("USD", currency))
if not rate:
frappe.throw(
frappe._("No demo currency profile for {0}. Known: {1}.").format(
currency, ", ".join(sorted(CURRENCY_PROFILES))
)
frappe._(
"Demo data has no price profile for {0} and no USD exchange rate to derive one. "
"Add a Currency Exchange from USD to {0}, or use one of: {1}."
).format(currency, ", ".join(sorted(CURRENCY_PROFILES)))
)

# One rounding step per order of magnitude of the rate: 3.75 rounds to tens, 16,000 to ten-thousands.
step = 10 ** max(0, round(math.log10(rate)))
profile = {"symbol": symbol, "price_multiplier": rate, "price_rounding": step}
for key, amount in DOLLAR_PROFILE.items():
profile[key] = max(step, round(amount * rate / step) * step)

return profile


def get_demo_company():
"""The company to seed, refused until the setup wizard has given it a currency and a chart."""
company = get_company()
if not company or not frappe.db.exists("Company", company):
frappe.throw(frappe._("Please set up a company first by completing the setup wizard."))

if not frappe.db.get_value("Company", company, "default_currency") or not frappe.db.exists(
"Account", {"company": company}
):
frappe.throw(
frappe._(
"Please finish setting up {0} first: it needs a default currency and a chart of accounts."
).format(company)
)

return company


def seed_storefront_analytics():
"""Give the analytics screen a store worth looking at."""
# Must run after the catalogue: the events and orders it writes reference real item codes.
install_analytics_demo_data()


def configure_site_defaults(currency, profile):
def configure_site_defaults(currency, country):
"""The site-level settings that silently break a paid checkout later."""
# A blank System Settings.language crashes money_in_words (num2words(lang=None)) on every invoice;
# a Contact whose email_id is not an address is rejected by the gateways.
frappe.db.set_single_value("System Settings", "language", "en")
frappe.db.set_single_value("System Settings", "country", profile["country"])
frappe.db.set_single_value("System Settings", "country", country)
frappe.db.set_default("currency", currency)
# A second, separate currency surface: utils.get_currency_symbol reads Global Defaults, not this default.
frappe.db.set_single_value("Global Defaults", "default_currency", currency)
frappe.db.set_single_value("Global Defaults", "country", profile["country"])
frappe.db.set_single_value("Global Defaults", "country", country)
# Frappe ships most currencies disabled, and a disabled one cannot be picked on any document.
frappe.db.set_value("Currency", currency, "enabled", 1)

Expand All @@ -181,19 +225,9 @@ def configure_site_defaults(currency, profile):


def align_store_currency(currency):
"""Put the company, its accounts and every price its catalogue carries into one currency."""
# The three-way mismatch this undoes (system INR, company SAR, price list USD) throws
# "Party Account ... currency and document currency should be same" on every Sales Invoice.
company = get_company()
frappe.db.set_value("Company", company, "default_currency", currency)

# ponytail: written straight to the table because ERPNext refuses a currency change once a
# company has transactions — safe on a seeded demo, revisit if this is ever run on a live store.
account = frappe.qb.DocType("Account")
frappe.qb.update(account).set(account.account_currency, currency).where(
(account.company == company) & (account.account_currency != currency)
).run()

"""Put every price the catalogue carries into the company's currency; the company itself is left alone."""
# A price list in another currency (USD from install_demo_data) throws "Party Account ... currency
# and document currency should be same" on every Sales Invoice.
price_list = frappe.qb.DocType("Price List")
frappe.qb.update(price_list).set(price_list.currency, currency).where(
price_list.currency != currency
Expand Down Expand Up @@ -288,7 +322,8 @@ def apply_store_copy(profile):
# Without this, set_cod_charges throws "Set a Charge Account Head in Commera Settings...".
settings.charge_account_head = ensure_cod_charge_account()

settings.contact_phone = profile["contact_phone"]
if profile.get("contact_phone"):
settings.contact_phone = profile["contact_phone"]
for fieldname, value in STORE_COPY.items():
settings.set(fieldname, value)

Expand Down Expand Up @@ -462,18 +497,34 @@ def allow_shoppers_to_select_accounts():

def get_freight_account():
company = get_company()
abbr = frappe.db.get_value("Company", company, "abbr")
account = frappe.db.get_value(
"Account", {"account_name": "Freight and Forwarding Charges", "company": company}, "name"
)
if account:
return account

# Only the standard chart ships this account; country charts don't, so make it where the
# standard chart would put it, or under the deepest expense group the chart has.
expense_group = {"company": company, "root_type": "Expense", "is_group": 1}
parent_account = frappe.db.get_value(
"Account", {**expense_group, "account_name": "Stock Expenses"}, "name"
) or frappe.db.get_value("Account", expense_group, "name", order_by="lft desc")
return (
frappe.db.get_value(
"Account", {"account_name": "Freight and Forwarding Charges", "company": company}, "name"
frappe.get_doc(
{
"doctype": "Account",
"account_name": "Freight and Forwarding Charges",
"company": company,
"parent_account": parent_account,
"account_type": "Chargeable",
"root_type": "Expense",
"is_group": 0,
}
)
or f"Freight and Forwarding Charges - {abbr}"
.insert(ignore_permissions=True)
.name
)


def get_company():
return frappe.defaults.get_user_default("Company") or frappe.db.get_value("Company", {}, "name")


def get_company_currency():
return frappe.db.get_value("Company", get_company(), "default_currency") or DEFAULT_CURRENCY
Loading
Loading