You've already forked wc-composable-product
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6507f4d8bb | |||
| 29a68b0be4 | |||
| fb8ddf903e | |||
| 33d2836de0 | |||
| c036a37602 | |||
| efedd1bf29 | |||
| 12388af5a0 | |||
| c39c13ffed | |||
| 7931dbeef9 | |||
| ee81de86c2 | |||
| 669888817b | |||
| 5564b888fc | |||
| 91aca25169 | |||
| 4195fb2651 | |||
| ba28ae174f | |||
| 755108a7d3 | |||
| 85983d5473 | |||
| 252b187600 | |||
| 8185a77697 | |||
| 6c2e317230 | |||
| 58f5329bc4 | |||
| 0767016370 | |||
| fa7ec0e422 | |||
| f4d2543d4e | |||
| 9e4513f911 | |||
| 4dc7b767a8 | |||
| f763e35d19 | |||
| 8b271c90c0 | |||
| 0dd4408b23 | |||
| 7a4a0a0135 | |||
| c6a48d6404 | |||
| ac1cb9b135 | |||
| f5bc0d0335 | |||
| 88a907c4dd | |||
| 03a7624564 | |||
| 1c3f44f3c2 | |||
| 287f8b778b | |||
| 63d8f9ed52 | |||
| 601570d724 | |||
| e9b2d1c79b | |||
| d27dd4b7bd | |||
| 1b7c7a0257 | |||
| 4f65c8e5e0 | |||
| 054617f320 | |||
| 8fc0614334 | |||
| 867abc8f63 | |||
| 818fd51502 | |||
| 392559dedc | |||
| 17d5312df3 | |||
| 037be97ece | |||
| 28d2223306 | |||
| 413b5d8acd | |||
| 8aaf30de99 | |||
| 18d340d029 | |||
| f1382490ec | |||
| f1b255a7f8 | |||
| 7520a37b05 | |||
| 7bde9e2f0c | |||
| e491dadc67 | |||
| 91f44b0080 | |||
| 7b1b778f5b | |||
| 67bc61cf91 |
205
.gitea/workflows/release.yml
Normal file
205
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
name: Create Release Package
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-release:
|
||||||
|
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, zip, intl, gettext
|
||||||
|
tools: composer:v2
|
||||||
|
|
||||||
|
- name: Get version from tag
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=${GITHUB_REF_NAME#v}
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Building version: $VERSION"
|
||||||
|
|
||||||
|
- name: Validate composer.json
|
||||||
|
run: composer validate --strict
|
||||||
|
|
||||||
|
- name: Install Composer dependencies (production)
|
||||||
|
run: |
|
||||||
|
composer config platform.php 8.3.0
|
||||||
|
composer install --no-dev --optimize-autoloader --no-interaction
|
||||||
|
|
||||||
|
- name: Install gettext
|
||||||
|
run: apt-get update && apt-get install -y gettext
|
||||||
|
|
||||||
|
- name: Compile translations
|
||||||
|
run: |
|
||||||
|
for po in languages/*.po; do
|
||||||
|
if [ -f "$po" ]; then
|
||||||
|
mo="${po%.po}.mo"
|
||||||
|
echo "Compiling $po to $mo"
|
||||||
|
msgfmt -o "$mo" "$po"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Verify plugin version matches tag
|
||||||
|
run: |
|
||||||
|
PLUGIN_VERSION=$(grep -oP "Version:\s*\K[0-9]+\.[0-9]+\.[0-9]+" wc-composable-product.php | head -1)
|
||||||
|
TAG_VERSION=${{ steps.version.outputs.version }}
|
||||||
|
if [ "$PLUGIN_VERSION" != "$TAG_VERSION" ]; then
|
||||||
|
echo "Error: Plugin version ($PLUGIN_VERSION) does not match tag version ($TAG_VERSION)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Version verified: $PLUGIN_VERSION"
|
||||||
|
|
||||||
|
- name: Create release directory
|
||||||
|
run: mkdir -p releases
|
||||||
|
|
||||||
|
- name: Build release package
|
||||||
|
run: |
|
||||||
|
VERSION=${{ steps.version.outputs.version }}
|
||||||
|
PLUGIN_NAME="wc-composable-product"
|
||||||
|
RELEASE_FILE="releases/${PLUGIN_NAME}-${VERSION}.zip"
|
||||||
|
|
||||||
|
# Move to parent directory for proper zip structure
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Create zip with proper WordPress plugin structure
|
||||||
|
zip -r "${PLUGIN_NAME}/${RELEASE_FILE}" "${PLUGIN_NAME}" \
|
||||||
|
-x "${PLUGIN_NAME}/.git/*" \
|
||||||
|
-x "${PLUGIN_NAME}/.gitea/*" \
|
||||||
|
-x "${PLUGIN_NAME}/.github/*" \
|
||||||
|
-x "${PLUGIN_NAME}/.vscode/*" \
|
||||||
|
-x "${PLUGIN_NAME}/.claude/*" \
|
||||||
|
-x "${PLUGIN_NAME}/CLAUDE.md" \
|
||||||
|
-x "${PLUGIN_NAME}/wp-core" \
|
||||||
|
-x "${PLUGIN_NAME}/wp-core/*" \
|
||||||
|
-x "${PLUGIN_NAME}/wp-plugins" \
|
||||||
|
-x "${PLUGIN_NAME}/wp-plugins/*" \
|
||||||
|
-x "${PLUGIN_NAME}/releases/*" \
|
||||||
|
-x "${PLUGIN_NAME}/cache/*" \
|
||||||
|
-x "${PLUGIN_NAME}/composer.lock" \
|
||||||
|
-x "${PLUGIN_NAME}/*.log" \
|
||||||
|
-x "${PLUGIN_NAME}/.gitignore" \
|
||||||
|
-x "${PLUGIN_NAME}/.editorconfig" \
|
||||||
|
-x "${PLUGIN_NAME}/phpcs.xml*" \
|
||||||
|
-x "${PLUGIN_NAME}/phpunit.xml*" \
|
||||||
|
-x "${PLUGIN_NAME}/tests/*" \
|
||||||
|
-x "${PLUGIN_NAME}/*.po~" \
|
||||||
|
-x "${PLUGIN_NAME}/*.bak" \
|
||||||
|
-x "*.DS_Store"
|
||||||
|
|
||||||
|
cd "${PLUGIN_NAME}"
|
||||||
|
echo "Created: ${RELEASE_FILE}"
|
||||||
|
|
||||||
|
- name: Generate checksums
|
||||||
|
run: |
|
||||||
|
VERSION=${{ steps.version.outputs.version }}
|
||||||
|
|
||||||
|
cd releases
|
||||||
|
sha256sum "wc-composable-product-${VERSION}.zip" > "wc-composable-product-${VERSION}.zip.sha256"
|
||||||
|
|
||||||
|
echo "SHA256:"
|
||||||
|
cat "wc-composable-product-${VERSION}.zip.sha256"
|
||||||
|
|
||||||
|
- name: Verify package structure
|
||||||
|
run: |
|
||||||
|
set +o pipefail
|
||||||
|
VERSION=${{ steps.version.outputs.version }}
|
||||||
|
echo "Package contents:"
|
||||||
|
unzip -l "releases/wc-composable-product-${VERSION}.zip" | head -50 || true
|
||||||
|
|
||||||
|
# Verify main file is at correct location
|
||||||
|
if unzip -l "releases/wc-composable-product-${VERSION}.zip" | grep -q "wc-composable-product/wc-composable-product.php"; then
|
||||||
|
echo "✓ Main plugin file at correct location"
|
||||||
|
else
|
||||||
|
echo "✗ Error: Main plugin file not found at wc-composable-product/wc-composable-product.php"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify vendor directory is included
|
||||||
|
if unzip -l "releases/wc-composable-product-${VERSION}.zip" | grep -q "wc-composable-product/vendor/"; then
|
||||||
|
echo "✓ Vendor directory included"
|
||||||
|
else
|
||||||
|
echo "✗ Error: Vendor directory not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Extract changelog for release notes
|
||||||
|
id: changelog
|
||||||
|
run: |
|
||||||
|
VERSION=${{ steps.version.outputs.version }}
|
||||||
|
# Extract changelog section for this version
|
||||||
|
NOTES=$(sed -n "/^## \[${VERSION}\]/,/^## \[/p" CHANGELOG.md | sed '$ d' | tail -n +2)
|
||||||
|
if [ -z "$NOTES" ]; then
|
||||||
|
NOTES="Release version ${VERSION}"
|
||||||
|
fi
|
||||||
|
# Save to file for multi-line output
|
||||||
|
echo "$NOTES" > release_notes.txt
|
||||||
|
echo "Release notes extracted"
|
||||||
|
|
||||||
|
- name: Create Gitea Release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.SRC_GITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
VERSION=${{ steps.version.outputs.version }}
|
||||||
|
TAG_NAME=${{ github.ref_name }}
|
||||||
|
PRERELEASE="false"
|
||||||
|
if [[ "$TAG_NAME" == *-* ]]; then
|
||||||
|
PRERELEASE="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read release notes
|
||||||
|
BODY=$(cat release_notes.txt)
|
||||||
|
|
||||||
|
# Check if release already exists for this tag and delete it
|
||||||
|
EXISTING_RELEASE=$(curl -s \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG_NAME}")
|
||||||
|
|
||||||
|
EXISTING_ID=$(echo "$EXISTING_RELEASE" | jq -r '.id // empty')
|
||||||
|
|
||||||
|
if [ -n "$EXISTING_ID" ] && [ "$EXISTING_ID" != "null" ]; then
|
||||||
|
echo "Deleting existing release ID: $EXISTING_ID"
|
||||||
|
curl -s -X DELETE \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${EXISTING_ID}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create release via Gitea API
|
||||||
|
RELEASE_RESPONSE=$(curl -s -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"Release ${VERSION}\", \"body\": $(echo "$BODY" | jq -Rs .), \"draft\": false, \"prerelease\": ${PRERELEASE}}" \
|
||||||
|
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases")
|
||||||
|
|
||||||
|
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id')
|
||||||
|
|
||||||
|
if [ "$RELEASE_ID" == "null" ] || [ -z "$RELEASE_ID" ]; then
|
||||||
|
echo "Failed to create release:"
|
||||||
|
echo "$RELEASE_RESPONSE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Created release ID: $RELEASE_ID"
|
||||||
|
|
||||||
|
# Upload attachments
|
||||||
|
for file in "releases/wc-composable-product-${VERSION}.zip" "releases/wc-composable-product-${VERSION}.zip.sha256"; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
FILENAME=$(basename "$file")
|
||||||
|
echo "Uploading $FILENAME..."
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary "@$file" \
|
||||||
|
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${FILENAME}"
|
||||||
|
echo "Uploaded $FILENAME"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Release created successfully: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG_NAME}"
|
||||||
4
.gitignore
vendored
Normal file → Executable file
4
.gitignore
vendored
Normal file → Executable file
@@ -1,6 +1,7 @@
|
|||||||
# Linked sources
|
# Linked sources
|
||||||
wp-core
|
wp-core
|
||||||
wp-plugins
|
wp-plugins
|
||||||
|
tpp
|
||||||
|
|
||||||
# Editor swap files
|
# Editor swap files
|
||||||
*.*swp
|
*.*swp
|
||||||
@@ -21,6 +22,3 @@ cache/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Release files
|
|
||||||
*.zip
|
|
||||||
releases/
|
|
||||||
|
|||||||
388
CHANGELOG.md
388
CHANGELOG.md
@@ -5,6 +5,394 @@ 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.2.0] - 2026-03-01
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **CRITICAL**: Product selection always empty regardless of configuration (categories, tags, or SKUs)
|
||||||
|
- Root cause: Product query used a `meta_query` checking `_product_type` in `wp_postmeta`, but WooCommerce stores product types in the `product_type` **taxonomy** — the `!=` comparison with a non-existent meta key caused an INNER JOIN returning zero results
|
||||||
|
- Fix: Replaced broken `meta_query` with correct `tax_query` using `product_type` taxonomy to exclude composable products
|
||||||
|
- **CRITICAL**: Cart price always 0.00 despite correct frontend price calculation
|
||||||
|
- Root cause: `composable_price_calculated` flag was persisted to the cart session, preventing price recalculation on subsequent page loads — but `set_price()` only modifies the in-memory product object and is lost between requests
|
||||||
|
- Fix: Removed per-item session flag; the existing static `$already_calculated` flag already prevents duplicate calculation within a single request
|
||||||
|
- **Admin tab rendering**: Both General and Composable Options panels visible on initial page load
|
||||||
|
- Root cause: JavaScript manually showed `#composable_product_data` via `.show()` without hiding the General panel
|
||||||
|
- Fix: Trigger WooCommerce's native tab click instead, so the tab system handles panel visibility correctly
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Gitea CI/CD release workflow** (`.gitea/workflows/release.yml`)
|
||||||
|
- Triggered on `v*` tags
|
||||||
|
- Installs PHP 8.3 with production Composer dependencies
|
||||||
|
- Compiles `.po` → `.mo` translations
|
||||||
|
- Verifies plugin version matches tag
|
||||||
|
- Builds release ZIP with proper WordPress directory structure
|
||||||
|
- Generates SHA-256 checksums
|
||||||
|
- Verifies package contains main plugin file and vendor directory
|
||||||
|
- Extracts changelog for release notes
|
||||||
|
- Creates Gitea release with attachments via API
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Debug logging from v1.1.14 (no longer needed after root cause identified)
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Modified files: includes/Product_Type.php, includes/Cart_Handler.php, assets/js/admin.js
|
||||||
|
- New file: .gitea/workflows/release.yml
|
||||||
|
- Product query now correctly uses `tax_query` with `product_type` taxonomy (`NOT IN` operator)
|
||||||
|
- Cart price recalculated on every request via `woocommerce_before_calculate_totals` hook
|
||||||
|
- Admin JS uses `$('ul.product_data_tabs li.composable_options a').trigger('click')` for native WooCommerce tab handling
|
||||||
|
|
||||||
|
## [1.1.14] - 2025-12-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **DEBUG**: Comprehensive debug logging to troubleshoot product retrieval issues
|
||||||
|
- Error log output shows selection criteria, query arguments, and results
|
||||||
|
- Logs each product/variation being added to help identify filtering issues
|
||||||
|
- Enable by setting WP_DEBUG to true in wp-config.php
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Modified file: includes/Product_Type.php (added error_log statements throughout get_available_products())
|
||||||
|
- Logs criteria array (categories, tags, SKUs)
|
||||||
|
- Logs WP_Query arguments before execution
|
||||||
|
- Logs number of posts found by query
|
||||||
|
- Logs each variable product's variation count
|
||||||
|
- Logs each variation/simple product being added with name
|
||||||
|
- Logs total products available at end
|
||||||
|
- All logging wrapped in WP_DEBUG checks (no performance impact in production)
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- This is a debug release to help diagnose why products aren't showing
|
||||||
|
- No functional changes from v1.1.13
|
||||||
|
- User should enable WP_DEBUG and check debug.log or error.log
|
||||||
|
- Log output will show exactly where products are being filtered out
|
||||||
|
- All translation files remain at 100% completion (57/57 strings)
|
||||||
|
|
||||||
|
## [1.1.13] - 2025-12-31
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **CRITICAL**: "No products available for selection" message showing even when products are configured
|
||||||
|
- Removed overly strict `is_in_stock()` requirement that was filtering out all products
|
||||||
|
- Products now show regardless of stock status (out-of-stock items are displayed but disabled)
|
||||||
|
- Added `'relation' => 'AND'` to meta_query for proper handling of multiple meta conditions
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Product retrieval now shows all purchasable products, not just in-stock ones
|
||||||
|
- Stock status still displayed on frontend with appropriate styling
|
||||||
|
- Out-of-stock items shown but disabled via checkbox and visual indicators
|
||||||
|
- Frontend stock management from v1.1.0 still fully functional
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Modified file: includes/Product_Type.php (lines 117-124, 177, 181)
|
||||||
|
- Changed from `$variation->is_in_stock() && $variation->is_purchasable()` to just `$variation->is_purchasable()`
|
||||||
|
- Changed from `$product->is_in_stock() && $product->is_purchasable()` to just `$product->is_purchasable()`
|
||||||
|
- Added `'relation' => 'AND'` to meta_query array for WordPress query compatibility
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- This fixes the issue where NO products were showing in the selector
|
||||||
|
- Stock validation still occurs at add-to-cart time (Stock_Manager class)
|
||||||
|
- Frontend still displays stock badges (in stock, low stock, out of stock)
|
||||||
|
- Out-of-stock items remain non-selectable via disabled checkboxes
|
||||||
|
- All translation files remain at 100% completion (57/57 strings)
|
||||||
|
|
||||||
|
## [1.1.12] - 2025-12-31
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **CRITICAL**: Variable product variations still not appearing in product selector after v1.1.11 release
|
||||||
|
- Changed variation retrieval method from `get_available_variations()` to `get_children()` for more reliable variation ID retrieval
|
||||||
|
- `get_available_variations()` returns complex data arrays which may not work in all contexts
|
||||||
|
- `get_children()` returns simple array of variation IDs directly, ensuring consistent results
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Modified file: includes/Product_Type.php (lines 171-184)
|
||||||
|
- Changed from `$product->get_available_variations()` to `$product->get_children()`
|
||||||
|
- More direct and reliable method for retrieving variation IDs
|
||||||
|
- Each variation ID passed to `wc_get_product()` for full product object
|
||||||
|
- Maintains all stock and purchasability checks from v1.1.11
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- This is a patch release fixing the variable product support introduced in v1.1.11
|
||||||
|
- User reported "nope, still no product selectable" after v1.1.11
|
||||||
|
- Root cause: `get_available_variations()` returns variation data arrays instead of clean IDs
|
||||||
|
- `get_children()` is the standard WooCommerce method for retrieving variation IDs
|
||||||
|
- All translation files remain at 100% completion (57/57 strings - no changes needed)
|
||||||
|
|
||||||
|
## [1.1.11] - 2025-12-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **FEATURE**: Variable product support - composable products can now include variable products and their variations
|
||||||
|
- Variable products automatically expand to show all available variations as selectable items
|
||||||
|
- Each variation displays with full attribute information (e.g., "Product - Color: Red, Size: Large")
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Products not showing in selector when all available products were variable products
|
||||||
|
- Variable products were being filtered out because parent products aren't directly purchasable
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Modified `get_available_products()` to detect and handle variable products
|
||||||
|
- Variable products now expand into their individual variations
|
||||||
|
- Each variation checked individually for stock status and purchasability
|
||||||
|
- Simple products continue to work exactly as before
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Modified file: includes/Product_Type.php (lines 160-188)
|
||||||
|
- Added logic to detect `is_type('variable')` products
|
||||||
|
- Uses `get_available_variations()` to retrieve all variations
|
||||||
|
- Each variation validated with `is_in_stock()` and `is_purchasable()`
|
||||||
|
- Maintains backward compatibility with simple products
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- This is a feature enhancement release, not a bug fix
|
||||||
|
- Resolves the issue where categories containing only variable products showed no selections
|
||||||
|
- Variations display with their parent product name plus selected attributes
|
||||||
|
- Stock management works correctly for both simple products and variations
|
||||||
|
- All translation files remain at 100% completion (57/57 strings - no new strings added)
|
||||||
|
|
||||||
|
## [1.1.10] - 2025-12-31
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **CRITICAL**: Admin panel - Both General and Composable tabs visible simultaneously on initial page load
|
||||||
|
- **CRITICAL**: Frontend - No products showing in product selector, only cart button and pricing visible
|
||||||
|
- Empty product grid now shows helpful message instead of blank space
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Added explicit `display: none` to `#composable_product_data` panel for proper initial hiding
|
||||||
|
- Panel now only shows when `body.product-type-composable` class is present
|
||||||
|
- Added empty state message in product selector template when no products are configured
|
||||||
|
- Cleared Twig cache to ensure template changes take effect
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Empty state message: "No products available for selection. Please configure the product criteria in the admin panel."
|
||||||
|
- Translations for empty state message in all 6 supported locales (de_DE, de_DE_informal, de_CH, de_CH_informal, fr_CH, it_CH)
|
||||||
|
- Recompiled .mo translation files
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Modified files: assets/css/admin.css (lines 7-16), templates/product-selector.twig (lines 12-15)
|
||||||
|
- Root cause (admin): Panel lacked explicit CSS hiding rule, relied only on `hidden` class
|
||||||
|
- Root cause (frontend): No feedback when products array is empty
|
||||||
|
- Solution: CSS specificity + empty state conditional in Twig template
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- This release fixes two critical bugs discovered immediately after v1.1.9
|
||||||
|
- Admin interface now correctly hides composable panel until product type is selected
|
||||||
|
- Frontend provides clear user feedback when product selection is unavailable
|
||||||
|
- All translation files now 100% complete (57/57 strings)
|
||||||
|
|
||||||
|
## [1.1.9] - 2025-12-31
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **CRITICAL**: Admin rendering completely broken - tabs disappeared and fields appeared out of context after v1.1.8 release
|
||||||
|
- CSS selectors were too broad, hiding tab navigation along with field groups
|
||||||
|
- Removed `!important` flags that caused overly aggressive hiding
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Made CSS selectors more specific: `.options_group.show_if_composable` for field groups only
|
||||||
|
- Added separate rule for tab links: `.product_data_tabs li.composable_options`
|
||||||
|
- Tab navigation now works correctly without hiding itself
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Modified files: assets/css/admin.css (lines 22-40)
|
||||||
|
- Root cause: `.show_if_composable` class used by WooCommerce for both tab links AND field groups
|
||||||
|
- Solution: Separate selectors for each use case to prevent unintended hiding
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- This release fixes critical regression introduced in v1.1.8
|
||||||
|
- Admin interface now renders correctly with visible tabs and properly positioned fields
|
||||||
|
- No `!important` flags needed with specific selectors
|
||||||
|
|
||||||
|
## [1.1.8] - 2025-12-31
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **CRITICAL**: Admin rendering bug where both General and Composable Options tabs showed simultaneously on initial page load
|
||||||
|
- **CRITICAL**: Frontend product selector not appearing on product pages - WooCommerce's default add-to-cart button now hidden for composable products
|
||||||
|
- **CRITICAL**: Price formatting not localized - prices now display with proper currency symbols, decimal separators, and thousand separators for all locales
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `wc_price()` Twig function for proper price formatting in templates
|
||||||
|
- `formatPrice()` JavaScript method with full WooCommerce locale support
|
||||||
|
- Price format localization data passed to frontend JavaScript (decimal/thousand separators, currency position, number of decimals)
|
||||||
|
- `hide_default_add_to_cart()` method to prevent WooCommerce's default purchase UI for composable products
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Enhanced CSS specificity with `!important` flags for proper tab visibility control
|
||||||
|
- Template now uses `{{ fixed_price_html|raw }}` instead of raw currency concatenation
|
||||||
|
- Product selector passes pre-formatted price HTML from `wc_price()` function
|
||||||
|
- Frontend JavaScript updates prices dynamically using WooCommerce format settings
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Modified files: assets/css/admin.css (+24 lines), includes/Cart_Handler.php (+14 lines), includes/Plugin.php (+7 lines), includes/Product_Selector.php (+2 lines), templates/product-selector.twig, assets/js/frontend.js (+28 lines)
|
||||||
|
- All PHP files pass syntax validation
|
||||||
|
- Supports Swiss format (CHF 50.-), European format (50,00 €), US format ($50.00), and all other WooCommerce locales
|
||||||
|
- Thousand separator support: comma (1,000), dot (1.000), apostrophe (1'000), space (1 000)
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- This release fixes all three critical UI bugs reported in CLAUDE.md
|
||||||
|
- Admin tabs now display correctly on initial page load without JavaScript flicker
|
||||||
|
- Frontend product selector is now the only purchase interface (no WooCommerce default button)
|
||||||
|
- All prices maintain proper locale formatting during dynamic updates
|
||||||
|
|
||||||
|
## [1.1.7] - 2025-12-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Compiled .mo translation files for all 6 supported locales (de_DE, de_DE_informal, de_CH, de_CH_informal, fr_CH, it_CH)
|
||||||
|
- WordPress can now load translations in admin and frontend areas
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **CRITICAL**: Missing translations in WordPress admin when using non-English locales
|
||||||
|
- Settings page ("Composable Products", "Default Selection Limit", etc.) now properly translated
|
||||||
|
- Product settings ("Composable Options", "Selection Criteria", etc.) now properly translated
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Compiled .mo files from .po sources using msgfmt
|
||||||
|
- All 6 locales now have complete translation coverage (56/56 strings translated and compiled)
|
||||||
|
- .mo files required for WordPress i18n system to display translations
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- Previous versions included .po translation files but WordPress requires compiled .mo files
|
||||||
|
- This release makes all existing translations actually visible to users
|
||||||
|
|
||||||
|
## [1.1.6] - 2025-12-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Complete translations for all admin area strings across all 6 supported locales
|
||||||
|
- "Fixed Price" field label and description translations
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated translation template (.pot) to version 1.1.6
|
||||||
|
- Simplified "How to calculate the price" description text
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- All .po files now include translations for v1.1.4 admin strings
|
||||||
|
- 100% translation coverage maintained across all locales (56/56 strings)
|
||||||
|
- German formal/informal variants properly differentiated (Sie vs. du)
|
||||||
|
|
||||||
|
## [1.1.5] - 2025-12-31
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **CRITICAL**: Fixed Twig template error "Unknown 'esc_attr' filter" when rendering product selector
|
||||||
|
- Template compatibility issue when other plugins (e.g., WooCommerce Tier and Package Prices) use Twig
|
||||||
|
- WordPress escaping functions now properly registered as both Twig functions AND filters
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Added `TwigFilter` registrations for `esc_html`, `esc_attr`, and `esc_url` in `Plugin::init_twig()`
|
||||||
|
- Template can now use both syntax styles: `{{ value|esc_attr }}` (filter) and `{{ esc_attr(value) }}` (function)
|
||||||
|
- Prevents conflicts when multiple plugins bundle their own Twig installations
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- Previous versions only registered escaping functions as Twig functions, not filters
|
||||||
|
- Template used filter syntax (`|esc_attr`) which failed when parsed by external Twig instances
|
||||||
|
- Fix ensures compatibility regardless of which Twig instance processes the template
|
||||||
|
|
||||||
|
## [1.1.4] - 2025-12-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Fixed price field in Composable Options tab for easier price configuration
|
||||||
|
- JavaScript toggle to show/hide fixed price field based on selected pricing mode
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Simplified pricing mode description text in admin interface
|
||||||
|
- Fixed price field now appears dynamically when "Fixed" pricing mode is selected
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Added `_regular_price` field with `composable_fixed_price_field` CSS class in `Product_Data.php`
|
||||||
|
- Implemented `toggleFixedPriceField()` JavaScript function in `assets/js/admin.js`
|
||||||
|
- Progressive disclosure pattern improves admin UX by showing relevant fields only
|
||||||
|
|
||||||
|
## [1.1.3] - 2024-12-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- WooCommerce HPOS (High-Performance Order Storage) compatibility declaration
|
||||||
|
- Prevents duplicate price calculations to avoid conflicts with other pricing plugins
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- WooCommerce compatibility warnings with Analytics and other WooCommerce extensions
|
||||||
|
- Price calculation conflicts with third-party pricing plugins
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Added `before_woocommerce_init` hook to declare HPOS compatibility
|
||||||
|
- Implemented static flag in `Cart_Handler::calculate_cart_item_price()` to prevent multiple executions
|
||||||
|
- Added `composable_price_calculated` flag to cart items to prevent re-calculation by other plugins
|
||||||
|
- Ensures composable products work with WooCommerce's modern order storage system
|
||||||
|
|
||||||
|
## [1.1.2] - 2024-12-31
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **CRITICAL**: Fixed persistent "Class WC_Settings_Page not found" error that continued in v1.1.1
|
||||||
|
- Root cause: Settings.php was being included too early (during plugin init) before WC_Settings_Page was loaded
|
||||||
|
- Solution: Delayed Settings.php inclusion until `woocommerce_get_settings_pages` filter when class is guaranteed to exist
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Removed `require_once Settings.php` from `Plugin::includes()` (line 93)
|
||||||
|
- Added `require_once Settings.php` to `Plugin::add_settings_page()` (line 196)
|
||||||
|
- Settings file now loads on-demand when WooCommerce requests settings pages
|
||||||
|
- Previous hook change (woocommerce_init) was insufficient - class loading order was the real issue
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- v1.1.1 attempted to fix this with hook change but the error persisted
|
||||||
|
- This version addresses the actual root cause: premature class extension
|
||||||
|
|
||||||
|
## [1.1.1] - 2024-12-31
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Settings page initialization timing issue causing "Class WC_Settings_Page not found" error
|
||||||
|
- Changed hook from `woocommerce_loaded` to `woocommerce_init` to ensure WC_Settings_Page class is available
|
||||||
|
- Plugin now initializes after all WooCommerce core classes are loaded
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Hook changed from `woocommerce_loaded` to `woocommerce_init` in wc-composable-product.php:65
|
||||||
|
- `woocommerce_init` fires after WooCommerce has finished loading all its core classes including settings
|
||||||
|
|
||||||
## [1.1.0] - 2024-12-31
|
## [1.1.0] - 2024-12-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -10,10 +10,13 @@ This plugin adds a new product type to WooCommerce that allows customers to buil
|
|||||||
|
|
||||||
- **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
|
||||||
|
- **Variable Product Support**: Automatically expands variable products into selectable variations
|
||||||
|
- **Stock Management**: Real-time stock validation, visual indicators, and automatic inventory tracking
|
||||||
- **Configurable Limits**: Set global or per-product selection limits
|
- **Configurable Limits**: Set global or per-product selection limits
|
||||||
- **Pricing Options**: Fixed price or sum of selected products
|
- **Pricing Options**: Fixed price or sum of selected products with full locale-aware formatting
|
||||||
- **Multi-language Support**: Fully translatable with i18n support
|
- **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
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -43,6 +46,7 @@ This plugin adds a new product type to WooCommerce that allows customers to buil
|
|||||||
### 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
|
||||||
- Default pricing mode
|
- Default pricing mode
|
||||||
- Display options
|
- Display options
|
||||||
@@ -60,10 +64,28 @@ composer install
|
|||||||
### Translation
|
### Translation
|
||||||
|
|
||||||
Generate POT file:
|
Generate POT file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wp i18n make-pot . languages/wc-composable-product.pot
|
wp i18n make-pot . languages/wc-composable-product.pot
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Compile translations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating Releases
|
||||||
|
|
||||||
|
Releases are automated via Gitea CI/CD. Push an annotated tag to trigger:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -a v1.2.0 -m "Release v1.2.0"
|
||||||
|
git push origin v1.2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
The workflow builds the release ZIP, compiles translations, generates checksums, and creates a Gitea release with attachments.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GPL v3 or later - see LICENSE file for details
|
GPL v3 or later - see LICENSE file for details
|
||||||
|
|||||||
23
assets/css/admin.css
Normal file → Executable file
23
assets/css/admin.css
Normal file → Executable file
@@ -4,10 +4,17 @@
|
|||||||
* @package WC_Composable_Product
|
* @package WC_Composable_Product
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* Hide composable panel by default */
|
||||||
#composable_product_data {
|
#composable_product_data {
|
||||||
|
display: none;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Show composable panel when composable type is selected */
|
||||||
|
body.product-type-composable #composable_product_data {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.composable_criteria_group {
|
.composable_criteria_group {
|
||||||
border-top: 1px solid #eee;
|
border-top: 1px solid #eee;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
@@ -19,11 +26,23 @@
|
|||||||
min-height: 150px;
|
min-height: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.show_if_composable {
|
/* Hide composable-specific elements by default (but not tabs) */
|
||||||
|
.options_group.show_if_composable {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-type-composable .show_if_composable {
|
/* Show composable elements when composable product type is selected */
|
||||||
|
body.product-type-composable .options_group.show_if_composable {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the Composable Options tab link by default */
|
||||||
|
.product_data_tabs li.composable_options {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show the Composable Options tab when composable type selected */
|
||||||
|
body.product-type-composable .product_data_tabs li.composable_options {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
0
assets/css/frontend.css
Normal file → Executable file
0
assets/css/frontend.css
Normal file → Executable file
25
assets/js/admin.js
Normal file → Executable file
25
assets/js/admin.js
Normal file → Executable file
@@ -17,12 +17,14 @@
|
|||||||
if (productType === 'composable') {
|
if (productType === 'composable') {
|
||||||
$('.show_if_composable').show();
|
$('.show_if_composable').show();
|
||||||
$('.hide_if_composable').hide();
|
$('.hide_if_composable').hide();
|
||||||
$('#composable_product_data').show();
|
// Show the composable tab, then click it so WooCommerce's
|
||||||
$('.product_data_tabs .composable_options a').show();
|
// native tab system hides all other panels properly
|
||||||
|
$('.product_data_tabs li.composable_options').show();
|
||||||
|
$('ul.product_data_tabs li.composable_options a').trigger('click');
|
||||||
} else {
|
} else {
|
||||||
$('.show_if_composable').hide();
|
$('.show_if_composable').hide();
|
||||||
|
$('.product_data_tabs li.composable_options').hide();
|
||||||
$('#composable_product_data').hide();
|
$('#composable_product_data').hide();
|
||||||
$('.product_data_tabs .composable_options a').hide();
|
|
||||||
}
|
}
|
||||||
}).trigger('change');
|
}).trigger('change');
|
||||||
|
|
||||||
@@ -36,6 +38,23 @@
|
|||||||
$('#composable_criteria_' + criteriaType).show();
|
$('#composable_criteria_' + criteriaType).show();
|
||||||
}).trigger('change');
|
}).trigger('change');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle fixed price field based on pricing mode
|
||||||
|
*/
|
||||||
|
function toggleFixedPriceField() {
|
||||||
|
const pricingMode = $('#_composable_pricing_mode').val();
|
||||||
|
const $fixedPriceField = $('.composable_fixed_price_field');
|
||||||
|
|
||||||
|
if (pricingMode === 'fixed') {
|
||||||
|
$fixedPriceField.show();
|
||||||
|
} else {
|
||||||
|
$fixedPriceField.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#_composable_pricing_mode').on('change', toggleFixedPriceField);
|
||||||
|
toggleFixedPriceField();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize enhanced select for categories and tags
|
* Initialize enhanced select for categories and tags
|
||||||
*/
|
*/
|
||||||
|
|||||||
34
assets/js/frontend.js
Normal file → Executable file
34
assets/js/frontend.js
Normal file → Executable file
@@ -63,6 +63,36 @@
|
|||||||
this.clearMessages($container);
|
this.clearMessages($container);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format price using WooCommerce settings
|
||||||
|
*
|
||||||
|
* @param {number} price Price amount
|
||||||
|
* @return {string} Formatted price HTML
|
||||||
|
*/
|
||||||
|
formatPrice: function(price) {
|
||||||
|
if (typeof wcComposableProduct === 'undefined' || !wcComposableProduct.price_format) {
|
||||||
|
return price.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const format = wcComposableProduct.price_format;
|
||||||
|
const decimals = parseInt(format.decimals, 10);
|
||||||
|
const decimalSep = format.decimal_separator;
|
||||||
|
const thousandSep = format.thousand_separator;
|
||||||
|
|
||||||
|
// Format number
|
||||||
|
let priceStr = price.toFixed(decimals);
|
||||||
|
const parts = priceStr.split('.');
|
||||||
|
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandSep);
|
||||||
|
priceStr = parts.join(decimalSep);
|
||||||
|
|
||||||
|
// Apply price format (e.g., "%1$s%2$s" for symbol+price or "%2$s%1$s" for price+symbol)
|
||||||
|
let formatted = format.price_format
|
||||||
|
.replace('%1$s', '<span class="woocommerce-Price-currencySymbol">' + format.currency_symbol + '</span>')
|
||||||
|
.replace('%2$s', priceStr);
|
||||||
|
|
||||||
|
return '<span class="woocommerce-Price-amount amount">' + formatted + '</span>';
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update total price
|
* Update total price
|
||||||
*
|
*
|
||||||
@@ -79,8 +109,8 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const currencySymbol = $container.find('.total-price').data('currency');
|
const formattedPrice = this.formatPrice(total);
|
||||||
$container.find('.calculated-total').text(currencySymbol + total.toFixed(2));
|
$container.find('.calculated-total').html(formattedPrice);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Marco Graetsch",
|
"name": "Marco Graetsch",
|
||||||
"email": "marco@example.com"
|
"email": "magdev3.0@gmail.com"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class Product_Data {
|
|||||||
woocommerce_wp_select([
|
woocommerce_wp_select([
|
||||||
'id' => '_composable_pricing_mode',
|
'id' => '_composable_pricing_mode',
|
||||||
'label' => __('Pricing Mode', 'wc-composable-product'),
|
'label' => __('Pricing Mode', 'wc-composable-product'),
|
||||||
'description' => __('How to calculate the price. Leave empty to use global default.', 'wc-composable-product'),
|
'description' => __('How to calculate the price.', 'wc-composable-product'),
|
||||||
'desc_tip' => true,
|
'desc_tip' => true,
|
||||||
'options' => [
|
'options' => [
|
||||||
'' => __('Use global default', 'wc-composable-product'),
|
'' => __('Use global default', 'wc-composable-product'),
|
||||||
@@ -72,6 +72,16 @@ class Product_Data {
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
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>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,21 @@ class Cart_Handler {
|
|||||||
add_action('woocommerce_before_calculate_totals', [$this, 'calculate_cart_item_price']);
|
add_action('woocommerce_before_calculate_totals', [$this, 'calculate_cart_item_price']);
|
||||||
add_action('woocommerce_single_product_summary', [$this, 'render_product_selector'], 25);
|
add_action('woocommerce_single_product_summary', [$this, 'render_product_selector'], 25);
|
||||||
add_action('woocommerce_checkout_create_order_line_item', [$this->stock_manager, 'store_selected_products_in_order'], 10, 3);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -185,6 +200,12 @@ class Cart_Handler {
|
|||||||
return;
|
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) {
|
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['data']) && $cart_item['data']->get_type() === 'composable') {
|
||||||
if (isset($cart_item['composable_products'])) {
|
if (isset($cart_item['composable_products'])) {
|
||||||
@@ -194,5 +215,7 @@ class Cart_Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$already_calculated = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,13 +84,20 @@ class Plugin {
|
|||||||
$this->twig->addFunction(new \Twig\TwigFunction('esc_html', 'esc_html'));
|
$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_attr', 'esc_attr'));
|
||||||
$this->twig->addFunction(new \Twig\TwigFunction('esc_url', 'esc_url'));
|
$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
|
||||||
|
$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_url', 'esc_url'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Include required files
|
* Include required files
|
||||||
*/
|
*/
|
||||||
private function includes() {
|
private function includes() {
|
||||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Settings.php';
|
// 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.
|
||||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Product_Data.php';
|
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Product_Data.php';
|
||||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Product_Type.php';
|
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Product_Type.php';
|
||||||
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Stock_Manager.php';
|
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Stock_Manager.php';
|
||||||
@@ -155,6 +162,13 @@ class Plugin {
|
|||||||
'max_items' => __('Maximum items selected', 'wc-composable-product'),
|
'max_items' => __('Maximum items selected', 'wc-composable-product'),
|
||||||
'min_items' => __('Please select at least one item', 'wc-composable-product'),
|
'min_items' => __('Please select at least one item', 'wc-composable-product'),
|
||||||
],
|
],
|
||||||
|
'price_format' => [
|
||||||
|
'currency_symbol' => get_woocommerce_currency_symbol(),
|
||||||
|
'decimal_separator' => wc_get_price_decimal_separator(),
|
||||||
|
'thousand_separator' => wc_get_price_thousand_separator(),
|
||||||
|
'decimals' => wc_get_price_decimals(),
|
||||||
|
'price_format' => get_woocommerce_price_format(),
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,6 +205,8 @@ class Plugin {
|
|||||||
* @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
|
||||||
|
require_once WC_COMPOSABLE_PRODUCT_PATH . 'includes/Admin/Settings.php';
|
||||||
$settings[] = new Admin\Settings();
|
$settings[] = new Admin\Settings();
|
||||||
return $settings;
|
return $settings;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ class Product_Selector {
|
|||||||
'show_prices' => $show_prices,
|
'show_prices' => $show_prices,
|
||||||
'show_total' => $show_total,
|
'show_total' => $show_total,
|
||||||
'fixed_price' => $product->get_price(),
|
'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(),
|
'currency_symbol' => get_woocommerce_currency_symbol(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -110,15 +110,17 @@ class Product_Type extends \WC_Product {
|
|||||||
'post_status' => 'publish',
|
'post_status' => 'publish',
|
||||||
'orderby' => 'title',
|
'orderby' => 'title',
|
||||||
'order' => 'ASC',
|
'order' => 'ASC',
|
||||||
'tax_query' => [],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Exclude composable products from selection
|
// Exclude composable products using the product_type taxonomy
|
||||||
$args['meta_query'] = [
|
// (WooCommerce stores product types as taxonomy terms, NOT as postmeta)
|
||||||
|
$args['tax_query'] = [
|
||||||
|
'relation' => 'AND',
|
||||||
[
|
[
|
||||||
'key' => '_product_type',
|
'taxonomy' => 'product_type',
|
||||||
'value' => 'composable',
|
'field' => 'slug',
|
||||||
'compare' => '!=',
|
'terms' => ['composable'],
|
||||||
|
'operator' => 'NOT IN',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -148,10 +150,12 @@ class Product_Type extends \WC_Product {
|
|||||||
case 'sku':
|
case 'sku':
|
||||||
if (!empty($criteria['skus'])) {
|
if (!empty($criteria['skus'])) {
|
||||||
$skus = array_map('trim', explode(',', $criteria['skus']));
|
$skus = array_map('trim', explode(',', $criteria['skus']));
|
||||||
$args['meta_query'][] = [
|
$args['meta_query'] = [
|
||||||
'key' => '_sku',
|
[
|
||||||
'value' => $skus,
|
'key' => '_sku',
|
||||||
'compare' => 'IN',
|
'value' => $skus,
|
||||||
|
'compare' => 'IN',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -163,13 +167,28 @@ class Product_Type extends \WC_Product {
|
|||||||
if ($query->have_posts()) {
|
if ($query->have_posts()) {
|
||||||
foreach ($query->posts as $post) {
|
foreach ($query->posts as $post) {
|
||||||
$product = wc_get_product($post->ID);
|
$product = wc_get_product($post->ID);
|
||||||
if ($product && $product->is_in_stock() && $product->is_purchasable()) {
|
|
||||||
|
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;
|
$products[] = $product;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wp_reset_postdata();
|
wp_reset_postdata();
|
||||||
|
|
||||||
return $products;
|
return $products;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
48
languages/wc-composable-product-de_CH.po
Normal file → Executable file
48
languages/wc-composable-product-de_CH.po
Normal file → Executable file
@@ -144,8 +144,16 @@ msgid "Pricing Mode"
|
|||||||
msgstr "Preismodus"
|
msgstr "Preismodus"
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "How to calculate the price. Leave empty to use global default."
|
msgid "How to calculate the price."
|
||||||
msgstr "Wie der Preis berechnet wird. Leer lassen, um den globalen Standard zu verwenden."
|
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
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "Use global default"
|
msgid "Use global default"
|
||||||
@@ -198,3 +206,39 @@ msgstr "Geben Sie Produkt-Artikelnummern durch Kommas getrennt ein."
|
|||||||
#: includes/Admin/Product_Data.php
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "SKU-1, SKU-2, SKU-3"
|
msgid "SKU-1, SKU-2, SKU-3"
|
||||||
msgstr "ART-1, ART-2, ART-3"
|
msgstr "ART-1, ART-2, ART-3"
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "\"%s\" is out of stock and cannot be selected."
|
||||||
|
msgstr "\"%s\" ist nicht an Lager und kann nicht ausgewählt werden."
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||||
|
msgstr "Nur %2$d von \"%1$s\" sind an Lager verfügbar."
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||||
|
msgstr "Lagerbestand reduziert für \"%1$s\": -%2$d (verbleibend: %3$d)"
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||||
|
msgstr "Lagerbestand wiederhergestellt für \"%1$s\": +%2$d (gesamt: %3$d)"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "Out of stock"
|
||||||
|
msgstr "Nicht an Lager"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "Only"
|
||||||
|
msgstr "Nur"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "left"
|
||||||
|
msgstr "übrig"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "In stock"
|
||||||
|
msgstr "An Lager"
|
||||||
|
|
||||||
|
#: templates/product-selector.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."
|
||||||
|
|||||||
48
languages/wc-composable-product-de_CH_informal.po
Normal file → Executable file
48
languages/wc-composable-product-de_CH_informal.po
Normal file → Executable file
@@ -144,8 +144,16 @@ msgid "Pricing Mode"
|
|||||||
msgstr "Preismodus"
|
msgstr "Preismodus"
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "How to calculate the price. Leave empty to use global default."
|
msgid "How to calculate the price."
|
||||||
msgstr "Wie der Preis berechnet wird. Leer lassen, um den globalen Standard zu verwenden."
|
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
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "Use global default"
|
msgid "Use global default"
|
||||||
@@ -198,3 +206,39 @@ msgstr "Gib Produkt-Artikelnummern durch Kommas getrennt ein."
|
|||||||
#: includes/Admin/Product_Data.php
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "SKU-1, SKU-2, SKU-3"
|
msgid "SKU-1, SKU-2, SKU-3"
|
||||||
msgstr "ART-1, ART-2, ART-3"
|
msgstr "ART-1, ART-2, ART-3"
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "\"%s\" is out of stock and cannot be selected."
|
||||||
|
msgstr "\"%s\" ist nicht an Lager und kann nicht ausgewählt werden."
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||||
|
msgstr "Nur %2$d von \"%1$s\" sind an Lager verfügbar."
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||||
|
msgstr "Lagerbestand reduziert für \"%1$s\": -%2$d (verbleibend: %3$d)"
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||||
|
msgstr "Lagerbestand wiederhergestellt für \"%1$s\": +%2$d (gesamt: %3$d)"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "Out of stock"
|
||||||
|
msgstr "Nicht an Lager"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "Only"
|
||||||
|
msgstr "Nur"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "left"
|
||||||
|
msgstr "übrig"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "In stock"
|
||||||
|
msgstr "An Lager"
|
||||||
|
|
||||||
|
#: templates/product-selector.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."
|
||||||
|
|||||||
48
languages/wc-composable-product-de_DE.po
Normal file → Executable file
48
languages/wc-composable-product-de_DE.po
Normal file → Executable file
@@ -144,8 +144,16 @@ msgid "Pricing Mode"
|
|||||||
msgstr "Preismodus"
|
msgstr "Preismodus"
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "How to calculate the price. Leave empty to use global default."
|
msgid "How to calculate the price."
|
||||||
msgstr "Wie der Preis berechnet wird. Leer lassen, um den globalen Standard zu verwenden."
|
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
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "Use global default"
|
msgid "Use global default"
|
||||||
@@ -198,3 +206,39 @@ msgstr "Geben Sie Produkt-Artikelnummern durch Kommas getrennt ein."
|
|||||||
#: includes/Admin/Product_Data.php
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "SKU-1, SKU-2, SKU-3"
|
msgid "SKU-1, SKU-2, SKU-3"
|
||||||
msgstr "ART-1, ART-2, ART-3"
|
msgstr "ART-1, ART-2, ART-3"
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "\"%s\" is out of stock and cannot be selected."
|
||||||
|
msgstr "\"%s\" ist nicht auf Lager und kann nicht ausgewählt werden."
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||||
|
msgstr "Nur %2$d von \"%1$s\" sind auf Lager verfügbar."
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||||
|
msgstr "Lagerbestand reduziert für \"%1$s\": -%2$d (verbleibend: %3$d)"
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||||
|
msgstr "Lagerbestand wiederhergestellt für \"%1$s\": +%2$d (gesamt: %3$d)"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "Out of stock"
|
||||||
|
msgstr "Nicht auf Lager"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "Only"
|
||||||
|
msgstr "Nur"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "left"
|
||||||
|
msgstr "übrig"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "In stock"
|
||||||
|
msgstr "Auf Lager"
|
||||||
|
|
||||||
|
#: templates/product-selector.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."
|
||||||
|
|||||||
48
languages/wc-composable-product-de_DE_informal.po
Normal file → Executable file
48
languages/wc-composable-product-de_DE_informal.po
Normal file → Executable file
@@ -144,8 +144,16 @@ msgid "Pricing Mode"
|
|||||||
msgstr "Preismodus"
|
msgstr "Preismodus"
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "How to calculate the price. Leave empty to use global default."
|
msgid "How to calculate the price."
|
||||||
msgstr "Wie der Preis berechnet wird. Leer lassen, um den globalen Standard zu verwenden."
|
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
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "Use global default"
|
msgid "Use global default"
|
||||||
@@ -198,3 +206,39 @@ msgstr "Gib Produkt-Artikelnummern durch Kommas getrennt ein."
|
|||||||
#: includes/Admin/Product_Data.php
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "SKU-1, SKU-2, SKU-3"
|
msgid "SKU-1, SKU-2, SKU-3"
|
||||||
msgstr "ART-1, ART-2, ART-3"
|
msgstr "ART-1, ART-2, ART-3"
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "\"%s\" is out of stock and cannot be selected."
|
||||||
|
msgstr "\"%s\" ist nicht auf Lager und kann nicht ausgewählt werden."
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||||
|
msgstr "Nur %2$d von \"%1$s\" sind auf Lager verfügbar."
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||||
|
msgstr "Lagerbestand reduziert für \"%1$s\": -%2$d (verbleibend: %3$d)"
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||||
|
msgstr "Lagerbestand wiederhergestellt für \"%1$s\": +%2$d (gesamt: %3$d)"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "Out of stock"
|
||||||
|
msgstr "Nicht auf Lager"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "Only"
|
||||||
|
msgstr "Nur"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "left"
|
||||||
|
msgstr "übrig"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "In stock"
|
||||||
|
msgstr "Auf Lager"
|
||||||
|
|
||||||
|
#: templates/product-selector.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."
|
||||||
|
|||||||
48
languages/wc-composable-product-fr_CH.po
Normal file → Executable file
48
languages/wc-composable-product-fr_CH.po
Normal file → Executable file
@@ -144,8 +144,16 @@ msgid "Pricing Mode"
|
|||||||
msgstr "Mode de tarification"
|
msgstr "Mode de tarification"
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "How to calculate the price. Leave empty to use global default."
|
msgid "How to calculate the price."
|
||||||
msgstr "Comment calculer le prix. Laisser vide pour utiliser la valeur par défaut globale."
|
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
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "Use global default"
|
msgid "Use global default"
|
||||||
@@ -198,3 +206,39 @@ msgstr "Entrez les références des produits séparées par des virgules."
|
|||||||
#: includes/Admin/Product_Data.php
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "SKU-1, SKU-2, SKU-3"
|
msgid "SKU-1, SKU-2, SKU-3"
|
||||||
msgstr "REF-1, REF-2, REF-3"
|
msgstr "REF-1, REF-2, REF-3"
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "\"%s\" is out of stock and cannot be selected."
|
||||||
|
msgstr "\"%s\" est en rupture de stock et ne peut pas être sélectionné."
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Only %2$d of \"%1$s\" are available in stock."
|
||||||
|
msgstr "Seulement %2$d de \"%1$s\" sont disponibles en stock."
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Stock reduced for \"%1$s\": -%2$d (remaining: %3$d)"
|
||||||
|
msgstr "Stock réduit pour \"%1$s\": -%2$d (restant: %3$d)"
|
||||||
|
|
||||||
|
#: includes/Stock_Manager.php
|
||||||
|
msgid "Stock restored for \"%1$s\": +%2$d (total: %3$d)"
|
||||||
|
msgstr "Stock restauré pour \"%1$s\": +%2$d (total: %3$d)"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "Out of stock"
|
||||||
|
msgstr "Rupture de stock"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "Only"
|
||||||
|
msgstr "Seulement"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "left"
|
||||||
|
msgstr "restant"
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "In stock"
|
||||||
|
msgstr "En stock"
|
||||||
|
|
||||||
|
#: templates/product-selector.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."
|
||||||
|
|||||||
16
languages/wc-composable-product-it_CH.po
Normal file → Executable file
16
languages/wc-composable-product-it_CH.po
Normal file → Executable file
@@ -144,8 +144,16 @@ msgid "Pricing Mode"
|
|||||||
msgstr "Modalità di prezzo"
|
msgstr "Modalità di prezzo"
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "How to calculate the price. Leave empty to use global default."
|
msgid "How to calculate the price."
|
||||||
msgstr "Come calcolare il prezzo. Lasciare vuoto per utilizzare il valore predefinito globale."
|
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
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "Use global default"
|
msgid "Use global default"
|
||||||
@@ -230,3 +238,7 @@ msgstr "rimasti"
|
|||||||
#: templates/product-selector.twig
|
#: templates/product-selector.twig
|
||||||
msgid "In stock"
|
msgid "In stock"
|
||||||
msgstr "Disponibile"
|
msgstr "Disponibile"
|
||||||
|
|
||||||
|
#: templates/product-selector.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."
|
||||||
|
|||||||
16
languages/wc-composable-product.pot
Normal file → Executable file
16
languages/wc-composable-product.pot
Normal file → Executable file
@@ -2,7 +2,7 @@
|
|||||||
# 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.0.0\n"
|
"Project-Id-Version: WooCommerce Composable Products 1.1.6\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: 2024-12-31 00:00+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
@@ -143,7 +143,15 @@ msgid "Pricing Mode"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
#: includes/Admin/Product_Data.php
|
||||||
msgid "How to calculate the price. Leave empty to use global default."
|
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 ""
|
msgstr ""
|
||||||
|
|
||||||
#: includes/Admin/Product_Data.php
|
#: includes/Admin/Product_Data.php
|
||||||
@@ -229,3 +237,7 @@ msgstr ""
|
|||||||
#: templates/product-selector.twig
|
#: templates/product-selector.twig
|
||||||
msgid "In stock"
|
msgid "In stock"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/product-selector.twig
|
||||||
|
msgid "No products available for selection. Please configure the product criteria in the admin panel."
|
||||||
|
msgstr ""
|
||||||
|
|||||||
12
templates/product-selector.twig
Normal file → Executable file
12
templates/product-selector.twig
Normal file → Executable file
@@ -9,6 +9,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="composable-products-grid">
|
<div class="composable-products-grid">
|
||||||
|
{% if products is empty %}
|
||||||
|
<div class="composable-no-products">
|
||||||
|
<p>{{ __('No products available for selection. Please configure the product criteria in the admin panel.') }}</p>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
{% for product in products %}
|
{% for product in products %}
|
||||||
<div class="composable-product-item{% if not product.in_stock %} out-of-stock{% endif %}" data-product-id="{{ product.id }}" data-price="{{ product.price }}" data-stock-status="{{ product.stock_status }}">
|
<div class="composable-product-item{% if not product.in_stock %} out-of-stock{% endif %}" data-product-id="{{ product.id }}" data-price="{{ product.price }}" data-stock-status="{{ product.stock_status }}">
|
||||||
<div class="product-item-inner">
|
<div class="product-item-inner">
|
||||||
@@ -52,16 +57,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if show_total %}
|
{% if show_total %}
|
||||||
<div class="composable-total">
|
<div class="composable-total">
|
||||||
<div class="total-label">{{ __('Total Price:') }}</div>
|
<div class="total-label">{{ __('Total Price:') }}</div>
|
||||||
<div class="total-price" data-currency="{{ currency_symbol }}">
|
<div class="total-price" data-currency="{{ currency_symbol }}" data-fixed-price="{{ fixed_price }}">
|
||||||
{% if pricing_mode == 'fixed' %}
|
{% if pricing_mode == 'fixed' %}
|
||||||
{{ currency_symbol }}{{ fixed_price }}
|
{{ fixed_price_html|raw }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="calculated-total">{{ currency_symbol }}0.00</span>
|
<span class="calculated-total">{{ zero_price_html|raw }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin Name: WooCommerce Composable Products
|
* Plugin Name: WooCommerce Composable Products
|
||||||
* Plugin URI: https://github.com/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.1.0
|
* Version: 1.2.0
|
||||||
* Author: Marco Graetsch
|
* Author: Marco Graetsch
|
||||||
* Author URI: https://example.com
|
* Author URI: https://src.bundespruefstelle.ch/magdev
|
||||||
* License: GPL v3 or later
|
* License: GPL v3 or later
|
||||||
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||||
* Text Domain: wc-composable-product
|
* Text Domain: wc-composable-product
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
// Define plugin constants
|
// Define plugin constants
|
||||||
define('WC_COMPOSABLE_PRODUCT_VERSION', '1.1.0');
|
define('WC_COMPOSABLE_PRODUCT_VERSION', '1.2.0');
|
||||||
define('WC_COMPOSABLE_PRODUCT_FILE', __FILE__);
|
define('WC_COMPOSABLE_PRODUCT_FILE', __FILE__);
|
||||||
define('WC_COMPOSABLE_PRODUCT_PATH', plugin_dir_path(__FILE__));
|
define('WC_COMPOSABLE_PRODUCT_PATH', plugin_dir_path(__FILE__));
|
||||||
define('WC_COMPOSABLE_PRODUCT_URL', plugin_dir_url(__FILE__));
|
define('WC_COMPOSABLE_PRODUCT_URL', plugin_dir_url(__FILE__));
|
||||||
@@ -61,8 +62,17 @@ function wc_composable_product_init() {
|
|||||||
// Initialize main plugin class
|
// Initialize main plugin class
|
||||||
WC_Composable_Product\Plugin::instance();
|
WC_Composable_Product\Plugin::instance();
|
||||||
}
|
}
|
||||||
// Use woocommerce_loaded to ensure WooCommerce classes are available
|
// Use woocommerce_init to ensure all WooCommerce classes including settings are loaded
|
||||||
add_action('woocommerce_loaded', 'wc_composable_product_init');
|
add_action('woocommerce_init', 'wc_composable_product_init');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declare HPOS compatibility
|
||||||
|
*/
|
||||||
|
add_action('before_woocommerce_init', function() {
|
||||||
|
if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) {
|
||||||
|
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activation hook
|
* Activation hook
|
||||||
|
|||||||
Reference in New Issue
Block a user