PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.17
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.17
1.10.25 1.10.24 1.10.23 1.10.22 1.10.21 1.10.20 1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 All 169 releases
← All changes | includes/Services/Receipt_Data_Builder.php +236 -95 1.10.24 → 1.10.17 View file →
@@ -18,9 +18,10 @@
18 18 /**
19 19 * Build a canonical receipt payload.
20 20 *
21 21 * @param WC_Abstract_Order $order Receipt order.
22 - * @param string $mode Receipt mode ('live', 'fiscal', 'preview'), passed unchanged to the receipt data filter.
22 + * @param string $mode Reserved for caller compatibility; the receipt mode is
23 + * carried in the request, not the payload.
23 24 * @param object|null $pos_store POS store object. Falls back to order meta or default.
24 25 *
25 26 * @return array
26 27 */
@@ -115,9 +116,9 @@
115 116 $customer_name = /* translators: Short WCPOS UI label; keep concise. */ __( 'Guest', 'woocommerce-pos' );
116 117 }
117 118
118 119 $tax_ids = ( new Tax_Id_Reader() )->read_for_order( $order );
119 - $tax_ids = Receipt_Sections::label_tax_ids( $tax_ids, 'customer', $presentation_hints['locale'] ?? '' );
120 + $tax_ids = self::with_customer_tax_id_labels( $tax_ids, $presentation_hints['locale'] ?? '' );
120 121
121 122 $customer = array(
122 123 'id' => $customer_id ? $customer_id : null,
123 124 'name' => $customer_name,
@@ -162,8 +163,9 @@
162 163 : 0.0;
163 164 $price_convenience = $this->get_line_price_convenience_fields(
164 165 $item,
165 166 $order,
167 + $display_incl,
166 168 $qty,
167 169 $unit_subtotal_incl,
168 170 $unit_subtotal_excl,
169 171 $line_subtotal_incl,
@@ -177,34 +179,29 @@
177 179 'sku' => $item->get_product() ? $item->get_product()->get_sku() : '',
178 180 'name' => $item->get_name(),
179 181 'qty' => $qty,
180 182 'qty_refunded' => $qty_refunded,
181 - 'unit_subtotal' => array(
182 - 'incl' => $unit_subtotal_incl,
183 - 'excl' => $unit_subtotal_excl,
184 - ),
185 - 'unit_price' => array(
186 - 'incl' => $unit_price_incl,
187 - 'excl' => $unit_price_excl,
188 - ),
189 - 'line_subtotal' => array(
190 - 'incl' => $line_subtotal_incl,
191 - 'excl' => $line_subtotal_excl,
192 - ),
193 - 'discounts' => array(
194 - 'incl' => $discounts_incl,
195 - 'excl' => $discounts_excl,
196 - ),
197 - 'line_total' => array(
198 - 'incl' => $line_total_incl,
199 - 'excl' => $line_total_excl,
200 - ),
183 + 'unit_subtotal' => $display_incl ? $unit_subtotal_incl : $unit_subtotal_excl,
184 + 'unit_subtotal_incl' => $unit_subtotal_incl,
185 + 'unit_subtotal_excl' => $unit_subtotal_excl,
186 + 'unit_price' => $display_incl ? $unit_price_incl : $unit_price_excl,
187 + 'unit_price_incl' => $unit_price_incl,
188 + 'unit_price_excl' => $unit_price_excl,
189 + 'line_subtotal' => $display_incl ? $line_subtotal_incl : $line_subtotal_excl,
190 + 'line_subtotal_incl' => $line_subtotal_incl,
191 + 'line_subtotal_excl' => $line_subtotal_excl,
192 + 'discounts' => $display_incl ? $discounts_incl : $discounts_excl,
193 + 'discounts_incl' => $discounts_incl,
194 + 'discounts_excl' => $discounts_excl,
195 + 'line_total' => $display_incl ? $line_total_incl : $line_total_excl,
196 + 'line_total_incl' => $line_total_incl,
197 + 'line_total_excl' => $line_total_excl,
201 198 'total_refunded' => $total_refunded,
202 199 'taxes' => $this->get_line_taxes( $item ),
203 200 'meta' => $this->get_item_meta_pairs( $item ),
204 201 'attributes' => $this->get_product_attribute_pairs( $item ),
205 202 );
206 - $lines[] = Receipt_Sections::line( array_merge( $line, $price_convenience ), $display_incl );
203 + $lines[] = array_merge( $line, $price_convenience );
207 204 }
208 205
209 206 $shipping = array();
210 207 foreach ( $order->get_items( 'shipping' ) as $shipping_item ) {
@@ -213,18 +210,16 @@
213 210 }
214 211 $ship_total_excl = (float) $shipping_item->get_total();
215 212 $ship_total_tax = (float) $shipping_item->get_total_tax();
216 213 $ship_total_incl = $ship_total_excl + $ship_total_tax;
217 - $shipping[] = Receipt_Sections::shipping(
218 - $shipping_item->get_name(),
219 - (string) $shipping_item->get_method_id(),
220 - array(
221 - 'incl' => $ship_total_incl,
222 - 'excl' => $ship_total_excl,
223 - ),
224 - $this->get_item_taxes( $shipping_item ),
225 - $this->get_item_meta_pairs( $shipping_item ),
226 - $display_incl
214 + $shipping[] = array(
215 + 'label' => $shipping_item->get_name(),
216 + 'method_id' => (string) $shipping_item->get_method_id(),
217 + 'total' => $display_incl ? $ship_total_incl : $ship_total_excl,
218 + 'total_incl' => $ship_total_incl,
219 + 'total_excl' => $ship_total_excl,
220 + 'taxes' => $this->get_item_taxes( $shipping_item ),
221 + 'meta' => $this->get_item_meta_pairs( $shipping_item ),
227 222 );
228 223 }
229 224
230 225 $fees = array();
@@ -231,17 +226,15 @@
231 226 foreach ( $order->get_fees() as $fee ) {
232 227 $fee_total_excl = (float) $fee->get_total();
233 228 $fee_total_tax = (float) $fee->get_total_tax();
234 229 $fee_total_incl = $fee_total_excl + $fee_total_tax;
235 - $fees[] = Receipt_Sections::fee(
236 - $fee->get_name(),
237 - array(
238 - 'incl' => $fee_total_incl,
239 - 'excl' => $fee_total_excl,
240 - ),
241 - $this->get_item_taxes( $fee ),
242 - $this->get_item_meta_pairs( $fee ),
243 - $display_incl
230 + $fees[] = array(
231 + 'label' => $fee->get_name(),
232 + 'total' => $display_incl ? $fee_total_incl : $fee_total_excl,
233 + 'total_incl' => $fee_total_incl,
234 + 'total_excl' => $fee_total_excl,
235 + 'taxes' => $this->get_item_taxes( $fee ),
236 + 'meta' => $this->get_item_meta_pairs( $fee ),
244 237 );
245 238 }
246 239
247 240 $discounts = array();
@@ -252,17 +245,15 @@
252 245 $coupon = $this->get_order_coupon( $coupon_item );
253 246 $coupon_excl = (float) $coupon_item->get_discount();
254 247 $coupon_tax = (float) $coupon_item->get_discount_tax();
255 248 $coupon_incl = $coupon_excl + $coupon_tax;
256 - $discounts[] = Receipt_Sections::discount(
257 - $this->get_coupon_label( $coupon_item, $coupon ),
258 - $coupon_item->get_code(),
259 - $coupon ? (string) $coupon->get_discount_type() : '',
260 - array(
261 - 'incl' => $coupon_incl,
262 - 'excl' => $coupon_excl,
263 - ),
264 - $display_incl
249 + $discounts[] = array(
250 + 'label' => $this->get_coupon_label( $coupon_item, $coupon ),
251 + 'code' => $coupon_item->get_code(),
252 + 'discount_type' => $coupon ? (string) $coupon->get_discount_type() : '',
253 + 'total' => $display_incl ? $coupon_incl : $coupon_excl,
254 + 'total_incl' => $coupon_incl,
255 + 'total_excl' => $coupon_excl,
265 256 );
266 257 }
267 258
268 259 $discount_total_excl = (float) $order->get_discount_total();
@@ -268,37 +259,116 @@
268 259 $discount_total_excl = (float) $order->get_discount_total();
269 260 $discount_total_tax = (float) $order->get_discount_tax();
270 261 $discount_total_incl = $discount_total_excl + $discount_total_tax;
271 262
263 + // Legacy POS lines already include regular-to-selling savings in WooCommerce's
264 + // discount total. Add only current-shape savings to total_saved to avoid overlap.
265 + $sale_savings_totals = array(
266 + 'incl' => 0.0,
267 + 'excl' => 0.0,
268 + );
269 + $additional_savings = array(
270 + 'incl' => 0.0,
271 + 'excl' => 0.0,
272 + );
273 + $savings_complete = array(
274 + 'incl' => true,
275 + 'excl' => true,
276 + );
277 + $price_precision = wc_get_price_decimals();
278 + foreach ( $lines as $line ) {
279 + foreach ( array( 'incl', 'excl' ) as $basis ) {
280 + $key = 'line_savings_' . $basis;
281 + if ( ! isset( $line[ $key ] ) || ! is_numeric( $line[ $key ] ) ) {
282 + $savings_complete[ $basis ] = false;
283 + continue;
284 + }
285 +
286 + $line_savings = (float) $line[ $key ];
287 + $sale_savings_totals[ $basis ] += $line_savings;
288 + if ( empty( $line['savings_in_discounts'] ) ) {
289 + $subtotal_key = 'line_subtotal_' . $basis;
290 + $selling_key = 'line_selling_total_' . $basis;
291 + if (
292 + $line_savings > 0.0
293 + && (
294 + ! isset( $line[ $subtotal_key ], $line[ $selling_key ] )
295 + || round( abs( (float) $line[ $subtotal_key ] - (float) $line[ $selling_key ] ), $price_precision ) > 0.0
296 + )
297 + ) {
298 + $savings_complete[ $basis ] = false;
299 + continue;
300 + }
301 + $additional_savings[ $basis ] += $line_savings;
302 + }
303 + }
304 + }
305 +
306 + $total_saved = array(
307 + 'incl' => $savings_complete['incl'] ? $discount_total_incl + $additional_savings['incl'] : null,
308 + 'excl' => $savings_complete['excl'] ? $discount_total_excl + $additional_savings['excl'] : null,
309 + );
310 + foreach ( array( 'incl', 'excl' ) as $basis ) {
311 + if ( ! $savings_complete[ $basis ] ) {
312 + $sale_savings_totals[ $basis ] = null;
313 + }
314 + }
315 + $display_basis = $display_incl ? 'incl' : 'excl';
316 +
317 + $subtotal_excl = array_sum( array_column( $lines, 'line_subtotal_excl' ) );
318 + $subtotal_incl = array_sum( array_column( $lines, 'line_subtotal_incl' ) );
319 +
320 + // Item count summaries — useful for packing slips and kitchen tickets
321 + // where Mustache can't sum/count an array at render time.
322 + $total_qty = (float) array_sum( array_column( $lines, 'qty' ) );
323 + $line_count = \count( $lines );
324 +
272 325 $tax_total = (float) $order->get_total_tax();
273 - $total = (float) $order->get_total();
274 - $refund_total = method_exists( $order, 'get_total_refunded' )
326 + $total = (float) $order->get_total();
327 +
328 + $total_excl = $total - $tax_total;
329 + $refund_total = method_exists( $order, 'get_total_refunded' )
275 330 ? abs( (float) $order->get_total_refunded() )
276 331 : 0.0;
277 - $totals = Receipt_Sections::totals(
278 - $lines,
279 - array(
280 - 'discount_total' => array(
281 - 'incl' => $discount_total_incl,
282 - 'excl' => $discount_total_excl,
283 - ),
284 - 'tax_total' => $tax_total,
285 - 'total' => $total,
286 - 'paid_total' => $total,
287 - 'change_total' => (float) $order->get_meta( '_pos_cash_change' ),
288 - 'refund_total' => $refund_total,
289 - ),
290 - $display_incl
332 + // Templates render the customer-facing balance after a partial refund.
333 + // Stays at 0 when nothing was refunded so detailed-receipt's section
334 + // guard `{{#totals.net_total}}…{{/totals.net_total}}` collapses.
335 + $net_total = $refund_total > 0 ? max( 0.0, $total - $refund_total ) : 0.0;
336 +
337 + $totals = array(
338 + 'subtotal' => $display_incl ? $subtotal_incl : $subtotal_excl,
339 + 'subtotal_incl' => $subtotal_incl,
340 + 'subtotal_excl' => $subtotal_excl,
341 + 'discount_total' => $display_incl ? $discount_total_incl : $discount_total_excl,
342 + 'discount_total_incl' => $discount_total_incl,
343 + 'discount_total_excl' => $discount_total_excl,
344 + 'sale_savings_total' => $sale_savings_totals[ $display_basis ],
345 + 'sale_savings_total_incl' => $sale_savings_totals['incl'],
346 + 'sale_savings_total_excl' => $sale_savings_totals['excl'],
347 + 'total_saved' => $total_saved[ $display_basis ],
348 + 'total_saved_incl' => $total_saved['incl'],
349 + 'total_saved_excl' => $total_saved['excl'],
350 + 'total_saved_complete' => $savings_complete[ $display_basis ],
351 + 'tax_total' => $tax_total,
352 + 'total' => $display_incl ? $total : $total_excl,
353 + 'total_incl' => $total,
354 + 'total_excl' => $total_excl,
355 + 'paid_total' => $total,
356 + 'change_total' => (float) $order->get_meta( '_pos_cash_change' ),
357 + 'refund_total' => $refund_total,
358 + 'net_total' => $net_total,
359 + 'total_qty' => $total_qty,
360 + 'line_count' => $line_count,
291 361 );
292 362
293 363 $payments = array(
294 - Receipt_Sections::payment(
295 - $order->get_payment_method(),
296 - $order->get_payment_method_title(),
297 - $total,
298 - (string) $order->get_transaction_id(),
299 - (float) $order->get_meta( '_pos_cash_amount_tendered' ),
300 - (float) $order->get_meta( '_pos_cash_change' )
364 + array(
365 + 'method_id' => $order->get_payment_method(),
366 + 'method_title' => $order->get_payment_method_title(),
367 + 'amount' => $total,
368 + 'transaction_id' => (string) $order->get_transaction_id(),
369 + 'tendered' => (float) $order->get_meta( '_pos_cash_amount_tendered' ),
370 + 'change' => (float) $order->get_meta( '_pos_cash_change' ),
301 371 ),
302 372 );
303 373
304 374 $tax_summary = $this->get_tax_summary( $order );
@@ -348,15 +418,11 @@
348 418 * templates. Extensions can add their own keys to any section (for
349 419 * example a flag on a `discounts[]` row) or adjust labels. Keys defined
350 420 * by Receipt_Data_Schema should keep their documented types.
351 421 *
352 - * Sample previews for the template editor and gallery also run through this filter,
353 - * with mode `preview` and an unsaved order whose id is 0; a plugin that needs a
354 - * persisted order should return `$data` unchanged when `$order->get_id()` is 0.
355 - *
356 422 * @param array $data Receipt data (see Receipt_Data_Schema).
357 423 * @param WC_Abstract_Order $order Order the receipt is for.
358 - * @param string $mode Receipt mode: 'live', 'fiscal' or 'preview', passed unchanged.
424 + * @param string $mode Receipt mode: 'live' or 'fiscal'.
359 425 *
360 426 * @since 1.10.8
361 427 *
362 428 * @hook woocommerce_pos_receipt_data
@@ -434,8 +500,9 @@
434 500 * Derive recorded prices and savings without changing WooCommerce discounts.
435 501 *
436 502 * @param \WC_Order_Item_Product $item Order item.
437 503 * @param WC_Abstract_Order $order Receipt order.
504 + * @param bool $display_incl Whether generic values include tax.
438 505 * @param float $qty Item quantity.
439 506 * @param float $unit_subtotal_incl Stored unit subtotal including tax.
440 507 * @param float $unit_subtotal_excl Stored unit subtotal excluding tax.
441 508 * @param float $subtotal_incl Stored line subtotal including tax.
@@ -442,13 +509,14 @@
442 509 * @param float $subtotal_excl Stored line subtotal excluding tax.
443 510 * @param float $total_incl Stored line total including tax.
444 511 * @param float $total_excl Stored line total excluding tax.
445 512 *
446 - * @return array<string,array|bool>
513 + * @return array<string,float|bool|null>
447 514 */
448 515 private function get_line_price_convenience_fields(
449 516 \WC_Order_Item_Product $item,
450 517 WC_Abstract_Order $order,
518 + bool $display_incl,
451 519 float $qty,
452 520 float $unit_subtotal_incl,
453 521 float $unit_subtotal_excl,
454 522 float $subtotal_incl,
@@ -540,16 +608,32 @@
540 608 && $stored_discount >= $recorded_savings;
541 609 break;
542 610 }
543 611
612 + $select = static function ( array $values ) use ( $display_incl ): ?float {
613 + return $display_incl ? $values['incl'] : $values['excl'];
614 + };
615 +
544 616 return array(
545 - 'regular_price' => $regular,
546 - 'selling_price' => $selling,
547 - 'unit_savings' => $unit_savings,
548 - 'line_regular_total' => $line_regular,
549 - 'line_selling_total' => $line_selling,
550 - 'line_savings' => $line_savings,
551 - 'savings_in_discounts' => $savings_in_discounts,
617 + 'regular_price' => $select( $regular ),
618 + 'regular_price_incl' => $regular['incl'],
619 + 'regular_price_excl' => $regular['excl'],
620 + 'selling_price' => $select( $selling ),
621 + 'selling_price_incl' => $selling['incl'],
622 + 'selling_price_excl' => $selling['excl'],
623 + 'unit_savings' => $select( $unit_savings ),
624 + 'unit_savings_incl' => $unit_savings['incl'],
625 + 'unit_savings_excl' => $unit_savings['excl'],
626 + 'line_regular_total' => $select( $line_regular ),
627 + 'line_regular_total_incl' => $line_regular['incl'],
628 + 'line_regular_total_excl' => $line_regular['excl'],
629 + 'line_selling_total' => $select( $line_selling ),
630 + 'line_selling_total_incl' => $line_selling['incl'],
631 + 'line_selling_total_excl' => $line_selling['excl'],
632 + 'line_savings' => $select( $line_savings ),
633 + 'line_savings_incl' => $line_savings['incl'],
634 + 'line_savings_excl' => $line_savings['excl'],
635 + 'savings_in_discounts' => $savings_in_discounts,
552 636 );
553 637 }
554 638
555 639 /**
@@ -615,16 +699,18 @@
615 699 $tax_amount = (float) $tax_item->get_tax_total() + (float) $tax_item->get_shipping_tax_total();
616 700 $rate = (float) $tax_item->get_rate_percent();
617 701 $rate_id = (string) $tax_item->get_rate_id();
618 702 $taxable_excl = $taxable_bases[ $rate_id ] ?? null;
703 + $taxable_incl = null !== $taxable_excl ? $taxable_excl + $tax_amount : null;
619 704
620 - $summary[] = Receipt_Sections::tax_summary_entry(
621 - $rate_id,
622 - $rate,
623 - $tax_item->get_label( $order ),
624 - method_exists( $tax_item, 'is_compound' ) ? (bool) $tax_item->is_compound() : false,
625 - $taxable_excl,
626 - $tax_amount
705 + $summary[] = array(
706 + 'code' => $rate_id,
707 + 'rate' => $rate > 0 ? $rate : null,
708 + 'label' => $tax_item->get_label( $order ),
709 + 'compound' => method_exists( $tax_item, 'is_compound' ) ? (bool) $tax_item->is_compound() : false,
710 + 'taxable_amount_excl' => $taxable_excl,
711 + 'tax_amount' => $tax_amount,
712 + 'taxable_amount_incl' => $taxable_incl,
627 713 );
628 714 }
629 715
630 716 return $summary;
@@ -813,9 +899,22 @@
813 899 return $pairs;
814 900 }
815 901
816 902 if ( $product instanceof \WC_Product_Variation ) {
817 - return Receipt_Sections::variation_attribute_pairs( $product );
903 + foreach ( $product->get_variation_attributes() as $attribute_key => $attribute_value ) {
904 + if ( '' === (string) $attribute_value ) {
905 + continue;
906 + }
907 +
908 + $taxonomy = preg_replace( '/^attribute_/', '', (string) $attribute_key );
909 + $value = $product->get_attribute( $taxonomy );
910 + $pairs[] = array(
911 + 'key' => wp_strip_all_tags( wc_attribute_label( $taxonomy, $product ) ),
912 + 'value' => wp_strip_all_tags( '' !== $value ? $value : (string) $attribute_value ),
913 + );
914 + }
915 +
916 + return $pairs;
818 917 }
819 918
820 919 foreach ( $product->get_attributes() as $attribute ) {
821 920 if ( ! $attribute instanceof \WC_Product_Attribute || ! $attribute->get_visible() ) {
@@ -980,6 +1079,48 @@
980 1079 );
981 1080 }
982 1081
983 1082 return $refunds;
1083 + }
1084 +
1085 +
1086 + /**
1087 + * Ensure customer tax IDs include display labels for logicless templates.
1088 + *
1089 + * @param array<int,array<string,mixed>> $tax_ids Customer tax IDs.
1090 + * @param string $locale Receipt locale.
1091 + * @return array<int,array<string,mixed>>
1092 + */
1093 + private static function with_customer_tax_id_labels( array $tax_ids, string $locale = '' ): array {
1094 + return self::with_tax_id_labels( $tax_ids, 'customer', $locale );
1095 + }
1096 +
1097 + /**
1098 + * Resolve a display label for each tax-ID entry. Precedence: explicit
1099 + * `label` → `<scope>_tax_id_label_<type>` i18n key → scope-specific
1100 + * `_other` fallback.
1101 + *
1102 + * @param array<int,array<string,mixed>> $tax_ids Tax IDs.
1103 + * @param string $scope "store" or "customer".
1104 + * @param string $locale Receipt locale.
1105 + * @return array<int,array<string,mixed>>
1106 + */
1107 + private static function with_tax_id_labels( array $tax_ids, string $scope, string $locale = '' ): array {
1108 + $labels = Receipt_I18n_Labels::get_labels( $locale );
1109 + $prefix = $scope . '_tax_id_label_';
1110 +
1111 + return array_map(
1112 + static function ( array $tax_id ) use ( $labels, $prefix ): array {
1113 + if ( ! empty( $tax_id['label'] ) ) {
1114 + return $tax_id;
1115 + }
1116 +
1117 + $type = isset( $tax_id['type'] ) ? (string) $tax_id['type'] : 'other';
1118 + $key = $prefix . $type;
1119 + $tax_id['label'] = $labels[ $key ] ?? $labels[ $prefix . 'other' ];
1120 +
1121 + return $tax_id;
1122 + },
1123 + $tax_ids
1124 + );
984 1125 }
985 1126 }