Implement comprehensive stock management integration (v1.1.0)

Added complete inventory tracking system for composable products:
- Stock validation during product selection and add-to-cart
- Automatic stock deduction on order completion/processing
- Automatic stock restoration on order cancellation/refund
- Stock status indicators with visual feedback (In stock, Low stock, Out of stock)
- Prevention of out-of-stock item selection
- Low stock warnings when 5 or fewer items remain
- Order notes documenting all stock changes

New files:
- includes/Stock_Manager.php: Core stock management logic

Modified files:
- includes/Cart_Handler.php: Integrated stock validation
- includes/Product_Selector.php: Added stock info to product data
- includes/Plugin.php: Added Stock_Manager to includes
- templates/product-selector.twig: Stock status display
- assets/css/frontend.css: Stock indicator styling
- languages/*.pot/*.po: 8 new translatable strings

Version bumped to 1.1.0 with updated CHANGELOG.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-31 16:41:53 +01:00
parent a581ef42e6
commit e9df6e4278
11 changed files with 489 additions and 5 deletions

View File

@@ -5,6 +5,35 @@ 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 ## [1.0.1] - 2024-12-31
### Fixed ### Fixed

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
} }

View File

@@ -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';

View File

@@ -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
View 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);
}
}
}

View File

@@ -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"

View File

@@ -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 ""

View File

@@ -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>

View File

@@ -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.1 * 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.1'); 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__));