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/install_demo_data.py b/ls_shop/install_demo_data.py new file mode 100644 index 0000000..5f7ff63 --- /dev/null +++ b/ls_shop/install_demo_data.py @@ -0,0 +1,960 @@ +""" +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 random + +import frappe +from frappe import _ +from frappe.utils import cint, flt, random_string + + +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 2.1: Create Ecommerce Structure + print("\nStep 2.1: Creating Ecommerce Structure...") + create_ecommerce_group() + create_ecommerce_categories() + create_default_footer_sections() + + # 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 (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() # nosemgrep: manual commit required for demo data installation completion + + except Exception as e: + frappe.db.rollback() + print(f"\n❌ Error during demo data installation: {e!s}") + 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() # nosemgrep: manual commit required for attribute creation in demo data + 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 = ["Brembo", "K&N", "WeatherTech", "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": "BRAKE-PADS", + "name": "Premium Brake Pads", + "item_group": "Brake System", + "brand": "Lifestyle Store", + "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": "AIR-FILTER", + "name": "High-Flow Air Filter", + "item_group": "Engine Parts", + "brand": "K&N", + "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[:3], # Use up to 3 sizes + "base_price": 49.99, + "sale_price": 39.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, + }, + ] + + 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", ["BRAKE-PADS", "AIR-FILTER", "FLOOR-MATS"]], + }, + 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(" Note: Routes are clean slugs - hooks.py adds /en/products/ prefix") + + +def create_ecommerce_group(): + """Create Ecommerce Website parent and car parts category item groups""" + parent = "Ecommerce Website" + + # 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() + + # Create parent group + frappe.get_doc( + { + "doctype": "Item Group", + "item_group_name": parent, + "is_group": True, + "parent_item_group": root_item_group, + "custom_displayname": parent, + "custom_item_group_display_name": parent, + } + ).insert(ignore_if_duplicate=True) + + # 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_name, + "is_group": True, + "parent_item_group": parent, + "custom_displayname": display_name, + "custom_item_group_display_name": display_name, + } + ).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 + + +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") + + +def create_default_footer_sections(): + """Create default footer sections in Lifestyle Settings""" + + if not frappe.db.exists("Lifestyle Settings", "Lifestyle Settings"): + frappe.errprint("Lifestyle Settings not found, skipping footer sections") + return + + settings = frappe.get_doc("Lifestyle Settings", "Lifestyle Settings") + + # Set default email templates if not already set (required fields) + if not settings.order_confirmation_email_template: + settings.order_confirmation_email_template = "Order Confirmation" + + if not settings.item_in_stock_email_template: + settings.item_in_stock_email_template = "Item In Stock" + + if not settings.order_cancellation_email_template: + settings.order_cancellation_email_template = "Order Cancellation" + + # Only create if no footer sections exist + if settings.footer_sections: + frappe.errprint("Footer sections already exist, skipping") + return + + # Default footer sections with common e-commerce links + default_sections = [ + { + "section_title": "My Account", + "section_order": 1, + "enabled": 1, + "footer_links": [ + {"link_label": "My Account", "link_url": "/account/dashboard", "link_order": 1, "enabled": 1}, + { + "link_label": "Orders History", + "link_url": "/account/orders", + "link_order": 2, + "enabled": 1, + }, + {"link_label": "Wishlist", "link_url": "/account/wishlist", "link_order": 3, "enabled": 1}, + {"link_label": "Track Order", "link_url": "#", "link_order": 4, "enabled": 1}, + ], + }, + { + "section_title": "Policies", + "section_order": 2, + "enabled": 1, + "footer_links": [ + {"link_label": "Privacy Policy", "link_url": "#", "link_order": 1, "enabled": 1}, + {"link_label": "Terms and Conditions", "link_url": "#", "link_order": 2, "enabled": 1}, + {"link_label": "Shipping Policy", "link_url": "#", "link_order": 3, "enabled": 1}, + {"link_label": "Returns Policy", "link_url": "#", "link_order": 4, "enabled": 1}, + {"link_label": "Payment Policy", "link_url": "#", "link_order": 5, "enabled": 1}, + ], + }, + { + "section_title": "Customer Service", + "section_order": 3, + "enabled": 1, + "footer_links": [ + {"link_label": "FAQ", "link_url": "#", "link_order": 1, "enabled": 1}, + {"link_label": "Contact Us", "link_url": "#", "link_order": 2, "enabled": 1}, + {"link_label": "Sizing Charts", "link_url": "#", "link_order": 3, "enabled": 1}, + {"link_label": "International Shipping", "link_url": "#", "link_order": 4, "enabled": 1}, + ], + }, + ] + + # Create Footer Section Config records and link them to Lifestyle Settings + for section_data in default_sections: + # Create Footer Section Config + section_config_name = section_data["section_title"] + if not frappe.db.exists("Footer Section Config", section_config_name): + section_config = frappe.get_doc( + { + "doctype": "Footer Section Config", + "section_title": section_config_name, + "section_order": section_data["section_order"], + "enabled": section_data["enabled"], + "footer_links": section_data["footer_links"], + } + ) + section_config.insert(ignore_permissions=True) + frappe.errprint(f"Created Footer Section Config: {section_config_name}") + + # Add to Lifestyle Settings footer_sections (Footer Section Mapping) + settings.append( + "footer_sections", + { + "footer_section": section_config_name, + "section_order": section_data["section_order"], + "enabled": section_data["enabled"], + }, + ) + + settings.save(ignore_permissions=True) + frappe.errprint("✅ Default footer sections created") + + +if __name__ == "__main__": + install_demo_data() 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..7a570c1 --- /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 +} 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..d715496 --- /dev/null +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/ecommerce_category/ecommerce_category.py @@ -0,0 +1,66 @@ +# 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, + ) 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..d4690a9 --- /dev/null +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/ecommerce_category/test_ecommerce_category.py @@ -0,0 +1,15 @@ +# 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 diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/footer_link/__init__.py b/ls_shop/lifestyle_shop_ecommerce/doctype/footer_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/footer_link/footer_link.json b/ls_shop/lifestyle_shop_ecommerce/doctype/footer_link/footer_link.json new file mode 100644 index 0000000..eb3bc9f --- /dev/null +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/footer_link/footer_link.json @@ -0,0 +1,95 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-10-04 19:16:34.572019", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": ["link_label", "link_url", "link_order", "enabled"], + "fields": [ + { + "fieldname": "link_label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Link Label", + "reqd": 1 + }, + { + "fieldname": "link_url", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Link URL", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "link_order", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Display Order" + }, + { + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Enabled" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2025-10-04 19:16:50.538264", + "modified_by": "Administrator", + "module": "Lifestyle Shop Ecommerce", + "name": "Footer Link", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Website Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "All" + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "link_order", + "sort_order": "ASC", + "states": [] +} diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/footer_link/footer_link.py b/ls_shop/lifestyle_shop_ecommerce/doctype/footer_link/footer_link.py new file mode 100644 index 0000000..91fc0ce --- /dev/null +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/footer_link/footer_link.py @@ -0,0 +1,26 @@ +# Copyright (c) 2025, hussain@buildwithhussain.com and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class FooterLink(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + enabled: DF.Check + link_label: DF.Data + link_order: DF.Int + link_url: DF.Data + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + # end: auto-generated types + + pass diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_config/__init__.py b/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_config/footer_section_config.js b/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_config/footer_section_config.js new file mode 100644 index 0000000..0db8a7e --- /dev/null +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_config/footer_section_config.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, hussain@buildwithhussain.com and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Footer Section Config", { +// refresh(frm) { + +// }, +// }); diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_config/footer_section_config.json b/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_config/footer_section_config.json new file mode 100644 index 0000000..3df4e37 --- /dev/null +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_config/footer_section_config.json @@ -0,0 +1,118 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:section_title", + "creation": "2025-10-04 19:13:28.685267", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "section_section", + "section_title", + "column_break_section", + "section_order", + "enabled", + "section_break_links", + "footer_links" + ], + "fields": [ + { + "fieldname": "section_section", + "fieldtype": "Section Break", + "label": "Section" + }, + { + "fieldname": "section_title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Section Title", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "column_break_section", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "section_order", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Display Order" + }, + { + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Enabled" + }, + { + "fieldname": "section_break_links", + "fieldtype": "Section Break", + "label": "Links" + }, + { + "fieldname": "footer_links", + "fieldtype": "Table", + "label": "Footer Links", + "options": "Footer Link" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-10-05 19:23:26.779649", + "modified_by": "Administrator", + "module": "Lifestyle Shop Ecommerce", + "name": "Footer Section Config", + "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 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Website Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "All" + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "section_order", + "sort_order": "ASC", + "states": [], + "title_field": "section_title" +} diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_config/footer_section_config.py b/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_config/footer_section_config.py new file mode 100644 index 0000000..f05929c --- /dev/null +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_config/footer_section_config.py @@ -0,0 +1,25 @@ +# Copyright (c) 2025, hussain@buildwithhussain.com and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class FooterSectionConfig(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from ls_shop.lifestyle_shop_ecommerce.doctype.footer_link.footer_link import FooterLink + + enabled: DF.Check + footer_links: DF.Table[FooterLink] + section_order: DF.Int + section_title: DF.Data + # end: auto-generated types + + pass diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_config/test_footer_section_config.py b/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_config/test_footer_section_config.py new file mode 100644 index 0000000..6fa03b0 --- /dev/null +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_config/test_footer_section_config.py @@ -0,0 +1,20 @@ +# Copyright (c) 2025, hussain@buildwithhussain.com and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + +# 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 IntegrationTestFooterSectionConfig(IntegrationTestCase): + """ + Integration tests for FooterSectionConfig. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_mapping/footer_section_mapping.json b/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_mapping/footer_section_mapping.json new file mode 100644 index 0000000..86c0385 --- /dev/null +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_mapping/footer_section_mapping.json @@ -0,0 +1,85 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-10-05 17:17:00.000000", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": ["footer_section", "section_order", "enabled"], + "fields": [ + { + "fieldname": "footer_section", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Footer Section", + "options": "Footer Section Config", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "section_order", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Display Order" + }, + { + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Enabled" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2025-10-05 17:17:00.000000", + "modified_by": "Administrator", + "module": "Lifestyle Shop Ecommerce", + "name": "Footer Section Mapping", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Website Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "All" + } + ], + "sort_field": "section_order", + "sort_order": "ASC", + "states": [] +} diff --git a/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_mapping/footer_section_mapping.py b/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_mapping/footer_section_mapping.py new file mode 100644 index 0000000..3e6db41 --- /dev/null +++ b/ls_shop/lifestyle_shop_ecommerce/doctype/footer_section_mapping/footer_section_mapping.py @@ -0,0 +1,22 @@ +# Copyright (c) 2025, company@bwhstudios.com and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class FooterSectionMapping(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + enabled: DF.Check + footer_section: DF.Link + section_order: DF.Int + # end: auto-generated types + + pass 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..5635890 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,44 @@ 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..bc22fd5 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 @@ -5,6 +5,13 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ + "branding_section", + "store_name", + "column_break_brand", + "brand_logo", + "footer_logo", + "column_break_favicon", + "favicon", "cod_section", "cod_charge_applicable_below", "charge_account_head", @@ -34,9 +41,62 @@ "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", + "contact_information_tab", + "contact_phone", + "column_break_contact", + "contact_email", + "working_hours", + "social_media_tab", + "facebook_url", + "twitter_url", + "column_break_social", + "instagram_url", + "snapchat_url", + "column_break_social2", + "tiktok_url", + "footer_customization_tab", + "newsletter_title", + "column_break_newsletter", + "newsletter_description", + "section_break_footer_assets", + "payment_methods_image", + "column_break_footer_assets", + "vat_certificate_image", + "section_break_copyright", + "copyright_text", + "section_break_footer_sections", + "footer_sections", + "color_scheme_tab", + "primary_color", + "primary_hover_color", + "column_break_colors1", + "link_color", + "link_hover_color", + "column_break_colors2", + "accent_color", + "border_accent_color", + "section_break_ui_colors", + "button_bg_color", + "badge_bg_color", + "strikethrough_color", + "column_break_ui_colors1", + "heading_accent_color", + "brand_text_color", + "column_break_ui_colors2", + "secondary_accent_color", + "form_accent_color", + "focus_ring_color", + "section_break_footer_colors", + "footer_bg_color", + "column_break_footer_colors", + "footer_text_color", "communication_tab", "order_section", "order_confirmation_email_template", @@ -48,6 +108,43 @@ "cc_email" ], "fields": [ + { + "fieldname": "branding_section", + "fieldtype": "Section Break", + "label": "Branding" + }, + { + "description": "Name displayed in browser tab and site title", + "fieldname": "store_name", + "fieldtype": "Data", + "label": "Store Name" + }, + { + "fieldname": "column_break_brand", + "fieldtype": "Column Break" + }, + { + "description": "Logo displayed in header navigation", + "fieldname": "brand_logo", + "fieldtype": "Attach Image", + "label": "Brand Logo" + }, + { + "description": "Logo displayed in footer", + "fieldname": "footer_logo", + "fieldtype": "Attach Image", + "label": "Footer Logo" + }, + { + "fieldname": "column_break_favicon", + "fieldtype": "Column Break" + }, + { + "description": "Icon displayed in browser tab (16x16 or 32x32 px)", + "fieldname": "favicon", + "fieldtype": "Attach", + "label": "Favicon" + }, { "fieldname": "delivery_section", "fieldtype": "Section Break", @@ -210,6 +307,264 @@ "fieldtype": "Section Break", "label": "Item Group" }, + { + "fieldname": "contact_information_tab", + "fieldtype": "Tab Break", + "label": "Contact Information" + }, + { + "fieldname": "contact_phone", + "fieldtype": "Data", + "label": "Contact Phone" + }, + { + "fieldname": "column_break_contact", + "fieldtype": "Column Break" + }, + { + "fieldname": "contact_email", + "fieldtype": "Data", + "label": "Contact Email", + "options": "Email" + }, + { + "fieldname": "working_hours", + "fieldtype": "Data", + "label": "Working Hours" + }, + { + "fieldname": "social_media_tab", + "fieldtype": "Tab Break", + "label": "Social Media" + }, + { + "fieldname": "facebook_url", + "fieldtype": "Data", + "label": "Facebook URL", + "options": "URL" + }, + { + "fieldname": "twitter_url", + "fieldtype": "Data", + "label": "Twitter/X URL", + "options": "URL" + }, + { + "fieldname": "column_break_social", + "fieldtype": "Column Break" + }, + { + "fieldname": "instagram_url", + "fieldtype": "Data", + "label": "Instagram URL", + "options": "URL" + }, + { + "fieldname": "snapchat_url", + "fieldtype": "Data", + "label": "Snapchat URL", + "options": "URL" + }, + { + "fieldname": "column_break_social2", + "fieldtype": "Column Break" + }, + { + "fieldname": "tiktok_url", + "fieldtype": "Data", + "label": "TikTok URL", + "options": "URL" + }, + { + "fieldname": "footer_customization_tab", + "fieldtype": "Tab Break", + "label": "Footer Customization" + }, + { + "fieldname": "newsletter_title", + "fieldtype": "Data", + "label": "Newsletter Title" + }, + { + "fieldname": "column_break_newsletter", + "fieldtype": "Column Break" + }, + { + "fieldname": "newsletter_description", + "fieldtype": "Text", + "label": "Newsletter Description" + }, + { + "fieldname": "section_break_footer_assets", + "fieldtype": "Section Break", + "label": "Footer Assets" + }, + { + "fieldname": "payment_methods_image", + "fieldtype": "Attach Image", + "label": "Payment Methods Image" + }, + { + "fieldname": "column_break_footer_assets", + "fieldtype": "Column Break" + }, + { + "fieldname": "vat_certificate_image", + "fieldtype": "Attach Image", + "label": "VAT Certificate Image" + }, + { + "fieldname": "section_break_copyright", + "fieldtype": "Section Break", + "label": "Copyright & Legal" + }, + { + "fieldname": "copyright_text", + "fieldtype": "Data", + "label": "Copyright Text" + }, + { + "fieldname": "section_break_footer_sections", + "fieldtype": "Section Break", + "label": "Footer Sections" + }, + { + "fieldname": "footer_sections", + "fieldtype": "Table", + "label": "Footer Sections", + "options": "Footer Section Mapping" + }, + { + "fieldname": "color_scheme_tab", + "fieldtype": "Tab Break", + "label": "Color Scheme" + }, + { + "default": "#b91c1c", + "fieldname": "primary_color", + "fieldtype": "Color", + "label": "Primary Color" + }, + { + "default": "#991b1b", + "fieldname": "primary_hover_color", + "fieldtype": "Color", + "label": "Primary Hover Color" + }, + { + "fieldname": "column_break_colors1", + "fieldtype": "Column Break" + }, + { + "default": "#7f1d1d", + "fieldname": "link_color", + "fieldtype": "Color", + "label": "Link Color" + }, + { + "default": "#991b1b", + "fieldname": "link_hover_color", + "fieldtype": "Color", + "label": "Link Hover Color" + }, + { + "fieldname": "column_break_colors2", + "fieldtype": "Column Break" + }, + { + "default": "#b91c1c", + "fieldname": "accent_color", + "fieldtype": "Color", + "label": "Accent Color" + }, + { + "default": "#b91c1c", + "fieldname": "border_accent_color", + "fieldtype": "Color", + "label": "Border Accent Color" + }, + { + "fieldname": "section_break_ui_colors", + "fieldtype": "Section Break", + "label": "UI Element Colors" + }, + { + "default": "#b91c1c", + "fieldname": "button_bg_color", + "fieldtype": "Color", + "label": "Button Background Color" + }, + { + "fieldname": "column_break_ui_colors1", + "fieldtype": "Column Break" + }, + { + "default": "#b91c1c", + "fieldname": "strikethrough_color", + "fieldtype": "Color", + "label": "Strikethrough Text Color" + }, + { + "default": "#b91c1c", + "fieldname": "badge_bg_color", + "fieldtype": "Color", + "label": "Badge Background Color" + }, + { + "fieldname": "column_break_ui_colors2", + "fieldtype": "Column Break" + }, + { + "default": "#991b1b", + "fieldname": "heading_accent_color", + "fieldtype": "Color", + "label": "Heading Accent Color" + }, + { + "default": "#b91c1c", + "fieldname": "brand_text_color", + "fieldtype": "Color", + "label": "Brand Text Color" + }, + { + "default": "#991b1b", + "fieldname": "secondary_accent_color", + "fieldtype": "Color", + "label": "Secondary Accent Color" + }, + { + "default": "#b91c1c", + "fieldname": "form_accent_color", + "fieldtype": "Color", + "label": "Form Accent Color" + }, + { + "default": "#b91c1c", + "fieldname": "focus_ring_color", + "fieldtype": "Color", + "label": "Focus Ring Color" + }, + { + "fieldname": "section_break_footer_colors", + "fieldtype": "Section Break", + "label": "Footer Colors" + }, + { + "default": "#111827", + "fieldname": "footer_bg_color", + "fieldtype": "Color", + "label": "Footer Background Color" + }, + { + "fieldname": "column_break_footer_colors", + "fieldtype": "Column Break" + }, + { + "default": "#ffffff", + "fieldname": "footer_text_color", + "fieldtype": "Color", + "label": "Footer Text Color" + }, { "fieldname": "ecommerce_item_group_mapping", "fieldtype": "Table", @@ -272,12 +627,31 @@ "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, "issingle": 1, "links": [], - "modified": "2025-06-30 13:14:47.225075", + "modified": "2025-10-07 15:34:41.977684", "modified_by": "Administrator", "module": "Lifestyle Shop Ecommerce", "name": "Lifestyle Settings", @@ -292,6 +666,30 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Website Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "All" } ], "row_format": "Dynamic", 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 8437ce0..fe94d73 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 @@ -15,36 +15,70 @@ class LifestyleSettings(Document): if TYPE_CHECKING: from frappe.types import DF - from ls_shop.lifestyle_shop_ecommerce.doctype.item_group_map.item_group_map import ( - ItemGroupMap, - ) - from ls_shop.lifestyle_shop_ecommerce.doctype.return_reason.return_reason import ( - ReturnReason, + from ls_shop.lifestyle_shop_ecommerce.doctype.footer_section_mapping.footer_section_mapping import ( + FooterSectionMapping, ) + from ls_shop.lifestyle_shop_ecommerce.doctype.item_group_map.item_group_map import ItemGroupMap + from ls_shop.lifestyle_shop_ecommerce.doctype.return_reason.return_reason import ReturnReason + accent_color: DF.Color | None attribute_name_field: DF.Data | None + badge_bg_color: DF.Color | None based_on_attribute: DF.Link | None + border_accent_color: DF.Color | None + brand_logo: DF.AttachImage | None + brand_text_color: DF.Color | None + button_bg_color: DF.Color | None cc_email: DF.Data | None charge_account_head: DF.Link | None cod_charge: DF.Currency cod_charge_applicable_below: DF.Currency cod_enabled: DF.Check + contact_email: DF.Data | None + contact_phone: DF.Data | None + copyright_text: DF.Data | None create_variants_automatically_on_configurator_creation: DF.Check default_price_list: DF.Link | None ecommerce_item_group_mapping: DF.Table[ItemGroupMap] ecommerce_warehouse: DF.Link | None + facebook_url: DF.Data | None + favicon: DF.Attach | None + focus_ring_color: DF.Color | None + footer_bg_color: DF.Color | None + footer_logo: DF.AttachImage | None + footer_sections: DF.Table[FooterSectionMapping] + footer_text_color: DF.Color | None + form_accent_color: DF.Color | None + heading_accent_color: DF.Color | None + instagram_url: DF.Data | None item_in_stock_email_template: DF.Link + link_color: DF.Color | None + link_hover_color: DF.Color | None logo_url: DF.Data | None + newsletter_description: DF.Text | None + newsletter_title: DF.Data | None order_cancellation_email_template: DF.Link order_confirmation_email_template: DF.Link + payment_methods_image: DF.AttachImage | None + primary_color: DF.Color | None + primary_hover_color: DF.Color | None print_format: DF.Link | None reason_for_return: DF.Table[ReturnReason] return_period: DF.Int sale_price_list: DF.Link | None + secondary_accent_color: DF.Color | None shipping_rule: DF.Link | None + snapchat_url: DF.Data | None + store_name: DF.Data | None + strikethrough_color: DF.Color | None tabby_enabled: DF.Check telr_enabled: DF.Check + tiktok_url: DF.Data | None + twitter_url: DF.Data | None + vat_certificate_image: DF.AttachImage | None + working_hours: DF.Data | None # end: auto-generated types + pass def validate(self): @@ -88,6 +122,93 @@ def sync_item_group_mapping_to_ecommerce_items(self): 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_theme_css(self): + """Generate CSS custom properties from color scheme settings""" + return f""" + + """ + def generate_configurators_for_all_templates(attribute: str, log_name: str): item = frappe.qb.DocType("Item") diff --git a/ls_shop/migrate.py b/ls_shop/migrate.py index 7ee5b72..2739d71 100644 --- a/ls_shop/migrate.py +++ b/ls_shop/migrate.py @@ -4,9 +4,14 @@ def after_install(): create_payment_modes() try: - create_ecommerce_group() - except Exception as _: - frappe.errprint("Error creating Ecommerce groups") + create_default_email_templates() + except Exception as e: + import traceback + + error_msg = f"Error creating default email templates: {e!s}" + frappe.log_error(traceback.format_exc(), "Lifestyle Shop Installation - Email Templates") + frappe.errprint(error_msg) + frappe.errprint(traceback.format_exc()) def create_payment_modes(): @@ -23,28 +28,60 @@ def create_payment_modes(): ).insert(ignore_if_duplicate=True) -# Create ecommerce item group +def create_default_email_templates(): + """Create default email templates required by Lifestyle Settings""" + + email_templates = [ + { + "name": "Order Confirmation", + "subject": "Order Confirmation - {{ doc.name }}", + "response": """Dear {{ doc.customer_name }}, + +Thank you for your order! Your order has been confirmed. + +Order Details: +Order ID: {{ doc.name }} +Date: {{ doc.transaction_date }} +Total: {{ doc.grand_total }} +You can track your order status at: {{ login_url }} -def create_ecommerce_group(): - parent = "Ecommerce Website" - parent_categories = {"Men", "Women", "Kids"} - # Create parent group - frappe.get_doc( +Best regards, +{{ company }}""", + "doctype": "Sales Order", + }, { - "doctype": "Item Group", - "item_group_name": parent, - "is_group": True, - "parent_item_group": "All Item Groups", - } - ).insert(ignore_if_duplicate=True) - for category in parent_categories: - frappe.get_doc( - { - "doctype": "Item Group", - "item_group_name": category, - "is_group": True, - "parent_item_group": parent, - "custom_item_group_display_name": category, - } - ).insert(ignore_if_duplicate=True) + "name": "Item In Stock", + "subject": "Item Back in Stock - {{ item.item_name }}", + "response": """Dear Customer, + +Great news! The item "{{ item.item_name }}" is now back in stock. + +You can purchase it now at: {{ item_url }} + +Best regards, +{{ company }}""", + "doctype": "Item", + }, + { + "name": "Order Cancellation", + "subject": "Order Cancellation Confirmation - {{ doc.name }}", + "response": """Dear {{ doc.customer_name }}, + +Your order {{ doc.name }} has been cancelled as requested. + +If you have any questions, please contact our customer service. + +Best regards, +{{ company }}""", + "doctype": "Sales Order", + }, + ] + + for template_data in email_templates: + if not frappe.db.exists("Email Template", template_data["name"]): + template = frappe.get_doc({"doctype": "Email Template", **template_data}) + template.insert(ignore_permissions=True) + frappe.errprint(f"Created Email Template: {template_data['name']}") + else: + frappe.errprint(f"Email Template '{template_data['name']}' already exists") diff --git a/ls_shop/publish_demo_items.py b/ls_shop/publish_demo_items.py new file mode 100644 index 0000000..2c59c12 --- /dev/null +++ b/ls_shop/publish_demo_items.py @@ -0,0 +1,117 @@ +""" +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() # nosemgrep: manual commit required for demo item publishing completion + frappe.clear_cache() + + print("\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("\nVisit: https://your-site.com/en/products") + print("\nNote: Routes should be clean slugs (e.g., 'tshirt-classic-black')") + print(" The /en/products/ prefix is added by hooks.py routing") + + +if __name__ == "__main__": + publish_all_demo_items() diff --git a/ls_shop/templates/components/brand_mobile_header_nav.html b/ls_shop/templates/components/brand_mobile_header_nav.html index 8e4090b..0529a4e 100644 --- a/ls_shop/templates/components/brand_mobile_header_nav.html +++ b/ls_shop/templates/components/brand_mobile_header_nav.html @@ -11,7 +11,7 @@