You've already forked wc-composable-product
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9df6e4278 | |||
| a581ef42e6 | |||
| bcbf12702e |
43
CHANGELOG.md
43
CHANGELOG.md
@@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.1.0] - 2024-12-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Stock Management Integration**: Complete inventory tracking system for composable products
|
||||||
|
- Stock validation during product selection and add-to-cart
|
||||||
|
- Automatic stock deduction when orders are completed/processed
|
||||||
|
- Automatic stock restoration on order cancellation/refund
|
||||||
|
- Stock status indicators in product selector (In stock, Low stock, Out of stock)
|
||||||
|
- Visual feedback for out-of-stock items (disabled checkboxes, reduced opacity)
|
||||||
|
- Low stock warnings when 5 or fewer items remain
|
||||||
|
- Prevention of out-of-stock item selection
|
||||||
|
- Order notes documenting stock changes
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- New `Stock_Manager` class handling all stock operations
|
||||||
|
- Integration with WooCommerce order status hooks
|
||||||
|
- Stock information passed to frontend via Twig template
|
||||||
|
- Enhanced CSS styling for stock status badges
|
||||||
|
- Stock data stored in order item meta for accurate tracking
|
||||||
|
- Backorder support detection and handling
|
||||||
|
|
||||||
|
### Translation
|
||||||
|
|
||||||
|
- Added 8 new translatable strings for stock messages
|
||||||
|
- Updated Italian (Switzerland) translation with stock-related terms
|
||||||
|
- Updated translation template (.pot file)
|
||||||
|
|
||||||
|
## [1.0.1] - 2024-12-31
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fatal error "Class WC_Settings_Page not found" during plugin activation
|
||||||
|
- Changed initialization hook from `plugins_loaded` to `woocommerce_loaded` to ensure WooCommerce classes are available before plugin initialization
|
||||||
|
- Settings page now correctly integrates as a tab in WooCommerce > Settings
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Plugin now waits for `woocommerce_loaded` action before initializing
|
||||||
|
- Prevents race condition where WooCommerce classes weren't loaded yet
|
||||||
|
- Settings tab appears correctly in WooCommerce settings interface
|
||||||
|
|
||||||
## [1.0.0] - 2024-12-31
|
## [1.0.0] - 2024-12-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
67
CLAUDE.md
67
CLAUDE.md
@@ -47,17 +47,23 @@ Text domain: `wc-composable-product`
|
|||||||
- Ready for translation to any locale
|
- Ready for translation to any locale
|
||||||
- All translatable strings properly marked with text domain
|
- All translatable strings properly marked with text domain
|
||||||
|
|
||||||
**Planned Translations:**
|
**Available Translations:**
|
||||||
|
|
||||||
- `en_US` - English (United States) [base language]
|
- `en_US` - English (United States) [base language - .pot template]
|
||||||
- `de_DE` - German (Germany, formal)
|
- `de_DE` - German (Germany, formal) ✓ Complete
|
||||||
- `de_DE_informal` - German (Germany, informal "du")
|
- `de_DE_informal` - German (Germany, informal "du") ✓ Complete
|
||||||
- `de_CH` - German (Switzerland, formal "Sie")
|
- `de_CH` - German (Switzerland, formal "Sie") ✓ Complete
|
||||||
- `de_CH_informal` - German (Switzerland, informal "du")
|
- `de_CH_informal` - German (Switzerland, informal "du") ✓ Complete
|
||||||
- `fr_CH` - French (Switzerland)
|
- `fr_CH` - French (Switzerland) ✓ Complete
|
||||||
- `it_CH` - Italian (Switzerland)
|
- `it_CH` - Italian (Switzerland) ✓ Complete
|
||||||
|
|
||||||
Note: Swiss locales should use CHF currency formatting in examples (e.g., "CHF 50.-")
|
All .po files created with 40+ translated strings. Swiss locales include CHF currency formatting in examples (e.g., "CHF 50.-").
|
||||||
|
|
||||||
|
To compile translations to .mo files for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
|
||||||
|
```
|
||||||
|
|
||||||
### Create releases
|
### Create releases
|
||||||
|
|
||||||
@@ -226,9 +232,9 @@ unzip -l wc-composable-product-vX.X.X.zip
|
|||||||
|
|
||||||
## Session History
|
## Session History
|
||||||
|
|
||||||
### v1.0.0 - Initial Implementation (2024-12-31)
|
### v1.0.0 - Initial Implementation & Release (2024-12-31)
|
||||||
|
|
||||||
**What was built:**
|
#### Session 1: Core Implementation
|
||||||
|
|
||||||
- Complete plugin implementation from scratch
|
- Complete plugin implementation from scratch
|
||||||
- All 6 core PHP classes with PSR-4 autoloading
|
- All 6 core PHP classes with PSR-4 autoloading
|
||||||
@@ -237,6 +243,26 @@ unzip -l wc-composable-product-vX.X.X.zip
|
|||||||
- Admin interface with WooCommerce integration
|
- Admin interface with WooCommerce integration
|
||||||
- Full i18n support with .pot template
|
- Full i18n support with .pot template
|
||||||
- Comprehensive documentation (README, INSTALL, IMPLEMENTATION)
|
- Comprehensive documentation (README, INSTALL, IMPLEMENTATION)
|
||||||
|
- Initial commit to `main` branch (1edb0be)
|
||||||
|
|
||||||
|
#### Session 2: Documentation & Translations
|
||||||
|
|
||||||
|
- Enhanced CLAUDE.md with complete architecture documentation
|
||||||
|
- Created 6 complete translation files (.po):
|
||||||
|
- German (Germany - formal & informal)
|
||||||
|
- German (Switzerland - formal & informal)
|
||||||
|
- French (Switzerland)
|
||||||
|
- Italian (Switzerland)
|
||||||
|
- All 40+ strings translated with locale-specific terminology
|
||||||
|
- Swiss locales include CHF currency formatting examples
|
||||||
|
|
||||||
|
#### Session 3: Release Creation
|
||||||
|
|
||||||
|
- Created annotated git tag `v1.0.0`
|
||||||
|
- Generated release package: `wc-composable-product-v1.0.0.zip` (371 KB)
|
||||||
|
- Verified vendor/ directory inclusion (336 Twig files)
|
||||||
|
- Created SHA-256 and MD5 checksums
|
||||||
|
- Stored in `releases/` directory (gitignored)
|
||||||
|
|
||||||
**Key decisions made:**
|
**Key decisions made:**
|
||||||
|
|
||||||
@@ -247,13 +273,13 @@ unzip -l wc-composable-product-vX.X.X.zip
|
|||||||
- AJAX add-to-cart for better UX
|
- AJAX add-to-cart for better UX
|
||||||
- Meta-based configuration storage
|
- Meta-based configuration storage
|
||||||
|
|
||||||
**Files created:** 21 files, 2,628 lines of code
|
**Files created:** 28 files total (21 PHP/templates + 7 translations), 3,842 lines of code
|
||||||
|
|
||||||
**Git workflow:**
|
**Git workflow:**
|
||||||
|
|
||||||
- Initial commit to `main` branch
|
- Main branch: Initial implementation (1edb0be)
|
||||||
- Created `dev` branch from `main`
|
- Dev branch: +2 commits for documentation and translations
|
||||||
- Both branches in sync at v1.0.0
|
- Tagged: v1.0.0 on dev branch (8c17734)
|
||||||
|
|
||||||
**What works:**
|
**What works:**
|
||||||
|
|
||||||
@@ -264,6 +290,8 @@ unzip -l wc-composable-product-vX.X.X.zip
|
|||||||
- AJAX add-to-cart ✓
|
- AJAX add-to-cart ✓
|
||||||
- Cart integration ✓
|
- Cart integration ✓
|
||||||
- Pricing calculation (both modes) ✓
|
- Pricing calculation (both modes) ✓
|
||||||
|
- Full multilingual support (6 locales) ✓
|
||||||
|
- Production-ready release package ✓
|
||||||
|
|
||||||
**Known limitations:**
|
**Known limitations:**
|
||||||
|
|
||||||
@@ -271,6 +299,14 @@ unzip -l wc-composable-product-vX.X.X.zip
|
|||||||
- No grouped product support
|
- No grouped product support
|
||||||
- No automatic stock deduction for selected items
|
- No automatic stock deduction for selected items
|
||||||
- Template cache requires manual clearing after updates
|
- Template cache requires manual clearing after updates
|
||||||
|
- Translations are .po files only (not compiled to .mo yet)
|
||||||
|
|
||||||
|
**Release details:**
|
||||||
|
|
||||||
|
- Package size: 371 KB
|
||||||
|
- Includes: All source + vendor dependencies + translations
|
||||||
|
- Checksums: SHA-256 and MD5 provided
|
||||||
|
- Ready for WordPress installation (no composer install needed)
|
||||||
|
|
||||||
**Future enhancements to consider:**
|
**Future enhancements to consider:**
|
||||||
|
|
||||||
@@ -280,6 +316,7 @@ unzip -l wc-composable-product-vX.X.X.zip
|
|||||||
- Product recommendations
|
- Product recommendations
|
||||||
- Stock management integration
|
- Stock management integration
|
||||||
- Selection presets/templates
|
- Selection presets/templates
|
||||||
|
- Compile .mo translation files
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ This document provides a technical overview of the WooCommerce Composable Produc
|
|||||||
|
|
||||||
### Plugin Structure
|
### Plugin Structure
|
||||||
|
|
||||||
```
|
```txt
|
||||||
wc-composable-product/
|
wc-composable-product/
|
||||||
├── assets/ # Frontend assets
|
├── assets/ # Frontend assets
|
||||||
│ ├── css/
|
│ ├── css/
|
||||||
@@ -42,6 +42,7 @@ wc-composable-product/
|
|||||||
### 1. Main Plugin Class (`Plugin.php`)
|
### 1. Main Plugin Class (`Plugin.php`)
|
||||||
|
|
||||||
**Responsibilities:**
|
**Responsibilities:**
|
||||||
|
|
||||||
- Singleton pattern implementation
|
- Singleton pattern implementation
|
||||||
- Twig template engine initialization
|
- Twig template engine initialization
|
||||||
- Hook registration
|
- Hook registration
|
||||||
@@ -49,6 +50,7 @@ wc-composable-product/
|
|||||||
- Asset enqueuing
|
- Asset enqueuing
|
||||||
|
|
||||||
**Key Methods:**
|
**Key Methods:**
|
||||||
|
|
||||||
- `instance()`: Get singleton instance
|
- `instance()`: Get singleton instance
|
||||||
- `init_twig()`: Initialize Twig with WordPress functions
|
- `init_twig()`: Initialize Twig with WordPress functions
|
||||||
- `render_template()`: Render Twig templates
|
- `render_template()`: Render Twig templates
|
||||||
@@ -59,6 +61,7 @@ wc-composable-product/
|
|||||||
**Extends:** `WC_Product`
|
**Extends:** `WC_Product`
|
||||||
|
|
||||||
**Key Features:**
|
**Key Features:**
|
||||||
|
|
||||||
- Custom product type: `composable`
|
- Custom product type: `composable`
|
||||||
- Selection limit management (per-product or global)
|
- Selection limit management (per-product or global)
|
||||||
- Pricing mode (fixed or sum)
|
- Pricing mode (fixed or sum)
|
||||||
@@ -67,6 +70,7 @@ wc-composable-product/
|
|||||||
- Price calculation
|
- Price calculation
|
||||||
|
|
||||||
**Key Methods:**
|
**Key Methods:**
|
||||||
|
|
||||||
- `get_selection_limit()`: Get max selectable items
|
- `get_selection_limit()`: Get max selectable items
|
||||||
- `get_pricing_mode()`: Get pricing calculation mode
|
- `get_pricing_mode()`: Get pricing calculation mode
|
||||||
- `get_available_products()`: Query available products
|
- `get_available_products()`: Query available products
|
||||||
@@ -77,6 +81,7 @@ wc-composable-product/
|
|||||||
**Extends:** `WC_Settings_Page`
|
**Extends:** `WC_Settings_Page`
|
||||||
|
|
||||||
**Global Settings:**
|
**Global Settings:**
|
||||||
|
|
||||||
- Default selection limit
|
- Default selection limit
|
||||||
- Default pricing mode
|
- Default pricing mode
|
||||||
- Display options (images, prices, total)
|
- Display options (images, prices, total)
|
||||||
@@ -86,12 +91,14 @@ wc-composable-product/
|
|||||||
### 4. Product Data Tab (`Admin/Product_Data.php`)
|
### 4. Product Data Tab (`Admin/Product_Data.php`)
|
||||||
|
|
||||||
**Responsibilities:**
|
**Responsibilities:**
|
||||||
|
|
||||||
- Add "Composable Options" tab to product edit page
|
- Add "Composable Options" tab to product edit page
|
||||||
- Render selection criteria fields
|
- Render selection criteria fields
|
||||||
- Save product meta data
|
- Save product meta data
|
||||||
- Dynamic field visibility based on criteria type
|
- Dynamic field visibility based on criteria type
|
||||||
|
|
||||||
**Saved Meta:**
|
**Saved Meta:**
|
||||||
|
|
||||||
- `_composable_selection_limit`: Item limit
|
- `_composable_selection_limit`: Item limit
|
||||||
- `_composable_pricing_mode`: Pricing calculation
|
- `_composable_pricing_mode`: Pricing calculation
|
||||||
- `_composable_criteria_type`: Selection method
|
- `_composable_criteria_type`: Selection method
|
||||||
@@ -102,11 +109,13 @@ wc-composable-product/
|
|||||||
### 5. Product Selector (`Product_Selector.php`)
|
### 5. Product Selector (`Product_Selector.php`)
|
||||||
|
|
||||||
**Responsibilities:**
|
**Responsibilities:**
|
||||||
|
|
||||||
- Render frontend product selection interface
|
- Render frontend product selection interface
|
||||||
- Prepare data for Twig template
|
- Prepare data for Twig template
|
||||||
- Apply display settings
|
- Apply display settings
|
||||||
|
|
||||||
**Template Variables:**
|
**Template Variables:**
|
||||||
|
|
||||||
- `products`: Available products array
|
- `products`: Available products array
|
||||||
- `selection_limit`: Max selections
|
- `selection_limit`: Max selections
|
||||||
- `pricing_mode`: Pricing calculation
|
- `pricing_mode`: Pricing calculation
|
||||||
@@ -115,12 +124,14 @@ wc-composable-product/
|
|||||||
### 6. Cart Handler (`Cart_Handler.php`)
|
### 6. Cart Handler (`Cart_Handler.php`)
|
||||||
|
|
||||||
**Responsibilities:**
|
**Responsibilities:**
|
||||||
|
|
||||||
- Validate product selection
|
- Validate product selection
|
||||||
- Add selected products to cart data
|
- Add selected products to cart data
|
||||||
- Calculate dynamic pricing
|
- Calculate dynamic pricing
|
||||||
- Display selected products in cart
|
- Display selected products in cart
|
||||||
|
|
||||||
**Hooks:**
|
**Hooks:**
|
||||||
|
|
||||||
- `woocommerce_add_to_cart_validation`: Validate selections
|
- `woocommerce_add_to_cart_validation`: Validate selections
|
||||||
- `woocommerce_add_cart_item_data`: Store selections
|
- `woocommerce_add_cart_item_data`: Store selections
|
||||||
- `woocommerce_before_calculate_totals`: Update prices
|
- `woocommerce_before_calculate_totals`: Update prices
|
||||||
@@ -131,6 +142,7 @@ wc-composable-product/
|
|||||||
### Product Selector Template (`product-selector.twig`)
|
### Product Selector Template (`product-selector.twig`)
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
|
|
||||||
- Responsive grid layout
|
- Responsive grid layout
|
||||||
- Checkbox-based selection
|
- Checkbox-based selection
|
||||||
- Product images and prices
|
- Product images and prices
|
||||||
@@ -138,6 +150,7 @@ wc-composable-product/
|
|||||||
- AJAX add-to-cart
|
- AJAX add-to-cart
|
||||||
|
|
||||||
**Data Attributes:**
|
**Data Attributes:**
|
||||||
|
|
||||||
- `data-product-id`: Composable product ID
|
- `data-product-id`: Composable product ID
|
||||||
- `data-selection-limit`: Max selections
|
- `data-selection-limit`: Max selections
|
||||||
- `data-pricing-mode`: Pricing mode
|
- `data-pricing-mode`: Pricing mode
|
||||||
@@ -146,6 +159,7 @@ wc-composable-product/
|
|||||||
### JavaScript (`frontend.js`)
|
### JavaScript (`frontend.js`)
|
||||||
|
|
||||||
**Functionality:**
|
**Functionality:**
|
||||||
|
|
||||||
- Selection limit enforcement
|
- Selection limit enforcement
|
||||||
- Visual feedback on selection
|
- Visual feedback on selection
|
||||||
- Real-time price updates (sum mode)
|
- Real-time price updates (sum mode)
|
||||||
@@ -153,6 +167,7 @@ wc-composable-product/
|
|||||||
- Error/success messages
|
- Error/success messages
|
||||||
|
|
||||||
**Key Functions:**
|
**Key Functions:**
|
||||||
|
|
||||||
- `handleCheckboxChange()`: Selection logic
|
- `handleCheckboxChange()`: Selection logic
|
||||||
- `updateTotalPrice()`: Calculate total
|
- `updateTotalPrice()`: Calculate total
|
||||||
- `addToCart()`: AJAX add-to-cart
|
- `addToCart()`: AJAX add-to-cart
|
||||||
@@ -161,6 +176,7 @@ wc-composable-product/
|
|||||||
### CSS Styling
|
### CSS Styling
|
||||||
|
|
||||||
**Approach:**
|
**Approach:**
|
||||||
|
|
||||||
- Grid-based layout (responsive)
|
- Grid-based layout (responsive)
|
||||||
- Card-style product items
|
- Card-style product items
|
||||||
- Visual selection states
|
- Visual selection states
|
||||||
@@ -242,6 +258,7 @@ wc-composable-product/
|
|||||||
Generated template: `languages/wc-composable-product.pot`
|
Generated template: `languages/wc-composable-product.pot`
|
||||||
|
|
||||||
**Supported Locales (per CLAUDE.md):**
|
**Supported Locales (per CLAUDE.md):**
|
||||||
|
|
||||||
- en_US (English)
|
- en_US (English)
|
||||||
- de_DE, de_DE_informal (German - Germany)
|
- de_DE, de_DE_informal (German - Germany)
|
||||||
- de_CH, de_CH_informal (German - Switzerland)
|
- de_CH, de_CH_informal (German - Switzerland)
|
||||||
@@ -273,11 +290,13 @@ Generated template: `languages/wc-composable-product.pot`
|
|||||||
### Hooks & Filters
|
### Hooks & Filters
|
||||||
|
|
||||||
**Available Filters:**
|
**Available Filters:**
|
||||||
|
|
||||||
- `wc_composable_settings`: Modify settings array
|
- `wc_composable_settings`: Modify settings array
|
||||||
- `woocommerce_product_class`: Custom product class
|
- `woocommerce_product_class`: Custom product class
|
||||||
- `product_type_selector`: Product type registration
|
- `product_type_selector`: Product type registration
|
||||||
|
|
||||||
**Customization Points:**
|
**Customization Points:**
|
||||||
|
|
||||||
- Twig templates (override in theme)
|
- Twig templates (override in theme)
|
||||||
- CSS styling (enqueue custom styles)
|
- CSS styling (enqueue custom styles)
|
||||||
- JavaScript behavior (extend object)
|
- JavaScript behavior (extend object)
|
||||||
@@ -304,6 +323,7 @@ if ($product->get_type() === 'composable') {
|
|||||||
## Testing Checklist
|
## Testing Checklist
|
||||||
|
|
||||||
### Admin Testing
|
### Admin Testing
|
||||||
|
|
||||||
- [ ] Product type appears in dropdown
|
- [ ] Product type appears in dropdown
|
||||||
- [ ] Composable Options tab displays
|
- [ ] Composable Options tab displays
|
||||||
- [ ] Selection criteria toggle works
|
- [ ] Selection criteria toggle works
|
||||||
@@ -312,6 +332,7 @@ if ($product->get_type() === 'composable') {
|
|||||||
- [ ] Global defaults apply
|
- [ ] Global defaults apply
|
||||||
|
|
||||||
### Frontend Testing
|
### Frontend Testing
|
||||||
|
|
||||||
- [ ] Product selector renders
|
- [ ] Product selector renders
|
||||||
- [ ] Selection limit enforced
|
- [ ] Selection limit enforced
|
||||||
- [ ] Price calculation accurate (both modes)
|
- [ ] Price calculation accurate (both modes)
|
||||||
@@ -320,6 +341,7 @@ if ($product->get_type() === 'composable') {
|
|||||||
- [ ] Checkout processes correctly
|
- [ ] Checkout processes correctly
|
||||||
|
|
||||||
### Edge Cases
|
### Edge Cases
|
||||||
|
|
||||||
- [ ] Empty criteria (no products)
|
- [ ] Empty criteria (no products)
|
||||||
- [ ] Out of stock products excluded
|
- [ ] Out of stock products excluded
|
||||||
- [ ] Invalid product selections rejected
|
- [ ] Invalid product selections rejected
|
||||||
@@ -349,12 +371,14 @@ Potential features for future versions:
|
|||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
### Runtime
|
### Runtime
|
||||||
|
|
||||||
- PHP 8.3+
|
- PHP 8.3+
|
||||||
- WordPress 6.0+
|
- WordPress 6.0+
|
||||||
- WooCommerce 8.0+
|
- WooCommerce 8.0+
|
||||||
- Twig 3.0 (via Composer)
|
- Twig 3.0 (via Composer)
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
- Composer for dependency management
|
- Composer for dependency management
|
||||||
- WP-CLI for i18n operations (optional)
|
- WP-CLI for i18n operations (optional)
|
||||||
|
|
||||||
@@ -372,6 +396,7 @@ Potential features for future versions:
|
|||||||
### Release Package
|
### Release Package
|
||||||
|
|
||||||
Must include:
|
Must include:
|
||||||
|
|
||||||
- All PHP files
|
- All PHP files
|
||||||
- `vendor/` directory
|
- `vendor/` directory
|
||||||
- Assets (CSS, JS)
|
- Assets (CSS, JS)
|
||||||
@@ -380,6 +405,7 @@ Must include:
|
|||||||
- Documentation
|
- Documentation
|
||||||
|
|
||||||
Must exclude:
|
Must exclude:
|
||||||
|
|
||||||
- `.git/` directory
|
- `.git/` directory
|
||||||
- `composer.lock`
|
- `composer.lock`
|
||||||
- Development files
|
- Development files
|
||||||
@@ -388,12 +414,14 @@ Must exclude:
|
|||||||
## Support & Maintenance
|
## Support & Maintenance
|
||||||
|
|
||||||
### Code Standards
|
### Code Standards
|
||||||
|
|
||||||
- WordPress Coding Standards
|
- WordPress Coding Standards
|
||||||
- WooCommerce best practices
|
- WooCommerce best practices
|
||||||
- PSR-4 autoloading
|
- PSR-4 autoloading
|
||||||
- Inline documentation
|
- Inline documentation
|
||||||
|
|
||||||
### Version Control
|
### Version Control
|
||||||
|
|
||||||
- Semantic versioning (MAJOR.MINOR.PATCH)
|
- Semantic versioning (MAJOR.MINOR.PATCH)
|
||||||
- Changelog maintained
|
- Changelog maintained
|
||||||
- Annotated git tags
|
- Annotated git tags
|
||||||
|
|||||||
@@ -54,6 +54,20 @@
|
|||||||
background: #f0f8ff;
|
background: #f0f8ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.composable-product-item.out-of-stock {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composable-product-item.out-of-stock:hover {
|
||||||
|
border-color: #e0e0e0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composable-product-item.out-of-stock .product-item-label {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.product-item-label {
|
.product-item-label {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
@@ -94,6 +108,33 @@
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-item-stock {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-status.in-stock {
|
||||||
|
color: #2e7d32;
|
||||||
|
background: #e8f5e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-status.low-stock {
|
||||||
|
color: #f57c00;
|
||||||
|
background: #fff3e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-status.out-of-stock {
|
||||||
|
color: #c62828;
|
||||||
|
background: #ffebee;
|
||||||
|
}
|
||||||
|
|
||||||
.product-item-checkmark {
|
.product-item-checkmark {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
|
|||||||
@@ -15,16 +15,26 @@ defined('ABSPATH') || exit;
|
|||||||
* Handles adding composable products to cart and calculating prices
|
* Handles adding composable products to cart and calculating prices
|
||||||
*/
|
*/
|
||||||
class Cart_Handler {
|
class Cart_Handler {
|
||||||
|
/**
|
||||||
|
* Stock manager instance
|
||||||
|
*
|
||||||
|
* @var Stock_Manager
|
||||||
|
*/
|
||||||
|
private $stock_manager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*/
|
*/
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
|
$this->stock_manager = new Stock_Manager();
|
||||||
|
|
||||||
add_filter('woocommerce_add_to_cart_validation', [$this, 'validate_add_to_cart'], 10, 3);
|
add_filter('woocommerce_add_to_cart_validation', [$this, 'validate_add_to_cart'], 10, 3);
|
||||||
add_filter('woocommerce_add_cart_item_data', [$this, 'add_cart_item_data'], 10, 2);
|
add_filter('woocommerce_add_cart_item_data', [$this, 'add_cart_item_data'], 10, 2);
|
||||||
add_filter('woocommerce_get_cart_item_from_session', [$this, 'get_cart_item_from_session'], 10, 2);
|
add_filter('woocommerce_get_cart_item_from_session', [$this, 'get_cart_item_from_session'], 10, 2);
|
||||||
add_filter('woocommerce_get_item_data', [$this, 'display_cart_item_data'], 10, 2);
|
add_filter('woocommerce_get_item_data', [$this, 'display_cart_item_data'], 10, 2);
|
||||||
add_action('woocommerce_before_calculate_totals', [$this, 'calculate_cart_item_price']);
|
add_action('woocommerce_before_calculate_totals', [$this, 'calculate_cart_item_price']);
|
||||||
add_action('woocommerce_single_product_summary', [$this, 'render_product_selector'], 25);
|
add_action('woocommerce_single_product_summary', [$this, 'render_product_selector'], 25);
|
||||||
|
add_action('woocommerce_checkout_create_order_line_item', [$this->stock_manager, 'store_selected_products_in_order'], 10, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,6 +97,13 @@ class Cart_Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate stock availability
|
||||||
|
$stock_validation = $this->stock_manager->validate_stock_availability($selected_products, $quantity);
|
||||||
|
if ($stock_validation !== true) {
|
||||||
|
wc_add_notice($stock_validation, 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return $passed;
|
return $passed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ class Plugin {
|
|||||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Settings.php';
|
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Settings.php';
|
||||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Product_Data.php';
|
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Product_Data.php';
|
||||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Product_Type.php';
|
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Product_Type.php';
|
||||||
|
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Stock_Manager.php';
|
||||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Cart_Handler.php';
|
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Cart_Handler.php';
|
||||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Product_Selector.php';
|
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Product_Selector.php';
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,14 @@ class Product_Selector {
|
|||||||
$show_prices = get_option('wc_composable_show_prices', 'yes') === 'yes';
|
$show_prices = get_option('wc_composable_show_prices', 'yes') === 'yes';
|
||||||
$show_total = get_option('wc_composable_show_total', 'yes') === 'yes';
|
$show_total = get_option('wc_composable_show_total', 'yes') === 'yes';
|
||||||
|
|
||||||
|
// Get stock manager for stock information
|
||||||
|
$stock_manager = new Stock_Manager();
|
||||||
|
|
||||||
// Prepare product data for template
|
// Prepare product data for template
|
||||||
$products_data = [];
|
$products_data = [];
|
||||||
foreach ($available_products as $available_product) {
|
foreach ($available_products as $available_product) {
|
||||||
|
$stock_info = $stock_manager->get_product_stock_info($available_product->get_id());
|
||||||
|
|
||||||
$products_data[] = [
|
$products_data[] = [
|
||||||
'id' => $available_product->get_id(),
|
'id' => $available_product->get_id(),
|
||||||
'name' => $available_product->get_name(),
|
'name' => $available_product->get_name(),
|
||||||
@@ -43,6 +48,11 @@ class Product_Selector {
|
|||||||
'price_html' => $available_product->get_price_html(),
|
'price_html' => $available_product->get_price_html(),
|
||||||
'image_url' => wp_get_attachment_image_url($available_product->get_image_id(), 'thumbnail'),
|
'image_url' => wp_get_attachment_image_url($available_product->get_image_id(), 'thumbnail'),
|
||||||
'permalink' => $available_product->get_permalink(),
|
'permalink' => $available_product->get_permalink(),
|
||||||
|
'stock_status' => $stock_info['stock_status'],
|
||||||
|
'in_stock' => $stock_info['in_stock'],
|
||||||
|
'stock_quantity' => $stock_info['stock_quantity'],
|
||||||
|
'managing_stock' => $stock_info['managing_stock'],
|
||||||
|
'backorders_allowed' => $stock_info['backorders_allowed'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
283
includes/Stock_Manager.php
Normal file
283
includes/Stock_Manager.php
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Stock Manager
|
||||||
|
*
|
||||||
|
* @package WC_Composable_Product
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WC_Composable_Product;
|
||||||
|
|
||||||
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stock Manager Class
|
||||||
|
*
|
||||||
|
* Handles stock management for composable products
|
||||||
|
*/
|
||||||
|
class Stock_Manager {
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
// Hook into order completion to reduce stock
|
||||||
|
add_action('woocommerce_order_status_completed', [$this, 'reduce_stock_on_order_complete'], 10, 1);
|
||||||
|
add_action('woocommerce_order_status_processing', [$this, 'reduce_stock_on_order_complete'], 10, 1);
|
||||||
|
|
||||||
|
// Hook into order cancellation/refund to restore stock
|
||||||
|
add_action('woocommerce_order_status_cancelled', [$this, 'restore_stock_on_order_cancel'], 10, 1);
|
||||||
|
add_action('woocommerce_order_status_refunded', [$this, 'restore_stock_on_order_cancel'], 10, 1);
|
||||||
|
|
||||||
|
// Prevent double stock reduction
|
||||||
|
add_filter('woocommerce_can_reduce_order_stock', [$this, 'prevent_composable_stock_reduction'], 10, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate stock availability for selected products
|
||||||
|
*
|
||||||
|
* @param array $selected_product_ids Array of product IDs
|
||||||
|
* @param int $quantity Quantity of composable product being added
|
||||||
|
* @return bool|string True if in stock, error message otherwise
|
||||||
|
*/
|
||||||
|
public function validate_stock_availability($selected_product_ids, $quantity = 1) {
|
||||||
|
foreach ($selected_product_ids as $product_id) {
|
||||||
|
$product = wc_get_product($product_id);
|
||||||
|
|
||||||
|
if (!$product) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip stock check if stock management is disabled for this product
|
||||||
|
if (!$product->managing_stock()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stock_quantity = $product->get_stock_quantity();
|
||||||
|
|
||||||
|
// Check if product is in stock
|
||||||
|
if (!$product->is_in_stock()) {
|
||||||
|
return sprintf(
|
||||||
|
/* translators: %s: product name */
|
||||||
|
__('"%s" is out of stock and cannot be selected.', 'wc-composable-product'),
|
||||||
|
$product->get_name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if enough stock is available
|
||||||
|
if ($stock_quantity !== null && $stock_quantity < $quantity) {
|
||||||
|
return sprintf(
|
||||||
|
/* translators: 1: product name, 2: stock quantity */
|
||||||
|
__('Only %2$d of "%1$s" are available in stock.', 'wc-composable-product'),
|
||||||
|
$product->get_name(),
|
||||||
|
$stock_quantity
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for backorders
|
||||||
|
if ($product->backorders_allowed()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a product has sufficient stock
|
||||||
|
*
|
||||||
|
* @param int $product_id Product ID
|
||||||
|
* @param int $required_quantity Required quantity
|
||||||
|
* @return array Stock information [in_stock, stock_quantity, backorders_allowed]
|
||||||
|
*/
|
||||||
|
public function get_product_stock_info($product_id, $required_quantity = 1) {
|
||||||
|
$product = wc_get_product($product_id);
|
||||||
|
|
||||||
|
if (!$product) {
|
||||||
|
return [
|
||||||
|
'in_stock' => false,
|
||||||
|
'stock_quantity' => 0,
|
||||||
|
'backorders_allowed' => false,
|
||||||
|
'stock_status' => 'outofstock',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stock_quantity = $product->get_stock_quantity();
|
||||||
|
$managing_stock = $product->managing_stock();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'in_stock' => $product->is_in_stock(),
|
||||||
|
'stock_quantity' => $stock_quantity,
|
||||||
|
'backorders_allowed' => $product->backorders_allowed(),
|
||||||
|
'stock_status' => $product->get_stock_status(),
|
||||||
|
'managing_stock' => $managing_stock,
|
||||||
|
'has_enough_stock' => !$managing_stock || $stock_quantity === null || $stock_quantity >= $required_quantity,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduce stock for composable products when order is completed
|
||||||
|
*
|
||||||
|
* @param int $order_id Order ID
|
||||||
|
*/
|
||||||
|
public function reduce_stock_on_order_complete($order_id) {
|
||||||
|
$order = wc_get_order($order_id);
|
||||||
|
|
||||||
|
if (!$order) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if stock has already been reduced
|
||||||
|
if ($order->get_meta('_composable_stock_reduced', true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($order->get_items() as $item) {
|
||||||
|
$product = $item->get_product();
|
||||||
|
|
||||||
|
if (!$product || $product->get_type() !== 'composable') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get selected products from order item meta
|
||||||
|
$selected_products = $item->get_meta('_composable_products', true);
|
||||||
|
|
||||||
|
if (empty($selected_products) || !is_array($selected_products)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$quantity = $item->get_quantity();
|
||||||
|
|
||||||
|
// Reduce stock for each selected product
|
||||||
|
foreach ($selected_products as $product_id) {
|
||||||
|
$selected_product = wc_get_product($product_id);
|
||||||
|
|
||||||
|
if (!$selected_product || !$selected_product->managing_stock()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stock_quantity = $selected_product->get_stock_quantity();
|
||||||
|
|
||||||
|
if ($stock_quantity !== null) {
|
||||||
|
$new_stock = $stock_quantity - $quantity;
|
||||||
|
$selected_product->set_stock_quantity($new_stock);
|
||||||
|
$selected_product->save();
|
||||||
|
|
||||||
|
// Add order note
|
||||||
|
$order->add_order_note(
|
||||||
|
sprintf(
|
||||||
|
/* translators: 1: product name, 2: quantity, 3: remaining stock */
|
||||||
|
__('Stock reduced for "%1$s": -%2$d (remaining: %3$d)', 'wc-composable-product'),
|
||||||
|
$selected_product->get_name(),
|
||||||
|
$quantity,
|
||||||
|
$new_stock
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark stock as reduced
|
||||||
|
$order->update_meta_data('_composable_stock_reduced', true);
|
||||||
|
$order->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore stock when order is cancelled or refunded
|
||||||
|
*
|
||||||
|
* @param int $order_id Order ID
|
||||||
|
*/
|
||||||
|
public function restore_stock_on_order_cancel($order_id) {
|
||||||
|
$order = wc_get_order($order_id);
|
||||||
|
|
||||||
|
if (!$order) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if stock was reduced
|
||||||
|
if (!$order->get_meta('_composable_stock_reduced', true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($order->get_items() as $item) {
|
||||||
|
$product = $item->get_product();
|
||||||
|
|
||||||
|
if (!$product || $product->get_type() !== 'composable') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get selected products from order item meta
|
||||||
|
$selected_products = $item->get_meta('_composable_products', true);
|
||||||
|
|
||||||
|
if (empty($selected_products) || !is_array($selected_products)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$quantity = $item->get_quantity();
|
||||||
|
|
||||||
|
// Restore stock for each selected product
|
||||||
|
foreach ($selected_products as $product_id) {
|
||||||
|
$selected_product = wc_get_product($product_id);
|
||||||
|
|
||||||
|
if (!$selected_product || !$selected_product->managing_stock()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stock_quantity = $selected_product->get_stock_quantity();
|
||||||
|
|
||||||
|
if ($stock_quantity !== null) {
|
||||||
|
$new_stock = $stock_quantity + $quantity;
|
||||||
|
$selected_product->set_stock_quantity($new_stock);
|
||||||
|
$selected_product->save();
|
||||||
|
|
||||||
|
// Add order note
|
||||||
|
$order->add_order_note(
|
||||||
|
sprintf(
|
||||||
|
/* translators: 1: product name, 2: quantity, 3: new stock */
|
||||||
|
__('Stock restored for "%1$s": +%2$d (total: %3$d)', 'wc-composable-product'),
|
||||||
|
$selected_product->get_name(),
|
||||||
|
$quantity,
|
||||||
|
$new_stock
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark stock as restored
|
||||||
|
$order->update_meta_data('_composable_stock_reduced', false);
|
||||||
|
$order->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent WooCommerce from reducing stock for composable products
|
||||||
|
* We handle stock reduction manually for selected products
|
||||||
|
*
|
||||||
|
* @param bool $reduce_stock Whether to reduce stock
|
||||||
|
* @param \WC_Order $order Order object
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function prevent_composable_stock_reduction($reduce_stock, $order) {
|
||||||
|
foreach ($order->get_items() as $item) {
|
||||||
|
$product = $item->get_product();
|
||||||
|
|
||||||
|
if ($product && $product->get_type() === 'composable') {
|
||||||
|
// We'll handle stock reduction manually
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $reduce_stock;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store selected products in order item meta
|
||||||
|
*
|
||||||
|
* @param \WC_Order_Item_Product $item Order item
|
||||||
|
* @param string $cart_item_key Cart item key
|
||||||
|
* @param array $values Cart item values
|
||||||
|
*/
|
||||||
|
public function store_selected_products_in_order($item, $cart_item_key, $values) {
|
||||||
|
if (isset($values['composable_products']) && !empty($values['composable_products'])) {
|
||||||
|
$item->add_meta_data('_composable_products', $values['composable_products'], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -198,3 +198,35 @@ msgstr "Inserire i codici articolo dei prodotti separati da virgole."
|
|||||||
#: includes/Admin/Product_Data.php
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "SKU-1, SKU-2, SKU-3"
|
msgid "SKU-1, SKU-2, SKU-3"
|
||||||
msgstr "COD-1, COD-2, COD-3"
|
msgstr "COD-1, COD-2, COD-3"
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "\"%s\" is out of stock and cannot be selected."
|
||||||
|
msgstr "\"%s\" è esaurito e non può essere selezionato."
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||||
|
msgstr "Solo %2$d di \"%1$s\" sono disponibili in magazzino."
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||||
|
msgstr "Giacenza ridotta per \"%1$s\": -%2$d (rimanenti: %3$d)"
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||||
|
msgstr "Giacenza ripristinata per \"%1$s\": +%2$d (totale: %3$d)"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "Out of stock"
|
||||||
|
msgstr "Esaurito"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "Only"
|
||||||
|
msgstr "Solo"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "left"
|
||||||
|
msgstr "rimasti"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "In stock"
|
||||||
|
msgstr "Disponibile"
|
||||||
|
|||||||
@@ -197,3 +197,35 @@ msgstr ""
|
|||||||
#: includes/Admin/Product_Data.php
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "SKU-1, SKU-2, SKU-3"
|
msgid "SKU-1, SKU-2, SKU-3"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "\"%s\" is out of stock and cannot be selected."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "Out of stock"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "Only"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "left"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "In stock"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<div class="composable-products-grid">
|
<div class="composable-products-grid">
|
||||||
{% for product in products %}
|
{% for product in products %}
|
||||||
<div class="composable-product-item" data-product-id="{{ product.id }}" data-price="{{ product.price }}">
|
<div class="composable-product-item{% if not product.in_stock %} out-of-stock{% endif %}" data-product-id="{{ product.id }}" data-price="{{ product.price }}" data-stock-status="{{ product.stock_status }}">
|
||||||
<div class="product-item-inner">
|
<div class="product-item-inner">
|
||||||
<label class="product-item-label">
|
<label class="product-item-label">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
@@ -18,7 +18,8 @@
|
|||||||
value="{{ product.id }}"
|
value="{{ product.id }}"
|
||||||
class="composable-product-checkbox"
|
class="composable-product-checkbox"
|
||||||
data-product-id="{{ product.id }}"
|
data-product-id="{{ product.id }}"
|
||||||
data-price="{{ product.price }}">
|
data-price="{{ product.price }}"
|
||||||
|
{% if not product.in_stock %}disabled{% endif %}>
|
||||||
|
|
||||||
{% if show_images and product.image_url %}
|
{% if show_images and product.image_url %}
|
||||||
<div class="product-item-image">
|
<div class="product-item-image">
|
||||||
@@ -34,6 +35,16 @@
|
|||||||
{{ product.price_html|raw }}
|
{{ product.price_html|raw }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="product-item-stock">
|
||||||
|
{% if not product.in_stock %}
|
||||||
|
<span class="stock-status out-of-stock">{{ __('Out of stock') }}</span>
|
||||||
|
{% elseif product.managing_stock and product.stock_quantity is not null and product.stock_quantity <= 5 %}
|
||||||
|
<span class="stock-status low-stock">{{ __('Only') }} {{ product.stock_quantity }} {{ __('left') }}</span>
|
||||||
|
{% elseif product.in_stock %}
|
||||||
|
<span class="stock-status in-stock">{{ __('In stock') }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="product-item-checkmark"></span>
|
<span class="product-item-checkmark"></span>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: WooCommerce Composable Products
|
* Plugin Name: WooCommerce Composable Products
|
||||||
* Plugin URI: https://github.com/magdev/wc-composable-product
|
* Plugin URI: https://github.com/magdev/wc-composable-product
|
||||||
* Description: Create composable products where customers select a limited number of items from a configurable set
|
* Description: Create composable products where customers select a limited number of items from a configurable set
|
||||||
* Version: 1.0.0
|
* Version: 1.1.0
|
||||||
* Author: Marco Graetsch
|
* Author: Marco Graetsch
|
||||||
* Author URI: https://example.com
|
* Author URI: https://example.com
|
||||||
* License: GPL v3 or later
|
* License: GPL v3 or later
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
// Define plugin constants
|
// Define plugin constants
|
||||||
define('WC_COMPOSABLE_PRODUCT_VERSION', '1.0.0');
|
define('WC_COMPOSABLE_PRODUCT_VERSION', '1.1.0');
|
||||||
define('WC_COMPOSABLE_PRODUCT_FILE', __FILE__);
|
define('WC_COMPOSABLE_PRODUCT_FILE', __FILE__);
|
||||||
define('WC_COMPOSABLE_PRODUCT_PATH', plugin_dir_path(__FILE__));
|
define('WC_COMPOSABLE_PRODUCT_PATH', plugin_dir_path(__FILE__));
|
||||||
define('WC_COMPOSABLE_PRODUCT_URL', plugin_dir_url(__FILE__));
|
define('WC_COMPOSABLE_PRODUCT_URL', plugin_dir_url(__FILE__));
|
||||||
@@ -61,7 +61,8 @@ function wc_composable_product_init() {
|
|||||||
// Initialize main plugin class
|
// Initialize main plugin class
|
||||||
WC_Composable_Product\Plugin::instance();
|
WC_Composable_Product\Plugin::instance();
|
||||||
}
|
}
|
||||||
add_action('plugins_loaded', 'wc_composable_product_init');
|
// Use woocommerce_loaded to ensure WooCommerce classes are available
|
||||||
|
add_action('woocommerce_loaded', 'wc_composable_product_init');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activation hook
|
* Activation hook
|
||||||
|
|||||||
Reference in New Issue
Block a user