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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ A modern, open-source CRM platform built with Django REST Framework and SvelteKi
![Svelte](https://img.shields.io/badge/svelte-5-orange.svg)
![Coverage](./coverage-badge.svg)

<https://github.com/MicroPyramid/Django-CRM/raw/master/docs/media/demo.webm>

## Overview

BottleCRM is a full-featured Customer Relationship Management system designed for startups and small businesses. It combines a powerful Django REST API backend with a modern SvelteKit frontend, featuring multi-tenant architecture with PostgreSQL Row-Level Security (RLS) for enterprise-grade data isolation.
Expand Down
60 changes: 49 additions & 11 deletions backend/common/management/commands/seed_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Usage:
python manage.py seed_data --email admin@example.com
python manage.py seed_data --email admin@example.com --orgs 2 --leads 100 --seed 42
python manage.py seed_data --email admin@example.com --currency EUR --country DE
python manage.py seed_data --email admin@example.com --clear --no-input
"""

Expand Down Expand Up @@ -82,6 +83,24 @@ class Command(BaseCommand):
"Inbound",
]

# Country code to Faker locale mapping
COUNTRY_FAKER_LOCALES = {
"US": "en_US",
"GB": "en_GB",
"CA": "en_CA",
"AU": "en_AU",
"DE": "de_DE",
"FR": "fr_FR",
"IN": "en_IN",
"JP": "ja_JP",
"CN": "zh_CN",
"BR": "pt_BR",
"MX": "es_MX",
"SG": "en_SG",
"AE": "ar_AE",
"CH": "de_CH",
}

# Team name templates
TEAM_NAMES = [
("Sales Team", "Primary sales team handling all inbound leads"),
Expand Down Expand Up @@ -182,6 +201,22 @@ def add_arguments(self, parser):
help="Tags per organization (default: 5)",
)

# Locale options
valid_currencies = [c[0] for c in CURRENCY_CODES]
parser.add_argument(
"--currency",
type=str,
default="USD",
choices=valid_currencies,
help=f"Default currency for organizations (default: USD). Choices: {', '.join(valid_currencies)}",
)
parser.add_argument(
"--country",
type=str,
default="US",
help="Default country code for organizations (default: US). Examples: US, GB, CA, AU, DE, FR, IN",
)
Comment on lines +213 to +218
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--country accepts any string, but Org.default_country is a CharField(max_length=2, choices=COUNTRIES). Passing a value longer than 2 characters (or not in the allowed choices) will raise a DB error during Org.objects.create(...) or create inconsistent seed data. Consider constraining the argument (choices and/or length validation) and normalizing to uppercase at parse-time.

Copilot uses AI. Check for mistakes.

# Invoice-related arguments
parser.add_argument(
"--products",
Expand Down Expand Up @@ -238,19 +273,25 @@ def add_arguments(self, parser):
)

def handle(self, *args, **options):
# Initialize Faker
# Initialize Faker with locale matching the country
country = options["country"].upper()
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

country is normalized to uppercase for locale selection, but the normalized value isn’t persisted back into options. Later seed_all() passes options["country"] into create_org(), so org/default_country (and downstream Contact/Account/Lead countries) can end up lowercased or otherwise different from what’s logged. Consider normalizing once (e.g., overwrite options["country"] with the uppercased value) and consistently pass/store the normalized country code.

Suggested change
country = options["country"].upper()
country = options["country"].upper()
options["country"] = country

Copilot uses AI. Check for mistakes.
locale = self.COUNTRY_FAKER_LOCALES.get(country, "en_US")

seed = options.get("seed")
if seed:
random.seed(seed)
self.fake = Faker(["en_US"])
self.fake = Faker([locale])
Faker.seed(seed)
else:
self.fake = Faker(["en_US", "en_GB", "en_CA", "en_AU"])
self.fake = Faker([locale])
Comment on lines 280 to +286
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if seed: treats 0 as “no seed”, so --seed 0 won’t actually seed random/Faker and will produce non-deterministic results. Use an explicit None check (e.g., if seed is not None:) so all integer seeds work.

Copilot uses AI. Check for mistakes.

# Initialize InvoiceSeeder
self.invoice_seeder = InvoiceSeeder(self.fake, self.stdout)

self.stdout.write(self.style.MIGRATE_HEADING("Seeding CRM database..."))
self.stdout.write(
f"Currency: {options['currency']}, Country: {country}, Locale: {locale}"
)
if seed:
self.stdout.write(f"Using seed: {seed}")

Expand Down Expand Up @@ -334,7 +375,7 @@ def seed_all(self, options):
"""Main seeding orchestration."""
for i in range(options["orgs"]):
self.stdout.write(f"\n--- Organization {i + 1}/{options['orgs']} ---")
org = self.create_org()
org = self.create_org(options["currency"], options["country"])
# Set RLS context for this org before creating org-scoped data
self.set_rls_context(org.id)
profiles = self.create_profiles(
Expand Down Expand Up @@ -404,15 +445,12 @@ def seed_all(self, options):
options["tasks"],
)

def create_org(self):
def create_org(self, currency, country):
"""Create an organization."""
currencies = [c[0] for c in CURRENCY_CODES]
countries = ["US", "GB", "CA", "AU", "DE", "FR", "IN"]

org = Org.objects.create(
name=self.fake.company(),
default_currency=random.choice(currencies),
default_country=random.choice(countries),
default_currency=currency,
default_country=country,
)
self.stats["orgs"] += 1
self.stdout.write(
Expand Down Expand Up @@ -524,7 +562,7 @@ def create_contacts(self, org, profiles, teams, tags, count):
city=self.fake.city(),
state=self.fake.state_abbr(),
postcode=self.fake.postcode(),
country=random.choice(["US", "GB", "CA", "AU"]),
country=org.default_country or "US",
description=self.fake.paragraph() if random.random() > 0.5 else None,
org=org,
)
Expand Down
18 changes: 9 additions & 9 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,23 @@
"@eslint/js": "^10.0.1",
"@internationalized/date": "^3.11.0",
"@lucide/svelte": "^0.575.0",
"@sveltejs/adapter-node": "^5.5.3",
"@sveltejs/kit": "^2.53.0",
"@sveltejs/adapter-node": "^5.5.4",
"@sveltejs/kit": "^2.53.4",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.2.1",
"@types/node": "^25.3.0",
"@types/node": "^25.3.3",
"bits-ui": "^2.16.2",
"eslint": "^10.0.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.15.0",
"globals": "^17.3.0",
"globals": "^17.4.0",
"mode-watcher": "^1.1.0",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.5.0",
"prettier-plugin-tailwindcss": "^0.7.2",
"svelte": "^5.53.3",
"svelte-check": "^4.4.3",
"svelte": "^5.53.6",
"svelte-check": "^4.4.4",
"svelte-sonner": "^1.0.7",
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3",
Expand All @@ -49,11 +49,11 @@
}
},
"dependencies": {
"@sentry/sveltekit": "^10.39.0",
"axios": "^1.13.5",
"@sentry/sveltekit": "^10.40.0",
"axios": "^1.13.6",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"libphonenumber-js": "^1.12.37",
"libphonenumber-js": "^1.12.38",
"svelte-dnd-action": "^0.9.69",
"tailwind-merge": "^3.5.0",
"tailwind-variants": "^3.2.2",
Expand Down
Loading
Loading