You've already forked wc-composable-product
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ac1e0d6f7 | |||
| 8877ce976a | |||
| dea1b055b2 | |||
| a7d6a57f01 | |||
| ea2261d8d7 | |||
| dd5965ae4c | |||
| 9bc7a62f20 | |||
| ed66c96d3d | |||
| ea64dbfb33 |
@@ -6,7 +6,75 @@ on:
|
|||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: PHP Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
extensions: mbstring, xml, zip
|
||||||
|
tools: composer:v2
|
||||||
|
|
||||||
|
- name: PHP Syntax Check
|
||||||
|
run: |
|
||||||
|
find includes -name "*.php" -print0 | xargs -0 -n1 php -l
|
||||||
|
find tests -name "*.php" -print0 | xargs -0 -n1 php -l
|
||||||
|
|
||||||
|
phpcs:
|
||||||
|
name: PHP CodeSniffer
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
extensions: mbstring, xml
|
||||||
|
tools: composer:v2
|
||||||
|
|
||||||
|
- name: Install Composer dependencies
|
||||||
|
run: |
|
||||||
|
composer config platform.php 8.3.0
|
||||||
|
composer install --optimize-autoloader --no-interaction
|
||||||
|
|
||||||
|
- name: Run PHPCS
|
||||||
|
run: vendor/bin/phpcs
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: PHP Unit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: lint
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
extensions: mbstring, xml
|
||||||
|
tools: composer:v2
|
||||||
|
|
||||||
|
- name: Install Composer dependencies
|
||||||
|
run: |
|
||||||
|
composer config platform.php 8.3.0
|
||||||
|
composer install --optimize-autoloader --no-interaction
|
||||||
|
|
||||||
|
- name: Run PHPUnit tests
|
||||||
|
run: vendor/bin/phpunit --testdox
|
||||||
|
|
||||||
build-release:
|
build-release:
|
||||||
|
needs: [test, phpcs]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,14 +1,8 @@
|
|||||||
# Linked sources
|
|
||||||
wp-core
|
|
||||||
wp-plugins
|
|
||||||
tpp
|
|
||||||
|
|
||||||
# Editor swap files
|
# Editor swap files
|
||||||
*.*swp
|
*.*swp
|
||||||
|
|
||||||
# Composer
|
# Composer
|
||||||
vendor/
|
vendor/
|
||||||
composer.lock
|
|
||||||
|
|
||||||
# Cache
|
# Cache
|
||||||
cache/
|
cache/
|
||||||
@@ -16,9 +10,21 @@ cache/
|
|||||||
# Development files
|
# Development files
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
logs/
|
||||||
|
wp-core
|
||||||
|
wc-core
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# OS files
|
# OS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
.directory
|
||||||
|
|
||||||
|
# PHPUnit local overrides
|
||||||
|
phpunit.xml
|
||||||
|
|
||||||
|
# Binary files
|
||||||
|
languages/*.mo
|
||||||
|
|
||||||
|
.phpunit.result.cache
|
||||||
|
.phpunit.cache/
|
||||||
|
|||||||
60
CHANGELOG.md
60
CHANGELOG.md
@@ -5,6 +5,66 @@ 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.3.2] - 2026-03-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Custom page template** for composable products — replaces the standard WooCommerce two-column layout (large image gallery + summary) with a compact product info header and full-width product selector grid
|
||||||
|
- New Twig template `single-product-composable.html.twig` with PHP loader for WooCommerce template override
|
||||||
|
- Body class `single-product-composable` for CSS scoping on composable product pages
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Renamed Twig templates from `*.twig` to `*.html.twig` (proper Twig naming convention)
|
||||||
|
- Refreshed translation catalog (.pot) with accurate file references and line numbers
|
||||||
|
- Updated all .po/.mo translation files via `msgmerge`
|
||||||
|
|
||||||
|
## [1.3.1] - 2026-03-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **PHPUnit test suite**: 57 unit tests covering all 6 core classes (ProductType, StockManager, CartHandler, Plugin, Admin/ProductData, Admin/Settings)
|
||||||
|
- **Brain Monkey + Mockery** for WordPress/WooCommerce function mocking without a full WP installation
|
||||||
|
- **WooCommerce class stubs** in `tests/stubs/` for classes extended by the plugin (WC_Product, WC_Settings_Page, etc.)
|
||||||
|
- **PHPCS** with WordPress-Extra and PHPCompatibilityWP coding standards (`phpcs.xml.dist`)
|
||||||
|
- **PHPCS job** in release workflow — coding standards must pass before release is built
|
||||||
|
- **PHP lint job** in release workflow — syntax-checks all PHP files before testing
|
||||||
|
- **Test job** in release workflow — tests must pass before release package is built
|
||||||
|
- Testing and linting sections in README and CLAUDE.md
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **PSR-4 refactored**: Renamed files to PascalCase (Product_Type → ProductType, etc.) and changed namespace from `WC_Composable_Product` to `Magdev\WcComposableProduct`
|
||||||
|
- Updated all cross-references in PHP files, main plugin file, composer.json, CSS/JS doc comments, and translation file source comments
|
||||||
|
- **PHPUnit upgraded** from 9.6 to 10 (Brain Monkey 2.7 supports both)
|
||||||
|
- **WPCS formatting** applied to all source files (tabs, Yoda conditions, strict `in_array`, `wp_json_encode`, long array syntax)
|
||||||
|
- Release workflow now has four stages: lint + phpcs (parallel) → test → build-release
|
||||||
|
- Composer platform pinned to PHP 8.3 to prevent incompatible dependency locks
|
||||||
|
|
||||||
|
## [1.3.0] - 2026-03-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Include Non-Public Products**: New option to include draft and private products in composable product selections
|
||||||
|
- Global setting under WooCommerce > Settings > Composable Products
|
||||||
|
- Per-product override in the Composable Options tab (Use global default / Yes / No)
|
||||||
|
- Useful when products should only be sold as part of a composition, not individually
|
||||||
|
- Translations for the new setting in all 6 locales (de_DE, de_CH, fr_CH, it_CH + informal variants)
|
||||||
|
|
||||||
|
## [1.2.1] - 2026-03-01
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Consolidated documentation: merged INSTALL.md into README.md, merged IMPLEMENTATION.md into CLAUDE.md
|
||||||
|
- Condensed CLAUDE.md from ~1960 lines to ~160 lines, keeping only essential architecture and lessons learned
|
||||||
|
- README.md now includes full installation guide, usage tutorial, and troubleshooting section
|
||||||
|
- Cleaned up .gitignore
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- INSTALL.md (content merged into README.md)
|
||||||
|
- IMPLEMENTATION.md (content merged into CLAUDE.md)
|
||||||
|
|
||||||
## [1.2.0] - 2026-03-01
|
## [1.2.0] - 2026-03-01
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,434 +0,0 @@
|
|||||||
# WooCommerce Composable Products - Implementation Summary
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document provides a technical overview of the WooCommerce Composable Products plugin implementation.
|
|
||||||
|
|
||||||
**Version:** 1.0.0
|
|
||||||
**Created:** 2024-12-31
|
|
||||||
**AI-Generated:** 100% created with Claude.AI assistance
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Plugin Structure
|
|
||||||
|
|
||||||
```txt
|
|
||||||
wc-composable-product/
|
|
||||||
├── assets/ # Frontend assets
|
|
||||||
│ ├── css/
|
|
||||||
│ │ ├── admin.css # Admin styles
|
|
||||||
│ │ └── frontend.css # Frontend styles
|
|
||||||
│ └── js/
|
|
||||||
│ ├── admin.js # Admin JavaScript
|
|
||||||
│ └── frontend.js # Frontend JavaScript
|
|
||||||
├── cache/ # Twig template cache
|
|
||||||
├── includes/ # PHP classes
|
|
||||||
│ ├── Admin/
|
|
||||||
│ │ ├── Product_Data.php # Product data tab
|
|
||||||
│ │ └── Settings.php # Settings page
|
|
||||||
│ ├── Cart_Handler.php # Cart integration
|
|
||||||
│ ├── Plugin.php # Main plugin class
|
|
||||||
│ ├── Product_Selector.php # Frontend selector
|
|
||||||
│ └── Product_Type.php # Custom product type
|
|
||||||
├── languages/ # Translation files
|
|
||||||
│ └── wc-composable-product.pot
|
|
||||||
├── templates/ # Twig templates
|
|
||||||
│ └── product-selector.twig
|
|
||||||
└── wc-composable-product.php # Main plugin file
|
|
||||||
```
|
|
||||||
|
|
||||||
## Core Components
|
|
||||||
|
|
||||||
### 1. Main Plugin Class (`Plugin.php`)
|
|
||||||
|
|
||||||
**Responsibilities:**
|
|
||||||
|
|
||||||
- Singleton pattern implementation
|
|
||||||
- Twig template engine initialization
|
|
||||||
- Hook registration
|
|
||||||
- Component initialization
|
|
||||||
- Asset enqueuing
|
|
||||||
|
|
||||||
**Key Methods:**
|
|
||||||
|
|
||||||
- `instance()`: Get singleton instance
|
|
||||||
- `init_twig()`: Initialize Twig with WordPress functions
|
|
||||||
- `render_template()`: Render Twig templates
|
|
||||||
- `add_product_type()`: Register composable product type
|
|
||||||
|
|
||||||
### 2. Product Type (`Product_Type.php`)
|
|
||||||
|
|
||||||
**Extends:** `WC_Product`
|
|
||||||
|
|
||||||
**Key Features:**
|
|
||||||
|
|
||||||
- Custom product type: `composable`
|
|
||||||
- Selection limit management (per-product or global)
|
|
||||||
- Pricing mode (fixed or sum)
|
|
||||||
- Product selection criteria (category/tag/SKU)
|
|
||||||
- Dynamic product availability
|
|
||||||
- Price calculation
|
|
||||||
|
|
||||||
**Key Methods:**
|
|
||||||
|
|
||||||
- `get_selection_limit()`: Get max selectable items
|
|
||||||
- `get_pricing_mode()`: Get pricing calculation mode
|
|
||||||
- `get_available_products()`: Query available products
|
|
||||||
- `calculate_composed_price()`: Calculate final price
|
|
||||||
|
|
||||||
### 3. Admin Settings (`Admin/Settings.php`)
|
|
||||||
|
|
||||||
**Extends:** `WC_Settings_Page`
|
|
||||||
|
|
||||||
**Global Settings:**
|
|
||||||
|
|
||||||
- Default selection limit
|
|
||||||
- Default pricing mode
|
|
||||||
- Display options (images, prices, total)
|
|
||||||
|
|
||||||
**Integration:** Adds tab to WooCommerce Settings
|
|
||||||
|
|
||||||
### 4. Product Data Tab (`Admin/Product_Data.php`)
|
|
||||||
|
|
||||||
**Responsibilities:**
|
|
||||||
|
|
||||||
- Add "Composable Options" tab to product edit page
|
|
||||||
- Render selection criteria fields
|
|
||||||
- Save product meta data
|
|
||||||
- Dynamic field visibility based on criteria type
|
|
||||||
|
|
||||||
**Saved Meta:**
|
|
||||||
|
|
||||||
- `_composable_selection_limit`: Item limit
|
|
||||||
- `_composable_pricing_mode`: Pricing calculation
|
|
||||||
- `_composable_criteria_type`: Selection method
|
|
||||||
- `_composable_categories`: Selected categories
|
|
||||||
- `_composable_tags`: Selected tags
|
|
||||||
- `_composable_skus`: SKU list
|
|
||||||
|
|
||||||
### 5. Product Selector (`Product_Selector.php`)
|
|
||||||
|
|
||||||
**Responsibilities:**
|
|
||||||
|
|
||||||
- Render frontend product selection interface
|
|
||||||
- Prepare data for Twig template
|
|
||||||
- Apply display settings
|
|
||||||
|
|
||||||
**Template Variables:**
|
|
||||||
|
|
||||||
- `products`: Available products array
|
|
||||||
- `selection_limit`: Max selections
|
|
||||||
- `pricing_mode`: Pricing calculation
|
|
||||||
- `show_images/prices/total`: Display flags
|
|
||||||
|
|
||||||
### 6. Cart Handler (`Cart_Handler.php`)
|
|
||||||
|
|
||||||
**Responsibilities:**
|
|
||||||
|
|
||||||
- Validate product selection
|
|
||||||
- Add selected products to cart data
|
|
||||||
- Calculate dynamic pricing
|
|
||||||
- Display selected products in cart
|
|
||||||
|
|
||||||
**Hooks:**
|
|
||||||
|
|
||||||
- `woocommerce_add_to_cart_validation`: Validate selections
|
|
||||||
- `woocommerce_add_cart_item_data`: Store selections
|
|
||||||
- `woocommerce_before_calculate_totals`: Update prices
|
|
||||||
- `woocommerce_get_item_data`: Display in cart
|
|
||||||
|
|
||||||
## Frontend Implementation
|
|
||||||
|
|
||||||
### Product Selector Template (`product-selector.twig`)
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
- Responsive grid layout
|
|
||||||
- Checkbox-based selection
|
|
||||||
- Product images and prices
|
|
||||||
- Real-time total calculation
|
|
||||||
- AJAX add-to-cart
|
|
||||||
|
|
||||||
**Data Attributes:**
|
|
||||||
|
|
||||||
- `data-product-id`: Composable product ID
|
|
||||||
- `data-selection-limit`: Max selections
|
|
||||||
- `data-pricing-mode`: Pricing mode
|
|
||||||
- `data-price`: Individual product prices
|
|
||||||
|
|
||||||
### JavaScript (`frontend.js`)
|
|
||||||
|
|
||||||
**Functionality:**
|
|
||||||
|
|
||||||
- Selection limit enforcement
|
|
||||||
- Visual feedback on selection
|
|
||||||
- Real-time price updates (sum mode)
|
|
||||||
- AJAX cart operations
|
|
||||||
- Error/success messages
|
|
||||||
|
|
||||||
**Key Functions:**
|
|
||||||
|
|
||||||
- `handleCheckboxChange()`: Selection logic
|
|
||||||
- `updateTotalPrice()`: Calculate total
|
|
||||||
- `addToCart()`: AJAX add-to-cart
|
|
||||||
- `showMessage()`: User feedback
|
|
||||||
|
|
||||||
### CSS Styling
|
|
||||||
|
|
||||||
**Approach:**
|
|
||||||
|
|
||||||
- Grid-based layout (responsive)
|
|
||||||
- Card-style product items
|
|
||||||
- Visual selection states
|
|
||||||
- Mobile-first design
|
|
||||||
- Breakpoints: 768px, 480px
|
|
||||||
|
|
||||||
## Data Flow
|
|
||||||
|
|
||||||
### Creating a Composable Product
|
|
||||||
|
|
||||||
1. Admin selects "Composable product" type
|
|
||||||
2. Configure selection limit and pricing mode
|
|
||||||
3. Choose selection criteria (category/tag/SKU)
|
|
||||||
4. Save product metadata
|
|
||||||
5. WooCommerce registers product with custom type
|
|
||||||
|
|
||||||
### Frontend Display
|
|
||||||
|
|
||||||
1. Customer visits product page
|
|
||||||
2. `Cart_Handler` renders `Product_Selector`
|
|
||||||
3. `Product_Type::get_available_products()` queries products
|
|
||||||
4. Twig template renders grid with products
|
|
||||||
5. JavaScript handles interactions
|
|
||||||
|
|
||||||
### Adding to Cart
|
|
||||||
|
|
||||||
1. Customer selects products (JavaScript validation)
|
|
||||||
2. Click "Add to Cart" button
|
|
||||||
3. AJAX request with selected product IDs
|
|
||||||
4. `Cart_Handler::validate_add_to_cart()` validates
|
|
||||||
5. `Cart_Handler::add_cart_item_data()` stores selections
|
|
||||||
6. `Cart_Handler::calculate_cart_item_price()` updates price
|
|
||||||
7. Product added to cart with custom data
|
|
||||||
|
|
||||||
### Cart Display
|
|
||||||
|
|
||||||
1. WooCommerce loads cart
|
|
||||||
2. `Cart_Handler::get_cart_item_from_session()` restores data
|
|
||||||
3. `Cart_Handler::display_cart_item_data()` shows selections
|
|
||||||
4. Price calculated dynamically on each cart load
|
|
||||||
|
|
||||||
## Security Implementation
|
|
||||||
|
|
||||||
### Input Sanitization
|
|
||||||
|
|
||||||
- **Integers:** `absint()` for IDs and limits
|
|
||||||
- **Text:** `sanitize_text_field()` for modes and types
|
|
||||||
- **Textarea:** `sanitize_textarea_field()` for SKUs
|
|
||||||
- **Arrays:** `array_map()` with sanitization functions
|
|
||||||
|
|
||||||
### Output Escaping
|
|
||||||
|
|
||||||
- **HTML:** `esc_html()`, `esc_html_e()`
|
|
||||||
- **Attributes:** `esc_attr()`
|
|
||||||
- **URLs:** `esc_url()`
|
|
||||||
- **JavaScript:** Localized scripts with escaped data
|
|
||||||
|
|
||||||
### Validation
|
|
||||||
|
|
||||||
- Selection limit enforcement
|
|
||||||
- Product availability verification
|
|
||||||
- Cart data validation
|
|
||||||
- Nonce verification (via WooCommerce)
|
|
||||||
|
|
||||||
## Internationalization
|
|
||||||
|
|
||||||
### Text Domain
|
|
||||||
|
|
||||||
`wc-composable-product`
|
|
||||||
|
|
||||||
### Translation Functions
|
|
||||||
|
|
||||||
- `__()`: Return translated string
|
|
||||||
- `_e()`: Echo translated string
|
|
||||||
- `sprintf()` with `__()`: Variable substitution
|
|
||||||
|
|
||||||
### POT File
|
|
||||||
|
|
||||||
Generated template: `languages/wc-composable-product.pot`
|
|
||||||
|
|
||||||
**Supported Locales (per CLAUDE.md):**
|
|
||||||
|
|
||||||
- en_US (English)
|
|
||||||
- de_DE, de_DE_informal (German - Germany)
|
|
||||||
- de_CH, de_CH_informal (German - Switzerland)
|
|
||||||
- fr_CH (French - Switzerland)
|
|
||||||
- it_CH (Italian - Switzerland)
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
|
|
||||||
### Caching
|
|
||||||
|
|
||||||
- Twig templates cached in `cache/` directory
|
|
||||||
- Auto-reload enabled in debug mode
|
|
||||||
- Optimized Composer autoloader
|
|
||||||
|
|
||||||
### Database Queries
|
|
||||||
|
|
||||||
- Efficient `WP_Query` for product selection
|
|
||||||
- Meta queries for SKU filtering
|
|
||||||
- Taxonomy queries for category/tag filtering
|
|
||||||
|
|
||||||
### Asset Loading
|
|
||||||
|
|
||||||
- Scripts only on relevant pages
|
|
||||||
- Minification ready (use build tools)
|
|
||||||
- Conditional enqueuing
|
|
||||||
|
|
||||||
## Extensibility
|
|
||||||
|
|
||||||
### Hooks & Filters
|
|
||||||
|
|
||||||
**Available Filters:**
|
|
||||||
|
|
||||||
- `wc_composable_settings`: Modify settings array
|
|
||||||
- `woocommerce_product_class`: Custom product class
|
|
||||||
- `product_type_selector`: Product type registration
|
|
||||||
|
|
||||||
**Customization Points:**
|
|
||||||
|
|
||||||
- Twig templates (override in theme)
|
|
||||||
- CSS styling (enqueue custom styles)
|
|
||||||
- JavaScript behavior (extend object)
|
|
||||||
|
|
||||||
### Developer API
|
|
||||||
|
|
||||||
```php
|
|
||||||
// Get composable product
|
|
||||||
$product = wc_get_product($product_id);
|
|
||||||
|
|
||||||
// Check if composable
|
|
||||||
if ($product->get_type() === 'composable') {
|
|
||||||
// Get available products
|
|
||||||
$products = $product->get_available_products();
|
|
||||||
|
|
||||||
// Get selection limit
|
|
||||||
$limit = $product->get_selection_limit();
|
|
||||||
|
|
||||||
// Calculate price
|
|
||||||
$price = $product->calculate_composed_price($selected_ids);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Checklist
|
|
||||||
|
|
||||||
### Admin Testing
|
|
||||||
|
|
||||||
- [ ] Product type appears in dropdown
|
|
||||||
- [ ] Composable Options tab displays
|
|
||||||
- [ ] Selection criteria toggle works
|
|
||||||
- [ ] Meta data saves correctly
|
|
||||||
- [ ] Settings page accessible
|
|
||||||
- [ ] Global defaults apply
|
|
||||||
|
|
||||||
### Frontend Testing
|
|
||||||
|
|
||||||
- [ ] Product selector renders
|
|
||||||
- [ ] Selection limit enforced
|
|
||||||
- [ ] Price calculation accurate (both modes)
|
|
||||||
- [ ] AJAX add-to-cart works
|
|
||||||
- [ ] Cart displays selections
|
|
||||||
- [ ] Checkout processes correctly
|
|
||||||
|
|
||||||
### Edge Cases
|
|
||||||
|
|
||||||
- [ ] Empty criteria (no products)
|
|
||||||
- [ ] Out of stock products excluded
|
|
||||||
- [ ] Invalid product selections rejected
|
|
||||||
- [ ] Multiple cart items unique
|
|
||||||
- [ ] Session persistence
|
|
||||||
|
|
||||||
## Known Limitations
|
|
||||||
|
|
||||||
1. **Variable Products:** Currently supports simple products in selection
|
|
||||||
2. **Grouped Products:** Cannot be used as selectable items
|
|
||||||
3. **Stock Management:** No automatic stock reduction for selected items
|
|
||||||
4. **Caching:** Template cache needs manual clearing after updates
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
Potential features for future versions:
|
|
||||||
|
|
||||||
- Variable product support in selection
|
|
||||||
- Quantity selection per item (not just presence)
|
|
||||||
- Visual bundle previews
|
|
||||||
- Advanced pricing rules
|
|
||||||
- Stock management integration
|
|
||||||
- Product recommendations
|
|
||||||
- Selection templates/presets
|
|
||||||
- Multi-currency support enhancements
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
### Runtime
|
|
||||||
|
|
||||||
- PHP 8.3+
|
|
||||||
- WordPress 6.0+
|
|
||||||
- WooCommerce 8.0+
|
|
||||||
- Twig 3.0 (via Composer)
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
- Composer for dependency management
|
|
||||||
- WP-CLI for i18n operations (optional)
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
### Production Checklist
|
|
||||||
|
|
||||||
1. Run `composer install --no-dev --optimize-autoloader`
|
|
||||||
2. Ensure `vendor/` directory is included
|
|
||||||
3. Ensure `cache/` directory is writable
|
|
||||||
4. Test on staging environment
|
|
||||||
5. Clear all caches after activation
|
|
||||||
6. Verify WooCommerce compatibility
|
|
||||||
|
|
||||||
### Release Package
|
|
||||||
|
|
||||||
Must include:
|
|
||||||
|
|
||||||
- All PHP files
|
|
||||||
- `vendor/` directory
|
|
||||||
- Assets (CSS, JS)
|
|
||||||
- Templates
|
|
||||||
- Language files
|
|
||||||
- Documentation
|
|
||||||
|
|
||||||
Must exclude:
|
|
||||||
|
|
||||||
- `.git/` directory
|
|
||||||
- `composer.lock`
|
|
||||||
- Development files
|
|
||||||
- `wp-core/`, `wp-plugins/` symlinks
|
|
||||||
|
|
||||||
## Support & Maintenance
|
|
||||||
|
|
||||||
### Code Standards
|
|
||||||
|
|
||||||
- WordPress Coding Standards
|
|
||||||
- WooCommerce best practices
|
|
||||||
- PSR-4 autoloading
|
|
||||||
- Inline documentation
|
|
||||||
|
|
||||||
### Version Control
|
|
||||||
|
|
||||||
- Semantic versioning (MAJOR.MINOR.PATCH)
|
|
||||||
- Changelog maintained
|
|
||||||
- Annotated git tags
|
|
||||||
- Development on `dev` branch
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated:** 2024-12-31
|
|
||||||
**Maintainer:** Marco Graetsch
|
|
||||||
**AI Assistant:** Claude.AI (Anthropic)
|
|
||||||
150
INSTALL.md
150
INSTALL.md
@@ -1,150 +0,0 @@
|
|||||||
# Installation Guide
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
Before installing the WooCommerce Composable Products plugin, ensure your system meets these requirements:
|
|
||||||
|
|
||||||
- **PHP**: 8.3 or higher
|
|
||||||
- **WordPress**: 6.0 or higher
|
|
||||||
- **WooCommerce**: 8.0 or higher
|
|
||||||
- **Composer**: For dependency management
|
|
||||||
|
|
||||||
## Installation Steps
|
|
||||||
|
|
||||||
### 1. Upload Plugin Files
|
|
||||||
|
|
||||||
Upload the plugin directory to your WordPress installation:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
/wp-content/plugins/wc-composable-product/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Install Dependencies
|
|
||||||
|
|
||||||
Navigate to the plugin directory and install dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /wp-content/plugins/wc-composable-product/
|
|
||||||
composer install --no-dev --optimize-autoloader
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Activate Plugin
|
|
||||||
|
|
||||||
1. Log in to your WordPress admin panel
|
|
||||||
2. Navigate to **Plugins > Installed Plugins**
|
|
||||||
3. Find "WooCommerce Composable Products"
|
|
||||||
4. Click **Activate**
|
|
||||||
|
|
||||||
### 4. Configure Settings
|
|
||||||
|
|
||||||
After activation, configure the plugin:
|
|
||||||
|
|
||||||
1. Navigate to **WooCommerce > Settings**
|
|
||||||
2. Click on the **Composable Products** tab
|
|
||||||
3. Configure default settings:
|
|
||||||
- **Default Selection Limit**: Number of items customers can select (default: 5)
|
|
||||||
- **Default Pricing Mode**: Choose between "Sum of selected products" or "Fixed price"
|
|
||||||
- **Display Options**: Toggle product images, prices, and totals
|
|
||||||
|
|
||||||
## Creating Your First Composable Product
|
|
||||||
|
|
||||||
### Step 1: Create a New Product
|
|
||||||
|
|
||||||
1. Go to **Products > Add New**
|
|
||||||
2. Enter a product name (e.g., "Custom Sticker Pack")
|
|
||||||
|
|
||||||
### Step 2: Set Product Type
|
|
||||||
|
|
||||||
1. In the **Product Data** panel, select **Composable product** from the dropdown
|
|
||||||
|
|
||||||
### Step 3: Configure General Settings
|
|
||||||
|
|
||||||
In the **General** tab:
|
|
||||||
- Set a **Regular price** (used if pricing mode is "Fixed")
|
|
||||||
- Configure **Selection Limit** (leave empty to use global default)
|
|
||||||
- Choose **Pricing Mode** (leave empty to use global default)
|
|
||||||
|
|
||||||
### Step 4: Configure Composable Options
|
|
||||||
|
|
||||||
Click on the **Composable Options** tab:
|
|
||||||
|
|
||||||
1. **Selection Criteria**: Choose how to select available products
|
|
||||||
- **By Category**: Select product categories
|
|
||||||
- **By Tag**: Select product tags
|
|
||||||
- **By SKU**: Enter comma-separated SKUs
|
|
||||||
|
|
||||||
2. Based on your selection:
|
|
||||||
- **Categories**: Select one or more categories from the dropdown
|
|
||||||
- **Tags**: Select one or more tags from the dropdown
|
|
||||||
- **SKUs**: Enter SKUs like: `STICKER-01, STICKER-02, STICKER-03`
|
|
||||||
|
|
||||||
### Step 5: Publish
|
|
||||||
|
|
||||||
Click **Publish** to make your composable product live.
|
|
||||||
|
|
||||||
## Frontend Usage
|
|
||||||
|
|
||||||
When customers visit your composable product:
|
|
||||||
|
|
||||||
1. They see a grid of available products based on your criteria
|
|
||||||
2. They can select up to the configured limit
|
|
||||||
3. The total price updates in real-time (if using sum pricing mode)
|
|
||||||
4. Click "Add to Cart" to add the composition to their cart
|
|
||||||
5. Selected products are displayed in the cart
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Plugin Won't Activate
|
|
||||||
|
|
||||||
- Ensure WooCommerce is installed and activated first
|
|
||||||
- Check PHP version (must be 8.3+)
|
|
||||||
- Verify Composer dependencies are installed
|
|
||||||
|
|
||||||
### Products Not Showing in Selector
|
|
||||||
|
|
||||||
- Check that products are published and in stock
|
|
||||||
- Verify the selection criteria (category/tag/SKU) is correct
|
|
||||||
- Ensure products match the criteria you configured
|
|
||||||
|
|
||||||
### Twig Template Errors
|
|
||||||
|
|
||||||
- Ensure the `vendor/` directory exists and contains Twig
|
|
||||||
- Run `composer install` again
|
|
||||||
- Check that the `cache/` directory is writable
|
|
||||||
|
|
||||||
### JavaScript Not Working
|
|
||||||
|
|
||||||
- Clear browser cache
|
|
||||||
- Check browser console for errors
|
|
||||||
- Ensure jQuery is loaded (WooCommerce includes it)
|
|
||||||
|
|
||||||
## Updating
|
|
||||||
|
|
||||||
When updating the plugin:
|
|
||||||
|
|
||||||
1. Deactivate the plugin
|
|
||||||
2. Replace plugin files
|
|
||||||
3. Run `composer install --no-dev --optimize-autoloader`
|
|
||||||
4. Reactivate the plugin
|
|
||||||
5. Clear all caches (WordPress, browser, CDN)
|
|
||||||
|
|
||||||
## Uninstallation
|
|
||||||
|
|
||||||
To completely remove the plugin:
|
|
||||||
|
|
||||||
1. Deactivate the plugin
|
|
||||||
2. Delete the plugin from the Plugins page
|
|
||||||
3. Optionally clean up database entries (WooCommerce will handle this automatically)
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For issues and feature requests:
|
|
||||||
- GitHub: https://github.com/magdev/wc-composable-product/issues
|
|
||||||
- Documentation: See README.md
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
- Customize the template by editing `templates/product-selector.twig`
|
|
||||||
- Modify styles in `assets/css/frontend.css`
|
|
||||||
- Translate the plugin using the provided `.pot` file
|
|
||||||
- Create categories/tags for easier product organization
|
|
||||||
120
README.md
120
README.md
@@ -1,12 +1,8 @@
|
|||||||
# WooCommerce Composable Products
|
# WooCommerce Composable Products
|
||||||
|
|
||||||
Create composable products where customers can select a limited number of items from a configurable set of products.
|
Create composable products where customers can select a limited number of items from a configurable set of products. Think of it as a "build your own gift box" or "create your sticker pack" feature.
|
||||||
|
|
||||||
## Description
|
## Key Features
|
||||||
|
|
||||||
This plugin adds a new product type to WooCommerce that allows customers to build their own product bundles by selecting from a predefined set of simple or variable products. Think of it as a "build your own gift box" or "create your sticker pack" feature.
|
|
||||||
|
|
||||||
### Key Features
|
|
||||||
|
|
||||||
- **Custom Product Type**: New "Composable Product" type in WooCommerce
|
- **Custom Product Type**: New "Composable Product" type in WooCommerce
|
||||||
- **Flexible Selection**: Define available products by category, tag, or SKU
|
- **Flexible Selection**: Define available products by category, tag, or SKU
|
||||||
@@ -16,40 +12,102 @@ This plugin adds a new product type to WooCommerce that allows customers to buil
|
|||||||
- **Pricing Options**: Fixed price or sum of selected products with full locale-aware formatting
|
- **Pricing Options**: Fixed price or sum of selected products with full locale-aware formatting
|
||||||
- **Multi-language Support**: Fully translated in 6 locales (de_DE, de_CH, fr_CH, it_CH + informal variants)
|
- **Multi-language Support**: Fully translated in 6 locales (de_DE, de_CH, fr_CH, it_CH + informal variants)
|
||||||
- **Modern UI**: Clean interface built with Twig templates and vanilla JavaScript
|
- **Modern UI**: Clean interface built with Twig templates and vanilla JavaScript
|
||||||
- **CI/CD**: Automated release workflow for Gitea
|
- **Tested**: 57 unit tests with PHPUnit, Brain Monkey, and Mockery
|
||||||
|
- **CI/CD**: Automated release workflow with test gate for Gitea
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- PHP 8.3 or higher
|
- PHP 8.3 or higher
|
||||||
- WordPress 6.0 or higher
|
- WordPress 6.0 or higher
|
||||||
- WooCommerce 8.0 or higher
|
- WooCommerce 8.0 or higher
|
||||||
|
- Composer (for dependency management)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Upload the plugin files to `/wp-content/plugins/wc-composable-product/`
|
### From Release Package
|
||||||
2. Run `composer install --no-dev` in the plugin directory
|
|
||||||
3. Activate the plugin through the 'Plugins' menu in WordPress
|
1. Download the latest release ZIP from the releases page
|
||||||
4. Configure global settings under WooCommerce > Settings > Composable Products
|
2. In WordPress admin, go to **Plugins > Add New > Upload Plugin**
|
||||||
|
3. Upload the ZIP file and click **Install Now**
|
||||||
|
4. Activate the plugin through the **Plugins** menu
|
||||||
|
5. Configure global settings under **WooCommerce > Settings > Composable Products**
|
||||||
|
|
||||||
|
### From Source
|
||||||
|
|
||||||
|
1. Upload the plugin directory to `/wp-content/plugins/wc-composable-product/`
|
||||||
|
2. Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /wp-content/plugins/wc-composable-product/
|
||||||
|
composer install --no-dev --optimize-autoloader
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Activate the plugin through the **Plugins** menu in WordPress
|
||||||
|
4. Configure global settings under **WooCommerce > Settings > Composable Products**
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Creating a Composable Product
|
|
||||||
|
|
||||||
1. Go to Products > Add New
|
|
||||||
2. Select "Composable Product" as the product type
|
|
||||||
3. Configure product details:
|
|
||||||
- Set the selection limit (or use global default)
|
|
||||||
- Choose pricing mode (fixed or sum)
|
|
||||||
- Define available products by category, tag, or SKU
|
|
||||||
4. Publish the product
|
|
||||||
|
|
||||||
### Global Settings
|
### Global Settings
|
||||||
|
|
||||||
Navigate to WooCommerce > Settings > Composable Products to configure:
|
Navigate to **WooCommerce > Settings > Composable Products** to configure:
|
||||||
|
|
||||||
- Default selection limit
|
- **Default Selection Limit**: Number of items customers can select (default: 5)
|
||||||
- Default pricing mode
|
- **Default Pricing Mode**: Choose between "Sum of selected products" or "Fixed price"
|
||||||
- Display options
|
- **Display Options**: Toggle product images, prices, and totals
|
||||||
|
|
||||||
|
### Creating a Composable Product
|
||||||
|
|
||||||
|
1. Go to **Products > Add New**
|
||||||
|
2. Select **Composable product** from the Product Data dropdown
|
||||||
|
3. In the **General** tab:
|
||||||
|
- Set a **Regular price** (used when pricing mode is "Fixed")
|
||||||
|
- Configure **Selection Limit** (leave empty to use global default)
|
||||||
|
- Choose **Pricing Mode** (leave empty to use global default)
|
||||||
|
4. Click the **Composable Options** tab:
|
||||||
|
- **Selection Criteria**: Choose how to define available products
|
||||||
|
- **By Category**: Select one or more product categories
|
||||||
|
- **By Tag**: Select one or more product tags
|
||||||
|
- **By SKU**: Enter comma-separated SKUs (e.g., `STICKER-01, STICKER-02`)
|
||||||
|
5. Click **Publish**
|
||||||
|
|
||||||
|
### Frontend Behavior
|
||||||
|
|
||||||
|
When customers visit a composable product page:
|
||||||
|
|
||||||
|
1. A grid of available products is displayed based on configured criteria
|
||||||
|
2. Customers select up to the configured limit via checkboxes
|
||||||
|
3. Total price updates in real-time (in sum pricing mode)
|
||||||
|
4. Stock indicators show availability (green/orange/red badges)
|
||||||
|
5. Click "Add to Cart" to add the composition to cart
|
||||||
|
6. Selected products are listed in the cart and checkout
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Plugin Won't Activate
|
||||||
|
|
||||||
|
- Ensure WooCommerce is installed and activated first
|
||||||
|
- Check PHP version (must be 8.3+)
|
||||||
|
- Verify Composer dependencies are installed (`vendor/` directory exists)
|
||||||
|
|
||||||
|
### Products Not Showing in Selector
|
||||||
|
|
||||||
|
- Check that products are published
|
||||||
|
- Verify the selection criteria (category/tag/SKU) matches existing products
|
||||||
|
- Ensure the criteria type and values are saved in the Composable Options tab
|
||||||
|
|
||||||
|
### Twig Template Errors
|
||||||
|
|
||||||
|
- Ensure the `vendor/` directory exists and contains Twig
|
||||||
|
- Run `composer install` again
|
||||||
|
- Check that the `cache/` directory is writable
|
||||||
|
|
||||||
|
### Updating
|
||||||
|
|
||||||
|
1. Deactivate the plugin
|
||||||
|
2. Replace plugin files (or upload new release ZIP)
|
||||||
|
3. If installed from source: run `composer install --no-dev --optimize-autoloader`
|
||||||
|
4. Reactivate the plugin
|
||||||
|
5. Clear all caches (WordPress, browser, CDN)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -61,6 +119,16 @@ This project was created with AI assistance (Claude.AI) and follows WordPress an
|
|||||||
composer install
|
composer install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
The plugin includes a PHPUnit test suite with Brain Monkey for WordPress function mocking:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vendor/bin/phpunit --testdox
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests run without a WordPress installation. WooCommerce classes are provided as minimal stubs in `tests/stubs/`.
|
||||||
|
|
||||||
### Translation
|
### Translation
|
||||||
|
|
||||||
Generate POT file:
|
Generate POT file:
|
||||||
@@ -96,4 +164,4 @@ Marco Graetsch
|
|||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
For issues and feature requests, please use the GitHub issue tracker.
|
For issues and feature requests, please use the issue tracker.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Admin Styles for Composable Products
|
* Admin Styles for Composable Products
|
||||||
*
|
*
|
||||||
* @package WC_Composable_Product
|
* @package Magdev\WcComposableProduct
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Hide composable panel by default */
|
/* Hide composable panel by default */
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Frontend Styles for Composable Products
|
* Frontend Styles for Composable Products
|
||||||
*
|
*
|
||||||
* @package WC_Composable_Product
|
* @package Magdev\WcComposableProduct
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.wc-composable-product-selector {
|
.wc-composable-product-selector {
|
||||||
@@ -240,3 +240,85 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* =========================================================================
|
||||||
|
Composable product page layout
|
||||||
|
========================================================================= */
|
||||||
|
|
||||||
|
/* Compact product header */
|
||||||
|
.composable-product-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composable-header-thumbnail {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composable-header-thumbnail img {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composable-header-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composable-product-layout .product_title {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composable-header-price {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #2c3e50;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composable-header-description {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composable-header-description p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Full-width selector area */
|
||||||
|
.composable-selector-area {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override WooCommerce default product layout for composable products */
|
||||||
|
.single-product-composable .composable-product-layout {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-product-composable .composable-product-layout .wc-composable-product-selector {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive: stack header vertically on small screens */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.composable-product-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composable-product-layout .product_title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Admin JavaScript for Composable Products
|
* Admin JavaScript for Composable Products
|
||||||
*
|
*
|
||||||
* @package WC_Composable_Product
|
* @package Magdev\WcComposableProduct
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function($) {
|
(function($) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Frontend JavaScript for Composable Products
|
* Frontend JavaScript for Composable Products
|
||||||
*
|
*
|
||||||
* @package WC_Composable_Product
|
* @package Magdev\WcComposableProduct
|
||||||
*/
|
*/
|
||||||
|
|
||||||
(function($) {
|
(function($) {
|
||||||
|
|||||||
@@ -15,11 +15,31 @@
|
|||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"WC_Composable_Product\\": "includes/"
|
"Magdev\\WcComposableProduct\\": "includes/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"brain/monkey": "^2.7",
|
||||||
|
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"phpcompatibility/phpcompatibility-wp": "*",
|
||||||
|
"phpunit/phpunit": "^10.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.7",
|
||||||
|
"wp-coding-standards/wpcs": "^3.0"
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Magdev\\WcComposableProduct\\Tests\\": "tests/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"optimize-autoloader": true,
|
"optimize-autoloader": true,
|
||||||
"sort-packages": true
|
"sort-packages": true,
|
||||||
|
"allow-plugins": {
|
||||||
|
"dealerdirect/phpcodesniffer-composer-installer": true
|
||||||
|
},
|
||||||
|
"platform": {
|
||||||
|
"php": "8.3.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2889
composer.lock
generated
Normal file
2889
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
245
includes/Admin/ProductData.php
Normal file
245
includes/Admin/ProductData.php
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Product Data Tab
|
||||||
|
*
|
||||||
|
* @package Magdev\WcComposableProduct
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Magdev\WcComposableProduct\Admin;
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Product Data Tab Class
|
||||||
|
*/
|
||||||
|
class ProductData {
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
add_filter( 'woocommerce_product_data_tabs', array( $this, 'add_product_data_tab' ) );
|
||||||
|
add_action( 'woocommerce_product_data_panels', array( $this, 'add_product_data_panel' ) );
|
||||||
|
add_action( 'woocommerce_process_product_meta_composable', array( $this, 'save_product_data' ) );
|
||||||
|
add_action( 'woocommerce_product_options_general_product_data', array( $this, 'add_general_fields' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add composable products tab
|
||||||
|
*
|
||||||
|
* @param array $tabs Product data tabs
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function add_product_data_tab( $tabs ) {
|
||||||
|
$tabs['composable'] = array(
|
||||||
|
'label' => __( 'Composable Options', 'wc-composable-product' ),
|
||||||
|
'target' => 'composable_product_data',
|
||||||
|
'class' => array( 'show_if_composable' ),
|
||||||
|
'priority' => 21,
|
||||||
|
);
|
||||||
|
return $tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add fields to general tab
|
||||||
|
*/
|
||||||
|
public function add_general_fields() {
|
||||||
|
global $product_object;
|
||||||
|
|
||||||
|
if ( $product_object && $product_object->get_type() === 'composable' ) {
|
||||||
|
echo '<div class="options_group show_if_composable">';
|
||||||
|
|
||||||
|
woocommerce_wp_text_input(
|
||||||
|
array(
|
||||||
|
'id' => '_composable_selection_limit',
|
||||||
|
'label' => __( 'Selection Limit', 'wc-composable-product' ),
|
||||||
|
'description' => __( 'Maximum number of items customers can select. Leave empty to use global default.', 'wc-composable-product' ),
|
||||||
|
'desc_tip' => true,
|
||||||
|
'type' => 'number',
|
||||||
|
'custom_attributes' => array(
|
||||||
|
'min' => '1',
|
||||||
|
'step' => '1',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
woocommerce_wp_select(
|
||||||
|
array(
|
||||||
|
'id' => '_composable_pricing_mode',
|
||||||
|
'label' => __( 'Pricing Mode', 'wc-composable-product' ),
|
||||||
|
'description' => __( 'How to calculate the price.', 'wc-composable-product' ),
|
||||||
|
'desc_tip' => true,
|
||||||
|
'options' => array(
|
||||||
|
'' => __( 'Use global default', 'wc-composable-product' ),
|
||||||
|
'sum' => __( 'Sum of selected products', 'wc-composable-product' ),
|
||||||
|
'fixed' => __( 'Fixed price', 'wc-composable-product' ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
woocommerce_wp_text_input(
|
||||||
|
array(
|
||||||
|
'id' => '_regular_price',
|
||||||
|
'label' => __( 'Fixed Price', 'wc-composable-product' ) . ' (' . get_woocommerce_currency_symbol() . ')',
|
||||||
|
'description' => __( 'Enter the fixed price for this composable product.', 'wc-composable-product' ),
|
||||||
|
'desc_tip' => true,
|
||||||
|
'type' => 'text',
|
||||||
|
'data_type' => 'price',
|
||||||
|
'wrapper_class' => 'composable_fixed_price_field',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add product data panel
|
||||||
|
*/
|
||||||
|
public function add_product_data_panel() {
|
||||||
|
global $post;
|
||||||
|
?>
|
||||||
|
<div id="composable_product_data" class="panel woocommerce_options_panel hidden">
|
||||||
|
<div class="options_group">
|
||||||
|
<?php
|
||||||
|
woocommerce_wp_select(
|
||||||
|
array(
|
||||||
|
'id' => '_composable_include_unpublished',
|
||||||
|
'label' => __( 'Include Non-Public Products', 'wc-composable-product' ),
|
||||||
|
'description' => __( 'Allow draft and private products in the selection. Useful when products should only be sold as part of a composition.', 'wc-composable-product' ),
|
||||||
|
'desc_tip' => true,
|
||||||
|
'options' => array(
|
||||||
|
'' => __( 'Use global default', 'wc-composable-product' ),
|
||||||
|
'yes' => __( 'Yes', 'wc-composable-product' ),
|
||||||
|
'no' => __( 'No', 'wc-composable-product' ),
|
||||||
|
),
|
||||||
|
'value' => get_post_meta( $post->ID, '_composable_include_unpublished', true ) ? get_post_meta( $post->ID, '_composable_include_unpublished', true ) : '',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
woocommerce_wp_select(
|
||||||
|
array(
|
||||||
|
'id' => '_composable_criteria_type',
|
||||||
|
'label' => __( 'Selection Criteria', 'wc-composable-product' ),
|
||||||
|
'description' => __( 'How to select available products.', 'wc-composable-product' ),
|
||||||
|
'desc_tip' => true,
|
||||||
|
'options' => array(
|
||||||
|
'category' => __( 'By Category', 'wc-composable-product' ),
|
||||||
|
'tag' => __( 'By Tag', 'wc-composable-product' ),
|
||||||
|
'sku' => __( 'By SKU', 'wc-composable-product' ),
|
||||||
|
),
|
||||||
|
'value' => get_post_meta( $post->ID, '_composable_criteria_type', true ) ? get_post_meta( $post->ID, '_composable_criteria_type', true ) : 'category',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="options_group composable_criteria_group" id="composable_criteria_category">
|
||||||
|
<p class="form-field">
|
||||||
|
<label for="_composable_categories"><?php esc_html_e( 'Select Categories', 'wc-composable-product' ); ?></label>
|
||||||
|
<select id="_composable_categories" name="_composable_categories[]" class="wc-enhanced-select" multiple="multiple" style="width: 50%;">
|
||||||
|
<?php
|
||||||
|
$selected_categories = get_post_meta( $post->ID, '_composable_categories', true );
|
||||||
|
$selected_categories = $selected_categories ? $selected_categories : array();
|
||||||
|
$categories = get_terms(
|
||||||
|
array(
|
||||||
|
'taxonomy' => 'product_cat',
|
||||||
|
'hide_empty' => false,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
foreach ( $categories as $category ) {
|
||||||
|
printf(
|
||||||
|
'<option value="%s" %s>%s</option>',
|
||||||
|
esc_attr( $category->term_id ),
|
||||||
|
selected( in_array( $category->term_id, (array) $selected_categories, true ), true, false ),
|
||||||
|
esc_html( $category->name )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</select>
|
||||||
|
<span class="description"><?php esc_html_e( 'Select product categories to include.', 'wc-composable-product' ); ?></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="options_group composable_criteria_group" id="composable_criteria_tag" style="display: none;">
|
||||||
|
<p class="form-field">
|
||||||
|
<label for="_composable_tags"><?php esc_html_e( 'Select Tags', 'wc-composable-product' ); ?></label>
|
||||||
|
<select id="_composable_tags" name="_composable_tags[]" class="wc-enhanced-select" multiple="multiple" style="width: 50%;">
|
||||||
|
<?php
|
||||||
|
$selected_tags = get_post_meta( $post->ID, '_composable_tags', true );
|
||||||
|
$selected_tags = $selected_tags ? $selected_tags : array();
|
||||||
|
$tags = get_terms(
|
||||||
|
array(
|
||||||
|
'taxonomy' => 'product_tag',
|
||||||
|
'hide_empty' => false,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
foreach ( $tags as $tag ) {
|
||||||
|
printf(
|
||||||
|
'<option value="%s" %s>%s</option>',
|
||||||
|
esc_attr( $tag->term_id ),
|
||||||
|
selected( in_array( $tag->term_id, (array) $selected_tags, true ), true, false ),
|
||||||
|
esc_html( $tag->name )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</select>
|
||||||
|
<span class="description"><?php esc_html_e( 'Select product tags to include.', 'wc-composable-product' ); ?></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="options_group composable_criteria_group" id="composable_criteria_sku" style="display: none;">
|
||||||
|
<?php
|
||||||
|
woocommerce_wp_textarea_input(
|
||||||
|
array(
|
||||||
|
'id' => '_composable_skus',
|
||||||
|
'label' => __( 'Product SKUs', 'wc-composable-product' ),
|
||||||
|
'description' => __( 'Enter product SKUs separated by commas.', 'wc-composable-product' ),
|
||||||
|
'desc_tip' => true,
|
||||||
|
'placeholder' => __( 'SKU-1, SKU-2, SKU-3', 'wc-composable-product' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save product data
|
||||||
|
*
|
||||||
|
* @param int $post_id Post ID
|
||||||
|
*/
|
||||||
|
public function save_product_data( $post_id ) {
|
||||||
|
// phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce verified by WooCommerce in woocommerce_process_product_meta.
|
||||||
|
|
||||||
|
// Save selection limit.
|
||||||
|
$selection_limit = isset( $_POST['_composable_selection_limit'] ) ? absint( $_POST['_composable_selection_limit'] ) : '';
|
||||||
|
update_post_meta( $post_id, '_composable_selection_limit', $selection_limit );
|
||||||
|
|
||||||
|
// Save pricing mode.
|
||||||
|
$pricing_mode = isset( $_POST['_composable_pricing_mode'] ) ? sanitize_text_field( $_POST['_composable_pricing_mode'] ) : '';
|
||||||
|
update_post_meta( $post_id, '_composable_pricing_mode', $pricing_mode );
|
||||||
|
|
||||||
|
// Save include unpublished.
|
||||||
|
$include_unpublished = isset( $_POST['_composable_include_unpublished'] ) ? sanitize_text_field( $_POST['_composable_include_unpublished'] ) : '';
|
||||||
|
update_post_meta( $post_id, '_composable_include_unpublished', $include_unpublished );
|
||||||
|
|
||||||
|
// Save criteria type.
|
||||||
|
$criteria_type = isset( $_POST['_composable_criteria_type'] ) ? sanitize_text_field( $_POST['_composable_criteria_type'] ) : 'category';
|
||||||
|
update_post_meta( $post_id, '_composable_criteria_type', $criteria_type );
|
||||||
|
|
||||||
|
// Save categories.
|
||||||
|
$categories = isset( $_POST['_composable_categories'] ) ? array_map( 'absint', $_POST['_composable_categories'] ) : array();
|
||||||
|
update_post_meta( $post_id, '_composable_categories', $categories );
|
||||||
|
|
||||||
|
// Save tags.
|
||||||
|
$tags = isset( $_POST['_composable_tags'] ) ? array_map( 'absint', $_POST['_composable_tags'] ) : array();
|
||||||
|
update_post_meta( $post_id, '_composable_tags', $tags );
|
||||||
|
|
||||||
|
// Save SKUs.
|
||||||
|
$skus = isset( $_POST['_composable_skus'] ) ? sanitize_textarea_field( $_POST['_composable_skus'] ) : '';
|
||||||
|
update_post_meta( $post_id, '_composable_skus', $skus );
|
||||||
|
|
||||||
|
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Product Data Tab
|
|
||||||
*
|
|
||||||
* @package WC_Composable_Product
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace WC_Composable_Product\Admin;
|
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Product Data Tab Class
|
|
||||||
*/
|
|
||||||
class Product_Data {
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*/
|
|
||||||
public function __construct() {
|
|
||||||
add_filter('woocommerce_product_data_tabs', [$this, 'add_product_data_tab']);
|
|
||||||
add_action('woocommerce_product_data_panels', [$this, 'add_product_data_panel']);
|
|
||||||
add_action('woocommerce_process_product_meta_composable', [$this, 'save_product_data']);
|
|
||||||
add_action('woocommerce_product_options_general_product_data', [$this, 'add_general_fields']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add composable products tab
|
|
||||||
*
|
|
||||||
* @param array $tabs Product data tabs
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function add_product_data_tab($tabs) {
|
|
||||||
$tabs['composable'] = [
|
|
||||||
'label' => __('Composable Options', 'wc-composable-product'),
|
|
||||||
'target' => 'composable_product_data',
|
|
||||||
'class' => ['show_if_composable'],
|
|
||||||
'priority' => 21,
|
|
||||||
];
|
|
||||||
return $tabs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add fields to general tab
|
|
||||||
*/
|
|
||||||
public function add_general_fields() {
|
|
||||||
global $product_object;
|
|
||||||
|
|
||||||
if ($product_object && $product_object->get_type() === 'composable') {
|
|
||||||
echo '<div class="options_group show_if_composable">';
|
|
||||||
|
|
||||||
woocommerce_wp_text_input([
|
|
||||||
'id' => '_composable_selection_limit',
|
|
||||||
'label' => __('Selection Limit', 'wc-composable-product'),
|
|
||||||
'description' => __('Maximum number of items customers can select. Leave empty to use global default.', 'wc-composable-product'),
|
|
||||||
'desc_tip' => true,
|
|
||||||
'type' => 'number',
|
|
||||||
'custom_attributes' => [
|
|
||||||
'min' => '1',
|
|
||||||
'step' => '1',
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
woocommerce_wp_select([
|
|
||||||
'id' => '_composable_pricing_mode',
|
|
||||||
'label' => __('Pricing Mode', 'wc-composable-product'),
|
|
||||||
'description' => __('How to calculate the price.', 'wc-composable-product'),
|
|
||||||
'desc_tip' => true,
|
|
||||||
'options' => [
|
|
||||||
'' => __('Use global default', 'wc-composable-product'),
|
|
||||||
'sum' => __('Sum of selected products', 'wc-composable-product'),
|
|
||||||
'fixed' => __('Fixed price', 'wc-composable-product'),
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
woocommerce_wp_text_input([
|
|
||||||
'id' => '_regular_price',
|
|
||||||
'label' => __('Fixed Price', 'wc-composable-product') . ' (' . get_woocommerce_currency_symbol() . ')',
|
|
||||||
'description' => __('Enter the fixed price for this composable product.', 'wc-composable-product'),
|
|
||||||
'desc_tip' => true,
|
|
||||||
'type' => 'text',
|
|
||||||
'data_type' => 'price',
|
|
||||||
'wrapper_class' => 'composable_fixed_price_field',
|
|
||||||
]);
|
|
||||||
|
|
||||||
echo '</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add product data panel
|
|
||||||
*/
|
|
||||||
public function add_product_data_panel() {
|
|
||||||
global $post;
|
|
||||||
?>
|
|
||||||
<div id="composable_product_data" class="panel woocommerce_options_panel hidden">
|
|
||||||
<div class="options_group">
|
|
||||||
<?php
|
|
||||||
woocommerce_wp_select([
|
|
||||||
'id' => '_composable_criteria_type',
|
|
||||||
'label' => __('Selection Criteria', 'wc-composable-product'),
|
|
||||||
'description' => __('How to select available products.', 'wc-composable-product'),
|
|
||||||
'desc_tip' => true,
|
|
||||||
'options' => [
|
|
||||||
'category' => __('By Category', 'wc-composable-product'),
|
|
||||||
'tag' => __('By Tag', 'wc-composable-product'),
|
|
||||||
'sku' => __('By SKU', 'wc-composable-product'),
|
|
||||||
],
|
|
||||||
'value' => get_post_meta($post->ID, '_composable_criteria_type', true) ?: 'category',
|
|
||||||
]);
|
|
||||||
?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="options_group composable_criteria_group" id="composable_criteria_category">
|
|
||||||
<p class="form-field">
|
|
||||||
<label for="_composable_categories"><?php esc_html_e('Select Categories', 'wc-composable-product'); ?></label>
|
|
||||||
<select id="_composable_categories" name="_composable_categories[]" class="wc-enhanced-select" multiple="multiple" style="width: 50%;">
|
|
||||||
<?php
|
|
||||||
$selected_categories = get_post_meta($post->ID, '_composable_categories', true) ?: [];
|
|
||||||
$categories = get_terms(['taxonomy' => 'product_cat', 'hide_empty' => false]);
|
|
||||||
foreach ($categories as $category) {
|
|
||||||
printf(
|
|
||||||
'<option value="%s" %s>%s</option>',
|
|
||||||
esc_attr($category->term_id),
|
|
||||||
selected(in_array($category->term_id, (array) $selected_categories), true, false),
|
|
||||||
esc_html($category->name)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</select>
|
|
||||||
<span class="description"><?php esc_html_e('Select product categories to include.', 'wc-composable-product'); ?></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="options_group composable_criteria_group" id="composable_criteria_tag" style="display: none;">
|
|
||||||
<p class="form-field">
|
|
||||||
<label for="_composable_tags"><?php esc_html_e('Select Tags', 'wc-composable-product'); ?></label>
|
|
||||||
<select id="_composable_tags" name="_composable_tags[]" class="wc-enhanced-select" multiple="multiple" style="width: 50%;">
|
|
||||||
<?php
|
|
||||||
$selected_tags = get_post_meta($post->ID, '_composable_tags', true) ?: [];
|
|
||||||
$tags = get_terms(['taxonomy' => 'product_tag', 'hide_empty' => false]);
|
|
||||||
foreach ($tags as $tag) {
|
|
||||||
printf(
|
|
||||||
'<option value="%s" %s>%s</option>',
|
|
||||||
esc_attr($tag->term_id),
|
|
||||||
selected(in_array($tag->term_id, (array) $selected_tags), true, false),
|
|
||||||
esc_html($tag->name)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</select>
|
|
||||||
<span class="description"><?php esc_html_e('Select product tags to include.', 'wc-composable-product'); ?></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="options_group composable_criteria_group" id="composable_criteria_sku" style="display: none;">
|
|
||||||
<?php
|
|
||||||
woocommerce_wp_textarea_input([
|
|
||||||
'id' => '_composable_skus',
|
|
||||||
'label' => __('Product SKUs', 'wc-composable-product'),
|
|
||||||
'description' => __('Enter product SKUs separated by commas.', 'wc-composable-product'),
|
|
||||||
'desc_tip' => true,
|
|
||||||
'placeholder' => __('SKU-1, SKU-2, SKU-3', 'wc-composable-product'),
|
|
||||||
]);
|
|
||||||
?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save product data
|
|
||||||
*
|
|
||||||
* @param int $post_id Post ID
|
|
||||||
*/
|
|
||||||
public function save_product_data($post_id) {
|
|
||||||
// Save selection limit
|
|
||||||
$selection_limit = isset($_POST['_composable_selection_limit']) ? absint($_POST['_composable_selection_limit']) : '';
|
|
||||||
update_post_meta($post_id, '_composable_selection_limit', $selection_limit);
|
|
||||||
|
|
||||||
// Save pricing mode
|
|
||||||
$pricing_mode = isset($_POST['_composable_pricing_mode']) ? sanitize_text_field($_POST['_composable_pricing_mode']) : '';
|
|
||||||
update_post_meta($post_id, '_composable_pricing_mode', $pricing_mode);
|
|
||||||
|
|
||||||
// Save criteria type
|
|
||||||
$criteria_type = isset($_POST['_composable_criteria_type']) ? sanitize_text_field($_POST['_composable_criteria_type']) : 'category';
|
|
||||||
update_post_meta($post_id, '_composable_criteria_type', $criteria_type);
|
|
||||||
|
|
||||||
// Save categories
|
|
||||||
$categories = isset($_POST['_composable_categories']) ? array_map('absint', $_POST['_composable_categories']) : [];
|
|
||||||
update_post_meta($post_id, '_composable_categories', $categories);
|
|
||||||
|
|
||||||
// Save tags
|
|
||||||
$tags = isset($_POST['_composable_tags']) ? array_map('absint', $_POST['_composable_tags']) : [];
|
|
||||||
update_post_meta($post_id, '_composable_tags', $tags);
|
|
||||||
|
|
||||||
// Save SKUs
|
|
||||||
$skus = isset($_POST['_composable_skus']) ? sanitize_textarea_field($_POST['_composable_skus']) : '';
|
|
||||||
update_post_meta($post_id, '_composable_skus', $skus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
/**
|
/**
|
||||||
* Admin Settings
|
* Admin Settings
|
||||||
*
|
*
|
||||||
* @package WC_Composable_Product
|
* @package Magdev\WcComposableProduct
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace WC_Composable_Product\Admin;
|
namespace Magdev\WcComposableProduct\Admin;
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings class
|
* Settings class
|
||||||
@@ -18,7 +18,7 @@ class Settings extends \WC_Settings_Page {
|
|||||||
*/
|
*/
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->id = 'composable_products';
|
$this->id = 'composable_products';
|
||||||
$this->label = __('Composable Products', 'wc-composable-product');
|
$this->label = __( 'Composable Products', 'wc-composable-product' );
|
||||||
|
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
@@ -29,65 +29,72 @@ class Settings extends \WC_Settings_Page {
|
|||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function get_settings() {
|
public function get_settings() {
|
||||||
$settings = [
|
$settings = array(
|
||||||
[
|
array(
|
||||||
'title' => __('Composable Products Settings', 'wc-composable-product'),
|
'title' => __( 'Composable Products Settings', 'wc-composable-product' ),
|
||||||
'type' => 'title',
|
'type' => 'title',
|
||||||
'desc' => __('Configure default settings for composable products.', 'wc-composable-product'),
|
'desc' => __( 'Configure default settings for composable products.', 'wc-composable-product' ),
|
||||||
'id' => 'wc_composable_settings',
|
'id' => 'wc_composable_settings',
|
||||||
],
|
),
|
||||||
[
|
array(
|
||||||
'title' => __('Default Selection Limit', 'wc-composable-product'),
|
'title' => __( 'Default Selection Limit', 'wc-composable-product' ),
|
||||||
'desc' => __('Default number of items customers can select.', 'wc-composable-product'),
|
'desc' => __( 'Default number of items customers can select.', 'wc-composable-product' ),
|
||||||
'id' => 'wc_composable_default_limit',
|
'id' => 'wc_composable_default_limit',
|
||||||
'type' => 'number',
|
'type' => 'number',
|
||||||
'default' => '5',
|
'default' => '5',
|
||||||
'custom_attributes' => [
|
'custom_attributes' => array(
|
||||||
'min' => '1',
|
'min' => '1',
|
||||||
'step' => '1',
|
'step' => '1',
|
||||||
],
|
),
|
||||||
'desc_tip' => true,
|
'desc_tip' => true,
|
||||||
],
|
),
|
||||||
[
|
array(
|
||||||
'title' => __('Default Pricing Mode', 'wc-composable-product'),
|
'title' => __( 'Default Pricing Mode', 'wc-composable-product' ),
|
||||||
'desc' => __('How to calculate the price of composable products.', 'wc-composable-product'),
|
'desc' => __( 'How to calculate the price of composable products.', 'wc-composable-product' ),
|
||||||
'id' => 'wc_composable_default_pricing',
|
'id' => 'wc_composable_default_pricing',
|
||||||
'type' => 'select',
|
'type' => 'select',
|
||||||
'default' => 'sum',
|
'default' => 'sum',
|
||||||
'options' => [
|
'options' => array(
|
||||||
'sum' => __('Sum of selected products', 'wc-composable-product'),
|
'sum' => __( 'Sum of selected products', 'wc-composable-product' ),
|
||||||
'fixed' => __('Fixed price', 'wc-composable-product'),
|
'fixed' => __( 'Fixed price', 'wc-composable-product' ),
|
||||||
],
|
),
|
||||||
'desc_tip' => true,
|
'desc_tip' => true,
|
||||||
],
|
),
|
||||||
[
|
array(
|
||||||
'title' => __('Show Product Images', 'wc-composable-product'),
|
'title' => __( 'Include Non-Public Products', 'wc-composable-product' ),
|
||||||
'desc' => __('Display product images in the selection interface.', 'wc-composable-product'),
|
'desc' => __( 'Allow draft and private products to appear in composable product selections. Useful when products should only be sold as part of a composition, not individually.', 'wc-composable-product' ),
|
||||||
|
'id' => 'wc_composable_include_unpublished',
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'default' => 'no',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'title' => __( 'Show Product Images', 'wc-composable-product' ),
|
||||||
|
'desc' => __( 'Display product images in the selection interface.', 'wc-composable-product' ),
|
||||||
'id' => 'wc_composable_show_images',
|
'id' => 'wc_composable_show_images',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'default' => 'yes',
|
'default' => 'yes',
|
||||||
],
|
),
|
||||||
[
|
array(
|
||||||
'title' => __('Show Product Prices', 'wc-composable-product'),
|
'title' => __( 'Show Product Prices', 'wc-composable-product' ),
|
||||||
'desc' => __('Display individual product prices in the selection interface.', 'wc-composable-product'),
|
'desc' => __( 'Display individual product prices in the selection interface.', 'wc-composable-product' ),
|
||||||
'id' => 'wc_composable_show_prices',
|
'id' => 'wc_composable_show_prices',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'default' => 'yes',
|
'default' => 'yes',
|
||||||
],
|
),
|
||||||
[
|
array(
|
||||||
'title' => __('Show Total Price', 'wc-composable-product'),
|
'title' => __( 'Show Total Price', 'wc-composable-product' ),
|
||||||
'desc' => __('Display the total price as customers make selections.', 'wc-composable-product'),
|
'desc' => __( 'Display the total price as customers make selections.', 'wc-composable-product' ),
|
||||||
'id' => 'wc_composable_show_total',
|
'id' => 'wc_composable_show_total',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'default' => 'yes',
|
'default' => 'yes',
|
||||||
],
|
),
|
||||||
[
|
array(
|
||||||
'type' => 'sectionend',
|
'type' => 'sectionend',
|
||||||
'id' => 'wc_composable_settings',
|
'id' => 'wc_composable_settings',
|
||||||
],
|
),
|
||||||
];
|
);
|
||||||
|
|
||||||
return apply_filters('wc_composable_settings', $settings);
|
return apply_filters( 'wc_composable_settings', $settings );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,7 +102,7 @@ class Settings extends \WC_Settings_Page {
|
|||||||
*/
|
*/
|
||||||
public function output() {
|
public function output() {
|
||||||
$settings = $this->get_settings();
|
$settings = $this->get_settings();
|
||||||
\WC_Admin_Settings::output_fields($settings);
|
\WC_Admin_Settings::output_fields( $settings );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,6 +110,6 @@ class Settings extends \WC_Settings_Page {
|
|||||||
*/
|
*/
|
||||||
public function save() {
|
public function save() {
|
||||||
$settings = $this->get_settings();
|
$settings = $this->get_settings();
|
||||||
\WC_Admin_Settings::save_fields($settings);
|
\WC_Admin_Settings::save_fields( $settings );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
228
includes/CartHandler.php
Normal file
228
includes/CartHandler.php
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Cart Handler
|
||||||
|
*
|
||||||
|
* @package Magdev\WcComposableProduct
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Magdev\WcComposableProduct;
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cart Handler Class
|
||||||
|
*
|
||||||
|
* Handles adding composable products to cart and calculating prices
|
||||||
|
*/
|
||||||
|
class CartHandler {
|
||||||
|
/**
|
||||||
|
* Stock manager instance
|
||||||
|
*
|
||||||
|
* @var StockManager
|
||||||
|
*/
|
||||||
|
private $stock_manager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->stock_manager = new StockManager();
|
||||||
|
|
||||||
|
add_filter( 'woocommerce_add_to_cart_validation', array( $this, 'validate_add_to_cart' ), 10, 3 );
|
||||||
|
add_filter( 'woocommerce_add_cart_item_data', array( $this, 'add_cart_item_data' ), 10, 2 );
|
||||||
|
add_filter( 'woocommerce_get_cart_item_from_session', array( $this, 'get_cart_item_from_session' ), 10, 2 );
|
||||||
|
add_filter( 'woocommerce_get_item_data', array( $this, 'display_cart_item_data' ), 10, 2 );
|
||||||
|
add_action( 'woocommerce_before_calculate_totals', array( $this, 'calculate_cart_item_price' ) );
|
||||||
|
add_action( 'woocommerce_single_product_summary', array( $this, 'render_product_selector' ), 25 );
|
||||||
|
add_action( 'woocommerce_checkout_create_order_line_item', array( $this->stock_manager, 'store_selected_products_in_order' ), 10, 3 );
|
||||||
|
add_filter( 'woocommerce_is_purchasable', array( $this, 'hide_default_add_to_cart' ), 10, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide default WooCommerce add to cart button for composable products
|
||||||
|
*
|
||||||
|
* @param bool $is_purchasable Is purchasable status
|
||||||
|
* @param \WC_Product $product Product object
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hide_default_add_to_cart( $is_purchasable, $product ) {
|
||||||
|
if ( $product && $product->get_type() === 'composable' ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return $is_purchasable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render product selector on product page
|
||||||
|
*/
|
||||||
|
public function render_product_selector() {
|
||||||
|
global $product;
|
||||||
|
|
||||||
|
if ( $product && $product->get_type() === 'composable' ) {
|
||||||
|
ProductSelector::render( $product );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate add to cart
|
||||||
|
*
|
||||||
|
* @param bool $passed Validation status
|
||||||
|
* @param int $product_id Product ID
|
||||||
|
* @param int $quantity Quantity
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function validate_add_to_cart( $passed, $product_id, $quantity ) {
|
||||||
|
$product = wc_get_product( $product_id );
|
||||||
|
|
||||||
|
if ( ! $product || $product->get_type() !== 'composable' ) {
|
||||||
|
return $passed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if selected products are provided.
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by WooCommerce add-to-cart handler.
|
||||||
|
if ( ! isset( $_POST['composable_products'] ) || empty( $_POST['composable_products'] ) ) {
|
||||||
|
wc_add_notice( __( 'Please select at least one product.', 'wc-composable-product' ), 'error' );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by WooCommerce add-to-cart handler.
|
||||||
|
$selected_products = array_map( 'absint', $_POST['composable_products'] );
|
||||||
|
$selection_limit = $product->get_selection_limit();
|
||||||
|
|
||||||
|
// Validate selection limit
|
||||||
|
if ( count( $selected_products ) > $selection_limit ) {
|
||||||
|
/* translators: %d: selection limit */
|
||||||
|
wc_add_notice( sprintf( __( 'You can select a maximum of %d products.', 'wc-composable-product' ), $selection_limit ), 'error' );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( count( $selected_products ) === 0 ) {
|
||||||
|
wc_add_notice( __( 'Please select at least one product.', 'wc-composable-product' ), 'error' );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that selected products are valid
|
||||||
|
$available_products = $product->get_available_products();
|
||||||
|
$available_ids = array_map(
|
||||||
|
function ( $p ) {
|
||||||
|
return $p->get_id();
|
||||||
|
},
|
||||||
|
$available_products
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $selected_products as $selected_id ) {
|
||||||
|
if ( ! in_array( $selected_id, $available_ids, true ) ) {
|
||||||
|
wc_add_notice( __( 'One or more selected products are not available.', 'wc-composable-product' ), 'error' );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate stock availability
|
||||||
|
$stock_validation = $this->stock_manager->validate_stock_availability( $selected_products, $quantity );
|
||||||
|
if ( true !== $stock_validation ) {
|
||||||
|
wc_add_notice( $stock_validation, 'error' );
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $passed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add cart item data
|
||||||
|
*
|
||||||
|
* @param array $cart_item_data Cart item data
|
||||||
|
* @param int $product_id Product ID
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function add_cart_item_data( $cart_item_data, $product_id ) {
|
||||||
|
$product = wc_get_product( $product_id );
|
||||||
|
|
||||||
|
if ( ! $product || $product->get_type() !== 'composable' ) {
|
||||||
|
return $cart_item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by WooCommerce add-to-cart handler.
|
||||||
|
if ( isset( $_POST['composable_products'] ) && ! empty( $_POST['composable_products'] ) ) {
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified by WooCommerce add-to-cart handler.
|
||||||
|
$selected_products = array_map( 'absint', $_POST['composable_products'] );
|
||||||
|
$cart_item_data['composable_products'] = $selected_products;
|
||||||
|
|
||||||
|
// Make cart item unique.
|
||||||
|
$cart_item_data['unique_key'] = md5( wp_json_encode( $selected_products ) . time() );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cart_item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cart item from session
|
||||||
|
*
|
||||||
|
* @param array $cart_item Cart item
|
||||||
|
* @param array $values Values from session
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_cart_item_from_session( $cart_item, $values ) {
|
||||||
|
if ( isset( $values['composable_products'] ) ) {
|
||||||
|
$cart_item['composable_products'] = $values['composable_products'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cart_item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display cart item data
|
||||||
|
*
|
||||||
|
* @param array $item_data Item data
|
||||||
|
* @param array $cart_item Cart item
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function display_cart_item_data( $item_data, $cart_item ) {
|
||||||
|
if ( isset( $cart_item['composable_products'] ) && ! empty( $cart_item['composable_products'] ) ) {
|
||||||
|
$product_names = array();
|
||||||
|
foreach ( $cart_item['composable_products'] as $product_id ) {
|
||||||
|
$product = wc_get_product( $product_id );
|
||||||
|
if ( $product ) {
|
||||||
|
$product_names[] = $product->get_name();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty( $product_names ) ) {
|
||||||
|
$item_data[] = array(
|
||||||
|
'key' => __( 'Selected Products', 'wc-composable-product' ),
|
||||||
|
'value' => implode( ', ', $product_names ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate cart item price
|
||||||
|
*
|
||||||
|
* @param \WC_Cart $cart Cart object
|
||||||
|
*/
|
||||||
|
public function calculate_cart_item_price( $cart ) {
|
||||||
|
if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use static flag to prevent multiple executions within the same request
|
||||||
|
static $already_calculated = false;
|
||||||
|
if ( $already_calculated ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) {
|
||||||
|
if ( isset( $cart_item['data'] ) && $cart_item['data']->get_type() === 'composable' ) {
|
||||||
|
if ( isset( $cart_item['composable_products'] ) ) {
|
||||||
|
$product = $cart_item['data'];
|
||||||
|
$price = $product->calculate_composed_price( $cart_item['composable_products'] );
|
||||||
|
$cart_item['data']->set_price( $price );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$already_calculated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Cart Handler
|
|
||||||
*
|
|
||||||
* @package WC_Composable_Product
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace WC_Composable_Product;
|
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cart Handler Class
|
|
||||||
*
|
|
||||||
* Handles adding composable products to cart and calculating prices
|
|
||||||
*/
|
|
||||||
class Cart_Handler {
|
|
||||||
/**
|
|
||||||
* Stock manager instance
|
|
||||||
*
|
|
||||||
* @var Stock_Manager
|
|
||||||
*/
|
|
||||||
private $stock_manager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*/
|
|
||||||
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_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_item_data', [$this, 'display_cart_item_data'], 10, 2);
|
|
||||||
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_checkout_create_order_line_item', [$this->stock_manager, 'store_selected_products_in_order'], 10, 3);
|
|
||||||
add_filter('woocommerce_is_purchasable', [$this, 'hide_default_add_to_cart'], 10, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide default WooCommerce add to cart button for composable products
|
|
||||||
*
|
|
||||||
* @param bool $is_purchasable Is purchasable status
|
|
||||||
* @param \WC_Product $product Product object
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function hide_default_add_to_cart($is_purchasable, $product) {
|
|
||||||
if ($product && $product->get_type() === 'composable') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return $is_purchasable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render product selector on product page
|
|
||||||
*/
|
|
||||||
public function render_product_selector() {
|
|
||||||
global $product;
|
|
||||||
|
|
||||||
if ($product && $product->get_type() === 'composable') {
|
|
||||||
Product_Selector::render($product);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate add to cart
|
|
||||||
*
|
|
||||||
* @param bool $passed Validation status
|
|
||||||
* @param int $product_id Product ID
|
|
||||||
* @param int $quantity Quantity
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function validate_add_to_cart($passed, $product_id, $quantity) {
|
|
||||||
$product = wc_get_product($product_id);
|
|
||||||
|
|
||||||
if (!$product || $product->get_type() !== 'composable') {
|
|
||||||
return $passed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if selected products are provided
|
|
||||||
if (!isset($_POST['composable_products']) || empty($_POST['composable_products'])) {
|
|
||||||
wc_add_notice(__('Please select at least one product.', 'wc-composable-product'), 'error');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$selected_products = array_map('absint', $_POST['composable_products']);
|
|
||||||
$selection_limit = $product->get_selection_limit();
|
|
||||||
|
|
||||||
// Validate selection limit
|
|
||||||
if (count($selected_products) > $selection_limit) {
|
|
||||||
/* translators: %d: selection limit */
|
|
||||||
wc_add_notice(sprintf(__('You can select a maximum of %d products.', 'wc-composable-product'), $selection_limit), 'error');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count($selected_products) === 0) {
|
|
||||||
wc_add_notice(__('Please select at least one product.', 'wc-composable-product'), 'error');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate that selected products are valid
|
|
||||||
$available_products = $product->get_available_products();
|
|
||||||
$available_ids = array_map(function($p) {
|
|
||||||
return $p->get_id();
|
|
||||||
}, $available_products);
|
|
||||||
|
|
||||||
foreach ($selected_products as $selected_id) {
|
|
||||||
if (!in_array($selected_id, $available_ids)) {
|
|
||||||
wc_add_notice(__('One or more selected products are not available.', 'wc-composable-product'), 'error');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add cart item data
|
|
||||||
*
|
|
||||||
* @param array $cart_item_data Cart item data
|
|
||||||
* @param int $product_id Product ID
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function add_cart_item_data($cart_item_data, $product_id) {
|
|
||||||
$product = wc_get_product($product_id);
|
|
||||||
|
|
||||||
if (!$product || $product->get_type() !== 'composable') {
|
|
||||||
return $cart_item_data;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_POST['composable_products']) && !empty($_POST['composable_products'])) {
|
|
||||||
$selected_products = array_map('absint', $_POST['composable_products']);
|
|
||||||
$cart_item_data['composable_products'] = $selected_products;
|
|
||||||
|
|
||||||
// Make cart item unique
|
|
||||||
$cart_item_data['unique_key'] = md5(json_encode($selected_products) . time());
|
|
||||||
}
|
|
||||||
|
|
||||||
return $cart_item_data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cart item from session
|
|
||||||
*
|
|
||||||
* @param array $cart_item Cart item
|
|
||||||
* @param array $values Values from session
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function get_cart_item_from_session($cart_item, $values) {
|
|
||||||
if (isset($values['composable_products'])) {
|
|
||||||
$cart_item['composable_products'] = $values['composable_products'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $cart_item;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display cart item data
|
|
||||||
*
|
|
||||||
* @param array $item_data Item data
|
|
||||||
* @param array $cart_item Cart item
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function display_cart_item_data($item_data, $cart_item) {
|
|
||||||
if (isset($cart_item['composable_products']) && !empty($cart_item['composable_products'])) {
|
|
||||||
$product_names = [];
|
|
||||||
foreach ($cart_item['composable_products'] as $product_id) {
|
|
||||||
$product = wc_get_product($product_id);
|
|
||||||
if ($product) {
|
|
||||||
$product_names[] = $product->get_name();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($product_names)) {
|
|
||||||
$item_data[] = [
|
|
||||||
'key' => __('Selected Products', 'wc-composable-product'),
|
|
||||||
'value' => implode(', ', $product_names),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $item_data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate cart item price
|
|
||||||
*
|
|
||||||
* @param \WC_Cart $cart Cart object
|
|
||||||
*/
|
|
||||||
public function calculate_cart_item_price($cart) {
|
|
||||||
if (is_admin() && !defined('DOING_AJAX')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use static flag to prevent multiple executions within the same request
|
|
||||||
static $already_calculated = false;
|
|
||||||
if ($already_calculated) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
|
|
||||||
if (isset($cart_item['data']) && $cart_item['data']->get_type() === 'composable') {
|
|
||||||
if (isset($cart_item['composable_products'])) {
|
|
||||||
$product = $cart_item['data'];
|
|
||||||
$price = $product->calculate_composed_price($cart_item['composable_products']);
|
|
||||||
$cart_item['data']->set_price($price);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$already_calculated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
/**
|
/**
|
||||||
* Main Plugin Class
|
* Main Plugin Class
|
||||||
*
|
*
|
||||||
* @package WC_Composable_Product
|
* @package Magdev\WcComposableProduct
|
||||||
*/
|
*/
|
||||||
|
|
||||||
namespace WC_Composable_Product;
|
namespace Magdev\WcComposableProduct;
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main plugin class - Singleton pattern
|
* Main plugin class - Singleton pattern
|
||||||
@@ -35,7 +35,7 @@ class Plugin {
|
|||||||
* @return Plugin
|
* @return Plugin
|
||||||
*/
|
*/
|
||||||
public static function instance() {
|
public static function instance() {
|
||||||
if (is_null(self::$instance)) {
|
if ( is_null( self::$instance ) ) {
|
||||||
self::$instance = new self();
|
self::$instance = new self();
|
||||||
}
|
}
|
||||||
return self::$instance;
|
return self::$instance;
|
||||||
@@ -55,41 +55,54 @@ class Plugin {
|
|||||||
*/
|
*/
|
||||||
private function init_hooks() {
|
private function init_hooks() {
|
||||||
// Register product type
|
// Register product type
|
||||||
add_filter('product_type_selector', [$this, 'add_product_type']);
|
add_filter( 'product_type_selector', array( $this, 'add_product_type' ) );
|
||||||
add_filter('woocommerce_product_class', [$this, 'product_class'], 10, 2);
|
add_filter( 'woocommerce_product_class', array( $this, 'product_class' ), 10, 2 );
|
||||||
|
|
||||||
// Enqueue scripts and styles
|
// Enqueue scripts and styles
|
||||||
add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_scripts']);
|
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_scripts' ) );
|
||||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']);
|
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
|
||||||
|
|
||||||
// Admin settings
|
// Admin settings
|
||||||
add_filter('woocommerce_get_settings_pages', [$this, 'add_settings_page']);
|
add_filter( 'woocommerce_get_settings_pages', array( $this, 'add_settings_page' ) );
|
||||||
|
|
||||||
|
// Custom page template for composable products
|
||||||
|
add_filter( 'wc_get_template_part', array( $this, 'override_single_product_template' ), 10, 3 );
|
||||||
|
add_filter( 'body_class', array( $this, 'add_composable_body_class' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize Twig template engine
|
* Initialize Twig template engine
|
||||||
*/
|
*/
|
||||||
private function init_twig() {
|
private function init_twig() {
|
||||||
$loader = new \Twig\Loader\FilesystemLoader(WC_COMPOSABLE_PRODUCT_PATH . 'templates');
|
$loader = new \Twig\Loader\FilesystemLoader( WC_COMPOSABLE_PRODUCT_PATH . 'templates' );
|
||||||
$this->twig = new \Twig\Environment($loader, [
|
$this->twig = new \Twig\Environment(
|
||||||
|
$loader,
|
||||||
|
array(
|
||||||
'cache' => WC_COMPOSABLE_PRODUCT_PATH . 'cache',
|
'cache' => WC_COMPOSABLE_PRODUCT_PATH . 'cache',
|
||||||
'auto_reload' => true,
|
'auto_reload' => true,
|
||||||
'debug' => defined('WP_DEBUG') && WP_DEBUG,
|
'debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
|
||||||
]);
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Add WordPress functions to Twig
|
// Add WordPress functions to Twig
|
||||||
$this->twig->addFunction(new \Twig\TwigFunction('__', function($text) {
|
$this->twig->addFunction(
|
||||||
return __($text, 'wc-composable-product');
|
new \Twig\TwigFunction(
|
||||||
}));
|
'__',
|
||||||
$this->twig->addFunction(new \Twig\TwigFunction('esc_html', 'esc_html'));
|
function ( $text ) {
|
||||||
$this->twig->addFunction(new \Twig\TwigFunction('esc_attr', 'esc_attr'));
|
// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText -- dynamic Twig template strings.
|
||||||
$this->twig->addFunction(new \Twig\TwigFunction('esc_url', 'esc_url'));
|
return __( $text, 'wc-composable-product' );
|
||||||
$this->twig->addFunction(new \Twig\TwigFunction('wc_price', 'wc_price'));
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_html', 'esc_html' ) );
|
||||||
|
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_attr', 'esc_attr' ) );
|
||||||
|
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_url', 'esc_url' ) );
|
||||||
|
$this->twig->addFunction( new \Twig\TwigFunction( 'wc_price', 'wc_price' ) );
|
||||||
|
|
||||||
// Add WordPress escaping functions as Twig filters
|
// Add WordPress escaping functions as Twig filters
|
||||||
$this->twig->addFilter(new \Twig\TwigFilter('esc_html', 'esc_html'));
|
$this->twig->addFilter( new \Twig\TwigFilter( 'esc_html', 'esc_html' ) );
|
||||||
$this->twig->addFilter(new \Twig\TwigFilter('esc_attr', 'esc_attr'));
|
$this->twig->addFilter( new \Twig\TwigFilter( 'esc_attr', 'esc_attr' ) );
|
||||||
$this->twig->addFilter(new \Twig\TwigFilter('esc_url', 'esc_url'));
|
$this->twig->addFilter( new \Twig\TwigFilter( 'esc_url', 'esc_url' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,15 +111,15 @@ class Plugin {
|
|||||||
private function includes() {
|
private function includes() {
|
||||||
// Note: Settings.php is NOT included here because it extends WC_Settings_Page
|
// Note: Settings.php is NOT included here because it extends WC_Settings_Page
|
||||||
// which isn't loaded until later. It's included in add_settings_page() instead.
|
// which isn't loaded until later. It's included in add_settings_page() instead.
|
||||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Product_Data.php';
|
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/ProductData.php';
|
||||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Product_Type.php';
|
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/ProductType.php';
|
||||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Stock_Manager.php';
|
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/StockManager.php';
|
||||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Cart_Handler.php';
|
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/CartHandler.php';
|
||||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Product_Selector.php';
|
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/ProductSelector.php';
|
||||||
|
|
||||||
// Initialize components
|
// Initialize components
|
||||||
new Admin\Product_Data();
|
new Admin\ProductData();
|
||||||
new Cart_Handler();
|
new CartHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,8 +128,8 @@ class Plugin {
|
|||||||
* @param array $types Product types
|
* @param array $types Product types
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function add_product_type($types) {
|
public function add_product_type( $types ) {
|
||||||
$types['composable'] = __('Composable product', 'wc-composable-product');
|
$types['composable'] = __( 'Composable product', 'wc-composable-product' );
|
||||||
return $types;
|
return $types;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,9 +140,9 @@ class Plugin {
|
|||||||
* @param string $product_type Product type
|
* @param string $product_type Product type
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function product_class($classname, $product_type) {
|
public function product_class( $classname, $product_type ) {
|
||||||
if ($product_type === 'composable') {
|
if ( 'composable' === $product_type ) {
|
||||||
$classname = 'WC_Composable_Product\Product_Type';
|
$classname = 'Magdev\WcComposableProduct\ProductType';
|
||||||
}
|
}
|
||||||
return $classname;
|
return $classname;
|
||||||
}
|
}
|
||||||
@@ -138,59 +151,63 @@ class Plugin {
|
|||||||
* Enqueue frontend scripts and styles
|
* Enqueue frontend scripts and styles
|
||||||
*/
|
*/
|
||||||
public function enqueue_frontend_scripts() {
|
public function enqueue_frontend_scripts() {
|
||||||
if (is_product()) {
|
if ( is_product() ) {
|
||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
'wc-composable-product',
|
'wc-composable-product',
|
||||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/css/frontend.css',
|
WC_COMPOSABLE_PRODUCT_URL . 'assets/css/frontend.css',
|
||||||
[],
|
array(),
|
||||||
WC_COMPOSABLE_PRODUCT_VERSION
|
WC_COMPOSABLE_PRODUCT_VERSION
|
||||||
);
|
);
|
||||||
|
|
||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
'wc-composable-product',
|
'wc-composable-product',
|
||||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/js/frontend.js',
|
WC_COMPOSABLE_PRODUCT_URL . 'assets/js/frontend.js',
|
||||||
['jquery'],
|
array( 'jquery' ),
|
||||||
WC_COMPOSABLE_PRODUCT_VERSION,
|
WC_COMPOSABLE_PRODUCT_VERSION,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
wp_localize_script('wc-composable-product', 'wcComposableProduct', [
|
wp_localize_script(
|
||||||
'ajax_url' => admin_url('admin-ajax.php'),
|
'wc-composable-product',
|
||||||
'nonce' => wp_create_nonce('wc_composable_product_nonce'),
|
'wcComposableProduct',
|
||||||
'i18n' => [
|
array(
|
||||||
'select_items' => __('Please select items', 'wc-composable-product'),
|
'ajax_url' => admin_url( 'admin-ajax.php' ),
|
||||||
'max_items' => __('Maximum items selected', 'wc-composable-product'),
|
'nonce' => wp_create_nonce( 'wc_composable_product_nonce' ),
|
||||||
'min_items' => __('Please select at least one item', 'wc-composable-product'),
|
'i18n' => array(
|
||||||
],
|
'select_items' => __( 'Please select items', 'wc-composable-product' ),
|
||||||
'price_format' => [
|
'max_items' => __( 'Maximum items selected', 'wc-composable-product' ),
|
||||||
|
'min_items' => __( 'Please select at least one item', 'wc-composable-product' ),
|
||||||
|
),
|
||||||
|
'price_format' => array(
|
||||||
'currency_symbol' => get_woocommerce_currency_symbol(),
|
'currency_symbol' => get_woocommerce_currency_symbol(),
|
||||||
'decimal_separator' => wc_get_price_decimal_separator(),
|
'decimal_separator' => wc_get_price_decimal_separator(),
|
||||||
'thousand_separator' => wc_get_price_thousand_separator(),
|
'thousand_separator' => wc_get_price_thousand_separator(),
|
||||||
'decimals' => wc_get_price_decimals(),
|
'decimals' => wc_get_price_decimals(),
|
||||||
'price_format' => get_woocommerce_price_format(),
|
'price_format' => get_woocommerce_price_format(),
|
||||||
],
|
),
|
||||||
]);
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enqueue admin scripts and styles
|
* Enqueue admin scripts and styles
|
||||||
*/
|
*/
|
||||||
public function enqueue_admin_scripts($hook) {
|
public function enqueue_admin_scripts( $hook ) {
|
||||||
if ('post.php' === $hook || 'post-new.php' === $hook) {
|
if ( 'post.php' === $hook || 'post-new.php' === $hook ) {
|
||||||
global $post_type;
|
global $post_type;
|
||||||
if ('product' === $post_type) {
|
if ( 'product' === $post_type ) {
|
||||||
wp_enqueue_style(
|
wp_enqueue_style(
|
||||||
'wc-composable-product-admin',
|
'wc-composable-product-admin',
|
||||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/css/admin.css',
|
WC_COMPOSABLE_PRODUCT_URL . 'assets/css/admin.css',
|
||||||
[],
|
array(),
|
||||||
WC_COMPOSABLE_PRODUCT_VERSION
|
WC_COMPOSABLE_PRODUCT_VERSION
|
||||||
);
|
);
|
||||||
|
|
||||||
wp_enqueue_script(
|
wp_enqueue_script(
|
||||||
'wc-composable-product-admin',
|
'wc-composable-product-admin',
|
||||||
WC_COMPOSABLE_PRODUCT_URL . 'assets/js/admin.js',
|
WC_COMPOSABLE_PRODUCT_URL . 'assets/js/admin.js',
|
||||||
['jquery', 'wc-admin-product-meta-boxes'],
|
array( 'jquery', 'wc-admin-product-meta-boxes' ),
|
||||||
WC_COMPOSABLE_PRODUCT_VERSION,
|
WC_COMPOSABLE_PRODUCT_VERSION,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@@ -204,13 +221,50 @@ class Plugin {
|
|||||||
* @param array $settings WooCommerce settings pages
|
* @param array $settings WooCommerce settings pages
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function add_settings_page($settings) {
|
public function add_settings_page( $settings ) {
|
||||||
// Include Settings.php here, when WC_Settings_Page is guaranteed to be loaded
|
// Include Settings.php here, when WC_Settings_Page is guaranteed to be loaded
|
||||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Settings.php';
|
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Settings.php';
|
||||||
$settings[] = new Admin\Settings();
|
$settings[] = new Admin\Settings();
|
||||||
return $settings;
|
return $settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override single product template for composable products
|
||||||
|
*
|
||||||
|
* @param string $template Template path
|
||||||
|
* @param string $slug Template slug
|
||||||
|
* @param string $name Template name
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function override_single_product_template( $template, $slug, $name ) {
|
||||||
|
if ( 'content' === $slug && 'single-product' === $name ) {
|
||||||
|
global $product;
|
||||||
|
if ( $product && $product->get_type() === 'composable' ) {
|
||||||
|
$custom_template = WC_COMPOSABLE_PRODUCT_PATH . 'templates/content-single-product-composable.php';
|
||||||
|
if ( file_exists( $custom_template ) ) {
|
||||||
|
return $custom_template;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add body class for composable product pages
|
||||||
|
*
|
||||||
|
* @param array $classes Body CSS classes
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function add_composable_body_class( $classes ) {
|
||||||
|
if ( is_product() ) {
|
||||||
|
global $product;
|
||||||
|
if ( $product && $product->get_type() === 'composable' ) {
|
||||||
|
$classes[] = 'single-product-composable';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $classes;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Twig environment
|
* Get Twig environment
|
||||||
*
|
*
|
||||||
@@ -227,7 +281,7 @@ class Plugin {
|
|||||||
* @param array $context Template variables
|
* @param array $context Template variables
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function render_template($template, $context = []) {
|
public function render_template( $template, $context = array() ) {
|
||||||
return $this->twig->render($template, $context);
|
return $this->twig->render( $template, $context );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
78
includes/ProductSelector.php
Normal file
78
includes/ProductSelector.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Product Selector
|
||||||
|
*
|
||||||
|
* @package Magdev\WcComposableProduct
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Magdev\WcComposableProduct;
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Product Selector Class
|
||||||
|
*
|
||||||
|
* Handles rendering the product selection interface
|
||||||
|
*/
|
||||||
|
class ProductSelector {
|
||||||
|
/**
|
||||||
|
* Render product selector
|
||||||
|
*
|
||||||
|
* @param ProductType $product Composable product
|
||||||
|
*/
|
||||||
|
public static function render( $product ) {
|
||||||
|
if ( ! $product || $product->get_type() !== 'composable' ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$available_products = $product->get_available_products();
|
||||||
|
$selection_limit = $product->get_selection_limit();
|
||||||
|
$pricing_mode = $product->get_pricing_mode();
|
||||||
|
|
||||||
|
$show_images = get_option( 'wc_composable_show_images', 'yes' ) === 'yes';
|
||||||
|
$show_prices = get_option( 'wc_composable_show_prices', 'yes' ) === 'yes';
|
||||||
|
$show_total = get_option( 'wc_composable_show_total', 'yes' ) === 'yes';
|
||||||
|
|
||||||
|
// Get stock manager for stock information
|
||||||
|
$stock_manager = new StockManager();
|
||||||
|
|
||||||
|
// Prepare product data for template
|
||||||
|
$products_data = array();
|
||||||
|
foreach ( $available_products as $available_product ) {
|
||||||
|
$stock_info = $stock_manager->get_product_stock_info( $available_product->get_id() );
|
||||||
|
|
||||||
|
$products_data[] = array(
|
||||||
|
'id' => $available_product->get_id(),
|
||||||
|
'name' => $available_product->get_name(),
|
||||||
|
'price' => $available_product->get_price(),
|
||||||
|
'price_html' => $available_product->get_price_html(),
|
||||||
|
'image_url' => wp_get_attachment_image_url( $available_product->get_image_id(), 'thumbnail' ),
|
||||||
|
'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'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$context = array(
|
||||||
|
'product_id' => $product->get_id(),
|
||||||
|
'products' => $products_data,
|
||||||
|
'selection_limit' => $selection_limit,
|
||||||
|
'pricing_mode' => $pricing_mode,
|
||||||
|
'show_images' => $show_images,
|
||||||
|
'show_prices' => $show_prices,
|
||||||
|
'show_total' => $show_total,
|
||||||
|
'fixed_price' => $product->get_price(),
|
||||||
|
'fixed_price_html' => wc_price( $product->get_price() ),
|
||||||
|
'zero_price_html' => wc_price( 0 ),
|
||||||
|
'currency_symbol' => get_woocommerce_currency_symbol(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render template — Twig handles escaping via registered esc_html/esc_attr/esc_url functions.
|
||||||
|
$plugin = Plugin::instance();
|
||||||
|
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped by Twig template.
|
||||||
|
echo $plugin->render_template( 'product-selector.html.twig', $context );
|
||||||
|
}
|
||||||
|
}
|
||||||
254
includes/ProductType.php
Normal file
254
includes/ProductType.php
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Composable Product Type
|
||||||
|
*
|
||||||
|
* @package Magdev\WcComposableProduct
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Magdev\WcComposableProduct;
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable Product Type Class
|
||||||
|
*/
|
||||||
|
class ProductType extends \WC_Product {
|
||||||
|
/**
|
||||||
|
* Product type
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $product_type = 'composable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param mixed $product Product ID or object
|
||||||
|
*/
|
||||||
|
public function __construct( $product = 0 ) {
|
||||||
|
$this->supports[] = 'ajax_add_to_cart';
|
||||||
|
parent::__construct( $product );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get product type
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function get_type() {
|
||||||
|
return 'composable';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get selection limit
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function get_selection_limit() {
|
||||||
|
$limit = $this->get_meta( '_composable_selection_limit', true );
|
||||||
|
if ( empty( $limit ) ) {
|
||||||
|
$limit = get_option( 'wc_composable_default_limit', 5 );
|
||||||
|
}
|
||||||
|
return absint( $limit );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pricing mode
|
||||||
|
*
|
||||||
|
* @return string 'fixed' or 'sum'
|
||||||
|
*/
|
||||||
|
public function get_pricing_mode() {
|
||||||
|
$mode = $this->get_meta( '_composable_pricing_mode', true );
|
||||||
|
if ( empty( $mode ) ) {
|
||||||
|
$mode = get_option( 'wc_composable_default_pricing', 'sum' );
|
||||||
|
}
|
||||||
|
return $mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get product selection criteria
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function get_selection_criteria() {
|
||||||
|
$type = $this->get_meta( '_composable_criteria_type', true );
|
||||||
|
$categories = $this->get_meta( '_composable_categories', true );
|
||||||
|
$tags = $this->get_meta( '_composable_tags', true );
|
||||||
|
$skus = $this->get_meta( '_composable_skus', true );
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'type' => $type ? $type : 'category',
|
||||||
|
'categories' => $categories ? $categories : array(),
|
||||||
|
'tags' => $tags ? $tags : array(),
|
||||||
|
'skus' => $skus ? $skus : '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if product is purchasable
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function is_purchasable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if product is sold individually
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function is_sold_individually() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if non-public products should be included
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function should_include_unpublished() {
|
||||||
|
$per_product = $this->get_meta( '_composable_include_unpublished', true );
|
||||||
|
if ( 'yes' === $per_product ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ( 'no' === $per_product ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return 'yes' === get_option( 'wc_composable_include_unpublished', 'no' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available products based on criteria
|
||||||
|
*
|
||||||
|
* @return array Array of WC_Product objects
|
||||||
|
*/
|
||||||
|
public function get_available_products() {
|
||||||
|
$criteria = $this->get_selection_criteria();
|
||||||
|
$include_unpublished = $this->should_include_unpublished();
|
||||||
|
$args = array(
|
||||||
|
'post_type' => 'product',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'post_status' => $include_unpublished ? array( 'publish', 'draft', 'private' ) : 'publish',
|
||||||
|
'orderby' => 'title',
|
||||||
|
'order' => 'ASC',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Exclude composable products using the product_type taxonomy
|
||||||
|
// (WooCommerce stores product types as taxonomy terms, NOT as postmeta)
|
||||||
|
$args['tax_query'] = array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'taxonomy' => 'product_type',
|
||||||
|
'field' => 'slug',
|
||||||
|
'terms' => array( 'composable' ),
|
||||||
|
'operator' => 'NOT IN',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
switch ( $criteria['type'] ) {
|
||||||
|
case 'category':
|
||||||
|
if ( ! empty( $criteria['categories'] ) ) {
|
||||||
|
$args['tax_query'][] = array(
|
||||||
|
'taxonomy' => 'product_cat',
|
||||||
|
'field' => 'term_id',
|
||||||
|
'terms' => $criteria['categories'],
|
||||||
|
'operator' => 'IN',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'tag':
|
||||||
|
if ( ! empty( $criteria['tags'] ) ) {
|
||||||
|
$args['tax_query'][] = array(
|
||||||
|
'taxonomy' => 'product_tag',
|
||||||
|
'field' => 'term_id',
|
||||||
|
'terms' => $criteria['tags'],
|
||||||
|
'operator' => 'IN',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'sku':
|
||||||
|
if ( ! empty( $criteria['skus'] ) ) {
|
||||||
|
$skus = array_map( 'trim', explode( ',', $criteria['skus'] ) );
|
||||||
|
$args['meta_query'] = array(
|
||||||
|
array(
|
||||||
|
'key' => '_sku',
|
||||||
|
'value' => $skus,
|
||||||
|
'compare' => 'IN',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = new \WP_Query( $args );
|
||||||
|
$products = array();
|
||||||
|
|
||||||
|
if ( $query->have_posts() ) {
|
||||||
|
foreach ( $query->posts as $post ) {
|
||||||
|
$product = wc_get_product( $post->ID );
|
||||||
|
|
||||||
|
if ( ! $product ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle variable products by including their variations
|
||||||
|
if ( $product->is_type( 'variable' ) ) {
|
||||||
|
$variation_ids = $product->get_children();
|
||||||
|
foreach ( $variation_ids as $variation_id ) {
|
||||||
|
$variation = wc_get_product( $variation_id );
|
||||||
|
if ( $variation && ( $include_unpublished || $variation->is_purchasable() ) ) {
|
||||||
|
$products[] = $variation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ( $include_unpublished || $product->is_purchasable() ) {
|
||||||
|
$products[] = $product;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_reset_postdata();
|
||||||
|
|
||||||
|
return $products;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate price based on selected products
|
||||||
|
*
|
||||||
|
* @param array $selected_products Array of product IDs
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
public function calculate_composed_price( $selected_products ) {
|
||||||
|
$pricing_mode = $this->get_pricing_mode();
|
||||||
|
|
||||||
|
if ( 'fixed' === $pricing_mode ) {
|
||||||
|
return floatval( $this->get_regular_price() );
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = 0;
|
||||||
|
foreach ( $selected_products as $product_id ) {
|
||||||
|
$product = wc_get_product( $product_id );
|
||||||
|
if ( $product ) {
|
||||||
|
$total += floatval( $product->get_price() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add to cart validation
|
||||||
|
*
|
||||||
|
* @param int $product_id Product ID
|
||||||
|
* @param int $quantity Quantity
|
||||||
|
* @param int $variation_id Variation ID
|
||||||
|
* @param array $variations Variations
|
||||||
|
* @param array $cart_item_data Cart item data
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function add_to_cart_validation( $product_id, $quantity, $variation_id = 0, $variations = array(), $cart_item_data = array() ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Product Selector
|
|
||||||
*
|
|
||||||
* @package WC_Composable_Product
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace WC_Composable_Product;
|
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Product Selector Class
|
|
||||||
*
|
|
||||||
* Handles rendering the product selection interface
|
|
||||||
*/
|
|
||||||
class Product_Selector {
|
|
||||||
/**
|
|
||||||
* Render product selector
|
|
||||||
*
|
|
||||||
* @param Product_Type $product Composable product
|
|
||||||
*/
|
|
||||||
public static function render($product) {
|
|
||||||
if (!$product || $product->get_type() !== 'composable') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$available_products = $product->get_available_products();
|
|
||||||
$selection_limit = $product->get_selection_limit();
|
|
||||||
$pricing_mode = $product->get_pricing_mode();
|
|
||||||
|
|
||||||
$show_images = get_option('wc_composable_show_images', 'yes') === 'yes';
|
|
||||||
$show_prices = get_option('wc_composable_show_prices', '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
|
|
||||||
$products_data = [];
|
|
||||||
foreach ($available_products as $available_product) {
|
|
||||||
$stock_info = $stock_manager->get_product_stock_info($available_product->get_id());
|
|
||||||
|
|
||||||
$products_data[] = [
|
|
||||||
'id' => $available_product->get_id(),
|
|
||||||
'name' => $available_product->get_name(),
|
|
||||||
'price' => $available_product->get_price(),
|
|
||||||
'price_html' => $available_product->get_price_html(),
|
|
||||||
'image_url' => wp_get_attachment_image_url($available_product->get_image_id(), 'thumbnail'),
|
|
||||||
'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'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$context = [
|
|
||||||
'product_id' => $product->get_id(),
|
|
||||||
'products' => $products_data,
|
|
||||||
'selection_limit' => $selection_limit,
|
|
||||||
'pricing_mode' => $pricing_mode,
|
|
||||||
'show_images' => $show_images,
|
|
||||||
'show_prices' => $show_prices,
|
|
||||||
'show_total' => $show_total,
|
|
||||||
'fixed_price' => $product->get_price(),
|
|
||||||
'fixed_price_html' => wc_price($product->get_price()),
|
|
||||||
'zero_price_html' => wc_price(0),
|
|
||||||
'currency_symbol' => get_woocommerce_currency_symbol(),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Render template
|
|
||||||
$plugin = Plugin::instance();
|
|
||||||
echo $plugin->render_template('product-selector.twig', $context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Composable Product Type
|
|
||||||
*
|
|
||||||
* @package WC_Composable_Product
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace WC_Composable_Product;
|
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable Product Type Class
|
|
||||||
*/
|
|
||||||
class Product_Type extends \WC_Product {
|
|
||||||
/**
|
|
||||||
* Product type
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $product_type = 'composable';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*
|
|
||||||
* @param mixed $product Product ID or object
|
|
||||||
*/
|
|
||||||
public function __construct($product = 0) {
|
|
||||||
$this->supports[] = 'ajax_add_to_cart';
|
|
||||||
parent::__construct($product);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get product type
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function get_type() {
|
|
||||||
return 'composable';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get selection limit
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function get_selection_limit() {
|
|
||||||
$limit = $this->get_meta('_composable_selection_limit', true);
|
|
||||||
if (empty($limit)) {
|
|
||||||
$limit = get_option('wc_composable_default_limit', 5);
|
|
||||||
}
|
|
||||||
return absint($limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get pricing mode
|
|
||||||
*
|
|
||||||
* @return string 'fixed' or 'sum'
|
|
||||||
*/
|
|
||||||
public function get_pricing_mode() {
|
|
||||||
$mode = $this->get_meta('_composable_pricing_mode', true);
|
|
||||||
if (empty($mode)) {
|
|
||||||
$mode = get_option('wc_composable_default_pricing', 'sum');
|
|
||||||
}
|
|
||||||
return $mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get product selection criteria
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function get_selection_criteria() {
|
|
||||||
return [
|
|
||||||
'type' => $this->get_meta('_composable_criteria_type', true) ?: 'category',
|
|
||||||
'categories' => $this->get_meta('_composable_categories', true) ?: [],
|
|
||||||
'tags' => $this->get_meta('_composable_tags', true) ?: [],
|
|
||||||
'skus' => $this->get_meta('_composable_skus', true) ?: '',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if product is purchasable
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function is_purchasable() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if product is sold individually
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function is_sold_individually() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get available products based on criteria
|
|
||||||
*
|
|
||||||
* @return array Array of WC_Product objects
|
|
||||||
*/
|
|
||||||
public function get_available_products() {
|
|
||||||
$criteria = $this->get_selection_criteria();
|
|
||||||
$args = [
|
|
||||||
'post_type' => 'product',
|
|
||||||
'posts_per_page' => -1,
|
|
||||||
'post_status' => 'publish',
|
|
||||||
'orderby' => 'title',
|
|
||||||
'order' => 'ASC',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Exclude composable products using the product_type taxonomy
|
|
||||||
// (WooCommerce stores product types as taxonomy terms, NOT as postmeta)
|
|
||||||
$args['tax_query'] = [
|
|
||||||
'relation' => 'AND',
|
|
||||||
[
|
|
||||||
'taxonomy' => 'product_type',
|
|
||||||
'field' => 'slug',
|
|
||||||
'terms' => ['composable'],
|
|
||||||
'operator' => 'NOT IN',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
switch ($criteria['type']) {
|
|
||||||
case 'category':
|
|
||||||
if (!empty($criteria['categories'])) {
|
|
||||||
$args['tax_query'][] = [
|
|
||||||
'taxonomy' => 'product_cat',
|
|
||||||
'field' => 'term_id',
|
|
||||||
'terms' => $criteria['categories'],
|
|
||||||
'operator' => 'IN',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'tag':
|
|
||||||
if (!empty($criteria['tags'])) {
|
|
||||||
$args['tax_query'][] = [
|
|
||||||
'taxonomy' => 'product_tag',
|
|
||||||
'field' => 'term_id',
|
|
||||||
'terms' => $criteria['tags'],
|
|
||||||
'operator' => 'IN',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'sku':
|
|
||||||
if (!empty($criteria['skus'])) {
|
|
||||||
$skus = array_map('trim', explode(',', $criteria['skus']));
|
|
||||||
$args['meta_query'] = [
|
|
||||||
[
|
|
||||||
'key' => '_sku',
|
|
||||||
'value' => $skus,
|
|
||||||
'compare' => 'IN',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$query = new \WP_Query($args);
|
|
||||||
$products = [];
|
|
||||||
|
|
||||||
if ($query->have_posts()) {
|
|
||||||
foreach ($query->posts as $post) {
|
|
||||||
$product = wc_get_product($post->ID);
|
|
||||||
|
|
||||||
if (!$product) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle variable products by including their variations
|
|
||||||
if ($product->is_type('variable')) {
|
|
||||||
$variation_ids = $product->get_children();
|
|
||||||
foreach ($variation_ids as $variation_id) {
|
|
||||||
$variation = wc_get_product($variation_id);
|
|
||||||
if ($variation && $variation->is_purchasable()) {
|
|
||||||
$products[] = $variation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} elseif ($product->is_purchasable()) {
|
|
||||||
$products[] = $product;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wp_reset_postdata();
|
|
||||||
|
|
||||||
return $products;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate price based on selected products
|
|
||||||
*
|
|
||||||
* @param array $selected_products Array of product IDs
|
|
||||||
* @return float
|
|
||||||
*/
|
|
||||||
public function calculate_composed_price($selected_products) {
|
|
||||||
$pricing_mode = $this->get_pricing_mode();
|
|
||||||
|
|
||||||
if ($pricing_mode === 'fixed') {
|
|
||||||
return floatval($this->get_regular_price());
|
|
||||||
}
|
|
||||||
|
|
||||||
$total = 0;
|
|
||||||
foreach ($selected_products as $product_id) {
|
|
||||||
$product = wc_get_product($product_id);
|
|
||||||
if ($product) {
|
|
||||||
$total += floatval($product->get_price());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $total;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add to cart validation
|
|
||||||
*
|
|
||||||
* @param int $product_id Product ID
|
|
||||||
* @param int $quantity Quantity
|
|
||||||
* @param int $variation_id Variation ID
|
|
||||||
* @param array $variations Variations
|
|
||||||
* @param array $cart_item_data Cart item data
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function add_to_cart_validation($product_id, $quantity, $variation_id = 0, $variations = [], $cart_item_data = []) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
283
includes/StockManager.php
Normal file
283
includes/StockManager.php
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Stock Manager
|
||||||
|
*
|
||||||
|
* @package Magdev\WcComposableProduct
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Magdev\WcComposableProduct;
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stock Manager Class
|
||||||
|
*
|
||||||
|
* Handles stock management for composable products
|
||||||
|
*/
|
||||||
|
class StockManager {
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
// Hook into order completion to reduce stock
|
||||||
|
add_action( 'woocommerce_order_status_completed', array( $this, 'reduce_stock_on_order_complete' ), 10, 1 );
|
||||||
|
add_action( 'woocommerce_order_status_processing', array( $this, 'reduce_stock_on_order_complete' ), 10, 1 );
|
||||||
|
|
||||||
|
// Hook into order cancellation/refund to restore stock
|
||||||
|
add_action( 'woocommerce_order_status_cancelled', array( $this, 'restore_stock_on_order_cancel' ), 10, 1 );
|
||||||
|
add_action( 'woocommerce_order_status_refunded', array( $this, 'restore_stock_on_order_cancel' ), 10, 1 );
|
||||||
|
|
||||||
|
// Prevent double stock reduction
|
||||||
|
add_filter( 'woocommerce_can_reduce_order_stock', array( $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 ( null !== $stock_quantity && $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 array(
|
||||||
|
'in_stock' => false,
|
||||||
|
'stock_quantity' => 0,
|
||||||
|
'backorders_allowed' => false,
|
||||||
|
'stock_status' => 'outofstock',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stock_quantity = $product->get_stock_quantity();
|
||||||
|
$managing_stock = $product->managing_stock();
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'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 || null === $stock_quantity || $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 ( null !== $stock_quantity ) {
|
||||||
|
$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 ( null !== $stock_quantity ) {
|
||||||
|
$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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
<?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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,9 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: WooCommerce Composable Products 1.0.0\n"
|
"Project-Id-Version: WooCommerce Composable Products 1.0.0\n"
|
||||||
"Report-Msgid-Bugs-To: https://github.com/magdev/wc-composable-product/issues\n"
|
"Report-Msgid-Bugs-To: https://github.com/magdev/wc-composable-product/"
|
||||||
"POT-Creation-Date: 2024-12-31 00:00+0000\n"
|
"issues\n"
|
||||||
|
"POT-Creation-Date: 2026-03-01 13:47+0100\n"
|
||||||
"PO-Revision-Date: 2024-12-31 00:00+0000\n"
|
"PO-Revision-Date: 2024-12-31 00:00+0000\n"
|
||||||
"Last-Translator: Claude AI\n"
|
"Last-Translator: Claude AI\n"
|
||||||
"Language-Team: German (Switzerland)\n"
|
"Language-Team: German (Switzerland)\n"
|
||||||
@@ -15,230 +16,301 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:44
|
||||||
msgid "WooCommerce Composable Products requires WooCommerce to be installed and active."
|
msgid ""
|
||||||
msgstr "WooCommerce Composable Products erfordert eine installierte und aktive WooCommerce-Installation."
|
"WooCommerce Composable Products requires WooCommerce to be installed and "
|
||||||
|
"active."
|
||||||
|
msgstr ""
|
||||||
|
"WooCommerce Composable Products erfordert eine installierte und aktive "
|
||||||
|
"WooCommerce-Installation."
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:90
|
||||||
msgid "This plugin requires WooCommerce to be installed and active."
|
msgid "This plugin requires WooCommerce to be installed and active."
|
||||||
msgstr "Dieses Plugin erfordert eine installierte und aktive WooCommerce-Installation."
|
msgstr ""
|
||||||
|
"Dieses Plugin erfordert eine installierte und aktive WooCommerce-"
|
||||||
|
"Installation."
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:91
|
||||||
msgid "Plugin Activation Error"
|
msgid "Plugin Activation Error"
|
||||||
msgstr "Plugin-Aktivierungsfehler"
|
msgstr "Plugin-Aktivierungsfehler"
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
#. translators: %s: product name
|
||||||
msgid "Composable Products"
|
#: includes/StockManager.php:60
|
||||||
msgstr "Zusammenstellbare Produkte"
|
#, php-format
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Composable Products Settings"
|
|
||||||
msgstr "Einstellungen für zusammenstellbare Produkte"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Configure default settings for composable products."
|
|
||||||
msgstr "Konfigurieren Sie die Standardeinstellungen für zusammenstellbare Produkte."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default Selection Limit"
|
|
||||||
msgstr "Standard-Auswahllimit"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default number of items customers can select."
|
|
||||||
msgstr "Standardanzahl der Artikel, die Kunden auswählen können (z.B. 5 Sticker für CHF 50.-)."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default Pricing Mode"
|
|
||||||
msgstr "Standard-Preismodus"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "How to calculate the price of composable products."
|
|
||||||
msgstr "Wie der Preis für zusammenstellbare Produkte berechnet wird (z.B. Festpreis CHF 50.- oder Summe)."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Sum of selected products"
|
|
||||||
msgstr "Summe der ausgewählten Produkte"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Fixed price"
|
|
||||||
msgstr "Festpreis"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Product Images"
|
|
||||||
msgstr "Produktbilder anzeigen"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display product images in the selection interface."
|
|
||||||
msgstr "Produktbilder in der Auswahlschnittstelle anzeigen."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Product Prices"
|
|
||||||
msgstr "Produktpreise anzeigen"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display individual product prices in the selection interface."
|
|
||||||
msgstr "Einzelne Produktpreise in der Auswahlschnittstelle anzeigen."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Total Price"
|
|
||||||
msgstr "Gesamtpreis anzeigen"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display the total price as customers make selections."
|
|
||||||
msgstr "Den Gesamtpreis anzeigen, während Kunden Auswahlen treffen."
|
|
||||||
|
|
||||||
#: includes/Plugin.php
|
|
||||||
msgid "Composable product"
|
|
||||||
msgstr "Zusammenstellbares Produkt"
|
|
||||||
|
|
||||||
#: includes/Product_Selector.php, templates/product-selector.twig
|
|
||||||
msgid "Select Your Products"
|
|
||||||
msgstr "Wählen Sie Ihre Produkte"
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Choose up to"
|
|
||||||
msgstr "Wählen Sie bis zu"
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "items from the selection below."
|
|
||||||
msgstr "Artikel aus der untenstehenden Auswahl."
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Total Price:"
|
|
||||||
msgstr "Gesamtpreis:"
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Add to Cart"
|
|
||||||
msgstr "In den Warenkorb"
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "Please select at least one product."
|
|
||||||
msgstr "Bitte wählen Sie mindestens ein Produkt aus."
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "You can select a maximum of %d products."
|
|
||||||
msgstr "Sie können maximal %d Produkte auswählen."
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "One or more selected products are not available."
|
|
||||||
msgstr "Ein oder mehrere ausgewählte Produkte sind nicht verfügbar."
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "Selected Products"
|
|
||||||
msgstr "Ausgewählte Produkte"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Composable Options"
|
|
||||||
msgstr "Zusammenstellungsoptionen"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Selection Limit"
|
|
||||||
msgstr "Auswahllimit"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Maximum number of items customers can select. Leave empty to use global default."
|
|
||||||
msgstr "Maximale Anzahl der Artikel, die Kunden auswählen können. Leer lassen, um den globalen Standard zu verwenden."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Pricing Mode"
|
|
||||||
msgstr "Preismodus"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "How to calculate the price."
|
|
||||||
msgstr "Wie der Preis berechnet wird."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Fixed Price"
|
|
||||||
msgstr "Festpreis"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Enter the fixed price for this composable product."
|
|
||||||
msgstr "Geben Sie den Festpreis für dieses zusammenstellbare Produkt ein."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Use global default"
|
|
||||||
msgstr "Globalen Standard verwenden"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Selection Criteria"
|
|
||||||
msgstr "Auswahlkriterien"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "How to select available products."
|
|
||||||
msgstr "Wie verfügbare Produkte ausgewählt werden."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By Category"
|
|
||||||
msgstr "Nach Kategorie"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By Tag"
|
|
||||||
msgstr "Nach Schlagwort"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By SKU"
|
|
||||||
msgstr "Nach Artikelnummer"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select Categories"
|
|
||||||
msgstr "Kategorien auswählen"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select product categories to include."
|
|
||||||
msgstr "Produktkategorien auswählen, die einbezogen werden sollen."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select Tags"
|
|
||||||
msgstr "Schlagwörter auswählen"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select product tags to include."
|
|
||||||
msgstr "Produkt-Schlagwörter auswählen, die einbezogen werden sollen."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Product SKUs"
|
|
||||||
msgstr "Produkt-Artikelnummern"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Enter product SKUs separated by commas."
|
|
||||||
msgstr "Geben Sie Produkt-Artikelnummern durch Kommas getrennt ein."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "SKU-1, SKU-2, SKU-3"
|
|
||||||
msgstr "ART-1, ART-2, ART-3"
|
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
|
||||||
msgid "\"%s\" is out of stock and cannot be selected."
|
msgid "\"%s\" is out of stock and cannot be selected."
|
||||||
msgstr "\"%s\" ist nicht an Lager und kann nicht ausgewählt werden."
|
msgstr "\"%s\" ist nicht an Lager und kann nicht ausgewählt werden."
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: templates/product-selector.html.twig
|
||||||
msgid "Only %2$d of \"%1$s\" are available in stock."
|
msgid "Add to Cart"
|
||||||
msgstr "Nur %2$d von \"%1$s\" sind an Lager verfügbar."
|
msgstr "In den Warenkorb"
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: includes/Admin/ProductData.php:108
|
||||||
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
msgid ""
|
||||||
msgstr "Lagerbestand reduziert für \"%1$s\": -%2$d (verbleibend: %3$d)"
|
"Allow draft and private products in the selection. Useful when products "
|
||||||
|
"should only be sold as part of a composition."
|
||||||
|
msgstr ""
|
||||||
|
"Entwürfe und private Produkte in der Auswahl zulassen. Nützlich, wenn "
|
||||||
|
"Produkte nur als Teil einer Zusammenstellung verkauft werden sollen."
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: includes/Admin/Settings.php:65
|
||||||
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
msgid ""
|
||||||
msgstr "Lagerbestand wiederhergestellt für \"%1$s\": +%2$d (gesamt: %3$d)"
|
"Allow draft and private products to appear in composable product selections. "
|
||||||
|
"Useful when products should only be sold as part of a composition, not "
|
||||||
|
"individually."
|
||||||
|
msgstr ""
|
||||||
|
"Entwürfe und private Produkte in der Auswahl zusammenstellbarer Produkte "
|
||||||
|
"anzeigen. Nützlich, wenn Produkte nur als Teil einer Zusammenstellung "
|
||||||
|
"verkauft werden sollen, nicht einzeln."
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:126
|
||||||
msgid "Out of stock"
|
msgid "By Category"
|
||||||
msgstr "Nicht an Lager"
|
msgstr "Nach Kategorie"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:128
|
||||||
msgid "Only"
|
msgid "By SKU"
|
||||||
msgstr "Nur"
|
msgstr "Nach Artikelnummer"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:127
|
||||||
msgid "left"
|
msgid "By Tag"
|
||||||
msgstr "übrig"
|
msgstr "Nach Schlagwort"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Choose up to"
|
||||||
|
msgstr "Wählen Sie bis zu"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:34
|
||||||
|
msgid "Composable Options"
|
||||||
|
msgstr "Zusammenstellungsoptionen"
|
||||||
|
|
||||||
|
#: includes/Plugin.php:132
|
||||||
|
msgid "Composable product"
|
||||||
|
msgstr "Zusammenstellbares Produkt"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:21
|
||||||
|
msgid "Composable Products"
|
||||||
|
msgstr "Zusammenstellbare Produkte"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:34
|
||||||
|
msgid "Composable Products Settings"
|
||||||
|
msgstr "Einstellungen für zusammenstellbare Produkte"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:36
|
||||||
|
msgid "Configure default settings for composable products."
|
||||||
|
msgstr ""
|
||||||
|
"Konfigurieren Sie die Standardeinstellungen für zusammenstellbare Produkte."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:52
|
||||||
|
msgid "Default Pricing Mode"
|
||||||
|
msgstr "Standard-Preismodus"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:40
|
||||||
|
msgid "Default Selection Limit"
|
||||||
|
msgstr "Standard-Auswahllimit"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:41
|
||||||
|
msgid "Default number of items customers can select."
|
||||||
|
msgstr ""
|
||||||
|
"Standardanzahl der Artikel, die Kunden auswählen können (z.B. 5 Sticker für "
|
||||||
|
"CHF 50.-)."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:79
|
||||||
|
msgid "Display individual product prices in the selection interface."
|
||||||
|
msgstr "Einzelne Produktpreise in der Auswahlschnittstelle anzeigen."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:72
|
||||||
|
msgid "Display product images in the selection interface."
|
||||||
|
msgstr "Produktbilder in der Auswahlschnittstelle anzeigen."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:86
|
||||||
|
msgid "Display the total price as customers make selections."
|
||||||
|
msgstr "Den Gesamtpreis anzeigen, während Kunden Auswahlen treffen."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:196
|
||||||
|
msgid "Enter product SKUs separated by commas."
|
||||||
|
msgstr "Geben Sie Produkt-Artikelnummern durch Kommas getrennt ein."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:83
|
||||||
|
msgid "Enter the fixed price for this composable product."
|
||||||
|
msgstr "Geben Sie den Festpreis für dieses zusammenstellbare Produkt ein."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:82
|
||||||
|
msgid "Fixed Price"
|
||||||
|
msgstr "Festpreis"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:74 includes/Admin/Settings.php:59
|
||||||
|
msgid "Fixed price"
|
||||||
|
msgstr "Festpreis"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:53
|
||||||
|
msgid "How to calculate the price of composable products."
|
||||||
|
msgstr ""
|
||||||
|
"Wie der Preis für zusammenstellbare Produkte berechnet wird (z.B. Festpreis "
|
||||||
|
"CHF 50.- oder Summe)."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:69
|
||||||
|
msgid "How to calculate the price."
|
||||||
|
msgstr "Wie der Preis berechnet wird."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:123
|
||||||
|
msgid "How to select available products."
|
||||||
|
msgstr "Wie verfügbare Produkte ausgewählt werden."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
msgid "In stock"
|
msgid "In stock"
|
||||||
msgstr "An Lager"
|
msgstr "An Lager"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:107 includes/Admin/Settings.php:64
|
||||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
msgid "Include Non-Public Products"
|
||||||
msgstr "Keine Produkte zur Auswahl verfügbar. Bitte konfigurieren Sie die Produktkriterien im Admin-Bereich."
|
msgstr "Nicht-öffentliche Produkte einbeziehen"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "items from the selection below."
|
||||||
|
msgstr "Artikel aus der untenstehenden Auswahl."
|
||||||
|
|
||||||
|
#: includes/Plugin.php:178
|
||||||
|
msgid "Maximum items selected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:55
|
||||||
|
msgid ""
|
||||||
|
"Maximum number of items customers can select. Leave empty to use global "
|
||||||
|
"default."
|
||||||
|
msgstr ""
|
||||||
|
"Maximale Anzahl der Artikel, die Kunden auswählen können. Leer lassen, um "
|
||||||
|
"den globalen Standard zu verwenden."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:113
|
||||||
|
msgid "No"
|
||||||
|
msgstr "Nein"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid ""
|
||||||
|
"No products available for selection. Please configure the product criteria "
|
||||||
|
"in the admin panel."
|
||||||
|
msgstr ""
|
||||||
|
"Keine Produkte zur Auswahl verfügbar. Bitte konfigurieren Sie die "
|
||||||
|
"Produktkriterien im Admin-Bereich."
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:115
|
||||||
|
msgid "One or more selected products are not available."
|
||||||
|
msgstr "Ein oder mehrere ausgewählte Produkte sind nicht verfügbar."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Only"
|
||||||
|
msgstr "Nur"
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: stock quantity
|
||||||
|
#: includes/StockManager.php:69
|
||||||
|
#, php-format
|
||||||
|
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||||
|
msgstr "Nur %2$d von \"%1$s\" sind an Lager verfügbar."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Out of stock"
|
||||||
|
msgstr "Nicht an Lager"
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:84 includes/CartHandler.php:100
|
||||||
|
msgid "Please select at least one product."
|
||||||
|
msgstr "Bitte wählen Sie mindestens ein Produkt aus."
|
||||||
|
|
||||||
|
#: includes/Plugin.php:179
|
||||||
|
msgid "Please select at least one item"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Plugin.php:177
|
||||||
|
msgid "Please select items"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:68
|
||||||
|
msgid "Pricing Mode"
|
||||||
|
msgstr "Preismodus"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:195
|
||||||
|
msgid "Product SKUs"
|
||||||
|
msgstr "Produkt-Artikelnummern"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:198
|
||||||
|
msgid "SKU-1, SKU-2, SKU-3"
|
||||||
|
msgstr "ART-1, ART-2, ART-3"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Select Your Products"
|
||||||
|
msgstr "Wählen Sie Ihre Produkte"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:138
|
||||||
|
msgid "Select Categories"
|
||||||
|
msgstr "Kategorien auswählen"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:165
|
||||||
|
msgid "Select Tags"
|
||||||
|
msgstr "Schlagwörter auswählen"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:159
|
||||||
|
msgid "Select product categories to include."
|
||||||
|
msgstr "Produktkategorien auswählen, die einbezogen werden sollen."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:186
|
||||||
|
msgid "Select product tags to include."
|
||||||
|
msgstr "Produkt-Schlagwörter auswählen, die einbezogen werden sollen."
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:191
|
||||||
|
msgid "Selected Products"
|
||||||
|
msgstr "Ausgewählte Produkte"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:122
|
||||||
|
msgid "Selection Criteria"
|
||||||
|
msgstr "Auswahlkriterien"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:54
|
||||||
|
msgid "Selection Limit"
|
||||||
|
msgstr "Auswahllimit"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:71
|
||||||
|
msgid "Show Product Images"
|
||||||
|
msgstr "Produktbilder anzeigen"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:78
|
||||||
|
msgid "Show Product Prices"
|
||||||
|
msgstr "Produktpreise anzeigen"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:85
|
||||||
|
msgid "Show Total Price"
|
||||||
|
msgstr "Gesamtpreis anzeigen"
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: quantity, 3: remaining stock
|
||||||
|
#: includes/StockManager.php:168
|
||||||
|
#, php-format
|
||||||
|
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||||
|
msgstr "Lagerbestand reduziert für \"%1$s\": -%2$d (verbleibend: %3$d)"
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: quantity, 3: new stock
|
||||||
|
#: includes/StockManager.php:235
|
||||||
|
#, php-format
|
||||||
|
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||||
|
msgstr "Lagerbestand wiederhergestellt für \"%1$s\": +%2$d (gesamt: %3$d)"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:73 includes/Admin/Settings.php:58
|
||||||
|
msgid "Sum of selected products"
|
||||||
|
msgstr "Summe der ausgewählten Produkte"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Total Price:"
|
||||||
|
msgstr "Gesamtpreis:"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:72 includes/Admin/ProductData.php:111
|
||||||
|
msgid "Use global default"
|
||||||
|
msgstr "Globalen Standard verwenden"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:112
|
||||||
|
msgid "Yes"
|
||||||
|
msgstr "Ja"
|
||||||
|
|
||||||
|
#. translators: %d: selection limit
|
||||||
|
#: includes/CartHandler.php:95
|
||||||
|
#, php-format
|
||||||
|
msgid "You can select a maximum of %d products."
|
||||||
|
msgstr "Sie können maximal %d Produkte auswählen."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "left"
|
||||||
|
msgstr "übrig"
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: WooCommerce Composable Products 1.0.0\n"
|
"Project-Id-Version: WooCommerce Composable Products 1.0.0\n"
|
||||||
"Report-Msgid-Bugs-To: https://github.com/magdev/wc-composable-product/issues\n"
|
"Report-Msgid-Bugs-To: https://github.com/magdev/wc-composable-product/"
|
||||||
"POT-Creation-Date: 2024-12-31 00:00+0000\n"
|
"issues\n"
|
||||||
|
"POT-Creation-Date: 2026-03-01 13:47+0100\n"
|
||||||
"PO-Revision-Date: 2024-12-31 00:00+0000\n"
|
"PO-Revision-Date: 2024-12-31 00:00+0000\n"
|
||||||
"Last-Translator: Claude AI\n"
|
"Last-Translator: Claude AI\n"
|
||||||
"Language-Team: German (Switzerland)\n"
|
"Language-Team: German (Switzerland)\n"
|
||||||
@@ -15,230 +16,300 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:44
|
||||||
msgid "WooCommerce Composable Products requires WooCommerce to be installed and active."
|
msgid ""
|
||||||
msgstr "WooCommerce Composable Products erfordert eine installierte und aktive WooCommerce-Installation."
|
"WooCommerce Composable Products requires WooCommerce to be installed and "
|
||||||
|
"active."
|
||||||
|
msgstr ""
|
||||||
|
"WooCommerce Composable Products erfordert eine installierte und aktive "
|
||||||
|
"WooCommerce-Installation."
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:90
|
||||||
msgid "This plugin requires WooCommerce to be installed and active."
|
msgid "This plugin requires WooCommerce to be installed and active."
|
||||||
msgstr "Dieses Plugin erfordert eine installierte und aktive WooCommerce-Installation."
|
msgstr ""
|
||||||
|
"Dieses Plugin erfordert eine installierte und aktive WooCommerce-"
|
||||||
|
"Installation."
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:91
|
||||||
msgid "Plugin Activation Error"
|
msgid "Plugin Activation Error"
|
||||||
msgstr "Plugin-Aktivierungsfehler"
|
msgstr "Plugin-Aktivierungsfehler"
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
#. translators: %s: product name
|
||||||
msgid "Composable Products"
|
#: includes/StockManager.php:60
|
||||||
msgstr "Zusammenstellbare Produkte"
|
#, php-format
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Composable Products Settings"
|
|
||||||
msgstr "Einstellungen für zusammenstellbare Produkte"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Configure default settings for composable products."
|
|
||||||
msgstr "Konfiguriere die Standardeinstellungen für zusammenstellbare Produkte."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default Selection Limit"
|
|
||||||
msgstr "Standard-Auswahllimit"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default number of items customers can select."
|
|
||||||
msgstr "Standardanzahl der Artikel, die Kunden auswählen können (z.B. 5 Sticker für CHF 50.-)."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default Pricing Mode"
|
|
||||||
msgstr "Standard-Preismodus"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "How to calculate the price of composable products."
|
|
||||||
msgstr "Wie der Preis für zusammenstellbare Produkte berechnet wird (z.B. Festpreis CHF 50.- oder Summe)."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Sum of selected products"
|
|
||||||
msgstr "Summe der ausgewählten Produkte"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Fixed price"
|
|
||||||
msgstr "Festpreis"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Product Images"
|
|
||||||
msgstr "Produktbilder anzeigen"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display product images in the selection interface."
|
|
||||||
msgstr "Produktbilder in der Auswahlschnittstelle anzeigen."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Product Prices"
|
|
||||||
msgstr "Produktpreise anzeigen"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display individual product prices in the selection interface."
|
|
||||||
msgstr "Einzelne Produktpreise in der Auswahlschnittstelle anzeigen."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Total Price"
|
|
||||||
msgstr "Gesamtpreis anzeigen"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display the total price as customers make selections."
|
|
||||||
msgstr "Den Gesamtpreis anzeigen, während Kunden Auswahlen treffen."
|
|
||||||
|
|
||||||
#: includes/Plugin.php
|
|
||||||
msgid "Composable product"
|
|
||||||
msgstr "Zusammenstellbares Produkt"
|
|
||||||
|
|
||||||
#: includes/Product_Selector.php, templates/product-selector.twig
|
|
||||||
msgid "Select Your Products"
|
|
||||||
msgstr "Wähle deine Produkte"
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Choose up to"
|
|
||||||
msgstr "Wähle bis zu"
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "items from the selection below."
|
|
||||||
msgstr "Artikel aus der untenstehenden Auswahl."
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Total Price:"
|
|
||||||
msgstr "Gesamtpreis:"
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Add to Cart"
|
|
||||||
msgstr "In den Warenkorb"
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "Please select at least one product."
|
|
||||||
msgstr "Bitte wähle mindestens ein Produkt aus."
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "You can select a maximum of %d products."
|
|
||||||
msgstr "Du kannst maximal %d Produkte auswählen."
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "One or more selected products are not available."
|
|
||||||
msgstr "Ein oder mehrere ausgewählte Produkte sind nicht verfügbar."
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "Selected Products"
|
|
||||||
msgstr "Ausgewählte Produkte"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Composable Options"
|
|
||||||
msgstr "Zusammenstellungsoptionen"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Selection Limit"
|
|
||||||
msgstr "Auswahllimit"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Maximum number of items customers can select. Leave empty to use global default."
|
|
||||||
msgstr "Maximale Anzahl der Artikel, die Kunden auswählen können. Leer lassen, um den globalen Standard zu verwenden."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Pricing Mode"
|
|
||||||
msgstr "Preismodus"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "How to calculate the price."
|
|
||||||
msgstr "Wie der Preis berechnet wird."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Fixed Price"
|
|
||||||
msgstr "Festpreis"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Enter the fixed price for this composable product."
|
|
||||||
msgstr "Gib den Festpreis für dieses zusammenstellbare Produkt ein."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Use global default"
|
|
||||||
msgstr "Globalen Standard verwenden"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Selection Criteria"
|
|
||||||
msgstr "Auswahlkriterien"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "How to select available products."
|
|
||||||
msgstr "Wie verfügbare Produkte ausgewählt werden."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By Category"
|
|
||||||
msgstr "Nach Kategorie"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By Tag"
|
|
||||||
msgstr "Nach Schlagwort"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By SKU"
|
|
||||||
msgstr "Nach Artikelnummer"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select Categories"
|
|
||||||
msgstr "Kategorien auswählen"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select product categories to include."
|
|
||||||
msgstr "Produktkategorien auswählen, die einbezogen werden sollen."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select Tags"
|
|
||||||
msgstr "Schlagwörter auswählen"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select product tags to include."
|
|
||||||
msgstr "Produkt-Schlagwörter auswählen, die einbezogen werden sollen."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Product SKUs"
|
|
||||||
msgstr "Produkt-Artikelnummern"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Enter product SKUs separated by commas."
|
|
||||||
msgstr "Gib Produkt-Artikelnummern durch Kommas getrennt ein."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "SKU-1, SKU-2, SKU-3"
|
|
||||||
msgstr "ART-1, ART-2, ART-3"
|
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
|
||||||
msgid "\"%s\" is out of stock and cannot be selected."
|
msgid "\"%s\" is out of stock and cannot be selected."
|
||||||
msgstr "\"%s\" ist nicht an Lager und kann nicht ausgewählt werden."
|
msgstr "\"%s\" ist nicht an Lager und kann nicht ausgewählt werden."
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: templates/product-selector.html.twig
|
||||||
msgid "Only %2$d of \"%1$s\" are available in stock."
|
msgid "Add to Cart"
|
||||||
msgstr "Nur %2$d von \"%1$s\" sind an Lager verfügbar."
|
msgstr "In den Warenkorb"
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: includes/Admin/ProductData.php:108
|
||||||
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
msgid ""
|
||||||
msgstr "Lagerbestand reduziert für \"%1$s\": -%2$d (verbleibend: %3$d)"
|
"Allow draft and private products in the selection. Useful when products "
|
||||||
|
"should only be sold as part of a composition."
|
||||||
|
msgstr ""
|
||||||
|
"Entwürfe und private Produkte in der Auswahl zulassen. Nützlich, wenn "
|
||||||
|
"Produkte nur als Teil einer Zusammenstellung verkauft werden sollen."
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: includes/Admin/Settings.php:65
|
||||||
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
msgid ""
|
||||||
msgstr "Lagerbestand wiederhergestellt für \"%1$s\": +%2$d (gesamt: %3$d)"
|
"Allow draft and private products to appear in composable product selections. "
|
||||||
|
"Useful when products should only be sold as part of a composition, not "
|
||||||
|
"individually."
|
||||||
|
msgstr ""
|
||||||
|
"Entwürfe und private Produkte in der Auswahl zusammenstellbarer Produkte "
|
||||||
|
"anzeigen. Nützlich, wenn Produkte nur als Teil einer Zusammenstellung "
|
||||||
|
"verkauft werden sollen, nicht einzeln."
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:126
|
||||||
msgid "Out of stock"
|
msgid "By Category"
|
||||||
msgstr "Nicht an Lager"
|
msgstr "Nach Kategorie"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:128
|
||||||
msgid "Only"
|
msgid "By SKU"
|
||||||
msgstr "Nur"
|
msgstr "Nach Artikelnummer"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:127
|
||||||
msgid "left"
|
msgid "By Tag"
|
||||||
msgstr "übrig"
|
msgstr "Nach Schlagwort"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Choose up to"
|
||||||
|
msgstr "Wähle bis zu"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:34
|
||||||
|
msgid "Composable Options"
|
||||||
|
msgstr "Zusammenstellungsoptionen"
|
||||||
|
|
||||||
|
#: includes/Plugin.php:132
|
||||||
|
msgid "Composable product"
|
||||||
|
msgstr "Zusammenstellbares Produkt"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:21
|
||||||
|
msgid "Composable Products"
|
||||||
|
msgstr "Zusammenstellbare Produkte"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:34
|
||||||
|
msgid "Composable Products Settings"
|
||||||
|
msgstr "Einstellungen für zusammenstellbare Produkte"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:36
|
||||||
|
msgid "Configure default settings for composable products."
|
||||||
|
msgstr "Konfiguriere die Standardeinstellungen für zusammenstellbare Produkte."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:52
|
||||||
|
msgid "Default Pricing Mode"
|
||||||
|
msgstr "Standard-Preismodus"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:40
|
||||||
|
msgid "Default Selection Limit"
|
||||||
|
msgstr "Standard-Auswahllimit"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:41
|
||||||
|
msgid "Default number of items customers can select."
|
||||||
|
msgstr ""
|
||||||
|
"Standardanzahl der Artikel, die Kunden auswählen können (z.B. 5 Sticker für "
|
||||||
|
"CHF 50.-)."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:79
|
||||||
|
msgid "Display individual product prices in the selection interface."
|
||||||
|
msgstr "Einzelne Produktpreise in der Auswahlschnittstelle anzeigen."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:72
|
||||||
|
msgid "Display product images in the selection interface."
|
||||||
|
msgstr "Produktbilder in der Auswahlschnittstelle anzeigen."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:86
|
||||||
|
msgid "Display the total price as customers make selections."
|
||||||
|
msgstr "Den Gesamtpreis anzeigen, während Kunden Auswahlen treffen."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:196
|
||||||
|
msgid "Enter product SKUs separated by commas."
|
||||||
|
msgstr "Gib Produkt-Artikelnummern durch Kommas getrennt ein."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:83
|
||||||
|
msgid "Enter the fixed price for this composable product."
|
||||||
|
msgstr "Gib den Festpreis für dieses zusammenstellbare Produkt ein."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:82
|
||||||
|
msgid "Fixed Price"
|
||||||
|
msgstr "Festpreis"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:74 includes/Admin/Settings.php:59
|
||||||
|
msgid "Fixed price"
|
||||||
|
msgstr "Festpreis"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:53
|
||||||
|
msgid "How to calculate the price of composable products."
|
||||||
|
msgstr ""
|
||||||
|
"Wie der Preis für zusammenstellbare Produkte berechnet wird (z.B. Festpreis "
|
||||||
|
"CHF 50.- oder Summe)."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:69
|
||||||
|
msgid "How to calculate the price."
|
||||||
|
msgstr "Wie der Preis berechnet wird."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:123
|
||||||
|
msgid "How to select available products."
|
||||||
|
msgstr "Wie verfügbare Produkte ausgewählt werden."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
msgid "In stock"
|
msgid "In stock"
|
||||||
msgstr "An Lager"
|
msgstr "An Lager"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:107 includes/Admin/Settings.php:64
|
||||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
msgid "Include Non-Public Products"
|
||||||
msgstr "Keine Produkte zur Auswahl verfügbar. Bitte konfiguriere die Produktkriterien im Admin-Bereich."
|
msgstr "Nicht-öffentliche Produkte einbeziehen"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "items from the selection below."
|
||||||
|
msgstr "Artikel aus der untenstehenden Auswahl."
|
||||||
|
|
||||||
|
#: includes/Plugin.php:178
|
||||||
|
msgid "Maximum items selected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:55
|
||||||
|
msgid ""
|
||||||
|
"Maximum number of items customers can select. Leave empty to use global "
|
||||||
|
"default."
|
||||||
|
msgstr ""
|
||||||
|
"Maximale Anzahl der Artikel, die Kunden auswählen können. Leer lassen, um "
|
||||||
|
"den globalen Standard zu verwenden."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:113
|
||||||
|
msgid "No"
|
||||||
|
msgstr "Nein"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid ""
|
||||||
|
"No products available for selection. Please configure the product criteria "
|
||||||
|
"in the admin panel."
|
||||||
|
msgstr ""
|
||||||
|
"Keine Produkte zur Auswahl verfügbar. Bitte konfiguriere die "
|
||||||
|
"Produktkriterien im Admin-Bereich."
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:115
|
||||||
|
msgid "One or more selected products are not available."
|
||||||
|
msgstr "Ein oder mehrere ausgewählte Produkte sind nicht verfügbar."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Only"
|
||||||
|
msgstr "Nur"
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: stock quantity
|
||||||
|
#: includes/StockManager.php:69
|
||||||
|
#, php-format
|
||||||
|
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||||
|
msgstr "Nur %2$d von \"%1$s\" sind an Lager verfügbar."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Out of stock"
|
||||||
|
msgstr "Nicht an Lager"
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:84 includes/CartHandler.php:100
|
||||||
|
msgid "Please select at least one product."
|
||||||
|
msgstr "Bitte wähle mindestens ein Produkt aus."
|
||||||
|
|
||||||
|
#: includes/Plugin.php:179
|
||||||
|
msgid "Please select at least one item"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Plugin.php:177
|
||||||
|
msgid "Please select items"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:68
|
||||||
|
msgid "Pricing Mode"
|
||||||
|
msgstr "Preismodus"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:195
|
||||||
|
msgid "Product SKUs"
|
||||||
|
msgstr "Produkt-Artikelnummern"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:198
|
||||||
|
msgid "SKU-1, SKU-2, SKU-3"
|
||||||
|
msgstr "ART-1, ART-2, ART-3"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Select Your Products"
|
||||||
|
msgstr "Wähle deine Produkte"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:138
|
||||||
|
msgid "Select Categories"
|
||||||
|
msgstr "Kategorien auswählen"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:165
|
||||||
|
msgid "Select Tags"
|
||||||
|
msgstr "Schlagwörter auswählen"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:159
|
||||||
|
msgid "Select product categories to include."
|
||||||
|
msgstr "Produktkategorien auswählen, die einbezogen werden sollen."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:186
|
||||||
|
msgid "Select product tags to include."
|
||||||
|
msgstr "Produkt-Schlagwörter auswählen, die einbezogen werden sollen."
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:191
|
||||||
|
msgid "Selected Products"
|
||||||
|
msgstr "Ausgewählte Produkte"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:122
|
||||||
|
msgid "Selection Criteria"
|
||||||
|
msgstr "Auswahlkriterien"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:54
|
||||||
|
msgid "Selection Limit"
|
||||||
|
msgstr "Auswahllimit"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:71
|
||||||
|
msgid "Show Product Images"
|
||||||
|
msgstr "Produktbilder anzeigen"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:78
|
||||||
|
msgid "Show Product Prices"
|
||||||
|
msgstr "Produktpreise anzeigen"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:85
|
||||||
|
msgid "Show Total Price"
|
||||||
|
msgstr "Gesamtpreis anzeigen"
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: quantity, 3: remaining stock
|
||||||
|
#: includes/StockManager.php:168
|
||||||
|
#, php-format
|
||||||
|
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||||
|
msgstr "Lagerbestand reduziert für \"%1$s\": -%2$d (verbleibend: %3$d)"
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: quantity, 3: new stock
|
||||||
|
#: includes/StockManager.php:235
|
||||||
|
#, php-format
|
||||||
|
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||||
|
msgstr "Lagerbestand wiederhergestellt für \"%1$s\": +%2$d (gesamt: %3$d)"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:73 includes/Admin/Settings.php:58
|
||||||
|
msgid "Sum of selected products"
|
||||||
|
msgstr "Summe der ausgewählten Produkte"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Total Price:"
|
||||||
|
msgstr "Gesamtpreis:"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:72 includes/Admin/ProductData.php:111
|
||||||
|
msgid "Use global default"
|
||||||
|
msgstr "Globalen Standard verwenden"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:112
|
||||||
|
msgid "Yes"
|
||||||
|
msgstr "Ja"
|
||||||
|
|
||||||
|
#. translators: %d: selection limit
|
||||||
|
#: includes/CartHandler.php:95
|
||||||
|
#, php-format
|
||||||
|
msgid "You can select a maximum of %d products."
|
||||||
|
msgstr "Du kannst maximal %d Produkte auswählen."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "left"
|
||||||
|
msgstr "übrig"
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: WooCommerce Composable Products 1.0.0\n"
|
"Project-Id-Version: WooCommerce Composable Products 1.0.0\n"
|
||||||
"Report-Msgid-Bugs-To: https://github.com/magdev/wc-composable-product/issues\n"
|
"Report-Msgid-Bugs-To: https://github.com/magdev/wc-composable-product/"
|
||||||
"POT-Creation-Date: 2024-12-31 00:00+0000\n"
|
"issues\n"
|
||||||
|
"POT-Creation-Date: 2026-03-01 13:47+0100\n"
|
||||||
"PO-Revision-Date: 2024-12-31 00:00+0000\n"
|
"PO-Revision-Date: 2024-12-31 00:00+0000\n"
|
||||||
"Last-Translator: Claude AI\n"
|
"Last-Translator: Claude AI\n"
|
||||||
"Language-Team: German\n"
|
"Language-Team: German\n"
|
||||||
@@ -15,230 +16,297 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:44
|
||||||
msgid "WooCommerce Composable Products requires WooCommerce to be installed and active."
|
msgid ""
|
||||||
msgstr "WooCommerce Composable Products erfordert eine installierte und aktive WooCommerce-Installation."
|
"WooCommerce Composable Products requires WooCommerce to be installed and "
|
||||||
|
"active."
|
||||||
|
msgstr ""
|
||||||
|
"WooCommerce Composable Products erfordert eine installierte und aktive "
|
||||||
|
"WooCommerce-Installation."
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:90
|
||||||
msgid "This plugin requires WooCommerce to be installed and active."
|
msgid "This plugin requires WooCommerce to be installed and active."
|
||||||
msgstr "Dieses Plugin erfordert eine installierte und aktive WooCommerce-Installation."
|
msgstr ""
|
||||||
|
"Dieses Plugin erfordert eine installierte und aktive WooCommerce-"
|
||||||
|
"Installation."
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:91
|
||||||
msgid "Plugin Activation Error"
|
msgid "Plugin Activation Error"
|
||||||
msgstr "Plugin-Aktivierungsfehler"
|
msgstr "Plugin-Aktivierungsfehler"
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
#. translators: %s: product name
|
||||||
msgid "Composable Products"
|
#: includes/StockManager.php:60
|
||||||
msgstr "Zusammenstellbare Produkte"
|
#, php-format
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Composable Products Settings"
|
|
||||||
msgstr "Einstellungen für zusammenstellbare Produkte"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Configure default settings for composable products."
|
|
||||||
msgstr "Konfigurieren Sie die Standardeinstellungen für zusammenstellbare Produkte."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default Selection Limit"
|
|
||||||
msgstr "Standard-Auswahllimit"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default number of items customers can select."
|
|
||||||
msgstr "Standardanzahl der Artikel, die Kunden auswählen können."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default Pricing Mode"
|
|
||||||
msgstr "Standard-Preismodus"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "How to calculate the price of composable products."
|
|
||||||
msgstr "Wie der Preis für zusammenstellbare Produkte berechnet wird."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Sum of selected products"
|
|
||||||
msgstr "Summe der ausgewählten Produkte"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Fixed price"
|
|
||||||
msgstr "Festpreis"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Product Images"
|
|
||||||
msgstr "Produktbilder anzeigen"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display product images in the selection interface."
|
|
||||||
msgstr "Produktbilder in der Auswahlschnittstelle anzeigen."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Product Prices"
|
|
||||||
msgstr "Produktpreise anzeigen"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display individual product prices in the selection interface."
|
|
||||||
msgstr "Einzelne Produktpreise in der Auswahlschnittstelle anzeigen."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Total Price"
|
|
||||||
msgstr "Gesamtpreis anzeigen"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display the total price as customers make selections."
|
|
||||||
msgstr "Den Gesamtpreis anzeigen, während Kunden Auswahlen treffen."
|
|
||||||
|
|
||||||
#: includes/Plugin.php
|
|
||||||
msgid "Composable product"
|
|
||||||
msgstr "Zusammenstellbares Produkt"
|
|
||||||
|
|
||||||
#: includes/Product_Selector.php, templates/product-selector.twig
|
|
||||||
msgid "Select Your Products"
|
|
||||||
msgstr "Wählen Sie Ihre Produkte"
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Choose up to"
|
|
||||||
msgstr "Wählen Sie bis zu"
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "items from the selection below."
|
|
||||||
msgstr "Artikel aus der untenstehenden Auswahl."
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Total Price:"
|
|
||||||
msgstr "Gesamtpreis:"
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Add to Cart"
|
|
||||||
msgstr "In den Warenkorb"
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "Please select at least one product."
|
|
||||||
msgstr "Bitte wählen Sie mindestens ein Produkt aus."
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "You can select a maximum of %d products."
|
|
||||||
msgstr "Sie können maximal %d Produkte auswählen."
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "One or more selected products are not available."
|
|
||||||
msgstr "Ein oder mehrere ausgewählte Produkte sind nicht verfügbar."
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "Selected Products"
|
|
||||||
msgstr "Ausgewählte Produkte"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Composable Options"
|
|
||||||
msgstr "Zusammenstellungsoptionen"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Selection Limit"
|
|
||||||
msgstr "Auswahllimit"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Maximum number of items customers can select. Leave empty to use global default."
|
|
||||||
msgstr "Maximale Anzahl der Artikel, die Kunden auswählen können. Leer lassen, um den globalen Standard zu verwenden."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Pricing Mode"
|
|
||||||
msgstr "Preismodus"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "How to calculate the price."
|
|
||||||
msgstr "Wie der Preis berechnet wird."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Fixed Price"
|
|
||||||
msgstr "Festpreis"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Enter the fixed price for this composable product."
|
|
||||||
msgstr "Geben Sie den Festpreis für dieses zusammenstellbare Produkt ein."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Use global default"
|
|
||||||
msgstr "Globalen Standard verwenden"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Selection Criteria"
|
|
||||||
msgstr "Auswahlkriterien"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "How to select available products."
|
|
||||||
msgstr "Wie verfügbare Produkte ausgewählt werden."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By Category"
|
|
||||||
msgstr "Nach Kategorie"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By Tag"
|
|
||||||
msgstr "Nach Schlagwort"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By SKU"
|
|
||||||
msgstr "Nach Artikelnummer"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select Categories"
|
|
||||||
msgstr "Kategorien auswählen"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select product categories to include."
|
|
||||||
msgstr "Produktkategorien auswählen, die einbezogen werden sollen."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select Tags"
|
|
||||||
msgstr "Schlagwörter auswählen"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select product tags to include."
|
|
||||||
msgstr "Produkt-Schlagwörter auswählen, die einbezogen werden sollen."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Product SKUs"
|
|
||||||
msgstr "Produkt-Artikelnummern"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Enter product SKUs separated by commas."
|
|
||||||
msgstr "Geben Sie Produkt-Artikelnummern durch Kommas getrennt ein."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "SKU-1, SKU-2, SKU-3"
|
|
||||||
msgstr "ART-1, ART-2, ART-3"
|
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
|
||||||
msgid "\"%s\" is out of stock and cannot be selected."
|
msgid "\"%s\" is out of stock and cannot be selected."
|
||||||
msgstr "\"%s\" ist nicht auf Lager und kann nicht ausgewählt werden."
|
msgstr "\"%s\" ist nicht auf Lager und kann nicht ausgewählt werden."
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: templates/product-selector.html.twig
|
||||||
msgid "Only %2$d of \"%1$s\" are available in stock."
|
msgid "Add to Cart"
|
||||||
msgstr "Nur %2$d von \"%1$s\" sind auf Lager verfügbar."
|
msgstr "In den Warenkorb"
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: includes/Admin/ProductData.php:108
|
||||||
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
msgid ""
|
||||||
msgstr "Lagerbestand reduziert für \"%1$s\": -%2$d (verbleibend: %3$d)"
|
"Allow draft and private products in the selection. Useful when products "
|
||||||
|
"should only be sold as part of a composition."
|
||||||
|
msgstr ""
|
||||||
|
"Entwürfe und private Produkte in der Auswahl zulassen. Nützlich, wenn "
|
||||||
|
"Produkte nur als Teil einer Zusammenstellung verkauft werden sollen."
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: includes/Admin/Settings.php:65
|
||||||
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
msgid ""
|
||||||
msgstr "Lagerbestand wiederhergestellt für \"%1$s\": +%2$d (gesamt: %3$d)"
|
"Allow draft and private products to appear in composable product selections. "
|
||||||
|
"Useful when products should only be sold as part of a composition, not "
|
||||||
|
"individually."
|
||||||
|
msgstr ""
|
||||||
|
"Entwürfe und private Produkte in der Auswahl zusammenstellbarer Produkte "
|
||||||
|
"anzeigen. Nützlich, wenn Produkte nur als Teil einer Zusammenstellung "
|
||||||
|
"verkauft werden sollen, nicht einzeln."
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:126
|
||||||
msgid "Out of stock"
|
msgid "By Category"
|
||||||
msgstr "Nicht auf Lager"
|
msgstr "Nach Kategorie"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:128
|
||||||
msgid "Only"
|
msgid "By SKU"
|
||||||
msgstr "Nur"
|
msgstr "Nach Artikelnummer"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:127
|
||||||
msgid "left"
|
msgid "By Tag"
|
||||||
msgstr "übrig"
|
msgstr "Nach Schlagwort"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Choose up to"
|
||||||
|
msgstr "Wählen Sie bis zu"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:34
|
||||||
|
msgid "Composable Options"
|
||||||
|
msgstr "Zusammenstellungsoptionen"
|
||||||
|
|
||||||
|
#: includes/Plugin.php:132
|
||||||
|
msgid "Composable product"
|
||||||
|
msgstr "Zusammenstellbares Produkt"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:21
|
||||||
|
msgid "Composable Products"
|
||||||
|
msgstr "Zusammenstellbare Produkte"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:34
|
||||||
|
msgid "Composable Products Settings"
|
||||||
|
msgstr "Einstellungen für zusammenstellbare Produkte"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:36
|
||||||
|
msgid "Configure default settings for composable products."
|
||||||
|
msgstr ""
|
||||||
|
"Konfigurieren Sie die Standardeinstellungen für zusammenstellbare Produkte."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:52
|
||||||
|
msgid "Default Pricing Mode"
|
||||||
|
msgstr "Standard-Preismodus"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:40
|
||||||
|
msgid "Default Selection Limit"
|
||||||
|
msgstr "Standard-Auswahllimit"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:41
|
||||||
|
msgid "Default number of items customers can select."
|
||||||
|
msgstr "Standardanzahl der Artikel, die Kunden auswählen können."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:79
|
||||||
|
msgid "Display individual product prices in the selection interface."
|
||||||
|
msgstr "Einzelne Produktpreise in der Auswahlschnittstelle anzeigen."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:72
|
||||||
|
msgid "Display product images in the selection interface."
|
||||||
|
msgstr "Produktbilder in der Auswahlschnittstelle anzeigen."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:86
|
||||||
|
msgid "Display the total price as customers make selections."
|
||||||
|
msgstr "Den Gesamtpreis anzeigen, während Kunden Auswahlen treffen."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:196
|
||||||
|
msgid "Enter product SKUs separated by commas."
|
||||||
|
msgstr "Geben Sie Produkt-Artikelnummern durch Kommas getrennt ein."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:83
|
||||||
|
msgid "Enter the fixed price for this composable product."
|
||||||
|
msgstr "Geben Sie den Festpreis für dieses zusammenstellbare Produkt ein."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:82
|
||||||
|
msgid "Fixed Price"
|
||||||
|
msgstr "Festpreis"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:74 includes/Admin/Settings.php:59
|
||||||
|
msgid "Fixed price"
|
||||||
|
msgstr "Festpreis"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:53
|
||||||
|
msgid "How to calculate the price of composable products."
|
||||||
|
msgstr "Wie der Preis für zusammenstellbare Produkte berechnet wird."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:69
|
||||||
|
msgid "How to calculate the price."
|
||||||
|
msgstr "Wie der Preis berechnet wird."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:123
|
||||||
|
msgid "How to select available products."
|
||||||
|
msgstr "Wie verfügbare Produkte ausgewählt werden."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
msgid "In stock"
|
msgid "In stock"
|
||||||
msgstr "Auf Lager"
|
msgstr "Auf Lager"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:107 includes/Admin/Settings.php:64
|
||||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
msgid "Include Non-Public Products"
|
||||||
msgstr "Keine Produkte zur Auswahl verfügbar. Bitte konfigurieren Sie die Produktkriterien im Admin-Bereich."
|
msgstr "Nicht-öffentliche Produkte einbeziehen"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "items from the selection below."
|
||||||
|
msgstr "Artikel aus der untenstehenden Auswahl."
|
||||||
|
|
||||||
|
#: includes/Plugin.php:178
|
||||||
|
msgid "Maximum items selected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:55
|
||||||
|
msgid ""
|
||||||
|
"Maximum number of items customers can select. Leave empty to use global "
|
||||||
|
"default."
|
||||||
|
msgstr ""
|
||||||
|
"Maximale Anzahl der Artikel, die Kunden auswählen können. Leer lassen, um "
|
||||||
|
"den globalen Standard zu verwenden."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:113
|
||||||
|
msgid "No"
|
||||||
|
msgstr "Nein"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid ""
|
||||||
|
"No products available for selection. Please configure the product criteria "
|
||||||
|
"in the admin panel."
|
||||||
|
msgstr ""
|
||||||
|
"Keine Produkte zur Auswahl verfügbar. Bitte konfigurieren Sie die "
|
||||||
|
"Produktkriterien im Admin-Bereich."
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:115
|
||||||
|
msgid "One or more selected products are not available."
|
||||||
|
msgstr "Ein oder mehrere ausgewählte Produkte sind nicht verfügbar."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Only"
|
||||||
|
msgstr "Nur"
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: stock quantity
|
||||||
|
#: includes/StockManager.php:69
|
||||||
|
#, php-format
|
||||||
|
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||||
|
msgstr "Nur %2$d von \"%1$s\" sind auf Lager verfügbar."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Out of stock"
|
||||||
|
msgstr "Nicht auf Lager"
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:84 includes/CartHandler.php:100
|
||||||
|
msgid "Please select at least one product."
|
||||||
|
msgstr "Bitte wählen Sie mindestens ein Produkt aus."
|
||||||
|
|
||||||
|
#: includes/Plugin.php:179
|
||||||
|
msgid "Please select at least one item"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Plugin.php:177
|
||||||
|
msgid "Please select items"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:68
|
||||||
|
msgid "Pricing Mode"
|
||||||
|
msgstr "Preismodus"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:195
|
||||||
|
msgid "Product SKUs"
|
||||||
|
msgstr "Produkt-Artikelnummern"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:198
|
||||||
|
msgid "SKU-1, SKU-2, SKU-3"
|
||||||
|
msgstr "ART-1, ART-2, ART-3"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Select Your Products"
|
||||||
|
msgstr "Wählen Sie Ihre Produkte"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:138
|
||||||
|
msgid "Select Categories"
|
||||||
|
msgstr "Kategorien auswählen"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:165
|
||||||
|
msgid "Select Tags"
|
||||||
|
msgstr "Schlagwörter auswählen"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:159
|
||||||
|
msgid "Select product categories to include."
|
||||||
|
msgstr "Produktkategorien auswählen, die einbezogen werden sollen."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:186
|
||||||
|
msgid "Select product tags to include."
|
||||||
|
msgstr "Produkt-Schlagwörter auswählen, die einbezogen werden sollen."
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:191
|
||||||
|
msgid "Selected Products"
|
||||||
|
msgstr "Ausgewählte Produkte"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:122
|
||||||
|
msgid "Selection Criteria"
|
||||||
|
msgstr "Auswahlkriterien"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:54
|
||||||
|
msgid "Selection Limit"
|
||||||
|
msgstr "Auswahllimit"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:71
|
||||||
|
msgid "Show Product Images"
|
||||||
|
msgstr "Produktbilder anzeigen"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:78
|
||||||
|
msgid "Show Product Prices"
|
||||||
|
msgstr "Produktpreise anzeigen"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:85
|
||||||
|
msgid "Show Total Price"
|
||||||
|
msgstr "Gesamtpreis anzeigen"
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: quantity, 3: remaining stock
|
||||||
|
#: includes/StockManager.php:168
|
||||||
|
#, php-format
|
||||||
|
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||||
|
msgstr "Lagerbestand reduziert für \"%1$s\": -%2$d (verbleibend: %3$d)"
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: quantity, 3: new stock
|
||||||
|
#: includes/StockManager.php:235
|
||||||
|
#, php-format
|
||||||
|
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||||
|
msgstr "Lagerbestand wiederhergestellt für \"%1$s\": +%2$d (gesamt: %3$d)"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:73 includes/Admin/Settings.php:58
|
||||||
|
msgid "Sum of selected products"
|
||||||
|
msgstr "Summe der ausgewählten Produkte"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Total Price:"
|
||||||
|
msgstr "Gesamtpreis:"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:72 includes/Admin/ProductData.php:111
|
||||||
|
msgid "Use global default"
|
||||||
|
msgstr "Globalen Standard verwenden"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:112
|
||||||
|
msgid "Yes"
|
||||||
|
msgstr "Ja"
|
||||||
|
|
||||||
|
#. translators: %d: selection limit
|
||||||
|
#: includes/CartHandler.php:95
|
||||||
|
#, php-format
|
||||||
|
msgid "You can select a maximum of %d products."
|
||||||
|
msgstr "Sie können maximal %d Produkte auswählen."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "left"
|
||||||
|
msgstr "übrig"
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: WooCommerce Composable Products 1.0.0\n"
|
"Project-Id-Version: WooCommerce Composable Products 1.0.0\n"
|
||||||
"Report-Msgid-Bugs-To: https://github.com/magdev/wc-composable-product/issues\n"
|
"Report-Msgid-Bugs-To: https://github.com/magdev/wc-composable-product/"
|
||||||
"POT-Creation-Date: 2024-12-31 00:00+0000\n"
|
"issues\n"
|
||||||
|
"POT-Creation-Date: 2026-03-01 13:47+0100\n"
|
||||||
"PO-Revision-Date: 2024-12-31 00:00+0000\n"
|
"PO-Revision-Date: 2024-12-31 00:00+0000\n"
|
||||||
"Last-Translator: Claude AI\n"
|
"Last-Translator: Claude AI\n"
|
||||||
"Language-Team: German\n"
|
"Language-Team: German\n"
|
||||||
@@ -15,230 +16,296 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:44
|
||||||
msgid "WooCommerce Composable Products requires WooCommerce to be installed and active."
|
msgid ""
|
||||||
msgstr "WooCommerce Composable Products erfordert eine installierte und aktive WooCommerce-Installation."
|
"WooCommerce Composable Products requires WooCommerce to be installed and "
|
||||||
|
"active."
|
||||||
|
msgstr ""
|
||||||
|
"WooCommerce Composable Products erfordert eine installierte und aktive "
|
||||||
|
"WooCommerce-Installation."
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:90
|
||||||
msgid "This plugin requires WooCommerce to be installed and active."
|
msgid "This plugin requires WooCommerce to be installed and active."
|
||||||
msgstr "Dieses Plugin erfordert eine installierte und aktive WooCommerce-Installation."
|
msgstr ""
|
||||||
|
"Dieses Plugin erfordert eine installierte und aktive WooCommerce-"
|
||||||
|
"Installation."
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:91
|
||||||
msgid "Plugin Activation Error"
|
msgid "Plugin Activation Error"
|
||||||
msgstr "Plugin-Aktivierungsfehler"
|
msgstr "Plugin-Aktivierungsfehler"
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
#. translators: %s: product name
|
||||||
msgid "Composable Products"
|
#: includes/StockManager.php:60
|
||||||
msgstr "Zusammenstellbare Produkte"
|
#, php-format
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Composable Products Settings"
|
|
||||||
msgstr "Einstellungen für zusammenstellbare Produkte"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Configure default settings for composable products."
|
|
||||||
msgstr "Konfiguriere die Standardeinstellungen für zusammenstellbare Produkte."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default Selection Limit"
|
|
||||||
msgstr "Standard-Auswahllimit"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default number of items customers can select."
|
|
||||||
msgstr "Standardanzahl der Artikel, die Kunden auswählen können."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default Pricing Mode"
|
|
||||||
msgstr "Standard-Preismodus"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "How to calculate the price of composable products."
|
|
||||||
msgstr "Wie der Preis für zusammenstellbare Produkte berechnet wird."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Sum of selected products"
|
|
||||||
msgstr "Summe der ausgewählten Produkte"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Fixed price"
|
|
||||||
msgstr "Festpreis"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Product Images"
|
|
||||||
msgstr "Produktbilder anzeigen"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display product images in the selection interface."
|
|
||||||
msgstr "Produktbilder in der Auswahlschnittstelle anzeigen."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Product Prices"
|
|
||||||
msgstr "Produktpreise anzeigen"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display individual product prices in the selection interface."
|
|
||||||
msgstr "Einzelne Produktpreise in der Auswahlschnittstelle anzeigen."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Total Price"
|
|
||||||
msgstr "Gesamtpreis anzeigen"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display the total price as customers make selections."
|
|
||||||
msgstr "Den Gesamtpreis anzeigen, während Kunden Auswahlen treffen."
|
|
||||||
|
|
||||||
#: includes/Plugin.php
|
|
||||||
msgid "Composable product"
|
|
||||||
msgstr "Zusammenstellbares Produkt"
|
|
||||||
|
|
||||||
#: includes/Product_Selector.php, templates/product-selector.twig
|
|
||||||
msgid "Select Your Products"
|
|
||||||
msgstr "Wähle deine Produkte"
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Choose up to"
|
|
||||||
msgstr "Wähle bis zu"
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "items from the selection below."
|
|
||||||
msgstr "Artikel aus der untenstehenden Auswahl."
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Total Price:"
|
|
||||||
msgstr "Gesamtpreis:"
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Add to Cart"
|
|
||||||
msgstr "In den Warenkorb"
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "Please select at least one product."
|
|
||||||
msgstr "Bitte wähle mindestens ein Produkt aus."
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "You can select a maximum of %d products."
|
|
||||||
msgstr "Du kannst maximal %d Produkte auswählen."
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "One or more selected products are not available."
|
|
||||||
msgstr "Ein oder mehrere ausgewählte Produkte sind nicht verfügbar."
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "Selected Products"
|
|
||||||
msgstr "Ausgewählte Produkte"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Composable Options"
|
|
||||||
msgstr "Zusammenstellungsoptionen"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Selection Limit"
|
|
||||||
msgstr "Auswahllimit"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Maximum number of items customers can select. Leave empty to use global default."
|
|
||||||
msgstr "Maximale Anzahl der Artikel, die Kunden auswählen können. Leer lassen, um den globalen Standard zu verwenden."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Pricing Mode"
|
|
||||||
msgstr "Preismodus"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "How to calculate the price."
|
|
||||||
msgstr "Wie der Preis berechnet wird."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Fixed Price"
|
|
||||||
msgstr "Festpreis"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Enter the fixed price for this composable product."
|
|
||||||
msgstr "Gib den Festpreis für dieses zusammenstellbare Produkt ein."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Use global default"
|
|
||||||
msgstr "Globalen Standard verwenden"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Selection Criteria"
|
|
||||||
msgstr "Auswahlkriterien"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "How to select available products."
|
|
||||||
msgstr "Wie verfügbare Produkte ausgewählt werden."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By Category"
|
|
||||||
msgstr "Nach Kategorie"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By Tag"
|
|
||||||
msgstr "Nach Schlagwort"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By SKU"
|
|
||||||
msgstr "Nach Artikelnummer"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select Categories"
|
|
||||||
msgstr "Kategorien auswählen"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select product categories to include."
|
|
||||||
msgstr "Produktkategorien auswählen, die einbezogen werden sollen."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select Tags"
|
|
||||||
msgstr "Schlagwörter auswählen"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select product tags to include."
|
|
||||||
msgstr "Produkt-Schlagwörter auswählen, die einbezogen werden sollen."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Product SKUs"
|
|
||||||
msgstr "Produkt-Artikelnummern"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Enter product SKUs separated by commas."
|
|
||||||
msgstr "Gib Produkt-Artikelnummern durch Kommas getrennt ein."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "SKU-1, SKU-2, SKU-3"
|
|
||||||
msgstr "ART-1, ART-2, ART-3"
|
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
|
||||||
msgid "\"%s\" is out of stock and cannot be selected."
|
msgid "\"%s\" is out of stock and cannot be selected."
|
||||||
msgstr "\"%s\" ist nicht auf Lager und kann nicht ausgewählt werden."
|
msgstr "\"%s\" ist nicht auf Lager und kann nicht ausgewählt werden."
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: templates/product-selector.html.twig
|
||||||
msgid "Only %2$d of \"%1$s\" are available in stock."
|
msgid "Add to Cart"
|
||||||
msgstr "Nur %2$d von \"%1$s\" sind auf Lager verfügbar."
|
msgstr "In den Warenkorb"
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: includes/Admin/ProductData.php:108
|
||||||
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
msgid ""
|
||||||
msgstr "Lagerbestand reduziert für \"%1$s\": -%2$d (verbleibend: %3$d)"
|
"Allow draft and private products in the selection. Useful when products "
|
||||||
|
"should only be sold as part of a composition."
|
||||||
|
msgstr ""
|
||||||
|
"Entwürfe und private Produkte in der Auswahl zulassen. Nützlich, wenn "
|
||||||
|
"Produkte nur als Teil einer Zusammenstellung verkauft werden sollen."
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: includes/Admin/Settings.php:65
|
||||||
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
msgid ""
|
||||||
msgstr "Lagerbestand wiederhergestellt für \"%1$s\": +%2$d (gesamt: %3$d)"
|
"Allow draft and private products to appear in composable product selections. "
|
||||||
|
"Useful when products should only be sold as part of a composition, not "
|
||||||
|
"individually."
|
||||||
|
msgstr ""
|
||||||
|
"Entwürfe und private Produkte in der Auswahl zusammenstellbarer Produkte "
|
||||||
|
"anzeigen. Nützlich, wenn Produkte nur als Teil einer Zusammenstellung "
|
||||||
|
"verkauft werden sollen, nicht einzeln."
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:126
|
||||||
msgid "Out of stock"
|
msgid "By Category"
|
||||||
msgstr "Nicht auf Lager"
|
msgstr "Nach Kategorie"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:128
|
||||||
msgid "Only"
|
msgid "By SKU"
|
||||||
msgstr "Nur"
|
msgstr "Nach Artikelnummer"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:127
|
||||||
msgid "left"
|
msgid "By Tag"
|
||||||
msgstr "übrig"
|
msgstr "Nach Schlagwort"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Choose up to"
|
||||||
|
msgstr "Wähle bis zu"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:34
|
||||||
|
msgid "Composable Options"
|
||||||
|
msgstr "Zusammenstellungsoptionen"
|
||||||
|
|
||||||
|
#: includes/Plugin.php:132
|
||||||
|
msgid "Composable product"
|
||||||
|
msgstr "Zusammenstellbares Produkt"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:21
|
||||||
|
msgid "Composable Products"
|
||||||
|
msgstr "Zusammenstellbare Produkte"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:34
|
||||||
|
msgid "Composable Products Settings"
|
||||||
|
msgstr "Einstellungen für zusammenstellbare Produkte"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:36
|
||||||
|
msgid "Configure default settings for composable products."
|
||||||
|
msgstr "Konfiguriere die Standardeinstellungen für zusammenstellbare Produkte."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:52
|
||||||
|
msgid "Default Pricing Mode"
|
||||||
|
msgstr "Standard-Preismodus"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:40
|
||||||
|
msgid "Default Selection Limit"
|
||||||
|
msgstr "Standard-Auswahllimit"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:41
|
||||||
|
msgid "Default number of items customers can select."
|
||||||
|
msgstr "Standardanzahl der Artikel, die Kunden auswählen können."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:79
|
||||||
|
msgid "Display individual product prices in the selection interface."
|
||||||
|
msgstr "Einzelne Produktpreise in der Auswahlschnittstelle anzeigen."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:72
|
||||||
|
msgid "Display product images in the selection interface."
|
||||||
|
msgstr "Produktbilder in der Auswahlschnittstelle anzeigen."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:86
|
||||||
|
msgid "Display the total price as customers make selections."
|
||||||
|
msgstr "Den Gesamtpreis anzeigen, während Kunden Auswahlen treffen."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:196
|
||||||
|
msgid "Enter product SKUs separated by commas."
|
||||||
|
msgstr "Gib Produkt-Artikelnummern durch Kommas getrennt ein."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:83
|
||||||
|
msgid "Enter the fixed price for this composable product."
|
||||||
|
msgstr "Gib den Festpreis für dieses zusammenstellbare Produkt ein."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:82
|
||||||
|
msgid "Fixed Price"
|
||||||
|
msgstr "Festpreis"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:74 includes/Admin/Settings.php:59
|
||||||
|
msgid "Fixed price"
|
||||||
|
msgstr "Festpreis"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:53
|
||||||
|
msgid "How to calculate the price of composable products."
|
||||||
|
msgstr "Wie der Preis für zusammenstellbare Produkte berechnet wird."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:69
|
||||||
|
msgid "How to calculate the price."
|
||||||
|
msgstr "Wie der Preis berechnet wird."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:123
|
||||||
|
msgid "How to select available products."
|
||||||
|
msgstr "Wie verfügbare Produkte ausgewählt werden."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
msgid "In stock"
|
msgid "In stock"
|
||||||
msgstr "Auf Lager"
|
msgstr "Auf Lager"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:107 includes/Admin/Settings.php:64
|
||||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
msgid "Include Non-Public Products"
|
||||||
msgstr "Keine Produkte zur Auswahl verfügbar. Bitte konfiguriere die Produktkriterien im Admin-Bereich."
|
msgstr "Nicht-öffentliche Produkte einbeziehen"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "items from the selection below."
|
||||||
|
msgstr "Artikel aus der untenstehenden Auswahl."
|
||||||
|
|
||||||
|
#: includes/Plugin.php:178
|
||||||
|
msgid "Maximum items selected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:55
|
||||||
|
msgid ""
|
||||||
|
"Maximum number of items customers can select. Leave empty to use global "
|
||||||
|
"default."
|
||||||
|
msgstr ""
|
||||||
|
"Maximale Anzahl der Artikel, die Kunden auswählen können. Leer lassen, um "
|
||||||
|
"den globalen Standard zu verwenden."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:113
|
||||||
|
msgid "No"
|
||||||
|
msgstr "Nein"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid ""
|
||||||
|
"No products available for selection. Please configure the product criteria "
|
||||||
|
"in the admin panel."
|
||||||
|
msgstr ""
|
||||||
|
"Keine Produkte zur Auswahl verfügbar. Bitte konfiguriere die "
|
||||||
|
"Produktkriterien im Admin-Bereich."
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:115
|
||||||
|
msgid "One or more selected products are not available."
|
||||||
|
msgstr "Ein oder mehrere ausgewählte Produkte sind nicht verfügbar."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Only"
|
||||||
|
msgstr "Nur"
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: stock quantity
|
||||||
|
#: includes/StockManager.php:69
|
||||||
|
#, php-format
|
||||||
|
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||||
|
msgstr "Nur %2$d von \"%1$s\" sind auf Lager verfügbar."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Out of stock"
|
||||||
|
msgstr "Nicht auf Lager"
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:84 includes/CartHandler.php:100
|
||||||
|
msgid "Please select at least one product."
|
||||||
|
msgstr "Bitte wähle mindestens ein Produkt aus."
|
||||||
|
|
||||||
|
#: includes/Plugin.php:179
|
||||||
|
msgid "Please select at least one item"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Plugin.php:177
|
||||||
|
msgid "Please select items"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:68
|
||||||
|
msgid "Pricing Mode"
|
||||||
|
msgstr "Preismodus"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:195
|
||||||
|
msgid "Product SKUs"
|
||||||
|
msgstr "Produkt-Artikelnummern"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:198
|
||||||
|
msgid "SKU-1, SKU-2, SKU-3"
|
||||||
|
msgstr "ART-1, ART-2, ART-3"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Select Your Products"
|
||||||
|
msgstr "Wähle deine Produkte"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:138
|
||||||
|
msgid "Select Categories"
|
||||||
|
msgstr "Kategorien auswählen"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:165
|
||||||
|
msgid "Select Tags"
|
||||||
|
msgstr "Schlagwörter auswählen"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:159
|
||||||
|
msgid "Select product categories to include."
|
||||||
|
msgstr "Produktkategorien auswählen, die einbezogen werden sollen."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:186
|
||||||
|
msgid "Select product tags to include."
|
||||||
|
msgstr "Produkt-Schlagwörter auswählen, die einbezogen werden sollen."
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:191
|
||||||
|
msgid "Selected Products"
|
||||||
|
msgstr "Ausgewählte Produkte"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:122
|
||||||
|
msgid "Selection Criteria"
|
||||||
|
msgstr "Auswahlkriterien"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:54
|
||||||
|
msgid "Selection Limit"
|
||||||
|
msgstr "Auswahllimit"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:71
|
||||||
|
msgid "Show Product Images"
|
||||||
|
msgstr "Produktbilder anzeigen"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:78
|
||||||
|
msgid "Show Product Prices"
|
||||||
|
msgstr "Produktpreise anzeigen"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:85
|
||||||
|
msgid "Show Total Price"
|
||||||
|
msgstr "Gesamtpreis anzeigen"
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: quantity, 3: remaining stock
|
||||||
|
#: includes/StockManager.php:168
|
||||||
|
#, php-format
|
||||||
|
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||||
|
msgstr "Lagerbestand reduziert für \"%1$s\": -%2$d (verbleibend: %3$d)"
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: quantity, 3: new stock
|
||||||
|
#: includes/StockManager.php:235
|
||||||
|
#, php-format
|
||||||
|
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||||
|
msgstr "Lagerbestand wiederhergestellt für \"%1$s\": +%2$d (gesamt: %3$d)"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:73 includes/Admin/Settings.php:58
|
||||||
|
msgid "Sum of selected products"
|
||||||
|
msgstr "Summe der ausgewählten Produkte"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Total Price:"
|
||||||
|
msgstr "Gesamtpreis:"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:72 includes/Admin/ProductData.php:111
|
||||||
|
msgid "Use global default"
|
||||||
|
msgstr "Globalen Standard verwenden"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:112
|
||||||
|
msgid "Yes"
|
||||||
|
msgstr "Ja"
|
||||||
|
|
||||||
|
#. translators: %d: selection limit
|
||||||
|
#: includes/CartHandler.php:95
|
||||||
|
#, php-format
|
||||||
|
msgid "You can select a maximum of %d products."
|
||||||
|
msgstr "Du kannst maximal %d Produkte auswählen."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "left"
|
||||||
|
msgstr "übrig"
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: WooCommerce Composable Products 1.0.0\n"
|
"Project-Id-Version: WooCommerce Composable Products 1.0.0\n"
|
||||||
"Report-Msgid-Bugs-To: https://github.com/magdev/wc-composable-product/issues\n"
|
"Report-Msgid-Bugs-To: https://github.com/magdev/wc-composable-product/"
|
||||||
"POT-Creation-Date: 2024-12-31 00:00+0000\n"
|
"issues\n"
|
||||||
|
"POT-Creation-Date: 2026-03-01 13:47+0100\n"
|
||||||
"PO-Revision-Date: 2024-12-31 00:00+0000\n"
|
"PO-Revision-Date: 2024-12-31 00:00+0000\n"
|
||||||
"Last-Translator: Claude AI\n"
|
"Last-Translator: Claude AI\n"
|
||||||
"Language-Team: French (Switzerland)\n"
|
"Language-Team: French (Switzerland)\n"
|
||||||
@@ -15,230 +16,300 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:44
|
||||||
msgid "WooCommerce Composable Products requires WooCommerce to be installed and active."
|
msgid ""
|
||||||
msgstr "WooCommerce Composable Products nécessite une installation WooCommerce active."
|
"WooCommerce Composable Products requires WooCommerce to be installed and "
|
||||||
|
"active."
|
||||||
|
msgstr ""
|
||||||
|
"WooCommerce Composable Products nécessite une installation WooCommerce "
|
||||||
|
"active."
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:90
|
||||||
msgid "This plugin requires WooCommerce to be installed and active."
|
msgid "This plugin requires WooCommerce to be installed and active."
|
||||||
msgstr "Cette extension nécessite une installation WooCommerce active."
|
msgstr "Cette extension nécessite une installation WooCommerce active."
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:91
|
||||||
msgid "Plugin Activation Error"
|
msgid "Plugin Activation Error"
|
||||||
msgstr "Erreur d'activation de l'extension"
|
msgstr "Erreur d'activation de l'extension"
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
#. translators: %s: product name
|
||||||
msgid "Composable Products"
|
#: includes/StockManager.php:60
|
||||||
msgstr "Produits Composables"
|
#, php-format
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Composable Products Settings"
|
|
||||||
msgstr "Paramètres des produits composables"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Configure default settings for composable products."
|
|
||||||
msgstr "Configurez les paramètres par défaut pour les produits composables."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default Selection Limit"
|
|
||||||
msgstr "Limite de sélection par défaut"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default number of items customers can select."
|
|
||||||
msgstr "Nombre par défaut d'articles que les clients peuvent sélectionner (p. ex. 5 stickers pour CHF 50.-)."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default Pricing Mode"
|
|
||||||
msgstr "Mode de tarification par défaut"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "How to calculate the price of composable products."
|
|
||||||
msgstr "Comment calculer le prix des produits composables (p. ex. prix fixe CHF 50.- ou somme)."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Sum of selected products"
|
|
||||||
msgstr "Somme des produits sélectionnés"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Fixed price"
|
|
||||||
msgstr "Prix fixe"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Product Images"
|
|
||||||
msgstr "Afficher les images des produits"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display product images in the selection interface."
|
|
||||||
msgstr "Afficher les images des produits dans l'interface de sélection."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Product Prices"
|
|
||||||
msgstr "Afficher les prix des produits"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display individual product prices in the selection interface."
|
|
||||||
msgstr "Afficher les prix individuels des produits dans l'interface de sélection."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Total Price"
|
|
||||||
msgstr "Afficher le prix total"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display the total price as customers make selections."
|
|
||||||
msgstr "Afficher le prix total pendant que les clients font leurs sélections."
|
|
||||||
|
|
||||||
#: includes/Plugin.php
|
|
||||||
msgid "Composable product"
|
|
||||||
msgstr "Produit composable"
|
|
||||||
|
|
||||||
#: includes/Product_Selector.php, templates/product-selector.twig
|
|
||||||
msgid "Select Your Products"
|
|
||||||
msgstr "Sélectionnez vos produits"
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Choose up to"
|
|
||||||
msgstr "Choisissez jusqu'à"
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "items from the selection below."
|
|
||||||
msgstr "articles de la sélection ci-dessous."
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Total Price:"
|
|
||||||
msgstr "Prix total :"
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Add to Cart"
|
|
||||||
msgstr "Ajouter au panier"
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "Please select at least one product."
|
|
||||||
msgstr "Veuillez sélectionner au moins un produit."
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "You can select a maximum of %d products."
|
|
||||||
msgstr "Vous pouvez sélectionner un maximum de %d produits."
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "One or more selected products are not available."
|
|
||||||
msgstr "Un ou plusieurs produits sélectionnés ne sont pas disponibles."
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "Selected Products"
|
|
||||||
msgstr "Produits sélectionnés"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Composable Options"
|
|
||||||
msgstr "Options de composition"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Selection Limit"
|
|
||||||
msgstr "Limite de sélection"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Maximum number of items customers can select. Leave empty to use global default."
|
|
||||||
msgstr "Nombre maximum d'articles que les clients peuvent sélectionner. Laisser vide pour utiliser la valeur par défaut globale."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Pricing Mode"
|
|
||||||
msgstr "Mode de tarification"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "How to calculate the price."
|
|
||||||
msgstr "Comment calculer le prix."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Fixed Price"
|
|
||||||
msgstr "Prix fixe"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Enter the fixed price for this composable product."
|
|
||||||
msgstr "Entrez le prix fixe pour ce produit composable."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Use global default"
|
|
||||||
msgstr "Utiliser la valeur par défaut globale"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Selection Criteria"
|
|
||||||
msgstr "Critères de sélection"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "How to select available products."
|
|
||||||
msgstr "Comment sélectionner les produits disponibles."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By Category"
|
|
||||||
msgstr "Par catégorie"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By Tag"
|
|
||||||
msgstr "Par étiquette"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By SKU"
|
|
||||||
msgstr "Par référence"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select Categories"
|
|
||||||
msgstr "Sélectionner les catégories"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select product categories to include."
|
|
||||||
msgstr "Sélectionner les catégories de produits à inclure."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select Tags"
|
|
||||||
msgstr "Sélectionner les étiquettes"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select product tags to include."
|
|
||||||
msgstr "Sélectionner les étiquettes de produits à inclure."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Product SKUs"
|
|
||||||
msgstr "Références des produits"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Enter product SKUs separated by commas."
|
|
||||||
msgstr "Entrez les références des produits séparées par des virgules."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "SKU-1, SKU-2, SKU-3"
|
|
||||||
msgstr "REF-1, REF-2, REF-3"
|
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
|
||||||
msgid "\"%s\" is out of stock and cannot be selected."
|
msgid "\"%s\" is out of stock and cannot be selected."
|
||||||
msgstr "\"%s\" est en rupture de stock et ne peut pas être sélectionné."
|
msgstr "\"%s\" est en rupture de stock et ne peut pas être sélectionné."
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: templates/product-selector.html.twig
|
||||||
msgid "Only %2$d of \"%1$s\" are available in stock."
|
msgid "Add to Cart"
|
||||||
msgstr "Seulement %2$d de \"%1$s\" sont disponibles en stock."
|
msgstr "Ajouter au panier"
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: includes/Admin/ProductData.php:108
|
||||||
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
msgid ""
|
||||||
msgstr "Stock réduit pour \"%1$s\": -%2$d (restant: %3$d)"
|
"Allow draft and private products in the selection. Useful when products "
|
||||||
|
"should only be sold as part of a composition."
|
||||||
|
msgstr ""
|
||||||
|
"Autoriser les brouillons et les produits privés dans la sélection. Utile "
|
||||||
|
"lorsque les produits ne doivent être vendus que dans le cadre d'une "
|
||||||
|
"composition."
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: includes/Admin/Settings.php:65
|
||||||
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
msgid ""
|
||||||
msgstr "Stock restauré pour \"%1$s\": +%2$d (total: %3$d)"
|
"Allow draft and private products to appear in composable product selections. "
|
||||||
|
"Useful when products should only be sold as part of a composition, not "
|
||||||
|
"individually."
|
||||||
|
msgstr ""
|
||||||
|
"Autoriser les brouillons et les produits privés dans les sélections de "
|
||||||
|
"produits composables. Utile lorsque les produits ne doivent être vendus que "
|
||||||
|
"dans le cadre d'une composition, pas individuellement."
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:126
|
||||||
msgid "Out of stock"
|
msgid "By Category"
|
||||||
msgstr "Rupture de stock"
|
msgstr "Par catégorie"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:128
|
||||||
msgid "Only"
|
msgid "By SKU"
|
||||||
msgstr "Seulement"
|
msgstr "Par référence"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:127
|
||||||
msgid "left"
|
msgid "By Tag"
|
||||||
msgstr "restant"
|
msgstr "Par étiquette"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Choose up to"
|
||||||
|
msgstr "Choisissez jusqu'à"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:34
|
||||||
|
msgid "Composable Options"
|
||||||
|
msgstr "Options de composition"
|
||||||
|
|
||||||
|
#: includes/Plugin.php:132
|
||||||
|
msgid "Composable product"
|
||||||
|
msgstr "Produit composable"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:21
|
||||||
|
msgid "Composable Products"
|
||||||
|
msgstr "Produits Composables"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:34
|
||||||
|
msgid "Composable Products Settings"
|
||||||
|
msgstr "Paramètres des produits composables"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:36
|
||||||
|
msgid "Configure default settings for composable products."
|
||||||
|
msgstr "Configurez les paramètres par défaut pour les produits composables."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:52
|
||||||
|
msgid "Default Pricing Mode"
|
||||||
|
msgstr "Mode de tarification par défaut"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:40
|
||||||
|
msgid "Default Selection Limit"
|
||||||
|
msgstr "Limite de sélection par défaut"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:41
|
||||||
|
msgid "Default number of items customers can select."
|
||||||
|
msgstr ""
|
||||||
|
"Nombre par défaut d'articles que les clients peuvent sélectionner (p. ex. 5 "
|
||||||
|
"stickers pour CHF 50.-)."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:79
|
||||||
|
msgid "Display individual product prices in the selection interface."
|
||||||
|
msgstr ""
|
||||||
|
"Afficher les prix individuels des produits dans l'interface de sélection."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:72
|
||||||
|
msgid "Display product images in the selection interface."
|
||||||
|
msgstr "Afficher les images des produits dans l'interface de sélection."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:86
|
||||||
|
msgid "Display the total price as customers make selections."
|
||||||
|
msgstr "Afficher le prix total pendant que les clients font leurs sélections."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:196
|
||||||
|
msgid "Enter product SKUs separated by commas."
|
||||||
|
msgstr "Entrez les références des produits séparées par des virgules."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:83
|
||||||
|
msgid "Enter the fixed price for this composable product."
|
||||||
|
msgstr "Entrez le prix fixe pour ce produit composable."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:82
|
||||||
|
msgid "Fixed Price"
|
||||||
|
msgstr "Prix fixe"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:74 includes/Admin/Settings.php:59
|
||||||
|
msgid "Fixed price"
|
||||||
|
msgstr "Prix fixe"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:53
|
||||||
|
msgid "How to calculate the price of composable products."
|
||||||
|
msgstr ""
|
||||||
|
"Comment calculer le prix des produits composables (p. ex. prix fixe CHF 50.- "
|
||||||
|
"ou somme)."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:69
|
||||||
|
msgid "How to calculate the price."
|
||||||
|
msgstr "Comment calculer le prix."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:123
|
||||||
|
msgid "How to select available products."
|
||||||
|
msgstr "Comment sélectionner les produits disponibles."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
msgid "In stock"
|
msgid "In stock"
|
||||||
msgstr "En stock"
|
msgstr "En stock"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:107 includes/Admin/Settings.php:64
|
||||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
msgid "Include Non-Public Products"
|
||||||
msgstr "Aucun produit disponible pour la sélection. Veuillez configurer les critères de produit dans le panneau d'administration."
|
msgstr "Inclure les produits non publics"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "items from the selection below."
|
||||||
|
msgstr "articles de la sélection ci-dessous."
|
||||||
|
|
||||||
|
#: includes/Plugin.php:178
|
||||||
|
msgid "Maximum items selected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:55
|
||||||
|
msgid ""
|
||||||
|
"Maximum number of items customers can select. Leave empty to use global "
|
||||||
|
"default."
|
||||||
|
msgstr ""
|
||||||
|
"Nombre maximum d'articles que les clients peuvent sélectionner. Laisser vide "
|
||||||
|
"pour utiliser la valeur par défaut globale."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:113
|
||||||
|
msgid "No"
|
||||||
|
msgstr "Non"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid ""
|
||||||
|
"No products available for selection. Please configure the product criteria "
|
||||||
|
"in the admin panel."
|
||||||
|
msgstr ""
|
||||||
|
"Aucun produit disponible pour la sélection. Veuillez configurer les critères "
|
||||||
|
"de produit dans le panneau d'administration."
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:115
|
||||||
|
msgid "One or more selected products are not available."
|
||||||
|
msgstr "Un ou plusieurs produits sélectionnés ne sont pas disponibles."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Only"
|
||||||
|
msgstr "Seulement"
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: stock quantity
|
||||||
|
#: includes/StockManager.php:69
|
||||||
|
#, php-format
|
||||||
|
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||||
|
msgstr "Seulement %2$d de \"%1$s\" sont disponibles en stock."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Out of stock"
|
||||||
|
msgstr "Rupture de stock"
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:84 includes/CartHandler.php:100
|
||||||
|
msgid "Please select at least one product."
|
||||||
|
msgstr "Veuillez sélectionner au moins un produit."
|
||||||
|
|
||||||
|
#: includes/Plugin.php:179
|
||||||
|
msgid "Please select at least one item"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Plugin.php:177
|
||||||
|
msgid "Please select items"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:68
|
||||||
|
msgid "Pricing Mode"
|
||||||
|
msgstr "Mode de tarification"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:195
|
||||||
|
msgid "Product SKUs"
|
||||||
|
msgstr "Références des produits"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:198
|
||||||
|
msgid "SKU-1, SKU-2, SKU-3"
|
||||||
|
msgstr "REF-1, REF-2, REF-3"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Select Your Products"
|
||||||
|
msgstr "Sélectionnez vos produits"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:138
|
||||||
|
msgid "Select Categories"
|
||||||
|
msgstr "Sélectionner les catégories"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:165
|
||||||
|
msgid "Select Tags"
|
||||||
|
msgstr "Sélectionner les étiquettes"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:159
|
||||||
|
msgid "Select product categories to include."
|
||||||
|
msgstr "Sélectionner les catégories de produits à inclure."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:186
|
||||||
|
msgid "Select product tags to include."
|
||||||
|
msgstr "Sélectionner les étiquettes de produits à inclure."
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:191
|
||||||
|
msgid "Selected Products"
|
||||||
|
msgstr "Produits sélectionnés"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:122
|
||||||
|
msgid "Selection Criteria"
|
||||||
|
msgstr "Critères de sélection"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:54
|
||||||
|
msgid "Selection Limit"
|
||||||
|
msgstr "Limite de sélection"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:71
|
||||||
|
msgid "Show Product Images"
|
||||||
|
msgstr "Afficher les images des produits"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:78
|
||||||
|
msgid "Show Product Prices"
|
||||||
|
msgstr "Afficher les prix des produits"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:85
|
||||||
|
msgid "Show Total Price"
|
||||||
|
msgstr "Afficher le prix total"
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: quantity, 3: remaining stock
|
||||||
|
#: includes/StockManager.php:168
|
||||||
|
#, php-format
|
||||||
|
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||||
|
msgstr "Stock réduit pour \"%1$s\": -%2$d (restant: %3$d)"
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: quantity, 3: new stock
|
||||||
|
#: includes/StockManager.php:235
|
||||||
|
#, php-format
|
||||||
|
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||||
|
msgstr "Stock restauré pour \"%1$s\": +%2$d (total: %3$d)"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:73 includes/Admin/Settings.php:58
|
||||||
|
msgid "Sum of selected products"
|
||||||
|
msgstr "Somme des produits sélectionnés"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Total Price:"
|
||||||
|
msgstr "Prix total :"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:72 includes/Admin/ProductData.php:111
|
||||||
|
msgid "Use global default"
|
||||||
|
msgstr "Utiliser la valeur par défaut globale"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:112
|
||||||
|
msgid "Yes"
|
||||||
|
msgstr "Oui"
|
||||||
|
|
||||||
|
#. translators: %d: selection limit
|
||||||
|
#: includes/CartHandler.php:95
|
||||||
|
#, php-format
|
||||||
|
msgid "You can select a maximum of %d products."
|
||||||
|
msgstr "Vous pouvez sélectionner un maximum de %d produits."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "left"
|
||||||
|
msgstr "restant"
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: WooCommerce Composable Products 1.0.0\n"
|
"Project-Id-Version: WooCommerce Composable Products 1.0.0\n"
|
||||||
"Report-Msgid-Bugs-To: https://github.com/magdev/wc-composable-product/issues\n"
|
"Report-Msgid-Bugs-To: https://github.com/magdev/wc-composable-product/"
|
||||||
"POT-Creation-Date: 2024-12-31 00:00+0000\n"
|
"issues\n"
|
||||||
|
"POT-Creation-Date: 2026-03-01 13:47+0100\n"
|
||||||
"PO-Revision-Date: 2024-12-31 00:00+0000\n"
|
"PO-Revision-Date: 2024-12-31 00:00+0000\n"
|
||||||
"Last-Translator: Claude AI\n"
|
"Last-Translator: Claude AI\n"
|
||||||
"Language-Team: Italian (Switzerland)\n"
|
"Language-Team: Italian (Switzerland)\n"
|
||||||
@@ -15,230 +16,299 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:44
|
||||||
msgid "WooCommerce Composable Products requires WooCommerce to be installed and active."
|
msgid ""
|
||||||
msgstr "WooCommerce Composable Products richiede un'installazione WooCommerce attiva."
|
"WooCommerce Composable Products requires WooCommerce to be installed and "
|
||||||
|
"active."
|
||||||
|
msgstr ""
|
||||||
|
"WooCommerce Composable Products richiede un'installazione WooCommerce attiva."
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:90
|
||||||
msgid "This plugin requires WooCommerce to be installed and active."
|
msgid "This plugin requires WooCommerce to be installed and active."
|
||||||
msgstr "Questo plugin richiede un'installazione WooCommerce attiva."
|
msgstr "Questo plugin richiede un'installazione WooCommerce attiva."
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:91
|
||||||
msgid "Plugin Activation Error"
|
msgid "Plugin Activation Error"
|
||||||
msgstr "Errore di attivazione del plugin"
|
msgstr "Errore di attivazione del plugin"
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
#. translators: %s: product name
|
||||||
msgid "Composable Products"
|
#: includes/StockManager.php:60
|
||||||
msgstr "Prodotti Componibili"
|
#, php-format
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Composable Products Settings"
|
|
||||||
msgstr "Impostazioni prodotti componibili"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Configure default settings for composable products."
|
|
||||||
msgstr "Configurare le impostazioni predefinite per i prodotti componibili."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default Selection Limit"
|
|
||||||
msgstr "Limite di selezione predefinito"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default number of items customers can select."
|
|
||||||
msgstr "Numero predefinito di articoli che i clienti possono selezionare (p. es. 5 adesivi per CHF 50.-)."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default Pricing Mode"
|
|
||||||
msgstr "Modalità di prezzo predefinita"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "How to calculate the price of composable products."
|
|
||||||
msgstr "Come calcolare il prezzo dei prodotti componibili (p. es. prezzo fisso CHF 50.- o somma)."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Sum of selected products"
|
|
||||||
msgstr "Somma dei prodotti selezionati"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Fixed price"
|
|
||||||
msgstr "Prezzo fisso"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Product Images"
|
|
||||||
msgstr "Mostra immagini prodotti"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display product images in the selection interface."
|
|
||||||
msgstr "Visualizzare le immagini dei prodotti nell'interfaccia di selezione."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Product Prices"
|
|
||||||
msgstr "Mostra prezzi prodotti"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display individual product prices in the selection interface."
|
|
||||||
msgstr "Visualizzare i prezzi individuali dei prodotti nell'interfaccia di selezione."
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Total Price"
|
|
||||||
msgstr "Mostra prezzo totale"
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display the total price as customers make selections."
|
|
||||||
msgstr "Visualizzare il prezzo totale mentre i clienti effettuano le selezioni."
|
|
||||||
|
|
||||||
#: includes/Plugin.php
|
|
||||||
msgid "Composable product"
|
|
||||||
msgstr "Prodotto componibile"
|
|
||||||
|
|
||||||
#: includes/Product_Selector.php, templates/product-selector.twig
|
|
||||||
msgid "Select Your Products"
|
|
||||||
msgstr "Seleziona i tuoi prodotti"
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Choose up to"
|
|
||||||
msgstr "Scegli fino a"
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "items from the selection below."
|
|
||||||
msgstr "articoli dalla selezione qui sotto."
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Total Price:"
|
|
||||||
msgstr "Prezzo totale:"
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Add to Cart"
|
|
||||||
msgstr "Aggiungi al carrello"
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "Please select at least one product."
|
|
||||||
msgstr "Seleziona almeno un prodotto."
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "You can select a maximum of %d products."
|
|
||||||
msgstr "Puoi selezionare un massimo di %d prodotti."
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "One or more selected products are not available."
|
|
||||||
msgstr "Uno o più prodotti selezionati non sono disponibili."
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "Selected Products"
|
|
||||||
msgstr "Prodotti selezionati"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Composable Options"
|
|
||||||
msgstr "Opzioni di composizione"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Selection Limit"
|
|
||||||
msgstr "Limite di selezione"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Maximum number of items customers can select. Leave empty to use global default."
|
|
||||||
msgstr "Numero massimo di articoli che i clienti possono selezionare. Lasciare vuoto per utilizzare il valore predefinito globale."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Pricing Mode"
|
|
||||||
msgstr "Modalità di prezzo"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "How to calculate the price."
|
|
||||||
msgstr "Come calcolare il prezzo."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Fixed Price"
|
|
||||||
msgstr "Prezzo fisso"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Enter the fixed price for this composable product."
|
|
||||||
msgstr "Inserisci il prezzo fisso per questo prodotto componibile."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Use global default"
|
|
||||||
msgstr "Usa predefinito globale"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Selection Criteria"
|
|
||||||
msgstr "Criteri di selezione"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "How to select available products."
|
|
||||||
msgstr "Come selezionare i prodotti disponibili."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By Category"
|
|
||||||
msgstr "Per categoria"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By Tag"
|
|
||||||
msgstr "Per etichetta"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By SKU"
|
|
||||||
msgstr "Per codice articolo"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select Categories"
|
|
||||||
msgstr "Seleziona categorie"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select product categories to include."
|
|
||||||
msgstr "Selezionare le categorie di prodotti da includere."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select Tags"
|
|
||||||
msgstr "Seleziona etichette"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select product tags to include."
|
|
||||||
msgstr "Selezionare le etichette dei prodotti da includere."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Product SKUs"
|
|
||||||
msgstr "Codici articolo prodotti"
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Enter product SKUs separated by commas."
|
|
||||||
msgstr "Inserire i codici articolo dei prodotti separati da virgole."
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "SKU-1, SKU-2, SKU-3"
|
|
||||||
msgstr "COD-1, COD-2, COD-3"
|
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
|
||||||
msgid "\"%s\" is out of stock and cannot be selected."
|
msgid "\"%s\" is out of stock and cannot be selected."
|
||||||
msgstr "\"%s\" è esaurito e non può essere selezionato."
|
msgstr "\"%s\" è esaurito e non può essere selezionato."
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: templates/product-selector.html.twig
|
||||||
msgid "Only %2$d of \"%1$s\" are available in stock."
|
msgid "Add to Cart"
|
||||||
msgstr "Solo %2$d di \"%1$s\" sono disponibili in magazzino."
|
msgstr "Aggiungi al carrello"
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: includes/Admin/ProductData.php:108
|
||||||
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
msgid ""
|
||||||
msgstr "Giacenza ridotta per \"%1$s\": -%2$d (rimanenti: %3$d)"
|
"Allow draft and private products in the selection. Useful when products "
|
||||||
|
"should only be sold as part of a composition."
|
||||||
|
msgstr ""
|
||||||
|
"Consenti bozze e prodotti privati nella selezione. Utile quando i prodotti "
|
||||||
|
"devono essere venduti solo come parte di una composizione."
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: includes/Admin/Settings.php:65
|
||||||
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
msgid ""
|
||||||
msgstr "Giacenza ripristinata per \"%1$s\": +%2$d (totale: %3$d)"
|
"Allow draft and private products to appear in composable product selections. "
|
||||||
|
"Useful when products should only be sold as part of a composition, not "
|
||||||
|
"individually."
|
||||||
|
msgstr ""
|
||||||
|
"Consenti la visualizzazione di bozze e prodotti privati nelle selezioni dei "
|
||||||
|
"prodotti componibili. Utile quando i prodotti devono essere venduti solo "
|
||||||
|
"come parte di una composizione, non singolarmente."
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:126
|
||||||
msgid "Out of stock"
|
msgid "By Category"
|
||||||
msgstr "Esaurito"
|
msgstr "Per categoria"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:128
|
||||||
msgid "Only"
|
msgid "By SKU"
|
||||||
msgstr "Solo"
|
msgstr "Per codice articolo"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:127
|
||||||
msgid "left"
|
msgid "By Tag"
|
||||||
msgstr "rimasti"
|
msgstr "Per etichetta"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Choose up to"
|
||||||
|
msgstr "Scegli fino a"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:34
|
||||||
|
msgid "Composable Options"
|
||||||
|
msgstr "Opzioni di composizione"
|
||||||
|
|
||||||
|
#: includes/Plugin.php:132
|
||||||
|
msgid "Composable product"
|
||||||
|
msgstr "Prodotto componibile"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:21
|
||||||
|
msgid "Composable Products"
|
||||||
|
msgstr "Prodotti Componibili"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:34
|
||||||
|
msgid "Composable Products Settings"
|
||||||
|
msgstr "Impostazioni prodotti componibili"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:36
|
||||||
|
msgid "Configure default settings for composable products."
|
||||||
|
msgstr "Configurare le impostazioni predefinite per i prodotti componibili."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:52
|
||||||
|
msgid "Default Pricing Mode"
|
||||||
|
msgstr "Modalità di prezzo predefinita"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:40
|
||||||
|
msgid "Default Selection Limit"
|
||||||
|
msgstr "Limite di selezione predefinito"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:41
|
||||||
|
msgid "Default number of items customers can select."
|
||||||
|
msgstr ""
|
||||||
|
"Numero predefinito di articoli che i clienti possono selezionare (p. es. 5 "
|
||||||
|
"adesivi per CHF 50.-)."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:79
|
||||||
|
msgid "Display individual product prices in the selection interface."
|
||||||
|
msgstr ""
|
||||||
|
"Visualizzare i prezzi individuali dei prodotti nell'interfaccia di selezione."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:72
|
||||||
|
msgid "Display product images in the selection interface."
|
||||||
|
msgstr "Visualizzare le immagini dei prodotti nell'interfaccia di selezione."
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:86
|
||||||
|
msgid "Display the total price as customers make selections."
|
||||||
|
msgstr ""
|
||||||
|
"Visualizzare il prezzo totale mentre i clienti effettuano le selezioni."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:196
|
||||||
|
msgid "Enter product SKUs separated by commas."
|
||||||
|
msgstr "Inserire i codici articolo dei prodotti separati da virgole."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:83
|
||||||
|
msgid "Enter the fixed price for this composable product."
|
||||||
|
msgstr "Inserisci il prezzo fisso per questo prodotto componibile."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:82
|
||||||
|
msgid "Fixed Price"
|
||||||
|
msgstr "Prezzo fisso"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:74 includes/Admin/Settings.php:59
|
||||||
|
msgid "Fixed price"
|
||||||
|
msgstr "Prezzo fisso"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:53
|
||||||
|
msgid "How to calculate the price of composable products."
|
||||||
|
msgstr ""
|
||||||
|
"Come calcolare il prezzo dei prodotti componibili (p. es. prezzo fisso CHF "
|
||||||
|
"50.- o somma)."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:69
|
||||||
|
msgid "How to calculate the price."
|
||||||
|
msgstr "Come calcolare il prezzo."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:123
|
||||||
|
msgid "How to select available products."
|
||||||
|
msgstr "Come selezionare i prodotti disponibili."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
msgid "In stock"
|
msgid "In stock"
|
||||||
msgstr "Disponibile"
|
msgstr "Disponibile"
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:107 includes/Admin/Settings.php:64
|
||||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
msgid "Include Non-Public Products"
|
||||||
msgstr "Nessun prodotto disponibile per la selezione. Si prega di configurare i criteri del prodotto nel pannello di amministrazione."
|
msgstr "Includi prodotti non pubblici"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "items from the selection below."
|
||||||
|
msgstr "articoli dalla selezione qui sotto."
|
||||||
|
|
||||||
|
#: includes/Plugin.php:178
|
||||||
|
msgid "Maximum items selected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:55
|
||||||
|
msgid ""
|
||||||
|
"Maximum number of items customers can select. Leave empty to use global "
|
||||||
|
"default."
|
||||||
|
msgstr ""
|
||||||
|
"Numero massimo di articoli che i clienti possono selezionare. Lasciare vuoto "
|
||||||
|
"per utilizzare il valore predefinito globale."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:113
|
||||||
|
msgid "No"
|
||||||
|
msgstr "No"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid ""
|
||||||
|
"No products available for selection. Please configure the product criteria "
|
||||||
|
"in the admin panel."
|
||||||
|
msgstr ""
|
||||||
|
"Nessun prodotto disponibile per la selezione. Si prega di configurare i "
|
||||||
|
"criteri del prodotto nel pannello di amministrazione."
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:115
|
||||||
|
msgid "One or more selected products are not available."
|
||||||
|
msgstr "Uno o più prodotti selezionati non sono disponibili."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Only"
|
||||||
|
msgstr "Solo"
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: stock quantity
|
||||||
|
#: includes/StockManager.php:69
|
||||||
|
#, php-format
|
||||||
|
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||||
|
msgstr "Solo %2$d di \"%1$s\" sono disponibili in magazzino."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Out of stock"
|
||||||
|
msgstr "Esaurito"
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:84 includes/CartHandler.php:100
|
||||||
|
msgid "Please select at least one product."
|
||||||
|
msgstr "Seleziona almeno un prodotto."
|
||||||
|
|
||||||
|
#: includes/Plugin.php:179
|
||||||
|
msgid "Please select at least one item"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Plugin.php:177
|
||||||
|
msgid "Please select items"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:68
|
||||||
|
msgid "Pricing Mode"
|
||||||
|
msgstr "Modalità di prezzo"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:195
|
||||||
|
msgid "Product SKUs"
|
||||||
|
msgstr "Codici articolo prodotti"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:198
|
||||||
|
msgid "SKU-1, SKU-2, SKU-3"
|
||||||
|
msgstr "COD-1, COD-2, COD-3"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Select Your Products"
|
||||||
|
msgstr "Seleziona i tuoi prodotti"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:138
|
||||||
|
msgid "Select Categories"
|
||||||
|
msgstr "Seleziona categorie"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:165
|
||||||
|
msgid "Select Tags"
|
||||||
|
msgstr "Seleziona etichette"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:159
|
||||||
|
msgid "Select product categories to include."
|
||||||
|
msgstr "Selezionare le categorie di prodotti da includere."
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:186
|
||||||
|
msgid "Select product tags to include."
|
||||||
|
msgstr "Selezionare le etichette dei prodotti da includere."
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:191
|
||||||
|
msgid "Selected Products"
|
||||||
|
msgstr "Prodotti selezionati"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:122
|
||||||
|
msgid "Selection Criteria"
|
||||||
|
msgstr "Criteri di selezione"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:54
|
||||||
|
msgid "Selection Limit"
|
||||||
|
msgstr "Limite di selezione"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:71
|
||||||
|
msgid "Show Product Images"
|
||||||
|
msgstr "Mostra immagini prodotti"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:78
|
||||||
|
msgid "Show Product Prices"
|
||||||
|
msgstr "Mostra prezzi prodotti"
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:85
|
||||||
|
msgid "Show Total Price"
|
||||||
|
msgstr "Mostra prezzo totale"
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: quantity, 3: remaining stock
|
||||||
|
#: includes/StockManager.php:168
|
||||||
|
#, php-format
|
||||||
|
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||||
|
msgstr "Giacenza ridotta per \"%1$s\": -%2$d (rimanenti: %3$d)"
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: quantity, 3: new stock
|
||||||
|
#: includes/StockManager.php:235
|
||||||
|
#, php-format
|
||||||
|
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||||
|
msgstr "Giacenza ripristinata per \"%1$s\": +%2$d (totale: %3$d)"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:73 includes/Admin/Settings.php:58
|
||||||
|
msgid "Sum of selected products"
|
||||||
|
msgstr "Somma dei prodotti selezionati"
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Total Price:"
|
||||||
|
msgstr "Prezzo totale:"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:72 includes/Admin/ProductData.php:111
|
||||||
|
msgid "Use global default"
|
||||||
|
msgstr "Usa predefinito globale"
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:112
|
||||||
|
msgid "Yes"
|
||||||
|
msgstr "Sì"
|
||||||
|
|
||||||
|
#. translators: %d: selection limit
|
||||||
|
#: includes/CartHandler.php:95
|
||||||
|
#, php-format
|
||||||
|
msgid "You can select a maximum of %d products."
|
||||||
|
msgstr "Puoi selezionare un massimo di %d prodotti."
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "left"
|
||||||
|
msgstr "rimasti"
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
# This file is distributed under the GPL v3 or later.
|
# This file is distributed under the GPL v3 or later.
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: WooCommerce Composable Products 1.1.6\n"
|
"Project-Id-Version: WooCommerce Composable Products 1.3.2\n"
|
||||||
"Report-Msgid-Bugs-To: https://github.com/magdev/wc-composable-product/issues\n"
|
"Report-Msgid-Bugs-To: https://github.com/magdev/wc-composable-product/issues\n"
|
||||||
"POT-Creation-Date: 2024-12-31 00:00+0000\n"
|
"POT-Creation-Date: 2026-03-01 13:47+0100\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -12,232 +12,273 @@ msgstr ""
|
|||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"X-Generator: WP-CLI\n"
|
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:44
|
||||||
msgid "WooCommerce Composable Products requires WooCommerce to be installed and active."
|
msgid "WooCommerce Composable Products requires WooCommerce to be installed and active."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:90
|
||||||
msgid "This plugin requires WooCommerce to be installed and active."
|
msgid "This plugin requires WooCommerce to be installed and active."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: wc-composable-product.php
|
#: wc-composable-product.php:91
|
||||||
msgid "Plugin Activation Error"
|
msgid "Plugin Activation Error"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
#. translators: %s: product name
|
||||||
msgid "Composable Products"
|
#: includes/StockManager.php:60
|
||||||
msgstr ""
|
#, php-format
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Composable Products Settings"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Configure default settings for composable products."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default Selection Limit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default number of items customers can select."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Default Pricing Mode"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "How to calculate the price of composable products."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Sum of selected products"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Fixed price"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Product Images"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display product images in the selection interface."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Product Prices"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display individual product prices in the selection interface."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Show Total Price"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Settings.php
|
|
||||||
msgid "Display the total price as customers make selections."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Plugin.php
|
|
||||||
msgid "Composable product"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Product_Selector.php, templates/product-selector.twig
|
|
||||||
msgid "Select Your Products"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Choose up to"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "items from the selection below."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Total Price:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
|
||||||
msgid "Add to Cart"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "Please select at least one product."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "You can select a maximum of %d products."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "One or more selected products are not available."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Cart_Handler.php
|
|
||||||
msgid "Selected Products"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Composable Options"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Selection Limit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Maximum number of items customers can select. Leave empty to use global default."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Pricing Mode"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "How to calculate the price."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Fixed Price"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Enter the fixed price for this composable product."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Use global default"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Selection Criteria"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "How to select available products."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By Category"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By Tag"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "By SKU"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select Categories"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select product categories to include."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select Tags"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Select product tags to include."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Product SKUs"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "Enter product SKUs separated by commas."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
|
||||||
msgid "SKU-1, SKU-2, SKU-3"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
|
||||||
msgid "\"%s\" is out of stock and cannot be selected."
|
msgid "\"%s\" is out of stock and cannot be selected."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: templates/product-selector.html.twig
|
||||||
msgid "Only %2$d of \"%1$s\" are available in stock."
|
msgid "Add to Cart"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: includes/Admin/ProductData.php:108
|
||||||
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
msgid "Allow draft and private products in the selection. Useful when products should only be sold as part of a composition."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: includes/Stock_Manager.php
|
#: includes/Admin/Settings.php:65
|
||||||
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
msgid "Allow draft and private products to appear in composable product selections. Useful when products should only be sold as part of a composition, not individually."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:126
|
||||||
msgid "Out of stock"
|
msgid "By Category"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:128
|
||||||
msgid "Only"
|
msgid "By SKU"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:127
|
||||||
msgid "left"
|
msgid "By Tag"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Choose up to"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:34
|
||||||
|
msgid "Composable Options"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Plugin.php:132
|
||||||
|
msgid "Composable product"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:21
|
||||||
|
msgid "Composable Products"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:34
|
||||||
|
msgid "Composable Products Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:36
|
||||||
|
msgid "Configure default settings for composable products."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:52
|
||||||
|
msgid "Default Pricing Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:40
|
||||||
|
msgid "Default Selection Limit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:41
|
||||||
|
msgid "Default number of items customers can select."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:79
|
||||||
|
msgid "Display individual product prices in the selection interface."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:72
|
||||||
|
msgid "Display product images in the selection interface."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:86
|
||||||
|
msgid "Display the total price as customers make selections."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:196
|
||||||
|
msgid "Enter product SKUs separated by commas."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:83
|
||||||
|
msgid "Enter the fixed price for this composable product."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:82
|
||||||
|
msgid "Fixed Price"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:74 includes/Admin/Settings.php:59
|
||||||
|
msgid "Fixed price"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:53
|
||||||
|
msgid "How to calculate the price of composable products."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:69
|
||||||
|
msgid "How to calculate the price."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:123
|
||||||
|
msgid "How to select available products."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
msgid "In stock"
|
msgid "In stock"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/product-selector.twig
|
#: includes/Admin/ProductData.php:107 includes/Admin/Settings.php:64
|
||||||
|
msgid "Include Non-Public Products"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "items from the selection below."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Plugin.php:178
|
||||||
|
msgid "Maximum items selected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:55
|
||||||
|
msgid "Maximum number of items customers can select. Leave empty to use global default."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:113
|
||||||
|
msgid "No"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:115
|
||||||
|
msgid "One or more selected products are not available."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Only"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: stock quantity
|
||||||
|
#: includes/StockManager.php:69
|
||||||
|
#, php-format
|
||||||
|
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Out of stock"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:84 includes/CartHandler.php:100
|
||||||
|
msgid "Please select at least one product."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Plugin.php:179
|
||||||
|
msgid "Please select at least one item"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Plugin.php:177
|
||||||
|
msgid "Please select items"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:68
|
||||||
|
msgid "Pricing Mode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:195
|
||||||
|
msgid "Product SKUs"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:198
|
||||||
|
msgid "SKU-1, SKU-2, SKU-3"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Select Your Products"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:138
|
||||||
|
msgid "Select Categories"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:165
|
||||||
|
msgid "Select Tags"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:159
|
||||||
|
msgid "Select product categories to include."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:186
|
||||||
|
msgid "Select product tags to include."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/CartHandler.php:191
|
||||||
|
msgid "Selected Products"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:122
|
||||||
|
msgid "Selection Criteria"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:54
|
||||||
|
msgid "Selection Limit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:71
|
||||||
|
msgid "Show Product Images"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:78
|
||||||
|
msgid "Show Product Prices"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/Settings.php:85
|
||||||
|
msgid "Show Total Price"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: quantity, 3: remaining stock
|
||||||
|
#: includes/StockManager.php:168
|
||||||
|
#, php-format
|
||||||
|
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: 1: product name, 2: quantity, 3: new stock
|
||||||
|
#: includes/StockManager.php:235
|
||||||
|
#, php-format
|
||||||
|
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:73 includes/Admin/Settings.php:58
|
||||||
|
msgid "Sum of selected products"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "Total Price:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:72 includes/Admin/ProductData.php:111
|
||||||
|
msgid "Use global default"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: includes/Admin/ProductData.php:112
|
||||||
|
msgid "Yes"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. translators: %d: selection limit
|
||||||
|
#: includes/CartHandler.php:95
|
||||||
|
#, php-format
|
||||||
|
msgid "You can select a maximum of %d products."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/product-selector.html.twig
|
||||||
|
msgid "left"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
34
phpcs.xml.dist
Normal file
34
phpcs.xml.dist
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<ruleset name="WC Composable Product">
|
||||||
|
<description>PHPCS rules for WooCommerce Composable Products plugin</description>
|
||||||
|
|
||||||
|
<!-- Scan plugin source -->
|
||||||
|
<file>includes</file>
|
||||||
|
<file>wc-composable-product.php</file>
|
||||||
|
|
||||||
|
<!-- Show progress and use colors -->
|
||||||
|
<arg value="ps"/>
|
||||||
|
<arg name="colors"/>
|
||||||
|
|
||||||
|
<!-- Use WordPress Extra standard (Core + Extra, without Docs) -->
|
||||||
|
<rule ref="WordPress-Extra">
|
||||||
|
<!-- Allow PSR-4 PascalCase file naming -->
|
||||||
|
<exclude name="WordPress.Files.FileName"/>
|
||||||
|
</rule>
|
||||||
|
|
||||||
|
<!-- Check PHP 8.3+ compatibility -->
|
||||||
|
<rule ref="PHPCompatibilityWP"/>
|
||||||
|
<config name="testVersion" value="8.3-"/>
|
||||||
|
|
||||||
|
<!-- WordPress minimum version -->
|
||||||
|
<config name="minimum_wp_version" value="6.0"/>
|
||||||
|
|
||||||
|
<!-- Enforce text domain -->
|
||||||
|
<rule ref="WordPress.WP.I18n">
|
||||||
|
<properties>
|
||||||
|
<property name="text_domain" type="array">
|
||||||
|
<element value="wc-composable-product"/>
|
||||||
|
</property>
|
||||||
|
</properties>
|
||||||
|
</rule>
|
||||||
|
</ruleset>
|
||||||
21
phpunit.xml.dist
Normal file
21
phpunit.xml.dist
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
|
||||||
|
bootstrap="tests/bootstrap.php"
|
||||||
|
colors="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
cacheDirectory=".phpunit.cache"
|
||||||
|
>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit">
|
||||||
|
<directory suffix="Test.php">tests/Unit</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">includes</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
</phpunit>
|
||||||
83
templates/content-single-product-composable.php
Normal file
83
templates/content-single-product-composable.php
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Custom single product template for composable products.
|
||||||
|
*
|
||||||
|
* Replaces the standard WooCommerce two-column layout (image + summary) with
|
||||||
|
* a compact header and full-width product selector.
|
||||||
|
*
|
||||||
|
* This is a thin PHP loader that captures WooCommerce hook output and passes
|
||||||
|
* it to the Twig template for rendering.
|
||||||
|
*
|
||||||
|
* @package Magdev\WcComposableProduct
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
|
global $product;
|
||||||
|
|
||||||
|
if ( ! $product || ! is_a( $product, 'WC_Product' ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce hook.
|
||||||
|
do_action( 'woocommerce_before_single_product' );
|
||||||
|
|
||||||
|
if ( post_password_required() ) {
|
||||||
|
echo get_the_password_form(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- WP core function.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporarily remove standard WooCommerce summary hooks — we render title,
|
||||||
|
// price, and description in the compact header instead. Our product selector
|
||||||
|
// (CartHandler::render_product_selector at priority 25) stays attached.
|
||||||
|
$hooks_to_remove = array(
|
||||||
|
array( 'woocommerce_template_single_title', 5 ),
|
||||||
|
array( 'woocommerce_template_single_rating', 10 ),
|
||||||
|
array( 'woocommerce_template_single_price', 10 ),
|
||||||
|
array( 'woocommerce_template_single_excerpt', 20 ),
|
||||||
|
array( 'woocommerce_template_single_add_to_cart', 30 ),
|
||||||
|
array( 'woocommerce_template_single_meta', 40 ),
|
||||||
|
array( 'woocommerce_template_single_sharing', 50 ),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $hooks_to_remove as $hook ) {
|
||||||
|
remove_action( 'woocommerce_single_product_summary', $hook[0], $hook[1] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture product selector output (our hook at priority 25 + structured data at 60).
|
||||||
|
ob_start();
|
||||||
|
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce hook.
|
||||||
|
do_action( 'woocommerce_single_product_summary' );
|
||||||
|
$selector_html = ob_get_clean();
|
||||||
|
|
||||||
|
// Restore removed hooks.
|
||||||
|
foreach ( $hooks_to_remove as $hook ) {
|
||||||
|
add_action( 'woocommerce_single_product_summary', $hook[0], $hook[1] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture after-summary output (product tabs, related products, etc.).
|
||||||
|
ob_start();
|
||||||
|
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce hook.
|
||||||
|
do_action( 'woocommerce_after_single_product_summary' );
|
||||||
|
$after_summary_html = ob_get_clean();
|
||||||
|
|
||||||
|
// Build template context.
|
||||||
|
$image_id = $product->get_image_id();
|
||||||
|
$context = array(
|
||||||
|
'product_id' => $product->get_id(),
|
||||||
|
'product_name' => $product->get_name(),
|
||||||
|
'price_html' => $product->get_price_html(),
|
||||||
|
'short_description' => $product->get_short_description(),
|
||||||
|
'image_html' => $image_id ? wp_get_attachment_image( $image_id, array( 100, 100 ), false, array( 'class' => 'composable-header-image' ) ) : '',
|
||||||
|
'permalink' => get_permalink( $product->get_id() ),
|
||||||
|
'product_class' => implode( ' ', wc_get_product_class( 'composable-product-layout', $product ) ),
|
||||||
|
'selector_html' => $selector_html,
|
||||||
|
'after_summary_html' => $after_summary_html,
|
||||||
|
);
|
||||||
|
|
||||||
|
$plugin = \Magdev\WcComposableProduct\Plugin::instance();
|
||||||
|
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- output escaped by Twig template.
|
||||||
|
echo $plugin->render_template( 'single-product-composable.html.twig', $context );
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce hook.
|
||||||
|
do_action( 'woocommerce_after_single_product' );
|
||||||
37
templates/single-product-composable.html.twig
Normal file
37
templates/single-product-composable.html.twig
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{# Custom single product template for composable products #}
|
||||||
|
<div id="product-{{ product_id }}" class="{{ product_class }}">
|
||||||
|
|
||||||
|
{# Compact product header — replaces the large image gallery #}
|
||||||
|
<div class="composable-product-header">
|
||||||
|
{% if image_html %}
|
||||||
|
<div class="composable-header-thumbnail">
|
||||||
|
{{ image_html|raw }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="composable-header-info">
|
||||||
|
<h1 class="product_title entry-title">{{ product_name|esc_html }}</h1>
|
||||||
|
|
||||||
|
{% if price_html %}
|
||||||
|
<div class="composable-header-price price">
|
||||||
|
{{ price_html|raw }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if short_description %}
|
||||||
|
<div class="composable-header-description">
|
||||||
|
{{ short_description|raw }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Full-width product selector #}
|
||||||
|
<div class="composable-selector-area">
|
||||||
|
{{ selector_html|raw }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# WooCommerce after-summary content (tabs, related products) #}
|
||||||
|
{{ after_summary_html|raw }}
|
||||||
|
|
||||||
|
</div>
|
||||||
75
tests/TestCase.php
Normal file
75
tests/TestCase.php
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Base Test Case
|
||||||
|
*
|
||||||
|
* @package Magdev\WcComposableProduct\Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Magdev\WcComposableProduct\Tests;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
|
||||||
|
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
|
||||||
|
use Brain\Monkey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base test case with Brain Monkey and Mockery integration.
|
||||||
|
*
|
||||||
|
* All test classes should extend this instead of PHPUnit\Framework\TestCase.
|
||||||
|
*/
|
||||||
|
abstract class TestCase extends PHPUnitTestCase
|
||||||
|
{
|
||||||
|
use MockeryPHPUnitIntegration;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Monkey\setUp();
|
||||||
|
|
||||||
|
// Stub common WordPress translation functions (__(), _e(), esc_html__(), etc.)
|
||||||
|
Monkey\Functions\stubTranslationFunctions();
|
||||||
|
|
||||||
|
// Stub common WordPress escaping functions (esc_html(), esc_attr(), etc.)
|
||||||
|
Monkey\Functions\stubEscapeFunctions();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
Monkey\tearDown();
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Mockery mock of WC_Product with sensible defaults.
|
||||||
|
*
|
||||||
|
* @param array $overrides Method return value overrides
|
||||||
|
* @return \Mockery\MockInterface
|
||||||
|
*/
|
||||||
|
protected function createProductMock(array $overrides = []): \Mockery\MockInterface
|
||||||
|
{
|
||||||
|
$defaults = [
|
||||||
|
'get_id' => 100,
|
||||||
|
'get_name' => 'Test Product',
|
||||||
|
'get_type' => 'simple',
|
||||||
|
'get_price' => '10.00',
|
||||||
|
'get_regular_price' => '10.00',
|
||||||
|
'get_price_html' => '<span>$10.00</span>',
|
||||||
|
'get_permalink' => 'https://example.com/product/test',
|
||||||
|
'get_image_id' => 1,
|
||||||
|
'get_stock_quantity' => null,
|
||||||
|
'get_stock_status' => 'instock',
|
||||||
|
'is_purchasable' => true,
|
||||||
|
'is_in_stock' => true,
|
||||||
|
'managing_stock' => false,
|
||||||
|
'backorders_allowed' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
$config = array_merge($defaults, $overrides);
|
||||||
|
$mock = \Mockery::mock('WC_Product');
|
||||||
|
|
||||||
|
foreach ($config as $method => $return) {
|
||||||
|
$mock->shouldReceive($method)->andReturn($return)->byDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mock;
|
||||||
|
}
|
||||||
|
}
|
||||||
108
tests/Unit/Admin/ProductDataTest.php
Normal file
108
tests/Unit/Admin/ProductDataTest.php
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Admin ProductData Tests
|
||||||
|
*
|
||||||
|
* @package Magdev\WcComposableProduct\Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Magdev\WcComposableProduct\Tests\Unit\Admin;
|
||||||
|
|
||||||
|
use Magdev\WcComposableProduct\Tests\TestCase;
|
||||||
|
use Magdev\WcComposableProduct\Admin\ProductData;
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
|
||||||
|
class ProductDataTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$_POST = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
$_POST = [];
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConstructor_RegistersExpectedHooks(): void
|
||||||
|
{
|
||||||
|
$productData = new ProductData();
|
||||||
|
|
||||||
|
self::assertNotFalse(has_filter('woocommerce_product_data_tabs', 'Magdev\WcComposableProduct\Admin\ProductData->add_product_data_tab()'));
|
||||||
|
self::assertNotFalse(has_action('woocommerce_product_data_panels', 'Magdev\WcComposableProduct\Admin\ProductData->add_product_data_panel()'));
|
||||||
|
self::assertNotFalse(has_action('woocommerce_process_product_meta_composable', 'Magdev\WcComposableProduct\Admin\ProductData->save_product_data()'));
|
||||||
|
self::assertNotFalse(has_action('woocommerce_product_options_general_product_data', 'Magdev\WcComposableProduct\Admin\ProductData->add_general_fields()'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAddProductDataTab_AddsComposableTab(): void
|
||||||
|
{
|
||||||
|
$productData = new ProductData();
|
||||||
|
$tabs = $productData->add_product_data_tab([]);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('composable', $tabs);
|
||||||
|
$this->assertSame('composable_product_data', $tabs['composable']['target']);
|
||||||
|
$this->assertContains('show_if_composable', $tabs['composable']['class']);
|
||||||
|
$this->assertSame(21, $tabs['composable']['priority']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSaveProductData_SavesAllFields(): void
|
||||||
|
{
|
||||||
|
$_POST = [
|
||||||
|
'_composable_selection_limit' => '5',
|
||||||
|
'_composable_pricing_mode' => 'fixed',
|
||||||
|
'_composable_include_unpublished' => 'yes',
|
||||||
|
'_composable_criteria_type' => 'tag',
|
||||||
|
'_composable_categories' => ['1', '2'],
|
||||||
|
'_composable_tags' => ['3', '4'],
|
||||||
|
'_composable_skus' => 'SKU-1, SKU-2',
|
||||||
|
];
|
||||||
|
|
||||||
|
Functions\expect('absint')->andReturnUsing(function ($val) {
|
||||||
|
return abs((int) $val);
|
||||||
|
});
|
||||||
|
Functions\expect('sanitize_text_field')->andReturnUsing(function ($val) {
|
||||||
|
return $val;
|
||||||
|
});
|
||||||
|
Functions\expect('sanitize_textarea_field')->andReturnUsing(function ($val) {
|
||||||
|
return $val;
|
||||||
|
});
|
||||||
|
|
||||||
|
Functions\expect('update_post_meta')->times(7);
|
||||||
|
|
||||||
|
$productData = new ProductData();
|
||||||
|
$productData->save_product_data(42);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSaveProductData_DefaultsWhenPostEmpty(): void
|
||||||
|
{
|
||||||
|
// No POST data at all
|
||||||
|
Functions\expect('absint')->andReturnUsing(function ($val) {
|
||||||
|
return abs((int) $val);
|
||||||
|
});
|
||||||
|
Functions\expect('sanitize_text_field')->andReturnUsing(function ($val) {
|
||||||
|
return $val;
|
||||||
|
});
|
||||||
|
Functions\expect('sanitize_textarea_field')->andReturnUsing(function ($val) {
|
||||||
|
return $val;
|
||||||
|
});
|
||||||
|
|
||||||
|
Functions\expect('update_post_meta')
|
||||||
|
->with(42, '_composable_selection_limit', \Mockery::any())->once();
|
||||||
|
Functions\expect('update_post_meta')
|
||||||
|
->with(42, '_composable_pricing_mode', '')->once();
|
||||||
|
Functions\expect('update_post_meta')
|
||||||
|
->with(42, '_composable_include_unpublished', '')->once();
|
||||||
|
Functions\expect('update_post_meta')
|
||||||
|
->with(42, '_composable_criteria_type', 'category')->once();
|
||||||
|
Functions\expect('update_post_meta')
|
||||||
|
->with(42, '_composable_categories', [])->once();
|
||||||
|
Functions\expect('update_post_meta')
|
||||||
|
->with(42, '_composable_tags', [])->once();
|
||||||
|
Functions\expect('update_post_meta')
|
||||||
|
->with(42, '_composable_skus', '')->once();
|
||||||
|
|
||||||
|
$productData = new ProductData();
|
||||||
|
$productData->save_product_data(42);
|
||||||
|
}
|
||||||
|
}
|
||||||
84
tests/Unit/Admin/SettingsTest.php
Normal file
84
tests/Unit/Admin/SettingsTest.php
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Admin Settings Tests
|
||||||
|
*
|
||||||
|
* @package Magdev\WcComposableProduct\Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Magdev\WcComposableProduct\Tests\Unit\Admin;
|
||||||
|
|
||||||
|
use Magdev\WcComposableProduct\Tests\TestCase;
|
||||||
|
use Magdev\WcComposableProduct\Admin\Settings;
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
|
||||||
|
class SettingsTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testConstructor_SetsIdAndLabel(): void
|
||||||
|
{
|
||||||
|
$settings = new Settings();
|
||||||
|
|
||||||
|
$this->assertSame('composable_products', $settings->get_id());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetSettings_ReturnsExpectedFieldIds(): void
|
||||||
|
{
|
||||||
|
Functions\expect('apply_filters')
|
||||||
|
->once()
|
||||||
|
->with('wc_composable_settings', \Mockery::type('array'))
|
||||||
|
->andReturnUsing(function ($hook, $settings) {
|
||||||
|
return $settings;
|
||||||
|
});
|
||||||
|
|
||||||
|
$settings = new Settings();
|
||||||
|
$fields = $settings->get_settings();
|
||||||
|
|
||||||
|
// Extract all field IDs
|
||||||
|
$ids = array_column($fields, 'id');
|
||||||
|
|
||||||
|
$this->assertContains('wc_composable_settings', $ids);
|
||||||
|
$this->assertContains('wc_composable_default_limit', $ids);
|
||||||
|
$this->assertContains('wc_composable_default_pricing', $ids);
|
||||||
|
$this->assertContains('wc_composable_include_unpublished', $ids);
|
||||||
|
$this->assertContains('wc_composable_show_images', $ids);
|
||||||
|
$this->assertContains('wc_composable_show_prices', $ids);
|
||||||
|
$this->assertContains('wc_composable_show_total', $ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetSettings_HasCorrectFieldTypes(): void
|
||||||
|
{
|
||||||
|
Functions\expect('apply_filters')
|
||||||
|
->once()
|
||||||
|
->andReturnUsing(function ($hook, $settings) {
|
||||||
|
return $settings;
|
||||||
|
});
|
||||||
|
|
||||||
|
$settings = new Settings();
|
||||||
|
$fields = $settings->get_settings();
|
||||||
|
|
||||||
|
// Index fields by ID for easy lookup
|
||||||
|
$indexed = [];
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
if (isset($field['id'])) {
|
||||||
|
$indexed[$field['id']] = $field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertSame('number', $indexed['wc_composable_default_limit']['type']);
|
||||||
|
$this->assertSame('select', $indexed['wc_composable_default_pricing']['type']);
|
||||||
|
$this->assertSame('checkbox', $indexed['wc_composable_include_unpublished']['type']);
|
||||||
|
$this->assertSame('checkbox', $indexed['wc_composable_show_images']['type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetSettings_AppliesFilter(): void
|
||||||
|
{
|
||||||
|
Functions\expect('apply_filters')
|
||||||
|
->once()
|
||||||
|
->with('wc_composable_settings', \Mockery::type('array'))
|
||||||
|
->andReturnUsing(function ($hook, $settings) {
|
||||||
|
return $settings;
|
||||||
|
});
|
||||||
|
|
||||||
|
$settings = new Settings();
|
||||||
|
$settings->get_settings();
|
||||||
|
}
|
||||||
|
}
|
||||||
184
tests/Unit/CartHandlerTest.php
Normal file
184
tests/Unit/CartHandlerTest.php
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* CartHandler Tests
|
||||||
|
*
|
||||||
|
* @package Magdev\WcComposableProduct\Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Magdev\WcComposableProduct\Tests\Unit;
|
||||||
|
|
||||||
|
use Magdev\WcComposableProduct\Tests\TestCase;
|
||||||
|
use Magdev\WcComposableProduct\CartHandler;
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use Brain\Monkey\Actions;
|
||||||
|
use Brain\Monkey\Filters;
|
||||||
|
|
||||||
|
class CartHandlerTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// Clean up POST superglobal between tests
|
||||||
|
$_POST = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
$_POST = [];
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConstructor_RegistersExpectedHooks(): void
|
||||||
|
{
|
||||||
|
$handler = new CartHandler();
|
||||||
|
|
||||||
|
self::assertNotFalse(has_filter('woocommerce_add_to_cart_validation', 'Magdev\WcComposableProduct\CartHandler->validate_add_to_cart()'));
|
||||||
|
self::assertNotFalse(has_filter('woocommerce_add_cart_item_data', 'Magdev\WcComposableProduct\CartHandler->add_cart_item_data()'));
|
||||||
|
self::assertNotFalse(has_filter('woocommerce_get_cart_item_from_session', 'Magdev\WcComposableProduct\CartHandler->get_cart_item_from_session()'));
|
||||||
|
self::assertNotFalse(has_filter('woocommerce_get_item_data', 'Magdev\WcComposableProduct\CartHandler->display_cart_item_data()'));
|
||||||
|
self::assertNotFalse(has_action('woocommerce_before_calculate_totals', 'Magdev\WcComposableProduct\CartHandler->calculate_cart_item_price()'));
|
||||||
|
self::assertNotFalse(has_action('woocommerce_single_product_summary', 'Magdev\WcComposableProduct\CartHandler->render_product_selector()'));
|
||||||
|
self::assertNotFalse(has_filter('woocommerce_is_purchasable', 'Magdev\WcComposableProduct\CartHandler->hide_default_add_to_cart()'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHideDefaultAddToCart_ReturnsFalseForComposable(): void
|
||||||
|
{
|
||||||
|
$handler = new CartHandler();
|
||||||
|
$product = $this->createProductMock(['get_type' => 'composable']);
|
||||||
|
|
||||||
|
$result = $handler->hide_default_add_to_cart(true, $product);
|
||||||
|
$this->assertFalse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHideDefaultAddToCart_PassesThroughForSimple(): void
|
||||||
|
{
|
||||||
|
$handler = new CartHandler();
|
||||||
|
$product = $this->createProductMock(['get_type' => 'simple']);
|
||||||
|
|
||||||
|
$result = $handler->hide_default_add_to_cart(true, $product);
|
||||||
|
$this->assertTrue($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateAddToCart_PassesThroughForNonComposable(): void
|
||||||
|
{
|
||||||
|
$product = $this->createProductMock(['get_type' => 'simple']);
|
||||||
|
Functions\expect('wc_get_product')->with(1)->andReturn($product);
|
||||||
|
|
||||||
|
$handler = new CartHandler();
|
||||||
|
$result = $handler->validate_add_to_cart(true, 1, 1);
|
||||||
|
|
||||||
|
$this->assertTrue($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateAddToCart_ReturnsFalseWhenNoProductsSelected(): void
|
||||||
|
{
|
||||||
|
$product = $this->createProductMock(['get_type' => 'composable']);
|
||||||
|
Functions\expect('wc_get_product')->with(1)->andReturn($product);
|
||||||
|
Functions\expect('wc_add_notice')->once();
|
||||||
|
|
||||||
|
$_POST['composable_products'] = [];
|
||||||
|
|
||||||
|
$handler = new CartHandler();
|
||||||
|
$result = $handler->validate_add_to_cart(true, 1, 1);
|
||||||
|
|
||||||
|
$this->assertFalse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateAddToCart_ReturnsFalseWhenNoPostData(): void
|
||||||
|
{
|
||||||
|
$product = $this->createProductMock(['get_type' => 'composable']);
|
||||||
|
Functions\expect('wc_get_product')->with(1)->andReturn($product);
|
||||||
|
Functions\expect('wc_add_notice')->once();
|
||||||
|
|
||||||
|
// No $_POST['composable_products'] at all
|
||||||
|
$handler = new CartHandler();
|
||||||
|
$result = $handler->validate_add_to_cart(true, 1, 1);
|
||||||
|
|
||||||
|
$this->assertFalse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAddCartItemData_AddsSelectionsForComposable(): void
|
||||||
|
{
|
||||||
|
$product = $this->createProductMock(['get_type' => 'composable']);
|
||||||
|
Functions\expect('wc_get_product')->with(1)->andReturn($product);
|
||||||
|
Functions\expect('absint')->andReturnUsing(function ($val) {
|
||||||
|
return abs((int) $val);
|
||||||
|
});
|
||||||
|
|
||||||
|
$_POST['composable_products'] = ['101', '102'];
|
||||||
|
|
||||||
|
$handler = new CartHandler();
|
||||||
|
$result = $handler->add_cart_item_data([], 1);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('composable_products', $result);
|
||||||
|
$this->assertSame([101, 102], $result['composable_products']);
|
||||||
|
$this->assertArrayHasKey('unique_key', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAddCartItemData_PassesThroughForNonComposable(): void
|
||||||
|
{
|
||||||
|
$product = $this->createProductMock(['get_type' => 'simple']);
|
||||||
|
Functions\expect('wc_get_product')->with(1)->andReturn($product);
|
||||||
|
|
||||||
|
$handler = new CartHandler();
|
||||||
|
$result = $handler->add_cart_item_data(['existing' => 'data'], 1);
|
||||||
|
|
||||||
|
$this->assertSame(['existing' => 'data'], $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCartItemFromSession_RestoresComposableProducts(): void
|
||||||
|
{
|
||||||
|
$handler = new CartHandler();
|
||||||
|
$result = $handler->get_cart_item_from_session(
|
||||||
|
['data' => 'test'],
|
||||||
|
['composable_products' => [101, 102]]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertSame([101, 102], $result['composable_products']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCartItemFromSession_PassesThroughWithoutComposableData(): void
|
||||||
|
{
|
||||||
|
$handler = new CartHandler();
|
||||||
|
$result = $handler->get_cart_item_from_session(
|
||||||
|
['data' => 'test'],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertArrayNotHasKey('composable_products', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDisplayCartItemData_FormatsProductNames(): void
|
||||||
|
{
|
||||||
|
$mock1 = $this->createProductMock(['get_name' => 'Product A']);
|
||||||
|
$mock2 = $this->createProductMock(['get_name' => 'Product B']);
|
||||||
|
|
||||||
|
Functions\expect('wc_get_product')
|
||||||
|
->andReturnUsing(function ($id) use ($mock1, $mock2) {
|
||||||
|
return match ($id) {
|
||||||
|
101 => $mock1,
|
||||||
|
102 => $mock2,
|
||||||
|
default => false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
$handler = new CartHandler();
|
||||||
|
$result = $handler->display_cart_item_data(
|
||||||
|
[],
|
||||||
|
['composable_products' => [101, 102]]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertCount(1, $result);
|
||||||
|
$this->assertStringContainsString('Product A', $result[0]['value']);
|
||||||
|
$this->assertStringContainsString('Product B', $result[0]['value']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDisplayCartItemData_ReturnsEmptyForNonComposable(): void
|
||||||
|
{
|
||||||
|
$handler = new CartHandler();
|
||||||
|
$result = $handler->display_cart_item_data([], []);
|
||||||
|
|
||||||
|
$this->assertSame([], $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
tests/Unit/PluginTest.php
Normal file
72
tests/Unit/PluginTest.php
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Tests
|
||||||
|
*
|
||||||
|
* @package Magdev\WcComposableProduct\Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Magdev\WcComposableProduct\Tests\Unit;
|
||||||
|
|
||||||
|
use Magdev\WcComposableProduct\Tests\TestCase;
|
||||||
|
use Magdev\WcComposableProduct\Plugin;
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
|
||||||
|
class PluginTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// Reset the singleton instance between tests
|
||||||
|
$reflection = new \ReflectionClass(Plugin::class);
|
||||||
|
$property = $reflection->getProperty('instance');
|
||||||
|
$property->setAccessible(true);
|
||||||
|
$property->setValue(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInstance_ReturnsSingleton(): void
|
||||||
|
{
|
||||||
|
$instance1 = Plugin::instance();
|
||||||
|
$instance2 = Plugin::instance();
|
||||||
|
|
||||||
|
$this->assertSame($instance1, $instance2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInstance_ReturnsPluginClass(): void
|
||||||
|
{
|
||||||
|
$instance = Plugin::instance();
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Plugin::class, $instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAddProductType_AddsComposableToTypes(): void
|
||||||
|
{
|
||||||
|
$plugin = Plugin::instance();
|
||||||
|
$types = $plugin->add_product_type([]);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('composable', $types);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProductClass_ReturnsCustomClassForComposable(): void
|
||||||
|
{
|
||||||
|
$plugin = Plugin::instance();
|
||||||
|
$class = $plugin->product_class('WC_Product', 'composable');
|
||||||
|
|
||||||
|
$this->assertSame('Magdev\WcComposableProduct\ProductType', $class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProductClass_PassesThroughForOtherTypes(): void
|
||||||
|
{
|
||||||
|
$plugin = Plugin::instance();
|
||||||
|
$class = $plugin->product_class('WC_Product', 'simple');
|
||||||
|
|
||||||
|
$this->assertSame('WC_Product', $class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetTwig_ReturnsTwigEnvironment(): void
|
||||||
|
{
|
||||||
|
$plugin = Plugin::instance();
|
||||||
|
|
||||||
|
$this->assertInstanceOf(\Twig\Environment::class, $plugin->get_twig());
|
||||||
|
}
|
||||||
|
}
|
||||||
218
tests/Unit/ProductTypeTest.php
Normal file
218
tests/Unit/ProductTypeTest.php
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* ProductType Tests
|
||||||
|
*
|
||||||
|
* @package Magdev\WcComposableProduct\Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Magdev\WcComposableProduct\Tests\Unit;
|
||||||
|
|
||||||
|
use Magdev\WcComposableProduct\Tests\TestCase;
|
||||||
|
use Magdev\WcComposableProduct\ProductType;
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
|
||||||
|
class ProductTypeTest extends TestCase
|
||||||
|
{
|
||||||
|
private function createProductType(array $meta = []): ProductType
|
||||||
|
{
|
||||||
|
$product = new ProductType();
|
||||||
|
foreach ($meta as $key => $value) {
|
||||||
|
$product->update_meta_data($key, $value);
|
||||||
|
}
|
||||||
|
return $product;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetType_ReturnsComposable(): void
|
||||||
|
{
|
||||||
|
$product = $this->createProductType();
|
||||||
|
$this->assertSame('composable', $product->get_type());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsPurchasable_ReturnsTrue(): void
|
||||||
|
{
|
||||||
|
$product = $this->createProductType();
|
||||||
|
$this->assertTrue($product->is_purchasable());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsSoldIndividually_ReturnsTrue(): void
|
||||||
|
{
|
||||||
|
$product = $this->createProductType();
|
||||||
|
$this->assertTrue($product->is_sold_individually());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetSelectionLimit_UsesProductMeta(): void
|
||||||
|
{
|
||||||
|
Functions\expect('absint')->once()->andReturnUsing(function ($val) {
|
||||||
|
return abs((int) $val);
|
||||||
|
});
|
||||||
|
|
||||||
|
$product = $this->createProductType(['_composable_selection_limit' => '3']);
|
||||||
|
$this->assertSame(3, $product->get_selection_limit());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetSelectionLimit_FallsBackToGlobalDefault(): void
|
||||||
|
{
|
||||||
|
Functions\expect('get_option')
|
||||||
|
->once()
|
||||||
|
->with('wc_composable_default_limit', 5)
|
||||||
|
->andReturn(7);
|
||||||
|
|
||||||
|
Functions\expect('absint')->once()->andReturnUsing(function ($val) {
|
||||||
|
return abs((int) $val);
|
||||||
|
});
|
||||||
|
|
||||||
|
$product = $this->createProductType();
|
||||||
|
$this->assertSame(7, $product->get_selection_limit());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetSelectionLimit_FallsBackToHardDefault(): void
|
||||||
|
{
|
||||||
|
Functions\expect('get_option')
|
||||||
|
->once()
|
||||||
|
->with('wc_composable_default_limit', 5)
|
||||||
|
->andReturn(5);
|
||||||
|
|
||||||
|
Functions\expect('absint')->once()->andReturnUsing(function ($val) {
|
||||||
|
return abs((int) $val);
|
||||||
|
});
|
||||||
|
|
||||||
|
$product = $this->createProductType();
|
||||||
|
$this->assertSame(5, $product->get_selection_limit());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetPricingMode_UsesProductMeta(): void
|
||||||
|
{
|
||||||
|
$product = $this->createProductType(['_composable_pricing_mode' => 'fixed']);
|
||||||
|
$this->assertSame('fixed', $product->get_pricing_mode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetPricingMode_FallsBackToGlobalDefault(): void
|
||||||
|
{
|
||||||
|
Functions\expect('get_option')
|
||||||
|
->once()
|
||||||
|
->with('wc_composable_default_pricing', 'sum')
|
||||||
|
->andReturn('sum');
|
||||||
|
|
||||||
|
$product = $this->createProductType();
|
||||||
|
$this->assertSame('sum', $product->get_pricing_mode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldIncludeUnpublished_PerProductYes(): void
|
||||||
|
{
|
||||||
|
$product = $this->createProductType(['_composable_include_unpublished' => 'yes']);
|
||||||
|
$this->assertTrue($product->should_include_unpublished());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldIncludeUnpublished_PerProductNo(): void
|
||||||
|
{
|
||||||
|
$product = $this->createProductType(['_composable_include_unpublished' => 'no']);
|
||||||
|
$this->assertFalse($product->should_include_unpublished());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldIncludeUnpublished_FallsBackToGlobalYes(): void
|
||||||
|
{
|
||||||
|
Functions\expect('get_option')
|
||||||
|
->once()
|
||||||
|
->with('wc_composable_include_unpublished', 'no')
|
||||||
|
->andReturn('yes');
|
||||||
|
|
||||||
|
$product = $this->createProductType();
|
||||||
|
$this->assertTrue($product->should_include_unpublished());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testShouldIncludeUnpublished_FallsBackToGlobalNo(): void
|
||||||
|
{
|
||||||
|
Functions\expect('get_option')
|
||||||
|
->once()
|
||||||
|
->with('wc_composable_include_unpublished', 'no')
|
||||||
|
->andReturn('no');
|
||||||
|
|
||||||
|
$product = $this->createProductType();
|
||||||
|
$this->assertFalse($product->should_include_unpublished());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetSelectionCriteria_DefaultsToCategory(): void
|
||||||
|
{
|
||||||
|
$product = $this->createProductType();
|
||||||
|
$criteria = $product->get_selection_criteria();
|
||||||
|
|
||||||
|
$this->assertSame('category', $criteria['type']);
|
||||||
|
$this->assertSame([], $criteria['categories']);
|
||||||
|
$this->assertSame([], $criteria['tags']);
|
||||||
|
$this->assertSame('', $criteria['skus']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetSelectionCriteria_UsesProductMeta(): void
|
||||||
|
{
|
||||||
|
$product = $this->createProductType([
|
||||||
|
'_composable_criteria_type' => 'tag',
|
||||||
|
'_composable_tags' => [5, 10],
|
||||||
|
]);
|
||||||
|
$criteria = $product->get_selection_criteria();
|
||||||
|
|
||||||
|
$this->assertSame('tag', $criteria['type']);
|
||||||
|
$this->assertSame([5, 10], $criteria['tags']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCalculateComposedPrice_FixedMode(): void
|
||||||
|
{
|
||||||
|
$product = $this->createProductType(['_composable_pricing_mode' => 'fixed']);
|
||||||
|
|
||||||
|
// Set regular price via the stub's data property
|
||||||
|
$reflection = new \ReflectionClass($product);
|
||||||
|
$dataProp = $reflection->getProperty('data');
|
||||||
|
$dataProp->setAccessible(true);
|
||||||
|
$data = $dataProp->getValue($product);
|
||||||
|
$data['regular_price'] = '25.00';
|
||||||
|
$dataProp->setValue($product, $data);
|
||||||
|
|
||||||
|
$price = $product->calculate_composed_price([101, 102]);
|
||||||
|
$this->assertSame(25.0, $price);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCalculateComposedPrice_SumMode(): void
|
||||||
|
{
|
||||||
|
Functions\expect('get_option')
|
||||||
|
->with('wc_composable_default_pricing', 'sum')
|
||||||
|
->andReturn('sum');
|
||||||
|
|
||||||
|
$mock1 = $this->createProductMock(['get_price' => '5.00']);
|
||||||
|
$mock2 = $this->createProductMock(['get_price' => '7.50']);
|
||||||
|
|
||||||
|
Functions\expect('wc_get_product')
|
||||||
|
->andReturnUsing(function ($id) use ($mock1, $mock2) {
|
||||||
|
return match ($id) {
|
||||||
|
101 => $mock1,
|
||||||
|
102 => $mock2,
|
||||||
|
default => false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
$product = $this->createProductType();
|
||||||
|
$price = $product->calculate_composed_price([101, 102]);
|
||||||
|
|
||||||
|
$this->assertSame(12.5, $price);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCalculateComposedPrice_SumMode_SkipsInvalidProducts(): void
|
||||||
|
{
|
||||||
|
Functions\expect('get_option')
|
||||||
|
->with('wc_composable_default_pricing', 'sum')
|
||||||
|
->andReturn('sum');
|
||||||
|
|
||||||
|
$mock1 = $this->createProductMock(['get_price' => '5.00']);
|
||||||
|
|
||||||
|
Functions\expect('wc_get_product')
|
||||||
|
->andReturnUsing(function ($id) use ($mock1) {
|
||||||
|
return match ($id) {
|
||||||
|
101 => $mock1,
|
||||||
|
default => false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
$product = $this->createProductType();
|
||||||
|
$price = $product->calculate_composed_price([101, 999]);
|
||||||
|
|
||||||
|
$this->assertSame(5.0, $price);
|
||||||
|
}
|
||||||
|
}
|
||||||
226
tests/Unit/StockManagerTest.php
Normal file
226
tests/Unit/StockManagerTest.php
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* StockManager Tests
|
||||||
|
*
|
||||||
|
* @package Magdev\WcComposableProduct\Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Magdev\WcComposableProduct\Tests\Unit;
|
||||||
|
|
||||||
|
use Magdev\WcComposableProduct\Tests\TestCase;
|
||||||
|
use Magdev\WcComposableProduct\StockManager;
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
|
||||||
|
class StockManagerTest extends TestCase
|
||||||
|
{
|
||||||
|
private StockManager $manager;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->manager = new StockManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- validate_stock_availability ---
|
||||||
|
|
||||||
|
public function testValidateStock_ReturnsTrueWhenAllInStock(): void
|
||||||
|
{
|
||||||
|
$mock = $this->createProductMock([
|
||||||
|
'managing_stock' => false,
|
||||||
|
'is_in_stock' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||||
|
Functions\expect('wc_get_product')->with(2)->andReturn($mock);
|
||||||
|
|
||||||
|
$result = $this->manager->validate_stock_availability([1, 2]);
|
||||||
|
$this->assertTrue($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateStock_ReturnsTrueWhenNotManagingStock(): void
|
||||||
|
{
|
||||||
|
$mock = $this->createProductMock(['managing_stock' => false]);
|
||||||
|
|
||||||
|
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||||
|
|
||||||
|
$result = $this->manager->validate_stock_availability([1]);
|
||||||
|
$this->assertTrue($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateStock_ReturnsErrorForOutOfStock(): void
|
||||||
|
{
|
||||||
|
$mock = $this->createProductMock([
|
||||||
|
'managing_stock' => true,
|
||||||
|
'is_in_stock' => false,
|
||||||
|
'get_name' => 'Widget',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||||
|
|
||||||
|
$result = $this->manager->validate_stock_availability([1]);
|
||||||
|
$this->assertIsString($result);
|
||||||
|
$this->assertStringContainsString('Widget', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateStock_ReturnsErrorForInsufficientQuantity(): void
|
||||||
|
{
|
||||||
|
$mock = $this->createProductMock([
|
||||||
|
'managing_stock' => true,
|
||||||
|
'is_in_stock' => true,
|
||||||
|
'get_stock_quantity' => 1,
|
||||||
|
'get_name' => 'Widget',
|
||||||
|
'backorders_allowed' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||||
|
|
||||||
|
$result = $this->manager->validate_stock_availability([1], 5);
|
||||||
|
$this->assertIsString($result);
|
||||||
|
$this->assertStringContainsString('Widget', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateStock_PassesWhenBackordersAllowed(): void
|
||||||
|
{
|
||||||
|
// When stock_quantity is null the insufficient-stock check is skipped,
|
||||||
|
// and the backorders_allowed() branch is reached.
|
||||||
|
$mock = $this->createProductMock([
|
||||||
|
'managing_stock' => true,
|
||||||
|
'is_in_stock' => true,
|
||||||
|
'get_stock_quantity' => null,
|
||||||
|
'backorders_allowed' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||||
|
|
||||||
|
$result = $this->manager->validate_stock_availability([1], 5);
|
||||||
|
$this->assertTrue($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidateStock_SkipsNullProducts(): void
|
||||||
|
{
|
||||||
|
Functions\expect('wc_get_product')->with(999)->andReturn(false);
|
||||||
|
|
||||||
|
$result = $this->manager->validate_stock_availability([999]);
|
||||||
|
$this->assertTrue($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- get_product_stock_info ---
|
||||||
|
|
||||||
|
public function testGetProductStockInfo_ReturnsCorrectStructure(): void
|
||||||
|
{
|
||||||
|
$mock = $this->createProductMock([
|
||||||
|
'is_in_stock' => true,
|
||||||
|
'get_stock_quantity' => 10,
|
||||||
|
'backorders_allowed' => false,
|
||||||
|
'get_stock_status' => 'instock',
|
||||||
|
'managing_stock' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||||
|
|
||||||
|
$info = $this->manager->get_product_stock_info(1);
|
||||||
|
|
||||||
|
$this->assertTrue($info['in_stock']);
|
||||||
|
$this->assertSame(10, $info['stock_quantity']);
|
||||||
|
$this->assertFalse($info['backorders_allowed']);
|
||||||
|
$this->assertSame('instock', $info['stock_status']);
|
||||||
|
$this->assertTrue($info['managing_stock']);
|
||||||
|
$this->assertTrue($info['has_enough_stock']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetProductStockInfo_ReturnsFallbackForInvalidProduct(): void
|
||||||
|
{
|
||||||
|
Functions\expect('wc_get_product')->with(999)->andReturn(false);
|
||||||
|
|
||||||
|
$info = $this->manager->get_product_stock_info(999);
|
||||||
|
|
||||||
|
$this->assertFalse($info['in_stock']);
|
||||||
|
$this->assertSame(0, $info['stock_quantity']);
|
||||||
|
$this->assertFalse($info['backorders_allowed']);
|
||||||
|
$this->assertSame('outofstock', $info['stock_status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetProductStockInfo_HasEnoughStockTrueWhenNotManaging(): void
|
||||||
|
{
|
||||||
|
$mock = $this->createProductMock([
|
||||||
|
'managing_stock' => false,
|
||||||
|
'get_stock_quantity' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||||
|
|
||||||
|
$info = $this->manager->get_product_stock_info(1, 100);
|
||||||
|
$this->assertTrue($info['has_enough_stock']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetProductStockInfo_HasEnoughStockFalseWhenInsufficient(): void
|
||||||
|
{
|
||||||
|
$mock = $this->createProductMock([
|
||||||
|
'managing_stock' => true,
|
||||||
|
'get_stock_quantity' => 2,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Functions\expect('wc_get_product')->with(1)->andReturn($mock);
|
||||||
|
|
||||||
|
$info = $this->manager->get_product_stock_info(1, 5);
|
||||||
|
$this->assertFalse($info['has_enough_stock']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- prevent_composable_stock_reduction ---
|
||||||
|
|
||||||
|
public function testPreventStockReduction_ReturnsFalseForComposableItem(): void
|
||||||
|
{
|
||||||
|
$productMock = $this->createProductMock(['get_type' => 'composable']);
|
||||||
|
|
||||||
|
$itemMock = \Mockery::mock('WC_Order_Item_Product');
|
||||||
|
$itemMock->shouldReceive('get_product')->andReturn($productMock);
|
||||||
|
|
||||||
|
$orderMock = \Mockery::mock('WC_Order');
|
||||||
|
$orderMock->shouldReceive('get_items')->andReturn([$itemMock]);
|
||||||
|
|
||||||
|
$result = $this->manager->prevent_composable_stock_reduction(true, $orderMock);
|
||||||
|
$this->assertFalse($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPreventStockReduction_PassesThroughForNonComposable(): void
|
||||||
|
{
|
||||||
|
$productMock = $this->createProductMock(['get_type' => 'simple']);
|
||||||
|
|
||||||
|
$itemMock = \Mockery::mock('WC_Order_Item_Product');
|
||||||
|
$itemMock->shouldReceive('get_product')->andReturn($productMock);
|
||||||
|
|
||||||
|
$orderMock = \Mockery::mock('WC_Order');
|
||||||
|
$orderMock->shouldReceive('get_items')->andReturn([$itemMock]);
|
||||||
|
|
||||||
|
$result = $this->manager->prevent_composable_stock_reduction(true, $orderMock);
|
||||||
|
$this->assertTrue($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- store_selected_products_in_order ---
|
||||||
|
|
||||||
|
public function testStoreSelectedProducts_AddsMetaWhenPresent(): void
|
||||||
|
{
|
||||||
|
$itemMock = \Mockery::mock('WC_Order_Item_Product');
|
||||||
|
$itemMock->shouldReceive('add_meta_data')
|
||||||
|
->once()
|
||||||
|
->with('_composable_products', [1, 2], true);
|
||||||
|
|
||||||
|
$this->manager->store_selected_products_in_order(
|
||||||
|
$itemMock,
|
||||||
|
'cart_key',
|
||||||
|
['composable_products' => [1, 2]]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStoreSelectedProducts_DoesNothingWithoutData(): void
|
||||||
|
{
|
||||||
|
$itemMock = \Mockery::mock('WC_Order_Item_Product');
|
||||||
|
$itemMock->shouldNotReceive('add_meta_data');
|
||||||
|
|
||||||
|
$this->manager->store_selected_products_in_order(
|
||||||
|
$itemMock,
|
||||||
|
'cart_key',
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
tests/bootstrap.php
Normal file
33
tests/bootstrap.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* PHPUnit Bootstrap
|
||||||
|
*
|
||||||
|
* Sets up WooCommerce stubs, plugin constants, and the Composer autoloader
|
||||||
|
* for running unit tests without a full WordPress installation.
|
||||||
|
*
|
||||||
|
* @package Magdev\WcComposableProduct\Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Composer autoloader (loads Brain Monkey, Mockery, plugin classes)
|
||||||
|
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Define WordPress constants that the plugin expects
|
||||||
|
defined('ABSPATH') || define('ABSPATH', '/tmp/wordpress/');
|
||||||
|
defined('WP_DEBUG') || define('WP_DEBUG', true);
|
||||||
|
defined('DOING_AJAX') || define('DOING_AJAX', false);
|
||||||
|
|
||||||
|
// Define plugin constants
|
||||||
|
defined('WC_COMPOSABLE_PRODUCT_VERSION') || define('WC_COMPOSABLE_PRODUCT_VERSION', '1.3.1');
|
||||||
|
defined('WC_COMPOSABLE_PRODUCT_FILE') || define('WC_COMPOSABLE_PRODUCT_FILE', dirname(__DIR__) . '/wc-composable-product.php');
|
||||||
|
defined('WC_COMPOSABLE_PRODUCT_PATH') || define('WC_COMPOSABLE_PRODUCT_PATH', dirname(__DIR__) . '/');
|
||||||
|
defined('WC_COMPOSABLE_PRODUCT_URL') || define('WC_COMPOSABLE_PRODUCT_URL', 'https://example.com/wp-content/plugins/wc-composable-product/');
|
||||||
|
defined('WC_COMPOSABLE_PRODUCT_BASENAME') || define('WC_COMPOSABLE_PRODUCT_BASENAME', 'wc-composable-product/wc-composable-product.php');
|
||||||
|
|
||||||
|
// Load WooCommerce class stubs (parent before child)
|
||||||
|
require_once __DIR__ . '/stubs/class-wc-data.php';
|
||||||
|
require_once __DIR__ . '/stubs/class-wc-product.php';
|
||||||
|
require_once __DIR__ . '/stubs/class-wc-settings-page.php';
|
||||||
|
require_once __DIR__ . '/stubs/class-wc-order.php';
|
||||||
|
require_once __DIR__ . '/stubs/class-wc-order-item-product.php';
|
||||||
|
require_once __DIR__ . '/stubs/class-wc-cart.php';
|
||||||
|
require_once __DIR__ . '/stubs/class-wc-admin-settings.php';
|
||||||
11
tests/stubs/class-wc-admin-settings.php
Normal file
11
tests/stubs/class-wc-admin-settings.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Minimal WC_Admin_Settings stub for unit testing.
|
||||||
|
*/
|
||||||
|
class WC_Admin_Settings {
|
||||||
|
public static function output_fields($options) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function save_fields($options) {
|
||||||
|
}
|
||||||
|
}
|
||||||
15
tests/stubs/class-wc-cart.php
Normal file
15
tests/stubs/class-wc-cart.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Minimal WC_Cart stub for unit testing.
|
||||||
|
*/
|
||||||
|
class WC_Cart {
|
||||||
|
protected $cart_contents = [];
|
||||||
|
|
||||||
|
public function get_cart() {
|
||||||
|
return $this->cart_contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set_cart_contents($contents) {
|
||||||
|
$this->cart_contents = $contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
tests/stubs/class-wc-data.php
Normal file
35
tests/stubs/class-wc-data.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Minimal WC_Data stub for unit testing.
|
||||||
|
*/
|
||||||
|
class WC_Data {
|
||||||
|
protected $id = 0;
|
||||||
|
protected $data = [];
|
||||||
|
protected $meta_data = [];
|
||||||
|
|
||||||
|
public function __construct($id = 0) {
|
||||||
|
if (is_numeric($id) && $id > 0) {
|
||||||
|
$this->id = (int) $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_id() {
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set_id($id) {
|
||||||
|
$this->id = (int) $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_meta($key, $single = true, $context = 'view') {
|
||||||
|
return $this->meta_data[$key] ?? ($single ? '' : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update_meta_data($key, $value, $meta_id = 0) {
|
||||||
|
$this->meta_data[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save() {
|
||||||
|
return $this->get_id();
|
||||||
|
}
|
||||||
|
}
|
||||||
28
tests/stubs/class-wc-order-item-product.php
Normal file
28
tests/stubs/class-wc-order-item-product.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Minimal WC_Order_Item_Product stub for unit testing.
|
||||||
|
*/
|
||||||
|
class WC_Order_Item_Product extends WC_Data {
|
||||||
|
protected $product = null;
|
||||||
|
protected $quantity = 1;
|
||||||
|
|
||||||
|
public function get_product() {
|
||||||
|
return $this->product;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set_product($product) {
|
||||||
|
$this->product = $product;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_quantity() {
|
||||||
|
return $this->quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set_quantity($quantity) {
|
||||||
|
$this->quantity = $quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add_meta_data($key, $value, $unique = false) {
|
||||||
|
$this->meta_data[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
tests/stubs/class-wc-order.php
Normal file
24
tests/stubs/class-wc-order.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Minimal WC_Order stub for unit testing.
|
||||||
|
*/
|
||||||
|
class WC_Order extends WC_Data {
|
||||||
|
protected $items = [];
|
||||||
|
protected $order_notes = [];
|
||||||
|
|
||||||
|
public function get_items($type = '') {
|
||||||
|
return $this->items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set_items($items) {
|
||||||
|
$this->items = $items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add_order_note($note) {
|
||||||
|
$this->order_notes[] = $note;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_order_notes() {
|
||||||
|
return $this->order_notes;
|
||||||
|
}
|
||||||
|
}
|
||||||
88
tests/stubs/class-wc-product.php
Normal file
88
tests/stubs/class-wc-product.php
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Minimal WC_Product stub for unit testing.
|
||||||
|
*/
|
||||||
|
class WC_Product extends WC_Data {
|
||||||
|
protected $supports = [];
|
||||||
|
protected $data = [
|
||||||
|
'name' => '',
|
||||||
|
'price' => '',
|
||||||
|
'regular_price' => '',
|
||||||
|
'status' => 'publish',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function get_type() {
|
||||||
|
return 'simple';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_name($context = 'view') {
|
||||||
|
return $this->data['name'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_price($context = 'view') {
|
||||||
|
return $this->data['price'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_regular_price($context = 'view') {
|
||||||
|
return $this->data['regular_price'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_price_html() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_permalink() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_image_id($context = 'view') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_stock_quantity($context = 'view') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_stock_status($context = 'view') {
|
||||||
|
return 'instock';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_children() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function is_type($type) {
|
||||||
|
return $this->get_type() === $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function is_purchasable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function is_in_stock() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function managing_stock() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function backorders_allowed() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function is_sold_individually() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set_price($price) {
|
||||||
|
$this->data['price'] = $price;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set_stock_quantity($quantity) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supports($feature) {
|
||||||
|
return in_array($feature, $this->supports, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
tests/stubs/class-wc-settings-page.php
Normal file
29
tests/stubs/class-wc-settings-page.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Minimal WC_Settings_Page stub for unit testing.
|
||||||
|
*/
|
||||||
|
class WC_Settings_Page {
|
||||||
|
protected $id = '';
|
||||||
|
protected $label = '';
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_id() {
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_label() {
|
||||||
|
return $this->label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_settings() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function output() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* Plugin Name: WooCommerce Composable Products
|
* Plugin Name: WooCommerce Composable Products
|
||||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-composable-product
|
* Plugin URI: https://src.bundespruefstelle.ch/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.2.0
|
* Version: 1.3.2
|
||||||
* Author: Marco Graetsch
|
* Author: Marco Graetsch
|
||||||
* Author URI: https://src.bundespruefstelle.ch/magdev
|
* Author URI: https://src.bundespruefstelle.ch/magdev
|
||||||
* License: GPL v3 or later
|
* License: GPL v3 or later
|
||||||
@@ -17,17 +17,17 @@
|
|||||||
* WC tested up to: 10.0
|
* WC tested up to: 10.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined( 'ABSPATH' ) || exit;
|
||||||
|
|
||||||
// Define plugin constants
|
// Define plugin constants
|
||||||
define('WC_COMPOSABLE_PRODUCT_VERSION', '1.2.0');
|
define( 'WC_COMPOSABLE_PRODUCT_VERSION', '1.3.2' );
|
||||||
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__ ) );
|
||||||
define('WC_COMPOSABLE_PRODUCT_BASENAME', plugin_basename(__FILE__));
|
define( 'WC_COMPOSABLE_PRODUCT_BASENAME', plugin_basename( __FILE__ ) );
|
||||||
|
|
||||||
// Load Composer autoloader
|
// Load Composer autoloader
|
||||||
if (file_exists(WC_COMPOSABLE_PRODUCT_PATH . 'vendor/autoload.php')) {
|
if ( file_exists( WC_COMPOSABLE_PRODUCT_PATH . 'vendor/autoload.php' ) ) {
|
||||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'vendor/autoload.php';
|
require_once WC_COMPOSABLE_PRODUCT_PATH . 'vendor/autoload.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,14 +35,17 @@ if (file_exists(WC_COMPOSABLE_PRODUCT_PATH . 'vendor/autoload.php')) {
|
|||||||
* Check if WooCommerce is active
|
* Check if WooCommerce is active
|
||||||
*/
|
*/
|
||||||
function wc_composable_product_check_woocommerce() {
|
function wc_composable_product_check_woocommerce() {
|
||||||
if (!class_exists('WooCommerce')) {
|
if ( ! class_exists( 'WooCommerce' ) ) {
|
||||||
add_action('admin_notices', function() {
|
add_action(
|
||||||
|
'admin_notices',
|
||||||
|
function () {
|
||||||
?>
|
?>
|
||||||
<div class="notice notice-error">
|
<div class="notice notice-error">
|
||||||
<p><?php esc_html_e('WooCommerce Composable Products requires WooCommerce to be installed and active.', 'wc-composable-product'); ?></p>
|
<p><?php esc_html_e( 'WooCommerce Composable Products requires WooCommerce to be installed and active.', 'wc-composable-product' ); ?></p>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
});
|
}
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -52,39 +55,42 @@ function wc_composable_product_check_woocommerce() {
|
|||||||
* Initialize the plugin
|
* Initialize the plugin
|
||||||
*/
|
*/
|
||||||
function wc_composable_product_init() {
|
function wc_composable_product_init() {
|
||||||
if (!wc_composable_product_check_woocommerce()) {
|
if ( ! wc_composable_product_check_woocommerce() ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load text domain
|
// Load text domain
|
||||||
load_plugin_textdomain('wc-composable-product', false, dirname(WC_COMPOSABLE_PRODUCT_BASENAME) . '/languages');
|
load_plugin_textdomain( 'wc-composable-product', false, dirname( WC_COMPOSABLE_PRODUCT_BASENAME ) . '/languages' );
|
||||||
|
|
||||||
// Initialize main plugin class
|
// Initialize main plugin class
|
||||||
WC_Composable_Product\Plugin::instance();
|
Magdev\WcComposableProduct\Plugin::instance();
|
||||||
}
|
}
|
||||||
// Use woocommerce_init to ensure all WooCommerce classes including settings are loaded
|
// Use woocommerce_init to ensure all WooCommerce classes including settings are loaded
|
||||||
add_action('woocommerce_init', 'wc_composable_product_init');
|
add_action( 'woocommerce_init', 'wc_composable_product_init' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Declare HPOS compatibility
|
* Declare HPOS compatibility
|
||||||
*/
|
*/
|
||||||
add_action('before_woocommerce_init', function() {
|
add_action(
|
||||||
if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) {
|
'before_woocommerce_init',
|
||||||
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true);
|
function () {
|
||||||
|
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
|
||||||
|
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activation hook
|
* Activation hook
|
||||||
*/
|
*/
|
||||||
function wc_composable_product_activate() {
|
function wc_composable_product_activate() {
|
||||||
if (!class_exists('WooCommerce')) {
|
if ( ! class_exists( 'WooCommerce' ) ) {
|
||||||
deactivate_plugins(WC_COMPOSABLE_PRODUCT_BASENAME);
|
deactivate_plugins( WC_COMPOSABLE_PRODUCT_BASENAME );
|
||||||
wp_die(
|
wp_die(
|
||||||
esc_html__('This plugin requires WooCommerce to be installed and active.', 'wc-composable-product'),
|
esc_html__( 'This plugin requires WooCommerce to be installed and active.', 'wc-composable-product' ),
|
||||||
esc_html__('Plugin Activation Error', 'wc-composable-product'),
|
esc_html__( 'Plugin Activation Error', 'wc-composable-product' ),
|
||||||
array('back_link' => true)
|
array( 'back_link' => true )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
register_activation_hook(__FILE__, 'wc_composable_product_activate');
|
register_activation_hook( __FILE__, 'wc_composable_product_activate' );
|
||||||
|
|||||||
Reference in New Issue
Block a user