From 6b8e1292bff56b58ec762818b6483a76e9bf74c2 Mon Sep 17 00:00:00 2001 From: altrix-one Date: Fri, 3 Oct 2025 10:54:34 +0000 Subject: [PATCH 01/23] Issues Identified and Fixed: Missing Required Custom Fields Added custom_displayname and custom_item_group_display_name to all Item Group creations Missing Root Item Group Created get_root_item_group() function to dynamically find or create the root Root Item Group Also Missing Custom Fields Updated root Item Group creation (lines 84-85) to include both required fields --- ls_shop/migrate.py | 48 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/ls_shop/migrate.py b/ls_shop/migrate.py index 7ee5b72..640d198 100644 --- a/ls_shop/migrate.py +++ b/ls_shop/migrate.py @@ -5,8 +5,12 @@ def after_install(): create_payment_modes() try: create_ecommerce_group() - except Exception as _: - frappe.errprint("Error creating Ecommerce groups") + except Exception as e: + import traceback + error_msg = f"Error creating Ecommerce groups: {str(e)}" + frappe.log_error(traceback.format_exc(), "Lifestyle Shop Installation - Ecommerce Groups") + frappe.errprint(error_msg) + frappe.errprint(traceback.format_exc()) def create_payment_modes(): @@ -29,15 +33,22 @@ def create_payment_modes(): def create_ecommerce_group(): parent = "Ecommerce Website" parent_categories = {"Men", "Women", "Kids"} + + # Find or create root item group + root_item_group = get_root_item_group() + # Create parent group frappe.get_doc( { "doctype": "Item Group", "item_group_name": parent, "is_group": True, - "parent_item_group": "All Item Groups", + "parent_item_group": root_item_group, + "custom_displayname": parent, + "custom_item_group_display_name": parent, } ).insert(ignore_if_duplicate=True) + for category in parent_categories: frappe.get_doc( { @@ -45,6 +56,37 @@ def create_ecommerce_group(): "item_group_name": category, "is_group": True, "parent_item_group": parent, + "custom_displayname": category, "custom_item_group_display_name": category, } ).insert(ignore_if_duplicate=True) + + +def get_root_item_group(): + """Find or create the root item group""" + # Try to find existing root item group (parent_item_group is null) + root_groups = frappe.get_all( + "Item Group", + filters={"parent_item_group": ["in", ["", None]]}, + fields=["name"], + limit=1 + ) + + if root_groups: + return root_groups[0].name + + # If no root exists, create "All Item Groups" + root_name = "All Item Groups" + if not frappe.db.exists("Item Group", root_name): + frappe.get_doc( + { + "doctype": "Item Group", + "item_group_name": root_name, + "is_group": True, + "parent_item_group": "", + "custom_displayname": root_name, + "custom_item_group_display_name": root_name, + } + ).insert(ignore_permissions=True) + + return root_name From 01f9091437aebbb9ba114dc3d68a4251c850f628 Mon Sep 17 00:00:00 2001 From: altrix-one Date: Fri, 3 Oct 2025 11:07:52 +0000 Subject: [PATCH 02/23] =?UTF-8?q?Fixed=20Landing=20Page=20IndexError=20?= =?UTF-8?q?=E2=9C=85=20Issue:=20Products=20list=20page=20crashed=20with=20?= =?UTF-8?q?IndexError:=20list=20index=20out=20of=20range=20when=20no=20bra?= =?UTF-8?q?nds=20exist.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed in list.py: Changed if not brands[0]: to if not brands: (line 27) Added filter to remove None/empty brand values: brands = [b.title() for b in brands if b] (line 29) This prevents the error when: No products have brands assigned Brand field is empty/None Fresh installation with no products yet The landing page should now load correctly even without any products or brands in the system. --- ls_shop/www/products/list.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ls_shop/www/products/list.py b/ls_shop/www/products/list.py index de579d1..a172bd5 100644 --- a/ls_shop/www/products/list.py +++ b/ls_shop/www/products/list.py @@ -24,9 +24,10 @@ def get_filter_brands(filters=None): query = query.select(item.brand).distinct().orderby(item.brand) brands = query.run(pluck=True) print(brands) - if not brands[0]: + if not brands: return [] - brands = [b.title() for b in brands] + # Filter out None or empty brand values + brands = [b.title() for b in brands if b] return brands From 121a3ee14cfec7ada81e6f4c64ce749a2a2938ee Mon Sep 17 00:00:00 2001 From: altrix-one Date: Fri, 3 Oct 2025 14:04:15 +0000 Subject: [PATCH 03/23] Demo Data and Fixes --- DEMO_DATA_README.md | 399 +++++++++ SETUP_GUIDE.md | 607 ++++++++++++++ ls_shop/debug_products.py | 82 ++ ls_shop/fix_sav_publication.py | 42 + ls_shop/install_demo_data.py | 761 ++++++++++++++++++ .../lifestyle_settings/lifestyle_settings.js | 32 + .../lifestyle_settings.json | 23 + .../lifestyle_settings/lifestyle_settings.py | 26 + ls_shop/publish_demo_items.py | 118 +++ 9 files changed, 2090 insertions(+) create mode 100644 DEMO_DATA_README.md create mode 100644 SETUP_GUIDE.md create mode 100644 ls_shop/debug_products.py create mode 100644 ls_shop/fix_sav_publication.py create mode 100644 ls_shop/install_demo_data.py create mode 100644 ls_shop/publish_demo_items.py diff --git a/DEMO_DATA_README.md b/DEMO_DATA_README.md new file mode 100644 index 0000000..f819edf --- /dev/null +++ b/DEMO_DATA_README.md @@ -0,0 +1,399 @@ +# LS Shop Demo Data Script + +This script automatically populates your LS Shop installation with demo data for testing the complete e-commerce workflow. + +## What Gets Created + +### ERPNext Prerequisites +- **Item Attributes** + - Color (Red, Blue, Black, White, Green, Navy, Gray) + - Size (XS, S, M, L, XL, XXL) + +- **Brands** + - Adidas + - Nike + - Puma + - Lifestyle Store + +- **Price Lists** + - Standard Selling (USD) + - Sale Price List (USD) + +- **Shipping Rule** + - Standard Shipping + - $10 shipping for orders under $50 + - Free shipping for orders $50 and above + +### LS Shop Configuration + +- **Lifestyle Settings** - Fully configured with: + - Default price lists + - Ecommerce warehouse + - Shipping rules + - Return period (30 days) + - COD settings ($5 fee for orders under $100) + - Email templates + - Return reasons + +### Demo Products + +The script creates **3 complete product lines** with full variant support: + +#### 1. Classic Cotton T-Shirt +- **Brand**: Lifestyle Store +- **Category**: Men +- **Colors**: Black, White, Navy, Gray (4 colors) +- **Sizes**: S, M, L, XL, XXL (5 sizes) +- **Price**: $29.99 (Regular) / $24.99 (Sale) +- **Total Variants**: 20 SKUs + +#### 2. Slim Fit Denim Jeans +- **Brand**: Lifestyle Store +- **Category**: Men +- **Colors**: Blue, Black (2 colors) +- **Sizes**: S, M, L, XL (4 sizes) +- **Price**: $79.99 (Regular) / $69.99 (Sale) +- **Total Variants**: 8 SKUs + +#### 3. Running Sneakers +- **Brand**: Adidas +- **Category**: Men +- **Colors**: Black, White, Blue (3 colors) +- **Sizes**: S, M, L, XL (4 sizes) +- **Price**: $119.99 (Regular) / $99.99 (Sale) +- **Total Variants**: 12 SKUs + +**Total Demo SKUs**: ~40 product variants + +### Additional Features + +- ✅ **Style Attribute Configurators** (SAC) - One per product template +- ✅ **Style Attribute Variants** (SAV) - One per color variant with demo images +- ✅ **Color Size Items** - Links all size variants to their colors +- ✅ **Proper Routing** - All products use `/en/products/` format +- ✅ **Demo Images** - Placeholder images with product names and colors +- ✅ **Stock Quantities** - Random stock (50-200 units per variant) +- ✅ **Dual Pricing** - Regular and sale prices + +--- + +## Installation + +### Prerequisites + +1. **LS Shop must be installed first**: + ```bash + bench --site your-site-name install-app ls_shop + ``` + +2. **ERPNext Company and Warehouse** should exist + - At least one Company created + - At least one non-group Warehouse available + +### Running the Script + +```bash +bench --site your-site-name execute ls_shop.ls_shop.install_demo_data.install_demo_data +``` + +### Expected Output + +``` +============================================================ +Installing LS Shop Demo Data +============================================================ + +Step 1: Creating Prerequisites... + - Creating Item Attributes... + ✓ Color attribute created + ✓ Size attribute created + - Creating Brands... + ✓ Brand 'Adidas' created + ✓ Brand 'Nike' created + ✓ Brand 'Puma' created + ✓ Brand 'Lifestyle Store' created + - Creating Price Lists... + ✓ Price List 'Standard Selling' created + ✓ Price List 'Sale Price List' created + - Creating Shipping Rule... + ✓ Shipping Rule 'Standard Shipping' created + +Step 2: Configuring Lifestyle Settings... + - Configuring Lifestyle Settings... + ✓ Lifestyle Settings configured + +Step 3: Creating Demo Products... + + Creating product: Classic Cotton T-Shirt + ✓ Template 'TSHIRT-CLASSIC' created + ✓ Configurator created for 'TSHIRT-CLASSIC' + ✓ Style variant 'Black' created + ✓ Style variant 'White' created + ✓ Style variant 'Navy' created + ✓ Style variant 'Gray' created + ✓ All variants created for TSHIRT-CLASSIC + + Creating product: Slim Fit Denim Jeans + ✓ Template 'JEANS-SLIM' created + ✓ Configurator created for 'JEANS-SLIM' + ✓ Style variant 'Blue' created + ✓ Style variant 'Black' created + ✓ All variants created for JEANS-SLIM + + Creating product: Running Sneakers + ✓ Template 'SNEAKER-RUN' created + ✓ Configurator created for 'SNEAKER-RUN' + ✓ Style variant 'Black' created + ✓ Style variant 'White' created + ✓ Style variant 'Blue' created + ✓ All variants created for SNEAKER-RUN + +Step 4: Fixing Product Routes... + - Updating product routes... + ✓ Updated 40 product routes + +============================================================ +✅ Demo Data Installation Complete! +============================================================ + +You can now access the shop at: + English: https://your-site.com/en/products + Arabic: https://your-site.com/ar/products + +Demo products created: + - Classic T-Shirt (Multiple colors & sizes) + - Slim Fit Jeans (Multiple colors & sizes) + - Running Sneakers (Multiple colors & sizes) +``` + +--- + +## What You Can Test + +After running the demo data script, you can test: + +### 1. Product Browsing +- Visit `/en/products` to see all products +- Filter by brand, color, size +- Search for products +- View product details + +### 2. Product Variants +- Select different colors to see images +- Choose sizes and see stock availability +- View different price points (regular vs sale) + +### 3. Shopping Cart +- Add products to cart +- Update quantities +- Remove items +- See price calculations with shipping + +### 4. Checkout Process +- Guest checkout +- User registration and login +- Address management +- Payment method selection (COD configured) + +### 5. Backend Management +- View **Style Attribute Configurators** (SAC) +- Manage **Style Attribute Variants** (SAV) +- Check **Website Items** with proper routes +- Review **Item Prices** for both price lists + +--- + +## Customizing Demo Data + +### Adding More Products + +Edit the `products` list in [`install_demo_data.py`](install_demo_data.py:56): + +```python +products = [ + { + "code": "YOUR-PRODUCT-CODE", + "name": "Your Product Name", + "item_group": "Men", # or "Women", "Kids" + "brand": "Your Brand", + "description": "Product description", + "colors": ["Red", "Blue", "Black"], + "sizes": ["S", "M", "L", "XL"], + "base_price": 49.99, + "sale_price": 39.99 + }, + # ... add more products +] +``` + +### Using Real Images + +Replace placeholder image URLs in the `create_style_variant()` function: + +```python +"images": [ + { + "image": "https://your-cdn.com/product-images/tshirt-red.jpg", + } +] +``` + +### Changing Pricing + +Modify prices in the product definitions or adjust price list percentages. + +--- + +## Cleaning Up Demo Data + +To remove demo data: + +```python +# In bench console +bench --site your-site-name console + +# Then run: +import frappe + +# Delete demo items +demo_codes = ["TSHIRT-CLASSIC", "JEANS-SLIM", "SNEAKER-RUN"] +for code in demo_codes: + # Delete variants first + variants = frappe.get_all("Item", {"variant_of": code}) + for v in variants: + frappe.delete_doc("Item", v.name, force=True) + + # Delete template + if frappe.db.exists("Item", code): + frappe.delete_doc("Item", code, force=True) + +# Delete configurators +configurators = frappe.get_all("Style Attribute Configurator") +for c in configurators: + frappe.delete_doc("Style Attribute Configurator", c.name, force=True) + +frappe.db.commit() +``` + +--- + +## Troubleshooting + +### Script Fails with "Company not found" + +Create a company first: +```bash +bench --site your-site-name console +``` +Then: +```python +company = frappe.get_doc({ + "doctype": "Company", + "company_name": "Demo Company", + "abbr": "DC", + "country": "United States", + "default_currency": "USD" +}) +company.insert(ignore_permissions=True) +frappe.db.commit() +``` + +### Script Fails with "Warehouse not found" + +Create a warehouse first or the script will use default "Stores - LSE". + +### Products Not Showing on Website + +1. Check that Item Groups are published: + - Go to **Stock → Item Group → Men/Women/Kids** + - Ensure "Display on Website" is checked + - Ensure "Show in Website" is checked + +2. Clear cache: +```bash +bench --site your-site-name clear-cache +bench --site your-site-name clear-website-cache +``` + +### Routes Still Using Old Format + +Run the route fix separately: +```bash +bench --site your-site-name console +``` +Then: +```python +from ls_shop.install_demo_data import fix_product_routes +fix_product_routes() +frappe.db.commit() +``` + +--- + +## Next Steps After Demo Data + +1. **Explore the Frontend** + - Browse products at `/en/products` + - Test filtering by brand, color, size + - Try the search functionality + +2. **Test Complete Purchase Flow** + - Add items to cart + - Proceed to checkout + - Complete a test order with COD + +3. **Customize Products** + - Edit product descriptions + - Upload real product images + - Adjust pricing + +4. **Configure Payment Gateways** + - Set up Telr for online payments + - Configure Tabby for BNPL + - Test payment flows + +5. **Add Real Products** + - Follow the main setup guide to add your actual products + - Use the demo data as a reference for structure + +--- + +## Script Details + +### Functions Overview + +- `install_demo_data()` - Main orchestrator function +- `create_item_attributes()` - Creates Color and Size attributes +- `create_brands()` - Creates demo brands +- `create_price_lists()` - Creates selling price lists +- `create_shipping_rule()` - Creates shipping rule with conditions +- `configure_lifestyle_settings()` - Configures all LS Shop settings +- `create_demo_products()` - Creates all demo products +- `create_product_with_variants()` - Creates a single product with variants +- `create_item_template()` - Creates item template (parent) +- `create_configurator()` - Creates Style Attribute Configurator +- `create_style_variant()` - Creates Style Attribute Variant for each color +- `create_item_variants()` - Creates all actual item variants (SKUs) +- `fix_product_routes()` - Updates routes to LS Shop format + +### Database Transactions + +The script uses: +- `frappe.db.commit()` - On successful completion +- `frappe.db.rollback()` - On any error +- `ignore_permissions=True` - For demo data creation + +--- + +## Support + +If you encounter issues with the demo data script: + +1. Check the Error Log in Frappe Desk +2. Review the console output for specific errors +3. Ensure all prerequisites are met +4. Try running individual functions in bench console for debugging + +--- + +**Last Updated**: 2025-10-03 \ No newline at end of file diff --git a/SETUP_GUIDE.md b/SETUP_GUIDE.md new file mode 100644 index 0000000..d6ffd13 --- /dev/null +++ b/SETUP_GUIDE.md @@ -0,0 +1,607 @@ +# LS Shop - Complete Setup Guide + +This guide will walk you through setting up LS Shop from installation to publishing your first product on the website. + +## Table of Contents +1. [Prerequisites](#prerequisites) +2. [Installation](#installation) +3. [Initial Configuration](#initial-configuration) +4. [Creating Your First Product](#creating-your-first-product) +5. [Publishing to Website](#publishing-to-website) +6. [Accessing the Frontend](#accessing-the-frontend) +7. [Advanced Features](#advanced-features) +8. [Troubleshooting](#troubleshooting) + +--- + +## Prerequisites + +Before installing LS Shop, ensure you have the following: + +### Required Apps +- **Frappe Framework** (v13 or higher) +- **ERPNext** (must be installed first) +- **Webshop** (dependency for LS Shop) +- **Payments** (for payment gateway integration) + +### System Requirements +- Node.js and npm (for asset compilation) +- Python 3.8+ +- Database: MariaDB/PostgreSQL + +### ERPNext Setup Requirements +Before installing LS Shop, ensure these are configured in ERPNext: +- ✅ Company created and configured +- ✅ Default Warehouse created +- ✅ Price Lists created (e.g., "Standard Selling") +- ✅ Chart of Accounts configured +- ✅ At least one Item Attribute (e.g., "Color", "Size") + +--- + +## Installation + +### Quick Start with Demo Data + +If you want to quickly test LS Shop with demo products, you have two options: + +#### Option 1: Using Lifestyle Settings (Recommended) + +1. Navigate to: **Desk → Lifestyle Shop Ecommerce → Lifestyle Settings** +2. Scroll to **Demo Data & Testing** section +3. Click **"Install Demo Data"** button +4. Wait for completion notification +5. Click **"Publish All Items to Website"** button if items don't show +6. Visit `https://your-site.com/en/products` + +#### Option 2: Using Command Line + +```bash +# After installing ls_shop +bench --site your-site-name execute ls_shop.install_demo_data.install_demo_data +``` + +**What Gets Created:** +- ✅ Item Attributes (Color, Size) +- ✅ Brands (Adidas, Nike, Puma, Lifestyle Store) +- ✅ Price Lists (Standard Selling, Sale Price List) +- ✅ Shipping Rule (Free shipping over $50) +- ✅ 3 Demo Products with variants (~40 SKUs) +- ✅ Style Attribute Configurators & Variants +- ✅ Configured Lifestyle Settings +- ✅ Published to website with LS Shop routes + +After installation, immediately visit: +- `https://your-site.com/en/products` to see the demo store +- Test the complete purchase workflow + +--- + +### Step 1: Install Required Apps + +If not already installed, install the dependencies: + +```bash +# Install ERPNext (if not installed) +bench get-app erpnext +bench --site your-site-name install-app erpnext + +# Install Webshop +bench get-app webshop +bench --site your-site-name install-app webshop + +# Install Payments +bench get-app payments +bench --site your-site-name install-app payments +``` + +### Step 2: Install LS Shop + +```bash +# Get the LS Shop app +bench get-app https://github.com/BuildWithHussain/ls_shop + +# Install on your site +bench --site your-site-name install-app ls_shop + +# Build assets +bench build --app ls_shop +``` + +### Step 3: Restart Services + +```bash +bench restart +``` + +### Step 4: Verify Installation + +After installation, LS Shop will automatically create: +- ✅ Ecommerce Item Groups (All Item Groups → Ecommerce Website → Men, Women, Kids) +- ✅ Email Templates (Order Confirmation, Order Cancellation, Item In Stock) +- ✅ Payment Mode: Telr + +Check that these were created successfully by navigating to: +- **Desk → Stock → Item Group** - You should see "Ecommerce Website" with child groups + +--- + +## Initial Configuration + +### Step 1: Configure Lifestyle Settings + +Navigate to: **Desk → Lifestyle Shop Ecommerce → Lifestyle Settings** + +#### A. Price List Configuration +1. **Default Price List**: Select your main selling price list (e.g., "Standard Selling") +2. **Sale Price List**: Select price list for sale/discounted items (optional) + +#### B. Warehouse Configuration +- **Ecommerce Warehouse**: Select the warehouse that will be used for e-commerce orders + - This should be a warehouse with actual stock available + - Example: "Stores - Your Company" + +#### C. Shipping & Returns +1. **Shipping Rule**: + - Navigate to **Desk → Selling → Shipping Rule** + - Create a new Shipping Rule based on "Net Total" + - Example: Free shipping above $100, else $10 flat rate + - Select this rule in Lifestyle Settings + +2. **Return Period**: Enter number of days customers can return items (e.g., 14, 30) + +3. **Reason for Return**: Add reasons customers can select: + - Wrong Size + - Wrong Color + - Defective Product + - Changed Mind + - etc. + +4. **Print Format**: Select the print format for invoices (optional) + +#### D. Cash on Delivery (COD) Settings +1. **COD Charge**: Enter the fee for COD orders (e.g., 5.00) +2. **COD Charge Applicable Below**: Enter order value below which COD charge applies +3. **Charge Account Head**: Select the income account for COD charges + +#### E. Payment Modes +Enable/disable payment methods: +- ☑️ **Telr** - For online card payments (requires Telr Settings configuration) +- ☑️ **Tabby** - For Buy Now Pay Later (requires Tabby app installation) +- ☑️ **COD** - Cash on Delivery + +#### F. Email Configuration +1. **Order Confirmation Email Template**: "Order Confirmation" +2. **Order Cancellation Email Template**: "Order Cancellation" +3. **Item In Stock Email Template**: "Item In Stock" +4. **Logo URL**: Enter URL to your company logo for emails +5. **CC Email**: Optional email to CC on all order emails + +#### G. Item Group Mapping (Optional) +If you have existing item groups that should map to e-commerce groups: +1. Click on "eCommerce Item Group Mapping" table +2. Add rows mapping: + - **Original Item Group**: Your existing ERPNext item group + - **eCommerce Item Group**: Target e-commerce group (Men, Women, Kids, etc.) +3. Click "Sync Item Group Mapping to Existing Items" to apply + +**Click Save** after configuring all settings. + +--- + +## Creating Your First Product + +There are two ways to create products for the e-commerce website: + +### Option 1: Simple Product (Single Variant) + +#### Step 1: Create an Item in ERPNext + +Navigate to: **Desk → Stock → Item → New** + +Fill in the following fields: + +**Basic Information:** +- **Item Code**: Unique code (e.g., "TSHIRT-001") +- **Item Name**: Display name (e.g., "Classic Cotton T-Shirt") +- **Item Group**: Select from e-commerce groups (Men, Women, Kids) +- **Default Unit of Measure**: pcs, nos, etc. + +**Custom Fields (LS Shop specific):** +- **DisplayName**: Short display name (e.g., "Cotton Tee") +- **Ecommerce Display Name**: Full e-commerce name (e.g., "Classic Cotton T-Shirt") + +**Inventory:** +- ☑️ Maintain Stock +- **Is Stock Item**: Yes +- **Default Warehouse**: Your ecommerce warehouse +- **Opening Stock**: Enter initial quantity (e.g., 100) +- **Valuation Rate**: Cost price + +**Sales Details:** +- ☑️ Allow Item to be Sold +- **Default Income Account**: Select appropriate income account + +**Website Settings:** +- ☑️ Show in Website (publish to webshop) +- **Route**: Auto-generated or custom (e.g., "classic-cotton-tshirt") +- **Website Image**: Upload product image (recommended: 1000x1000px) +- **Website Warehouse**: Select your e-commerce warehouse +- **Website Item Groups**: Select relevant e-commerce groups + +**Item Images:** +- Click "Add Row" in Website Images table +- Upload multiple product images +- First image becomes the main display image + +**Description:** +- **Description**: Add detailed product description (supports HTML) +- **Web Long Description**: Extended description for product page + +**Pricing:** +After saving the item, create a price: +1. Click "Add Item Price" or go to **Desk → Stock → Item Price → New** +2. **Item Code**: Select your item +3. **Price List**: Select your default selling price list +4. **Rate**: Enter selling price (e.g., 29.99) +5. Save + +#### Step 2: Verify Item is Published + +1. Save the item +2. Navigate to: **Desk → Website → Website Item** +3. You should see a Website Item created automatically +4. Check that all details are correct + +### Option 2: Product with Variants (Using SAC/SAV) + +For products with multiple colors/sizes, use the Style Attribute Configurator: + +#### Step 1: Create Item Template + +Navigate to: **Desk → Stock → Item → New** + +**Basic Settings:** +- **Item Code**: Template code (e.g., "TSHIRT-CLASSIC-TEMPLATE") +- **Item Name**: Template name (e.g., "Classic T-Shirt Template") +- ☑️ **Has Variants**: Yes +- **Item Group**: Select e-commerce group + +**Attributes Tab:** +Add attributes for variants: +1. Click "Add Row" in Attributes table +2. **Attribute**: Select "Color" +3. Add color values (Red, Blue, Green, etc.) +4. Add another row for **Attribute**: "Size" +5. Add size values (S, M, L, XL, etc.) + +**Other Settings:** +- Configure sales details, descriptions, and images as above +- Set default pricing + +Save the item template. + +#### Step 2: Create Style Attribute Configurator (SAC) + +Navigate to: **Desk → Lifestyle Shop Ecommerce → Style Attribute Configurator → New** + +1. **Item Template**: Select your template (e.g., "TSHIRT-CLASSIC-TEMPLATE") +2. **Item Attribute**: Select primary attribute (usually "Color") +3. **Recommended Items**: Add items to show as "You May Also Like" (optional) +4. Save + +#### Step 3: Create Style Attribute Variants (SAV) + +Navigate to: **Desk → Lifestyle Shop Ecommerce → Style Attribute Variant → New** + +For each color variant: + +1. **Configurator**: Select the SAC you created +2. **Attribute Value**: Select color (e.g., "Red") +3. **Variant Item Code Prefix**: Enter prefix (e.g., "TSHIRT-RED") +4. **Image**: Upload image for this color variant +5. **Description**: Optional description specific to this variant + +**Size Configuration:** +Click "Add Row" in the Color Size Items table for each size: +- **Size**: Select size (S, M, L, XL) +- **Item Code Suffix**: Optional suffix (e.g., "-S", "-M") +- **Stock Qty**: Enter available quantity for this size +- **Published on Website**: ☑️ Check to publish +- **Regular Price**: Enter price (will use default if not specified) + +Repeat for each color variant. + +#### Step 4: Publish Variants + +After saving all SAV records: +1. Go back to **Lifestyle Settings** +2. Click "Publish Variants for All Templates" +3. This creates all variant combinations and publishes them to the website + +--- + +## Publishing to Website + +### Verify Product Publication + +1. Navigate to: **Desk → Website → Website Item** +2. Find your product +3. Check these fields: + - ☑️ Published + - Website Item Groups are set + - Item Group shows correct e-commerce group + - Images are uploaded + - Price is set + +### Set Item Group Display + +Navigate to: **Desk → Stock → Item Group** + +For each e-commerce group (Men, Women, Kids): +1. Open the Item Group +2. **Custom Fields:** + - **DisplayName**: Display name for backend + - **Ecommerce Display Name**: Display name for website + - ☑️ **Display on Website**: Check this +3. **Website Settings:** + - ☑️ **Show in Website**: Check this + - **Route**: Auto-generated or custom (e.g., "men", "women") + - **Website Title**: Title for the category page + - **Description**: Banner/description for category page (supports HTML) + - **Slideshow**: Optional slideshow for category page +4. Save + +--- + +## Accessing the Frontend + +### Default Routes + +The LS Shop frontend uses URL-based language routing: + +**English Routes:** +- Homepage: `https://your-site.com/en` +- Product Catalog: `https://your-site.com/en/products` +- Specific Product: `https://your-site.com/en/products/product-slug` +- Cart: `https://your-site.com/en/cart` +- Checkout: `https://your-site.com/en/cart/checkout` +- User Account: `https://your-site.com/en/account/dashboard` + +**Arabic Routes (RTL):** +- Homepage: `https://your-site.com/ar` +- Product Catalog: `https://your-site.com/ar/products` +- And so on... + +### Testing Your First Product + +1. Open browser to: `https://your-site.com/en/products` +2. You should see your published products +3. Click on a product to view details +4. Test add to cart functionality +5. Proceed to checkout to test the full flow + +### Guest Checkout vs User Accounts + +- **Guest Checkout**: Customers can checkout without creating an account +- **User Accounts**: Customers can register to track orders and save addresses + +--- + +## Advanced Features + +### Bulk Operations + +#### Bulk Image Upload +For uploading multiple product images at once: +1. Navigate to: **Desk → Lifestyle Shop Ecommerce → Bulk Image Upload** +2. Follow the tool to upload images in bulk + +#### Bulk Variant Publishing +To publish multiple variants at once: +1. Navigate to: **Lifestyle Settings → Bulk Actions / Import Tab** +2. Use "Publish Variants for All Templates" button + +### Landing Page Configuration + +Configure hero banners and featured sections: +1. Navigate to: **Desk → Lifestyle Shop Ecommerce → Landing Page Settings** +2. Add hero banners, featured categories, and promotional sections + +### Size Charts + +Add size charts for products: +1. Navigate to: **Desk → Lifestyle Shop Ecommerce → Size Chart** +2. Create size charts for different item groups +3. Link to items or item groups + +### Payment Gateway Configuration + +#### Telr Payment Gateway +1. Navigate to: **Desk → Lifestyle Shop Ecommerce → Telr Settings** +2. Enter: + - Store ID + - Auth Key + - Test/Live Mode settings +3. Save + +#### Tabby Buy Now Pay Later +Install the Tabby app and configure: +```bash +bench get-app https://github.com/cinnamonlabs/tabby_frappe +bench --site your-site-name install-app tabby_frappe +``` + +### Multi-Store Support + +LS Shop supports multiple physical store locations for pickup orders. + +--- + +## Troubleshooting + +### Product Not Showing on Website + +**Check these in order:** + +1. **Item Configuration:** + - ☑️ Is "Show in Website" checked? + - Is "Website Warehouse" set? + - Is stock available in the warehouse? + - Are images uploaded? + +2. **Website Item:** + - Navigate to Website Item + - ☑️ Is "Published" checked? + - Are "Website Item Groups" set? + - Is the item group correct? + +3. **Item Group:** + - Is the Item Group published to website? + - ☑️ Is "Show in Website" checked? + - Are custom display name fields filled? + +4. **Price:** + - Is an Item Price created? + - Is the Price List correct? + - Is the price greater than zero? + +### Installation Errors + +#### "Error creating Ecommerce groups" + +This has been fixed in the latest version. If you encounter this: +1. Check that custom fields are installed: `custom_displayname` and `custom_item_group_display_name` +2. Check Error Log in Frappe Desk for detailed error +3. Manually create root Item Group if needed + +#### Missing Dependencies + +```bash +# Reinstall dependencies +bench --site your-site-name install-app webshop +bench --site your-site-name install-app payments +``` + +### Frontend Issues + +#### Pages Not Loading / 404 Errors + +1. **Clear cache:** +```bash +bench --site your-site-name clear-cache +bench --site your-site-name clear-website-cache +``` + +2. **Rebuild assets:** +```bash +bench build --app ls_shop +``` + +3. **Restart services:** +```bash +bench restart +``` + +#### Images Not Displaying + +1. Check file permissions on `/public/files/` +2. Verify image paths in Website Item +3. Ensure images are in correct format (JPG, PNG, WebP) +4. Check image file size (recommended < 500KB for optimal performance) + +### Checkout Issues + +#### Payment Gateway Not Working + +1. **Verify payment gateway configuration:** + - Check Telr Settings are complete + - Test mode vs Live mode settings + - API credentials are correct + +2. **Check Lifestyle Settings:** + - Payment modes are enabled + - COD settings are configured if using COD + +#### Orders Not Creating + +1. **Check required settings:** + - Default Price List is set + - Ecommerce Warehouse is set + - Company is configured in ERPNext + - Tax templates are set up (if applicable) + +2. **Check user permissions:** + - Guest user has permissions to create Quotations + - Sales Order creation permissions are correct + +### Performance Optimization + +#### Slow Product Pages + +1. **Optimize images:** + - Use WebP format + - Compress images to < 200KB + - Use appropriate dimensions (1000x1000px for main images) + +2. **Enable caching:** +```bash +# In site_config.json +{ + "enable_website_cache": 1, + "cache_timeout": 3600 +} +``` + +3. **Database optimization:** +```bash +bench --site your-site-name mariadb +# Run MySQL optimization commands +``` + +--- + +## Getting Help + +### Resources + +- **Documentation**: [ERPNext E-Commerce Docs](https://docs.erpnext.com/docs/user/manual/en/e_commerce) +- **Community Forum**: [Frappe Forum](https://discuss.frappe.io/) +- **GitHub Issues**: [LS Shop Issues](https://github.com/BuildWithHussain/ls_shop/issues) + +### Common Support Channels + +1. Create an issue on GitHub with: + - Frappe/ERPNext versions + - Detailed error messages + - Steps to reproduce + - Screenshots if applicable + +2. Post on Frappe Forum with tag `ls_shop` + +--- + +## Next Steps + +After completing this setup: + +1. **Customize Design**: Modify templates and styling to match your brand +2. **Configure SEO**: Add meta descriptions, keywords for products +3. **Set Up Analytics**: Integrate Google Analytics or similar +4. **Configure Email**: Set up email domain and templates +5. **Test Complete Flow**: Go through entire purchase process +6. **Set Up Shipping**: Configure shipping rules and carriers +7. **Launch**: Announce your store and start selling! + +--- + +## Credits + +LS Shop is developed and maintained by **BWH Studios** - Specializing in Frappe customizations and consulting. + +--- + +**Last Updated**: 2025-10-03 +**Version**: 1.0 + +For the latest updates and detailed documentation, visit the [LS Shop GitHub Repository](https://github.com/BuildWithHussain/ls_shop). \ No newline at end of file diff --git a/ls_shop/debug_products.py b/ls_shop/debug_products.py new file mode 100644 index 0000000..32e1963 --- /dev/null +++ b/ls_shop/debug_products.py @@ -0,0 +1,82 @@ +""" +Debug script to check why products aren't showing on website +""" + +import frappe + + +def debug_product_display(): + """Debug why products aren't showing""" + + print("\n" + "="*60) + print("Debugging Product Display") + print("="*60 + "\n") + + # Check Style Attribute Variants + savs = frappe.get_all( + "Style Attribute Variant", + filters={"is_published": 1}, + fields=["name", "display_name", "is_published", "item_group"] + ) + print(f"1. Style Attribute Variants (published): {len(savs)}") + for sav in savs[:5]: + print(f" - {sav.name}: {sav.display_name} (Group: {sav.item_group})") + + # Check Color Size Items + csis = frappe.get_all( + "Color Size Item", + fields=["name", "parent", "size", "item_code"] + ) + print(f"\n2. Color Size Items: {len(csis)}") + for csi in csis[:5]: + print(f" - Parent: {csi.parent}, Size: {csi.size}, Item: {csi.item_code}") + + # Check Item Prices + prices = frappe.get_all( + "Item Price", + filters={"price_list": "Sale Price List"}, + fields=["item_code", "price_list_rate"] + ) + print(f"\n3. Item Prices (Sale Price List): {len(prices)}") + for price in prices[:5]: + print(f" - {price.item_code}: ${price.price_list_rate}") + + # Run the actual product query + from ls_shop.utils import get_product_base_query, get_product_list + + print("\n4. Running Product Query...") + try: + products = get_product_list(filters={}, page=1, page_length=10) + print(f" Products returned: {len(products)}") + for p in products[:3]: + print(f" - {p.get('display_name')}: ${p.get('sale_price')}") + except Exception as e: + print(f" ❌ Error: {str(e)}") + import traceback + traceback.print_exc() + + # Check query components + print("\n5. Checking Query Components...") + lifestyle_settings = frappe.get_doc("Lifestyle Settings") + print(f" Default Price List: {lifestyle_settings.default_price_list}") + print(f" Sale Price List: {lifestyle_settings.sale_price_list}") + + # Check if SAVs have sizes populated + print("\n6. Checking SAV Size Mappings...") + savs_with_sizes = frappe.db.sql(""" + SELECT sav.name, sav.display_name, COUNT(csi.name) as size_count + FROM `tabStyle Attribute Variant` sav + LEFT JOIN `tabColor Size Item` csi ON csi.parent = sav.name + WHERE sav.is_published = 1 + GROUP BY sav.name + LIMIT 5 + """, as_dict=True) + + for sav in savs_with_sizes: + print(f" - {sav.name}: {sav.size_count} sizes") + + print("\n" + "="*60) + + +if __name__ == "__main__": + debug_product_display() \ No newline at end of file diff --git a/ls_shop/fix_sav_publication.py b/ls_shop/fix_sav_publication.py new file mode 100644 index 0000000..14fdc13 --- /dev/null +++ b/ls_shop/fix_sav_publication.py @@ -0,0 +1,42 @@ +""" +Fix Style Attribute Variant publication +This script publishes all Style Attribute Variants that have Color Size Items +""" + +import frappe + + +def fix_sav_publication(): + """Publish all Style Attribute Variants""" + + print("Fixing Style Attribute Variant publication...") + + # Get all SAVs + savs = frappe.get_all( + "Style Attribute Variant", + fields=["name", "display_name", "is_published"] + ) + + print(f"Found {len(savs)} Style Attribute Variants") + + published_count = 0 + + for sav in savs: + # Check if it has any Color Size Items + has_sizes = frappe.db.exists("Color Size Item", {"parent": sav.name}) + + if has_sizes and not sav.is_published: + # Publish it + frappe.db.set_value("Style Attribute Variant", sav.name, "is_published", 1) + published_count += 1 + print(f" ✓ Published: {sav.display_name}") + + frappe.db.commit() + frappe.clear_cache() + + print(f"\n✅ Published {published_count} Style Attribute Variants") + print(f"Total published SAVs: {frappe.db.count('Style Attribute Variant', {'is_published': 1})}") + + +if __name__ == "__main__": + fix_sav_publication() \ No newline at end of file diff --git a/ls_shop/install_demo_data.py b/ls_shop/install_demo_data.py new file mode 100644 index 0000000..d40635e --- /dev/null +++ b/ls_shop/install_demo_data.py @@ -0,0 +1,761 @@ +""" +Demo Data Installation Script for LS Shop +========================================== + +This script populates the system with demo data to test the complete e-commerce workflow. + +Usage: + bench --site your-site-name execute ls_shop.install_demo_data.install_demo_data + +Features: +- Creates ERPNext prerequisites (Attributes, Price Lists, Brands, Shipping Rules) +- Creates demo product templates with variants +- Sets up Style Attribute Configurators and Variants +- Configures Lifestyle Settings +- Publishes products to website with proper routing +""" + +import frappe +from frappe.utils import flt, cint, random_string +import random + + +def install_demo_data(): + """Main function to install all demo data""" + frappe.flags.in_demo_data = True + + print("\n" + "="*60) + print("Installing LS Shop Demo Data") + print("="*60 + "\n") + + try: + # Step 1: Prerequisites + print("Step 1: Creating Prerequisites...") + create_item_attributes() + create_brands() + create_price_lists() + create_shipping_rule() + + # Step 2: Configure Lifestyle Settings + print("\nStep 2: Configuring Lifestyle Settings...") + configure_lifestyle_settings() + + # Step 3: Create demo products + print("\nStep 3: Creating Demo Products...") + create_demo_products() + + # Step 4: Publish Style Attribute Variants + print("\nStep 4: Publishing Style Attribute Variants...") + publish_style_variants() + + # Step 5: Create Website Items and Publish + print("\nStep 5: Creating and Publishing Website Items...") + create_website_items() + + # Step 6: Fix routing + print("\nStep 6: Fixing Product Routes...") + fix_product_routes() + + # Step 7: Clear cache + print("\nStep 7: Clearing Cache...") + frappe.clear_cache() + + print("\n" + "="*60) + print("✅ Demo Data Installation Complete!") + print("="*60) + print("\nYou can now access the shop at:") + print(" English: https://your-site.com/en/products") + print(" Arabic: https://your-site.com/ar/products") + print("\nDemo products created:") + print(" - Classic T-Shirt (Multiple colors & sizes)") + print(" - Slim Fit Jeans (Multiple colors & sizes)") + print(" - Running Sneakers (Multiple colors & sizes)") + print("\n") + + frappe.db.commit() + + except Exception as e: + frappe.db.rollback() + print(f"\n❌ Error during demo data installation: {str(e)}") + import traceback + traceback.print_exc() + raise + finally: + frappe.flags.in_demo_data = False + + +def create_item_attributes(): + """Create Item Attributes for variants""" + print(" - Creating Item Attributes...") + + # Color attribute + ensure_attribute_values("Color", [ + ("Red", "RED"), + ("Blue", "BLU"), + ("Black", "BLK"), + ("White", "WHT"), + ("Green", "GRN"), + ("Navy", "NVY"), + ("Gray", "GRY"), + ]) + + # Size attribute + ensure_attribute_values("Size", [ + ("XS", "XS"), + ("S", "S"), + ("M", "M"), + ("L", "L"), + ("XL", "XL"), + ("XXL", "XXL"), + ], numeric=True) + + +def ensure_attribute_values(attribute_name, values, numeric=False): + """Ensure an Item Attribute exists with all required values""" + + if not frappe.db.exists("Item Attribute", attribute_name): + # Create new attribute + attr = frappe.get_doc({ + "doctype": "Item Attribute", + "attribute_name": attribute_name, + "numeric_values": 1 if numeric else 0, + "item_attribute_values": [ + {"attribute_value": val, "abbr": abbr} + for val, abbr in values + ] + }) + attr.insert(ignore_permissions=True) + frappe.db.commit() + print(f" ✓ {attribute_name} attribute created") + else: + # Attribute exists, just print message + # Don't try to update to avoid abbreviation conflicts + print(f" • {attribute_name} attribute already exists") + + +def create_brands(): + """Create demo brands""" + print(" - Creating Brands...") + + brands = ["Adidas", "Nike", "Puma", "Lifestyle Store"] + + for brand_name in brands: + if not frappe.db.exists("Brand", brand_name): + brand = frappe.get_doc({ + "doctype": "Brand", + "brand": brand_name + }) + brand.insert(ignore_permissions=True) + print(f" ✓ Brand '{brand_name}' created") + else: + print(f" • Brand '{brand_name}' already exists") + + +def create_price_lists(): + """Create price lists""" + print(" - Creating Price Lists...") + + price_lists = [ + {"name": "Standard Selling", "currency": "USD", "enabled": 1, "buying": 0, "selling": 1}, + {"name": "Sale Price List", "currency": "USD", "enabled": 1, "buying": 0, "selling": 1}, + ] + + for pl_data in price_lists: + if not frappe.db.exists("Price List", pl_data["name"]): + pl = frappe.get_doc({ + "doctype": "Price List", + "price_list_name": pl_data["name"], + "currency": pl_data["currency"], + "enabled": pl_data["enabled"], + "buying": pl_data["buying"], + "selling": pl_data["selling"] + }) + pl.insert(ignore_permissions=True) + print(f" ✓ Price List '{pl_data['name']}' created") + else: + print(f" • Price List '{pl_data['name']}' already exists") + + +def create_shipping_rule(): + """Create a basic shipping rule""" + print(" - Creating Shipping Rule...") + + if not frappe.db.exists("Shipping Rule", "Standard Shipping"): + # Get company for shipping rule + company = frappe.defaults.get_user_default("Company") or frappe.db.get_value("Company", {}, "name") + + if not company: + print(" ⚠ Skipping Shipping Rule creation - no Company found") + return + + # Get required account and cost center + company_abbr = frappe.db.get_value("Company", company, "abbr") + + # Get or create Freight and Forwarding Charges account + account = frappe.db.get_value( + "Account", + {"account_name": "Freight and Forwarding Charges", "company": company}, + "name" + ) + if not account: + account = f"Freight and Forwarding Charges - {company_abbr}" + + # Get default cost center + cost_center = frappe.db.get_value("Cost Center", {"company": company, "is_group": 0}, "name") + if not cost_center: + cost_center = f"Main - {company_abbr}" + + shipping_rule = frappe.get_doc({ + "doctype": "Shipping Rule", + "label": "Standard Shipping", + "shipping_rule_type": "Selling", + "calculate_based_on": "Net Total", + "company": company, + "account": account, + "cost_center": cost_center, + "conditions": [ + { + "from_value": 0, + "to_value": 50, + "shipping_amount": 10 + }, + { + "from_value": 50, + "to_value": 999999, + "shipping_amount": 0 + } + ] + }) + shipping_rule.insert(ignore_permissions=True) + print(" ✓ Shipping Rule 'Standard Shipping' created") + else: + print(" • Shipping Rule already exists") + + +def ensure_warehouse_exists(): + """Ensure a warehouse exists for demo data""" + # Try to find existing non-group warehouse + warehouse = frappe.db.get_value("Warehouse", {"is_group": 0}, "name") + + if warehouse: + return warehouse + + # Get company + company = frappe.defaults.get_user_default("Company") or frappe.db.get_value("Company", {}, "name") + + if not company: + frappe.throw("Please create a Company first before running demo data") + + # Create default warehouse + company_abbr = frappe.db.get_value('Company', company, 'abbr') + warehouse_name = f"Stores - {company_abbr}" + + if not frappe.db.exists("Warehouse", warehouse_name): + warehouse = frappe.get_doc({ + "doctype": "Warehouse", + "warehouse_name": "Stores", + "company": company, + "is_group": 0 + }) + warehouse.insert(ignore_permissions=True) + print(f" ✓ Warehouse '{warehouse_name}' created") + return warehouse.name + + return warehouse_name + + +def configure_lifestyle_settings(): + """Configure Lifestyle Settings""" + print(" - Configuring Lifestyle Settings...") + + # Ensure warehouse exists + warehouse = ensure_warehouse_exists() + + # Get or create settings + if frappe.db.exists("Lifestyle Settings", "Lifestyle Settings"): + settings = frappe.get_doc("Lifestyle Settings", "Lifestyle Settings") + else: + settings = frappe.get_doc({"doctype": "Lifestyle Settings"}) + + # Configure settings + settings.default_price_list = "Standard Selling" + settings.sale_price_list = "Sale Price List" + settings.ecommerce_warehouse = warehouse + settings.shipping_rule = "Standard Shipping" if frappe.db.exists("Shipping Rule", "Standard Shipping") else None + settings.return_period = 30 + settings.cod_enabled = 1 + settings.cod_charge = 5.00 + settings.cod_charge_applicable_below = 100.00 + + # Email templates (use existing from fixtures) + settings.order_confirmation_email_template = "Order Confirmation" + settings.order_cancellation_email_template = "Order Cancellation" + settings.item_in_stock_email_template = "Item In Stock" + + # Return reasons + if not settings.reason_for_return: + reasons = [ + "Wrong Size", + "Wrong Color", + "Defective Product", + "Not as Described", + "Changed Mind" + ] + for reason_text in reasons: + settings.append("reason_for_return", { + "display_name": reason_text + }) + + settings.save(ignore_permissions=True) + print(" ✓ Lifestyle Settings configured") + + +def get_available_attribute_values(attribute_name): + """Get all available values for an attribute""" + if not frappe.db.exists("Item Attribute", attribute_name): + return [] + + return frappe.get_all( + "Item Attribute Value", + filters={"parent": attribute_name}, + fields=["attribute_value"], + pluck="attribute_value" + ) + + +def create_demo_products(): + """Create demo product templates and variants""" + + # Get available attribute values from system + available_colors = get_available_attribute_values("Color") + available_sizes = get_available_attribute_values("Size") + + if not available_colors: + print(" ⚠ No Color attribute values found. Skipping demo products.") + return + + if not available_sizes: + print(" ⚠ No Size attribute values found. Skipping demo products.") + return + + # Use only available colors and sizes + colors_to_use = [c for c in ["Black", "White", "Navy", "Gray", "Blue", "Red"] if c in available_colors] + sizes_to_use = [s for s in ["XS", "S", "M", "L", "XL", "XXL"] if s in available_sizes] + + if not colors_to_use: + colors_to_use = available_colors[:4] # Use first 4 available colors + + if not sizes_to_use: + sizes_to_use = available_sizes[:5] # Use first 5 available sizes + + products = [ + { + "code": "TSHIRT-CLASSIC", + "name": "Classic Cotton T-Shirt", + "item_group": "Men", + "brand": "Lifestyle Store", + "description": "Premium quality cotton t-shirt. Perfect for everyday wear.", + "colors": colors_to_use[:4], # Use up to 4 colors + "sizes": sizes_to_use, + "base_price": 29.99, + "sale_price": 24.99 + }, + { + "code": "JEANS-SLIM", + "name": "Slim Fit Denim Jeans", + "item_group": "Men", + "brand": "Lifestyle Store", + "description": "Modern slim fit jeans with premium denim fabric. Comfortable and stylish.", + "colors": colors_to_use[:2], # Use up to 2 colors + "sizes": sizes_to_use[:4], # Use up to 4 sizes + "base_price": 79.99, + "sale_price": 69.99 + }, + { + "code": "SNEAKER-RUN", + "name": "Running Sneakers", + "item_group": "Men", + "brand": "Adidas", + "description": "High-performance running sneakers with advanced cushioning technology.", + "colors": colors_to_use[:3], # Use up to 3 colors + "sizes": sizes_to_use[:4], # Use up to 4 sizes + "base_price": 119.99, + "sale_price": 99.99 + } + ] + + print(f" Using {len(colors_to_use)} colors: {', '.join(colors_to_use)}") + print(f" Using {len(sizes_to_use)} sizes: {', '.join(sizes_to_use)}") + + for product in products: + print(f"\n Creating product: {product['name']}") + create_product_with_variants(product) + + +def create_product_with_variants(product_data): + """Create a product template with all its variants""" + + # Step 1: Create item template + template = create_item_template(product_data) + if not template: + return + + # Step 2: Create Style Attribute Configurator + configurator = create_configurator(template.name, product_data) + if not configurator: + return + + # Step 3: Create Style Attribute Variants (one per color) + for color in product_data["colors"]: + create_style_variant(configurator.name, template.name, color, product_data) + + # Step 4: Create actual item variants and link them + create_item_variants(template.name, product_data) + + +def create_item_template(product_data): + """Create an item template (parent item with variants)""" + + item_code = product_data["code"] + + if frappe.db.exists("Item", item_code): + print(f" • Template '{item_code}' already exists") + return frappe.get_doc("Item", item_code) + + # Get company for default accounts + company = frappe.defaults.get_user_default("Company") or frappe.db.get_value("Company", {}, "name") + + # Get attribute values for template + color_values = frappe.get_all( + "Item Attribute Value", + filters={"parent": "Color"}, + fields=["attribute_value"], + pluck="attribute_value" + ) + + size_values = frappe.get_all( + "Item Attribute Value", + filters={"parent": "Size"}, + fields=["attribute_value"], + pluck="attribute_value" + ) + + item = frappe.get_doc({ + "doctype": "Item", + "item_code": item_code, + "item_name": product_data["name"], + "item_group": product_data["item_group"], + "brand": product_data["brand"], + "stock_uom": "Nos", + "is_stock_item": 1, + "include_item_in_manufacturing": 0, + "has_variants": 1, + "variant_based_on": "Item Attribute", + "description": product_data["description"], + "custom_displayname": product_data["name"], + "custom_item_group_display_name": product_data["name"], + "attributes": [ + { + "attribute": "Color", + "attribute_value": "\n".join(color_values) if color_values else "" + }, + { + "attribute": "Size", + "attribute_value": "\n".join(size_values) if size_values else "" + } + ] + }) + + item.insert(ignore_permissions=True) + print(f" ✓ Template '{item_code}' created") + return item + + +def create_configurator(template_name, product_data): + """Create Style Attribute Configurator""" + + # Check if configurator exists + existing = frappe.db.get_value( + "Style Attribute Configurator", + {"item_template": template_name}, + "name" + ) + + if existing: + print(f" • Configurator for '{template_name}' already exists") + return frappe.get_doc("Style Attribute Configurator", existing) + + configurator = frappe.get_doc({ + "doctype": "Style Attribute Configurator", + "item_template": template_name, + "item_attribute": "Color" + }) + + configurator.insert(ignore_permissions=True) + print(f" ✓ Configurator created for '{template_name}'") + return configurator + + +def create_style_variant(configurator_name, template_name, color, product_data): + """Create Style Attribute Variant for a specific color""" + + # Check if variant exists + existing = frappe.db.get_value( + "Style Attribute Variant", + {"configurator": configurator_name, "attribute_value": color}, + "name" + ) + + if existing: + print(f" • Style variant '{color}' already exists") + return frappe.get_doc("Style Attribute Variant", existing) + + # Route should NOT include language prefix - hooks.py handles that + route_slug = f"{product_data['code'].lower()}-{color.lower()}" + + variant = frappe.get_doc({ + "doctype": "Style Attribute Variant", + "configurator": configurator_name, + "item_style": template_name, + "attribute_value": color, + "attribute_name": "Color", + "display_name": f"{product_data['name']} - {color}", + "item_group": product_data["item_group"], + "is_published": 1, + "route": route_slug, + "images": [ + { + "image": f"https://via.placeholder.com/800x800/{'000000' if color == 'Black' else '0000FF' if color == 'Blue' else 'FF0000' if color == 'Red' else 'FFFFFF'}/FFFFFF?text={color}+{product_data['name'].replace(' ', '+')}", + } + ] + }) + + variant.insert(ignore_permissions=True) + print(f" ✓ Style variant '{color}' created") + return variant + + +def create_item_variants(template_name, product_data): + """Create actual item variants for all color/size combinations""" + + warehouse = ensure_warehouse_exists() + company = frappe.defaults.get_user_default("Company") or frappe.db.get_value("Company", {}, "name") + + for color in product_data["colors"]: + for size in product_data["sizes"]: + variant_code = f"{product_data['code']}-{color[:3].upper()}-{size}" + + if frappe.db.exists("Item", variant_code): + continue + + # Create variant item + variant = frappe.get_doc({ + "doctype": "Item", + "item_code": variant_code, + "item_name": f"{product_data['name']} - {color} - {size}", + "item_group": product_data["item_group"], + "brand": product_data["brand"], + "stock_uom": "Nos", + "is_stock_item": 1, + "variant_of": template_name, + "description": product_data["description"], + "custom_displayname": f"{product_data['name']} {color} {size}", + "custom_item_group_display_name": f"{product_data['name']} {color} {size}", + "attributes": [ + {"attribute": "Color", "attribute_value": color}, + {"attribute": "Size", "attribute_value": size} + ], + "opening_stock": random.randint(50, 200), + "valuation_rate": product_data["base_price"] * 0.5 + }) + + variant.insert(ignore_permissions=True) + + # Create item prices + create_item_price(variant_code, "Standard Selling", product_data["base_price"]) + create_item_price(variant_code, "Sale Price List", product_data["sale_price"]) + + # Link to Style Attribute Variant + link_to_style_variant(template_name, color, size, variant_code) + + print(f" ✓ All variants created for {template_name}") + + +def create_item_price(item_code, price_list, rate): + """Create item price""" + if not frappe.db.exists("Item Price", {"item_code": item_code, "price_list": price_list}): + price = frappe.get_doc({ + "doctype": "Item Price", + "item_code": item_code, + "price_list": price_list, + "price_list_rate": rate + }) + price.insert(ignore_permissions=True) + + +def link_to_style_variant(template_name, color, size, variant_code): + """Link item variant to Style Attribute Variant""" + + # Get configurator + configurator = frappe.db.get_value( + "Style Attribute Configurator", + {"item_template": template_name}, + "name" + ) + + if not configurator: + return + + # Get style variant + style_variant = frappe.db.get_value( + "Style Attribute Variant", + {"configurator": configurator, "attribute_value": color}, + "name" + ) + + if not style_variant: + return + + # Update style variant with size mapping + sv_doc = frappe.get_doc("Style Attribute Variant", style_variant) + + # Check if size already exists + existing = False + for row in sv_doc.sizes: + if row.size == size: + existing = True + break + + if not existing: + sv_doc.append("sizes", { + "size": size, + "item_code": variant_code + }) + sv_doc.save(ignore_permissions=True) + + +def publish_style_variants(): + """Ensure all Style Attribute Variants are published""" + print(" - Publishing Style Attribute Variants...") + + savs = frappe.get_all("Style Attribute Variant", fields=["name", "display_name"]) + + published = 0 + for sav in savs: + # Check if it has Color Size Items + has_sizes = frappe.db.exists("Color Size Item", {"parent": sav.name}) + + if has_sizes: + frappe.db.set_value("Style Attribute Variant", sav.name, "is_published", 1) + published += 1 + + print(f" ✓ Published {published} Style Attribute Variants") + + +def create_website_items(): + """Create and publish Website Items for all demo products""" + print(" - Creating Website Items...") + + # Get all demo items (variants only, not templates) + items = frappe.get_all( + "Item", + filters={ + "has_variants": 0, + "is_stock_item": 1, + "variant_of": ["in", ["TSHIRT-CLASSIC", "JEANS-SLIM", "SNEAKER-RUN"]] + }, + fields=["name", "item_name", "item_group", "brand", "description", "variant_of"] + ) + + warehouse = ensure_warehouse_exists() + + count_created = 0 + count_published = 0 + + for item in items: + # Get full item details + item_doc = frappe.get_doc("Item", item.name) + + # Check if Website Item already exists + existing_wi = frappe.db.get_value("Website Item", {"item_code": item.name}, "name") + + if not existing_wi: + # Get item group for website_item_groups + item_groups = [item.item_group] if item.item_group else [] + + # Create Website Item manually + wi = frappe.get_doc({ + "doctype": "Website Item", + "item_code": item.name, + "item_name": item.item_name, + "web_item_name": item.item_name, + "item_group": item.item_group, + "brand": item.brand, + "description": item.description or item_doc.description or "", + "website_warehouse": warehouse, + "published": 1, + "website_item_groups": [{"item_group": ig} for ig in item_groups] if item_groups else [] + }) + wi.insert(ignore_permissions=True) + count_created += 1 + else: + # Publish and update existing Website Item + wi_doc = frappe.get_doc("Website Item", existing_wi) + wi_doc.published = 1 + wi_doc.website_warehouse = warehouse + + # Ensure website_item_groups is set + if not wi_doc.website_item_groups and item.item_group: + wi_doc.append("website_item_groups", {"item_group": item.item_group}) + + wi_doc.save(ignore_permissions=True) + count_published += 1 + + print(f" ✓ Created {count_created} Website Items") + print(f" ✓ Published {count_published} existing Website Items") + + +def fix_product_routes(): + """Fix product routes - ensure clean slugs without prefixes""" + print(" - Fixing product routes...") + + # Fix Style Attribute Variant routes + savs = frappe.get_all("Style Attribute Variant", fields=["name", "route", "attribute_value", "item_style"]) + sav_count = 0 + + for sav in savs: + # Get template code for route generation + template_code = frappe.db.get_value("Item", sav.item_style, "item_code") + if template_code: + # Generate clean slug: template-color + clean_route = f"{template_code.lower()}-{sav.attribute_value.lower()}".replace("_", "-").replace(" ", "-") + + # Update if different + if sav.route != clean_route: + frappe.db.set_value("Style Attribute Variant", sav.name, "route", clean_route) + sav_count += 1 + + # Fix Website Item routes + website_items = frappe.get_all("Website Item", fields=["name", "item_code", "route"]) + wi_count = 0 + + for wi in website_items: + # Generate clean slug from item code + clean_route = wi.item_code.lower().replace("_", "-").replace(" ", "-") + + # Remove any existing prefix if present + if wi.route and ("en/products/" in wi.route or "ar/products/" in wi.route): + clean_route = wi.route.replace("en/products/", "").replace("ar/products/", "") + + # Update if needed + if wi.route != clean_route: + frappe.db.set_value("Website Item", wi.name, "route", clean_route) + wi_count += 1 + + print(f" ✓ Fixed {sav_count} SAV routes") + print(f" ✓ Fixed {wi_count} Website Item routes") + print(f" Note: Routes are clean slugs - hooks.py adds /en/products/ prefix") + + +if __name__ == "__main__": + install_demo_data() \ No newline at end of file diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.js b/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.js index f0ab98c..1037c0d 100644 --- a/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.js +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.js @@ -48,4 +48,36 @@ frappe.ui.form.on('Lifestyle Settings', { }, ); }, + + install_demo_data(frm) { + frappe.confirm( + __('This will create demo products, brands, price lists, and configure settings. Continue?'), + () => { + frm.call({ + doc: frm.doc, + method: 'install_demo_data' + }).then((r) => { + if (r.message) { + frappe.msgprint(r.message); + } + }); + } + ); + }, + + publish_all_items(frm) { + frappe.confirm( + __('This will publish all items to the website and fix routes. Continue?'), + () => { + frm.call({ + doc: frm.doc, + method: 'publish_all_items' + }).then((r) => { + if (r.message) { + frappe.msgprint(r.message); + } + }); + } + ); + }, }); diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.json b/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.json index 5d591c3..5852358 100644 --- a/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.json +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.json @@ -34,6 +34,10 @@ "attribute_name_field", "publish_variants_for_all_templates", "view_logs", + "demo_data_section", + "install_demo_data", + "column_break_demo", + "publish_all_items", "item_group_section", "ecommerce_item_group_mapping", "sync_item_group_mapping_to_ecommerce_items", @@ -272,6 +276,25 @@ "fieldname": "view_logs", "fieldtype": "Button", "label": "View Logs" + }, + { + "fieldname": "demo_data_section", + "fieldtype": "Section Break", + "label": "Demo Data & Testing" + }, + { + "fieldname": "install_demo_data", + "fieldtype": "Button", + "label": "Install Demo Data" + }, + { + "fieldname": "column_break_demo", + "fieldtype": "Column Break" + }, + { + "fieldname": "publish_all_items", + "fieldtype": "Button", + "label": "Publish All Items to Website" } ], "index_web_pages_for_search": 1, diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.py b/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.py index 952dd9c..74423b9 100644 --- a/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.py +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/lifestyle_settings/lifestyle_settings.py @@ -97,6 +97,32 @@ def sync_item_group_mapping_to_ecommerce_items(self): "item_group", mapping.ecommerce_item_group, ) + + @frappe.whitelist() + def install_demo_data(self): + """Install demo data for testing LS Shop""" + from ls_shop.install_demo_data import install_demo_data + + frappe.enqueue( + install_demo_data, + queue="long", + timeout=3000, + ) + + return "Demo data installation has been queued. This may take a few minutes. Check the background jobs for progress." + + @frappe.whitelist() + def publish_all_items(self): + """Publish all items to website""" + from ls_shop.publish_demo_items import publish_all_demo_items + + frappe.enqueue( + publish_all_demo_items, + queue="default", + timeout=600, + ) + + return "Publishing all items to website. This may take a moment. Refresh the page after completion." def generate_configurators_for_all_templates(attribute: str, log_name: str): diff --git a/ls_shop/publish_demo_items.py b/ls_shop/publish_demo_items.py new file mode 100644 index 0000000..79b01e5 --- /dev/null +++ b/ls_shop/publish_demo_items.py @@ -0,0 +1,118 @@ +""" +Quick script to publish all demo items to the website +Run this if products don't show on the website after running install_demo_data +""" + +import frappe + + +def publish_all_demo_items(): + """Publish all demo product items to website""" + + print("Publishing demo items to website...") + + # Get warehouse + warehouse = frappe.db.get_value("Warehouse", {"is_group": 0}, "name") + if not warehouse: + print("❌ No warehouse found") + return + + # Get all items + items = frappe.get_all( + "Item", + filters={"has_variants": 0, "is_stock_item": 1}, + fields=["name", "item_name", "item_group", "brand", "description"] + ) + + created = 0 + published = 0 + + for item in items: + wi_name = frappe.db.get_value("Website Item", {"item_code": item.name}, "name") + + if not wi_name: + # Create Website Item + wi = frappe.get_doc({ + "doctype": "Website Item", + "item_code": item.name, + "item_name": item.item_name, + "web_item_name": item.item_name, + "item_group": item.item_group, + "brand": item.brand or "", + "description": item.description or "", + "website_warehouse": warehouse, + "published": 1, + "website_item_groups": [{"item_group": item.item_group}] if item.item_group else [] + }) + wi.insert(ignore_permissions=True) + created += 1 + else: + # Update existing + frappe.db.set_value("Website Item", wi_name, { + "published": 1, + "website_warehouse": warehouse + }) + + # Ensure website_item_groups is set + wi = frappe.get_doc("Website Item", wi_name) + if not wi.website_item_groups and item.item_group: + wi.append("website_item_groups", {"item_group": item.item_group}) + wi.save(ignore_permissions=True) + + published += 1 + + # Fix Style Attribute Variant routes (remove language prefix) + savs = frappe.get_all("Style Attribute Variant", fields=["name", "route"]) + sav_fixed = 0 + + for sav in savs: + if sav.route: + # Remove any language/products prefix + clean_route = sav.route.replace("en/products/", "").replace("ar/products/", "") + if clean_route != sav.route: + frappe.db.set_value("Style Attribute Variant", sav.name, "route", clean_route) + sav_fixed += 1 + + # Publish all SAVs that have sizes + sav_published = 0 + all_savs = frappe.get_all("Style Attribute Variant", fields=["name"]) + for sav in all_savs: + has_sizes = frappe.db.exists("Color Size Item", {"parent": sav.name}) + if has_sizes: + frappe.db.set_value("Style Attribute Variant", sav.name, "is_published", 1) + sav_published += 1 + + # Fix Website Item routes (should be simple slug without prefix) + website_items = frappe.get_all("Website Item", fields=["name", "item_code", "route"]) + routed = 0 + + for wi in website_items: + # Generate clean route (just the slug) + item_code = wi.item_code.lower().replace("_", "-").replace(" ", "-") + + # Remove any existing prefix and ensure it's just the slug + if wi.route: + clean_route = wi.route.replace("en/products/", "").replace("ar/products/", "") + if clean_route != item_code: + frappe.db.set_value("Website Item", wi.name, "route", item_code) + routed += 1 + else: + frappe.db.set_value("Website Item", wi.name, "route", item_code) + routed += 1 + + frappe.db.commit() + frappe.clear_cache() + + print(f"\n✅ Complete!") + print(f" Created: {created} Website Items") + print(f" Published: {published} existing items") + print(f" Fixed SAV routes: {sav_fixed}") + print(f" Published SAVs: {sav_published}") + print(f" Fixed Website Item routes: {routed}") + print(f"\nVisit: https://your-site.com/en/products") + print(f"\nNote: Routes should be clean slugs (e.g., 'tshirt-classic-black')") + print(f" The /en/products/ prefix is added by hooks.py routing") + + +if __name__ == "__main__": + publish_all_demo_items() \ No newline at end of file From 2d199ff2851c7e5dd94d480580dc5dd5c5b6d07d Mon Sep 17 00:00:00 2001 From: altrix-one Date: Fri, 3 Oct 2025 14:54:43 +0000 Subject: [PATCH 04/23] Dynamic Item Groups --- ECOMMERCE_CATEGORY_IMPLEMENTATION.md | 342 ++++++++++++++++++ ls_shop/install_demo_data.py | 66 ++-- .../doctype/ecommerce_category/__init__.py | 0 .../ecommerce_category.json | 125 +++++++ .../ecommerce_category/ecommerce_category.py | 59 +++ .../test_ecommerce_category.py | 16 + ls_shop/migrate.py | 67 +++- .../templates/components/product_filter.html | 136 ++++--- ls_shop/templates/includes/header.html | 6 +- ls_shop/www/products/list.py | 24 +- 10 files changed, 728 insertions(+), 113 deletions(-) create mode 100644 ECOMMERCE_CATEGORY_IMPLEMENTATION.md create mode 100644 ls_shop/lifestyle_shop_ecommerce/doctype/ecommerce_category/__init__.py create mode 100644 ls_shop/lifestyle_shop_ecommerce/doctype/ecommerce_category/ecommerce_category.json create mode 100644 ls_shop/lifestyle_shop_ecommerce/doctype/ecommerce_category/ecommerce_category.py create mode 100644 ls_shop/lifestyle_shop_ecommerce/doctype/ecommerce_category/test_ecommerce_category.py diff --git a/ECOMMERCE_CATEGORY_IMPLEMENTATION.md b/ECOMMERCE_CATEGORY_IMPLEMENTATION.md new file mode 100644 index 0000000..c03c354 --- /dev/null +++ b/ECOMMERCE_CATEGORY_IMPLEMENTATION.md @@ -0,0 +1,342 @@ +# Ecommerce Category Implementation + +## Overview + +This implementation makes the category system dynamic and database-driven instead of hardcoded. Categories are now managed through the "Ecommerce Category" doctype, allowing administrators to easily customize categories without code changes. + +## Changes Made + +### 1. New Doctype: Ecommerce Category + +**Location:** `ls_shop/lifestyle_shop_ecommerce/doctype/ecommerce_category/` + +**Fields:** +- `category_name` - Unique identifier for the category +- `display_name` - Display name shown to users +- `route_slug` - URL-friendly slug for routing +- `enabled` - Toggle to enable/disable categories +- `display_order` - Sort order for display +- `item_group` - Optional link to existing Item Group for filtering +- `icon` - Optional icon name or CSS class +- `image` - Optional category image + +**Features:** +- Auto-generates route slug from category name if not provided +- Validates unique route slugs +- Sortable by display_order +- Full CRUD permissions for System Manager + +### 2. Updated Files + +#### migrate.py +- Added `create_ecommerce_categories()` function +- Creates 3 default categories on installation: + - Engine Parts (maps to Men item group) + - Brake System (maps to Women item group) + - Interior Accessories (maps to Kids item group) +- Categories are linked to existing item groups for backward compatibility + +#### www/products/list.py +- Modified `get_product_filters()` to query categories from Ecommerce Category doctype +- Dynamically loads categories based on enabled status +- Falls back to item group hierarchy for subcategories +- Maintains backward compatibility with existing filtering + +#### templates/includes/header.html +- Changed from hardcoded `[{"name":"Men"},{"name":"Women"},{"name":"Kids"}]` +- Now fetches from database: `frappe.db.get_all('Ecommerce Category')` +- Uses `item_group` field to link to existing category trees + +#### templates/components/product_filter.html +- Removed hardcoded checks for 'Men', 'Women', 'Kids' +- Now works with any categories from the doctype +- Dynamic rendering based on category structure +- Maintains full filtering functionality + +#### install_demo_data.py +- Updated demo products to car parts theme: + - Premium Brake Pads + - High-Flow Air Filter + - All-Weather Floor Mats +- Demo products map to the new categories via item groups + +## How It Works + +### Category Hierarchy + +``` +Ecommerce Category (Database) + ├── category_name: "Engine Parts" + ├── display_name: "Engine Parts" + ├── route_slug: "engine-parts" + ├── item_group: "Men" (links to existing Item Group tree) + └── enabled: 1 + +Item Group (ERPNext) + └── Men (parent) + ├── Subcategory 1 + ├── Subcategory 2 + └── Subcategory 3 +``` + +The system uses a two-tier approach: +1. **Top Level:** Ecommerce Category (managed in database) +2. **Subcategories:** Item Group hierarchy (existing ERPNext functionality) + +This allows: +- Easy customization of top-level categories +- Reuse of existing Item Group trees +- Backward compatibility with existing products + +## Installation & Setup + +### Fresh Installation + +When installing ls_shop, the `after_install()` hook automatically: +1. Creates default payment modes +2. Creates ecommerce item groups +3. Creates ecommerce categories + +### Existing Installation + +To add categories to an existing installation: + +```python +# In bench console +bench --site your-site-name console + +# Then run: +from ls_shop.migrate import create_ecommerce_categories +create_ecommerce_categories() +``` + +### Running Demo Data + +```bash +bench --site your-site-name execute ls_shop.install_demo_data.install_demo_data +``` + +This will create: +- 3 car parts themed products +- Ecommerce categories +- All necessary configurations + +## Customization Guide + +### Adding New Categories + +1. Go to: **Lifestyle Shop Ecommerce > Ecommerce Category > New** + +2. Fill in the fields: + ``` + Category Name: Exterior Parts + Display Name: Exterior Parts + Route Slug: exterior-parts (auto-generated) + Item Group: [Select existing Item Group or create new] + Enabled: ✓ + Display Order: 4 + ``` + +3. Save + +The new category will immediately appear in: +- Header navigation +- Product filters +- Category listings + +### Renaming Existing Categories + +The default categories (Engine Parts, Brake System, Interior Accessories) can be renamed: + +1. Open the Ecommerce Category +2. Change `display_name` to your preferred name +3. Optionally update `route_slug` +4. Save + +**Example:** Rename "Engine Parts" to "Performance Parts" +- This won't affect the underlying item group mapping +- Products will still filter correctly + +### Changing Category Order + +Update the `display_order` field: +- Lower numbers appear first +- Categories with same order are sorted alphabetically + +### Disabling Categories + +Uncheck `enabled` to temporarily hide a category without deleting it. + +### Linking to Different Item Groups + +Change the `item_group` field to map to different subcategory trees: + +``` +Engine Parts → Link to "Automotive" item group +Brake System → Link to "Safety Components" item group +``` + +## Testing Instructions + +### 1. Test Category Creation + +```python +# In bench console +import frappe + +# Create test category +cat = frappe.get_doc({ + "doctype": "Ecommerce Category", + "category_name": "Test Category", + "display_name": "Test Category", + "route_slug": "test-category", + "enabled": 1, + "display_order": 10 +}) +cat.insert() +frappe.db.commit() +``` + +**Expected Result:** Category appears in header navigation + +### 2. Test Header Navigation + +1. Visit `/products` page +2. Check header navigation shows categories from database +3. Verify category names match `display_name` field + +**Expected:** Dynamic categories appear in correct order + +### 3. Test Product Filtering + +1. Go to `/products` page +2. Open filter sidebar +3. Verify categories are listed +4. Select a category filter +5. Verify products filter correctly + +**Expected:** +- Categories from Ecommerce Category doctype appear +- Subcategories from linked Item Groups load +- Filtering works correctly + +### 4. Test Category Customization + +1. Edit an Ecommerce Category +2. Change `display_name` +3. Change `display_order` +4. Save and refresh frontend + +**Expected:** Changes reflect immediately (after cache clear) + +### 5. Test Backward Compatibility + +1. Verify existing products still display +2. Check old item group filters still work +3. Ensure category trees load correctly + +**Expected:** No breaking changes to existing functionality + +## API Methods + +### Python API + +```python +# Get all active categories +from ls_shop.lifestyle_shop_ecommerce.doctype.ecommerce_category.ecommerce_category import get_active_categories + +categories = get_active_categories() +# Returns: [{"name": "...", "display_name": "...", "route_slug": "...", ...}] + +# Get category by slug +from ls_shop.lifestyle_shop_ecommerce.doctype.ecommerce_category.ecommerce_category import get_category_by_slug + +category = get_category_by_slug("engine-parts") +``` + +### Jinja Template Usage + +```html +{% set categories = frappe.db.get_all('Ecommerce Category', + {"enabled":1}, + fields=["name","display_name","route_slug"], + order_by="display_order asc") %} + +{% for category in categories %} + + {{ category.display_name }} + +{% endfor %} +``` + +## Migration Notes + +### From Hardcoded to Dynamic + +**Before:** +```python +parent_categories = [{"name":"Men"},{"name":"Women"},{"name":"Kids"}] +``` + +**After:** +```python +parent_categories = frappe.db.get_all('Ecommerce Category', + {"enabled":1}, + fields=["name","display_name","route_slug","item_group"], + order_by="display_order asc") +``` + +### Key Benefits + +1. **No Code Changes Required** - Add/remove/modify categories through UI +2. **Multi-tenant Friendly** - Different sites can have different categories +3. **Customizable Order** - Control display order without code +4. **Icon Support** - Add icons/images to categories +5. **Easy Disable** - Temporarily hide categories +6. **SEO Friendly** - Custom route slugs for each category + +## Troubleshooting + +### Categories Not Showing + +1. Check `enabled` field is checked +2. Verify `display_order` is set +3. Clear cache: `bench --site site-name clear-cache` +4. Check browser console for errors + +### Filtering Not Working + +1. Verify `item_group` field is set correctly +2. Check Item Group exists and has products +3. Ensure Item Group has `custom_display_on_website` enabled +4. Check console logs for filter state + +### Route Slugs Conflict + +1. Ensure `route_slug` is unique +2. System validates on save +3. Use frappe.scrub() format (lowercase-with-dashes) + +## Future Enhancements + +Potential additions: +- Multi-level category hierarchy within Ecommerce Category +- Category-specific banners and descriptions +- Category SEO meta tags +- Category images in navigation +- Category-specific sorting rules +- Featured products per category + +## Support + +For issues or questions: +1. Check error logs: **Error Log** in Frappe Desk +2. Review console logs in browser developer tools +3. Verify database queries in **Query Report** +4. Test with demo data for comparison + +--- + +**Implementation Date:** 2025-10-03 +**Version:** 1.0.0 +**Compatibility:** Frappe v15+, ERPNext v15+ \ No newline at end of file diff --git a/ls_shop/install_demo_data.py b/ls_shop/install_demo_data.py index d40635e..b0e3da2 100644 --- a/ls_shop/install_demo_data.py +++ b/ls_shop/install_demo_data.py @@ -66,10 +66,14 @@ def install_demo_data(): print("\nYou can now access the shop at:") print(" English: https://your-site.com/en/products") print(" Arabic: https://your-site.com/ar/products") - print("\nDemo products created:") - print(" - Classic T-Shirt (Multiple colors & sizes)") - print(" - Slim Fit Jeans (Multiple colors & sizes)") - print(" - Running Sneakers (Multiple colors & sizes)") + print("\nDemo products created (Car Parts Theme):") + print(" - Premium Brake Pads (Multiple colors & sizes)") + print(" - High-Flow Air Filter (Multiple colors & sizes)") + print(" - All-Weather Floor Mats (Multiple colors & sizes)") + print("\nDemo Categories created:") + print(" - Engine Parts") + print(" - Brake System") + print(" - Interior Accessories") print("\n") frappe.db.commit() @@ -137,7 +141,7 @@ def create_brands(): """Create demo brands""" print(" - Creating Brands...") - brands = ["Adidas", "Nike", "Puma", "Lifestyle Store"] + brands = ["Brembo", "K&N", "WeatherTech", "Lifestyle Store"] for brand_name in brands: if not frappe.db.exists("Brand", brand_name): @@ -350,37 +354,37 @@ def create_demo_products(): products = [ { - "code": "TSHIRT-CLASSIC", - "name": "Classic Cotton T-Shirt", - "item_group": "Men", + "code": "BRAKE-PADS", + "name": "Premium Brake Pads", + "item_group": "Brake System", "brand": "Lifestyle Store", - "description": "Premium quality cotton t-shirt. Perfect for everyday wear.", - "colors": colors_to_use[:4], # Use up to 4 colors - "sizes": sizes_to_use, - "base_price": 29.99, - "sale_price": 24.99 + "description": "High-performance ceramic brake pads with superior stopping power and minimal dust.", + "colors": colors_to_use[:3], # Use up to 3 colors + "sizes": sizes_to_use[:4], # Use up to 4 sizes (can represent different fitment types) + "base_price": 89.99, + "sale_price": 74.99 }, { - "code": "JEANS-SLIM", - "name": "Slim Fit Denim Jeans", - "item_group": "Men", - "brand": "Lifestyle Store", - "description": "Modern slim fit jeans with premium denim fabric. Comfortable and stylish.", + "code": "AIR-FILTER", + "name": "High-Flow Air Filter", + "item_group": "Engine Parts", + "brand": "Adidas", + "description": "Performance air filter for improved engine airflow and horsepower. Washable and reusable.", "colors": colors_to_use[:2], # Use up to 2 colors - "sizes": sizes_to_use[:4], # Use up to 4 sizes - "base_price": 79.99, - "sale_price": 69.99 + "sizes": sizes_to_use[:3], # Use up to 3 sizes + "base_price": 49.99, + "sale_price": 39.99 }, { - "code": "SNEAKER-RUN", - "name": "Running Sneakers", - "item_group": "Men", - "brand": "Adidas", - "description": "High-performance running sneakers with advanced cushioning technology.", - "colors": colors_to_use[:3], # Use up to 3 colors - "sizes": sizes_to_use[:4], # Use up to 4 sizes - "base_price": 119.99, - "sale_price": 99.99 + "code": "FLOOR-MATS", + "name": "All-Weather Floor Mats", + "item_group": "Interior Accessories", + "brand": "Lifestyle Store", + "description": "Durable rubber floor mats with raised edges to contain spills and debris. Perfect fit guaranteed.", + "colors": colors_to_use[:4], # Use up to 4 colors + "sizes": sizes_to_use[:5], # Use up to 5 sizes (different vehicle models) + "base_price": 59.99, + "sale_price": 49.99 } ] @@ -662,7 +666,7 @@ def create_website_items(): filters={ "has_variants": 0, "is_stock_item": 1, - "variant_of": ["in", ["TSHIRT-CLASSIC", "JEANS-SLIM", "SNEAKER-RUN"]] + "variant_of": ["in", ["BRAKE-PADS", "AIR-FILTER", "FLOOR-MATS"]] }, fields=["name", "item_name", "item_group", "brand", "description", "variant_of"] ) diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/ecommerce_category/__init__.py b/ls_shop/lifestyle_shop_ecommerce/doctype/ecommerce_category/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/ecommerce_category/ecommerce_category.json b/ls_shop/lifestyle_shop_ecommerce/doctype/ecommerce_category/ecommerce_category.json new file mode 100644 index 0000000..029730f --- /dev/null +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/ecommerce_category/ecommerce_category.json @@ -0,0 +1,125 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:category_name", + "creation": "2025-10-03 14:20:00.000000", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "category_name", + "display_name", + "column_break_1", + "enabled", + "display_order", + "section_break_2", + "route_slug", + "item_group", + "section_break_3", + "icon", + "image" + ], + "fields": [ + { + "fieldname": "category_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Category Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "display_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Display Name", + "reqd": 1 + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Enabled" + }, + { + "default": "0", + "fieldname": "display_order", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Display Order" + }, + { + "fieldname": "section_break_2", + "fieldtype": "Section Break", + "label": "Linking" + }, + { + "description": "URL slug for this category (e.g., 'engine-parts')", + "fieldname": "route_slug", + "fieldtype": "Data", + "label": "Route Slug" + }, + { + "description": "Optional link to existing Item Group for filtering", + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group" + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break", + "label": "Display" + }, + { + "description": "Icon name or CSS class", + "fieldname": "icon", + "fieldtype": "Data", + "label": "Icon" + }, + { + "fieldname": "image", + "fieldtype": "Attach Image", + "label": "Image" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-10-03 14:20:00.000000", + "modified_by": "Administrator", + "module": "Lifestyle Shop Ecommerce", + "name": "Ecommerce Category", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "share": 1 + } + ], + "sort_field": "display_order", + "sort_order": "ASC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/ecommerce_category/ecommerce_category.py b/ls_shop/lifestyle_shop_ecommerce/doctype/ecommerce_category/ecommerce_category.py new file mode 100644 index 0000000..304794a --- /dev/null +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/ecommerce_category/ecommerce_category.py @@ -0,0 +1,59 @@ +# Copyright (c) 2025, Frappe Technologies and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe.utils import cstr + + +class EcommerceCategory(Document): + def validate(self): + """Validate before saving""" + self.validate_route_slug() + self.set_defaults() + + def validate_route_slug(self): + """Ensure route slug is unique and properly formatted""" + if not self.route_slug: + # Auto-generate from category_name + self.route_slug = frappe.scrub(self.category_name) + else: + # Clean up the slug + self.route_slug = frappe.scrub(self.route_slug) + + # Check for duplicates + if self.route_slug: + duplicate = frappe.db.get_value( + "Ecommerce Category", + {"route_slug": self.route_slug, "name": ["!=", self.name]}, + "name" + ) + if duplicate: + frappe.throw(f"Route slug '{self.route_slug}' is already used by {duplicate}") + + def set_defaults(self): + """Set default values""" + if not self.display_name: + self.display_name = self.category_name + + +@frappe.whitelist() +def get_active_categories(): + """Get all active categories ordered by display_order""" + return frappe.get_all( + "Ecommerce Category", + filters={"enabled": 1}, + fields=["name", "category_name", "display_name", "route_slug", "item_group", "icon", "image", "display_order"], + order_by="display_order asc, category_name asc" + ) + + +@frappe.whitelist() +def get_category_by_slug(slug): + """Get category details by route slug""" + return frappe.get_all( + "Ecommerce Category", + filters={"route_slug": slug, "enabled": 1}, + fields=["name", "category_name", "display_name", "route_slug", "item_group", "icon", "image"], + limit=1 + ) \ No newline at end of file diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/ecommerce_category/test_ecommerce_category.py b/ls_shop/lifestyle_shop_ecommerce/doctype/ecommerce_category/test_ecommerce_category.py new file mode 100644 index 0000000..b2ee68a --- /dev/null +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/ecommerce_category/test_ecommerce_category.py @@ -0,0 +1,16 @@ +# Copyright (c) 2025, Frappe Technologies and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class TestEcommerceCategory(FrappeTestCase): + pass \ No newline at end of file diff --git a/ls_shop/migrate.py b/ls_shop/migrate.py index 640d198..70517c2 100644 --- a/ls_shop/migrate.py +++ b/ls_shop/migrate.py @@ -5,10 +5,11 @@ def after_install(): create_payment_modes() try: create_ecommerce_group() + create_ecommerce_categories() except Exception as e: import traceback - error_msg = f"Error creating Ecommerce groups: {str(e)}" - frappe.log_error(traceback.format_exc(), "Lifestyle Shop Installation - Ecommerce Groups") + error_msg = f"Error creating Ecommerce groups/categories: {str(e)}" + frappe.log_error(traceback.format_exc(), "Lifestyle Shop Installation - Ecommerce Setup") frappe.errprint(error_msg) frappe.errprint(traceback.format_exc()) @@ -31,8 +32,15 @@ def create_payment_modes(): def create_ecommerce_group(): + """Create Ecommerce Website parent and car parts category item groups""" parent = "Ecommerce Website" - parent_categories = {"Men", "Women", "Kids"} + + # Car parts categories (matching Ecommerce Category doctype) + parent_categories = { + "Engine Parts": "Engine Parts", + "Brake System": "Brake System", + "Interior Accessories": "Interior Accessories" + } # Find or create root item group root_item_group = get_root_item_group() @@ -49,15 +57,16 @@ def create_ecommerce_group(): } ).insert(ignore_if_duplicate=True) - for category in parent_categories: + # Create car parts category item groups + for category_name, display_name in parent_categories.items(): frappe.get_doc( { "doctype": "Item Group", - "item_group_name": category, + "item_group_name": category_name, "is_group": True, "parent_item_group": parent, - "custom_displayname": category, - "custom_item_group_display_name": category, + "custom_displayname": display_name, + "custom_item_group_display_name": display_name, } ).insert(ignore_if_duplicate=True) @@ -90,3 +99,47 @@ def get_root_item_group(): ).insert(ignore_permissions=True) return root_name + + +def create_ecommerce_categories(): + """Create default Ecommerce Categories (now database-driven instead of hardcoded)""" + + # Default categories - users can rename or modify these later + # Using car parts theme as per requirements + categories = [ + { + "category_name": "Engine Parts", + "display_name": "Engine Parts", + "route_slug": "engine-parts", + "item_group": "Engine Parts", # Links to Item Group created above + "enabled": 1, + "display_order": 1 + }, + { + "category_name": "Brake System", + "display_name": "Brake System", + "route_slug": "brake-system", + "item_group": "Brake System", # Links to Item Group created above + "enabled": 1, + "display_order": 2 + }, + { + "category_name": "Interior Accessories", + "display_name": "Interior Accessories", + "route_slug": "interior-accessories", + "item_group": "Interior Accessories", # Links to Item Group created above + "enabled": 1, + "display_order": 3 + } + ] + + for cat_data in categories: + if not frappe.db.exists("Ecommerce Category", cat_data["category_name"]): + cat = frappe.get_doc({ + "doctype": "Ecommerce Category", + **cat_data + }) + cat.insert(ignore_permissions=True) + frappe.errprint(f"Created Ecommerce Category: {cat_data['category_name']}") + else: + frappe.errprint(f"Ecommerce Category '{cat_data['category_name']}' already exists") diff --git a/ls_shop/templates/components/product_filter.html b/ls_shop/templates/components/product_filter.html index f081466..efbe2a7 100644 --- a/ls_shop/templates/components/product_filter.html +++ b/ls_shop/templates/components/product_filter.html @@ -78,7 +78,7 @@

-
- + - +
@@ -256,11 +256,11 @@

diff --git a/ls_shop/templates/components/wishlist_icon.html b/ls_shop/templates/components/wishlist_icon.html index aebd5d6..b385f27 100644 --- a/ls_shop/templates/components/wishlist_icon.html +++ b/ls_shop/templates/components/wishlist_icon.html @@ -1,7 +1,7 @@ {{icon(name="heart",classes="h-6 w-6 ")}} 0 diff --git a/ls_shop/templates/includes/footer.html b/ls_shop/templates/includes/footer.html index 23441bd..6cc19b1 100644 --- a/ls_shop/templates/includes/footer.html +++ b/ls_shop/templates/includes/footer.html @@ -2,39 +2,23 @@ {% set settings = frappe.get_cached_doc("Lifestyle Settings", "Lifestyle Settings") %} {% set footer_logo = settings.footer_logo or "/assets/ls_shop/images/lifestyle.png" %} {% set store_name = settings.store_name or "Lifestyle" %} -{% set my_account_links = [ - {"label": "Login/register", "url": "#"}, - {"label": "My account", "url": "#"}, - {"label": "Orders history", "url": "#"}, - {"label": "Track my order", "url": "#"}, - {"label": "Find our store in Saudi Arabia", "url": "#"} - ] -%} +{% set contact_phone = settings.contact_phone or "920000576" %} +{% set contact_email = settings.contact_email or "Contact@lifestyle.sa" %} +{% set working_hours = settings.working_hours or "Sun - Thu / 10.00 AM - 6.00 PM" %} +{% set newsletter_title = settings.newsletter_title or _('Newsletter sign up') %} +{% set newsletter_desc = settings.newsletter_description or _('Get all the latest information on Events, Sales and Offers. Sign up for the newsletter today') %} +{% set copyright_text = settings.copyright_text or store_name + '. All Rights Reserved' %} +{% set payment_image = settings.payment_methods_image or "/assets/ls_shop/images/payment_method.png" %} +{% set vat_image = settings.vat_certificate_image or "/assets/ls_shop/images/vat.png" %} -{% set policies_links = [ - {"label": "Offers policy", "url": "#"}, - {"label": "Payment policy", "url": "#"}, - {"label": "Returns policy", "url": "#"}, - {"label": "Shipping policy", "url": "#"}, - {"label": "Terms and conditions", "url": "#"}, - {"label": "Privacy and policy", "url": "#"}, - {"label": "Promotions", "url": "#"}, - ] -%} - -{% set customer_service_links = [ - {"label": "Sizing charts", "url": "#"}, - {"label": "FAQ", "url": "#"}, - {"label": "Contact us", "url": "#"}, - {"label": "International Shipping", "url": "#"}, - ] -%} +{# Get footer sections from settings or use defaults #} +{% set footer_sections = settings.footer_sections if settings.footer_sections else [] %} -