From 3c442eb28b187fe0d5223f36142cd71a17f23635 Mon Sep 17 00:00:00 2001 From: Roberto Guagliardo Date: Fri, 9 Jan 2026 19:31:53 +0000 Subject: [PATCH] feat: Add order item stock source tracking and styling for admin interface --- assets/css/order-admin.css | 41 ++++ .../class-siti-stock-inventory-manager.php | 182 +++++++++++++++++- siti-stock-plugin.php | 4 +- 3 files changed, 217 insertions(+), 10 deletions(-) create mode 100644 assets/css/order-admin.css diff --git a/assets/css/order-admin.css b/assets/css/order-admin.css new file mode 100644 index 0000000..6e7c499 --- /dev/null +++ b/assets/css/order-admin.css @@ -0,0 +1,41 @@ +.siti-stock-order-item-sources { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; + margin: 4px 0 6px; + font-size: 12px; +} + +.siti-stock-order-item-sources__label { + font-weight: 600; + color: #1d2327; + margin-right: 4px; +} + +.siti-stock-order-item-sources__badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 999px; + background: #f1f5f9; + color: #1d2327; +} + +.siti-stock-order-item-sources__badge .dashicons { + font-size: 13px; + width: 13px; + height: 13px; + line-height: 13px; +} + +.siti-stock-order-item-sources__badge.is-local { + background: #e6f8ef; + color: #17633d; +} + +.siti-stock-order-item-sources__badge.is-external { + background: #f1ecff; + color: #3e2a85; +} diff --git a/includes/class-siti-stock-inventory-manager.php b/includes/class-siti-stock-inventory-manager.php index 9791ccb..2efb1b4 100644 --- a/includes/class-siti-stock-inventory-manager.php +++ b/includes/class-siti-stock-inventory-manager.php @@ -14,11 +14,17 @@ class Siti_Stock_Inventory_Manager { */ private $external_stock_key; + /** + * @var string + */ + private $order_item_stock_source_key; + /** * @param string $meta_key Meta key used to store external stock. */ public function __construct( $meta_key ) { - $this->external_stock_key = $meta_key; + $this->external_stock_key = $meta_key; + $this->order_item_stock_source_key = '_siti_stock_source_breakdown'; } /** @@ -34,6 +40,8 @@ class Siti_Stock_Inventory_Manager { add_filter( 'woocommerce_product_get_stock_status', array( $this, 'filter_stock_status_with_external' ), 10, 2 ); add_filter( 'woocommerce_product_variation_get_stock_status', array( $this, 'filter_stock_status_with_external' ), 10, 2 ); add_action( 'woocommerce_reduce_order_item_stock', array( $this, 'rebalance_stock_after_order_reduction' ), 20, 3 ); + add_action( 'woocommerce_after_order_itemmeta', array( $this, 'render_order_item_stock_source' ), 10, 3 ); + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_order_admin_assets' ) ); } /** @@ -216,6 +224,71 @@ class Siti_Stock_Inventory_Manager { return max( 0, (int) $base_stock ) + $external_stock; } + /** + * Determine the quantity reduced for the order item. + * + * @param WC_Order_Item_Product $item Order line item. + * @param array $change Change context from WooCommerce. + * @return int + */ + private function determine_reduced_quantity( $item, $change ) { + if ( ! is_array( $change ) ) { + $change = array( + 'quantity' => $change, + ); + } + + $from = isset( $change['from'] ) ? $change['from'] : null; + $to = isset( $change['to'] ) ? $change['to'] : null; + + if ( null !== $from && null !== $to ) { + $qty = $from - $to; + } elseif ( isset( $change['quantity'] ) ) { + $qty = $change['quantity']; + } elseif ( $item instanceof WC_Order_Item_Product ) { + $qty = $item->get_quantity(); + } else { + $qty = 0; + } + + $qty = function_exists( 'wc_stock_amount' ) ? wc_stock_amount( $qty ) : (int) $qty; + + return max( 0, (int) $qty ); + } + + /** + * Record the stock source split for an order item. + * + * @param WC_Order_Item_Product $item Order line item. + * @param WC_Product $stock_holder Product that holds stock. + * @param int $quantity Total reduced quantity. + */ + private function record_order_item_stock_source_breakdown( $item, $stock_holder, $quantity ) { + $previous_local = (int) $stock_holder->get_stock_quantity( 'edit' ) + $quantity; + $local_available = max( 0, $previous_local ); + $external_available = (int) $this->get_external_stock( $stock_holder ); + $from_local = min( $quantity, $local_available ); + $remaining = max( 0, $quantity - $from_local ); + $from_external = min( $remaining, max( 0, $external_available ) ); + + $existing = $item->get_meta( $this->order_item_stock_source_key, true ); + $existing = is_array( $existing ) + ? array( + 'local' => isset( $existing['local'] ) ? (int) $existing['local'] : 0, + 'external' => isset( $existing['external'] ) ? (int) $existing['external'] : 0, + ) + : array( + 'local' => 0, + 'external' => 0, + ); + + $existing['local'] += $from_local; + $existing['external'] += $from_external; + + $item->update_meta_data( $this->order_item_stock_source_key, $existing ); + $item->save(); + } + /** * Ensure local stock only dips below zero when the external stock is depleted. * @@ -224,7 +297,7 @@ class Siti_Stock_Inventory_Manager { * @param WC_Order $order Order instance (unused). */ public function rebalance_stock_after_order_reduction( $item, $change, $order ) { - unset( $change, $order ); + unset( $order ); if ( ! $item instanceof WC_Order_Item_Product ) { return; @@ -243,17 +316,23 @@ class Siti_Stock_Inventory_Manager { return; } - $current_local = (int) $stock_holder->get_stock_quantity( 'edit' ); - $external_stock = $this->get_external_stock( $stock_holder ); + $quantity = $this->determine_reduced_quantity( $item, $change ); + + if ( $quantity > 0 ) { + $this->record_order_item_stock_source_breakdown( $item, $stock_holder, $quantity ); + } + + $current_local = (int) $stock_holder->get_stock_quantity( 'edit' ); + $external_stock = $this->get_external_stock( $stock_holder ); if ( $current_local >= 0 || $external_stock <= 0 ) { return; } - $shortage = min( abs( $current_local ), $external_stock ); - $new_local = $current_local + $shortage; - $new_external = $external_stock - $shortage; - $needs_save = false; + $shortage = min( abs( $current_local ), $external_stock ); + $new_local = $current_local + $shortage; + $new_external = $external_stock - $shortage; + $needs_save = false; if ( $new_local !== $current_local ) { $stock_holder->set_stock_quantity( $new_local ); @@ -269,4 +348,91 @@ class Siti_Stock_Inventory_Manager { $stock_holder->save(); } } + + /** + * Display the recorded stock source on the admin order screen. + * + * @param int $item_id Order item ID. + * @param WC_Order_Item $item Order item instance. + * @param WC_Product|false $product Product (unused). + */ + public function render_order_item_stock_source( $item_id, $item, $product ) { + unset( $item_id, $product ); + + if ( ! is_admin() || ! $item instanceof WC_Order_Item_Product ) { + return; + } + + $data = $item->get_meta( $this->order_item_stock_source_key, true ); + + if ( ! is_array( $data ) ) { + return; + } + + $local = isset( $data['local'] ) ? max( 0, (int) $data['local'] ) : 0; + $external = isset( $data['external'] ) ? max( 0, (int) $data['external'] ) : 0; + + if ( $local <= 0 && $external <= 0 ) { + return; + } + + echo '
'; + echo '' . esc_html__( 'Voorraadbron', 'siti-stock-plugin' ) . ':'; + + if ( $local > 0 ) { + $local_label = sprintf( + /* translators: %d: quantity fulfilled from regular stock. */ + _n( '%d uit reguliere voorraad', '%d uit reguliere voorraad', $local, 'siti-stock-plugin' ), + $local + ); + + printf( + '%s', + esc_html( $local_label ) + ); + } + + if ( $external > 0 ) { + $external_label = sprintf( + /* translators: %d: quantity fulfilled from external stock. */ + _n( '%d uit externe voorraad', '%d uit externe voorraad', $external, 'siti-stock-plugin' ), + $external + ); + + printf( + '%s', + esc_html( $external_label ) + ); + } + + echo '
'; + } + + /** + * Enqueue lightweight styling on the order edit screen. + * + * @param string $hook Current admin hook. + */ + public function enqueue_order_admin_assets( $hook ) { + if ( 'post.php' !== $hook && 'post-new.php' !== $hook ) { + return; + } + + if ( ! function_exists( 'get_current_screen' ) ) { + return; + } + + $screen = get_current_screen(); + + if ( ! $screen || 'shop_order' !== $screen->post_type ) { + return; + } + + wp_enqueue_style( + 'siti-stock-order-admin', + plugins_url( 'assets/css/order-admin.css', SITI_STOCK_PLUGIN_FILE ), + array(), + SITI_STOCK_PLUGIN_VERSION + ); + } } diff --git a/siti-stock-plugin.php b/siti-stock-plugin.php index 739c894..a22747b 100644 --- a/siti-stock-plugin.php +++ b/siti-stock-plugin.php @@ -3,7 +3,7 @@ * Plugin Name: Siti Stock Plugin * Plugin URI: https://github.com/SitiWeb/siti-stock-plugin * Description: Synchroniseert WooCommerce voorraad met het externe Siti voorraadplatform. - * Version: 1.1.1 + * Version: 1.2.0 * Author: Siti Web * Author URI: https://www.siti.nl * Requires PHP: 8.1 @@ -16,7 +16,7 @@ if ( ! defined( 'ABSPATH' ) ) { exit; } -define( 'SITI_STOCK_PLUGIN_VERSION', '0.1.0' ); +define( 'SITI_STOCK_PLUGIN_VERSION', '1.2.0' ); define( 'SITI_STOCK_PLUGIN_FILE', __FILE__ ); define( 'SITI_STOCK_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );