From eee82340b135e7ad45f24ab9ed39c790884c803b Mon Sep 17 00:00:00 2001 From: Pichinov-Jose Date: Fri, 11 Sep 2026 14:20:48 +0200 Subject: [PATCH 1/2] FIX ispaid must reflect the real payment status, not the delivery workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ispaid flag was computed as "order is validated OR delivered", which says nothing about payment. Any shop that ships before payment (B2B wire transfers) had its delivered-but-unpaid orders announced as paid — and on the Dolibarr side this flag drives Invoice::setPaid(), so unpaid invoices were being classified as paid with zero payment attached. WooCommerce already knows the truth: $order->is_paid() is driven by wc_get_is_paid_statuses(), which custom status plugins feed correctly. Co-Authored-By: Claude Fable 5 --- src/Objects/Order/StatusFlagsTrait.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Objects/Order/StatusFlagsTrait.php b/src/Objects/Order/StatusFlagsTrait.php index 7c68d98..cc31b66 100644 --- a/src/Objects/Order/StatusFlagsTrait.php +++ b/src/Objects/Order/StatusFlagsTrait.php @@ -140,9 +140,7 @@ protected function getStatusFlagsFields(string $key, string $fieldName): void break; case 'ispaid': - $this->out[$fieldName] = Status::isValidated($this->getSplashOrderStatus()) - || Status::isDelivered($this->getSplashOrderStatus()) - ; + $this->out[$fieldName] = $this->object->is_paid(); break; default: From 7f7c86853020c2daec9f295d5a2c0c4f992389e3 Mon Sep 17 00:00:00 2001 From: Pichinov-Jose Date: Sat, 12 Sep 2026 18:31:14 +0200 Subject: [PATCH 2/2] Combine both WooCommerce signals: is_paid() alone misreads refunds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit is_paid() reads the CURRENT status, and wc_get_is_paid_statuses() excludes 'refunded'. A refunded order is therefore announced as never paid — true of the order, false of the invoice, since the reversal is a credit note and the original invoice stays paid. get_date_paid() cannot stand alone either: WooCommerce stamps it on every transition into a paid status, even when no money moved. Measured on a 3 998-order shop — wrong announcements: 37 with the original workflow heuristic, 136 with is_paid() alone, 87 with get_date_paid() alone (81 completed orders carry no payment date), 0 with both plus the status. Co-Authored-By: Claude Opus 5 --- src/Objects/Order/StatusFlagsTrait.php | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Objects/Order/StatusFlagsTrait.php b/src/Objects/Order/StatusFlagsTrait.php index cc31b66..8c8514d 100644 --- a/src/Objects/Order/StatusFlagsTrait.php +++ b/src/Objects/Order/StatusFlagsTrait.php @@ -140,7 +140,31 @@ protected function getStatusFlagsFields(string $key, string $fieldName): void break; case 'ispaid': - $this->out[$fieldName] = $this->object->is_paid(); + //====================================================================// + // An invoice is settled when money was received for it, and a + // refund does not unsettle it: the reversal is a credit note, + // a separate document, and the original invoice stays paid. + // + // Neither WooCommerce method answers that on its own. + // is_paid() reads the CURRENT status, so it reports a refunded + // order as unpaid — true of the order, false of the invoice. + // get_date_paid() reads the past, but WooCommerce stamps it on + // every status transition into a paid status, even when no + // money moved: zero-value orders, and orders switched to + // processing and cancelled the same day, all carry one. + // + // So ask both, and let the status arbitrate. A cancelled or + // failed order never produced a receivable to settle; anything + // else that was once paid still has an invoice to match. + $this->out[$fieldName] = $this->object->is_paid() + || ( + (bool) $this->object->get_date_paid() + && !in_array( + $this->object->get_status(), + array('cancelled', 'failed', 'checkout-draft', 'trash'), + true + ) + ); break; default: