Changeset 3790681
- Timestamp:
- 11/08/2026 08:24:24 AM (5 hours ago)
- Location:
- linet-erp-woocommerce-integration/trunk
- Files:
-
- 4 edited
-
classes/class-wc-li-invoice-manager.php (modified) (3 diffs)
-
classes/class-wc-li-settings.php (modified) (1 diff)
-
readme.txt (modified) (2 diffs)
-
woocommerce-linet.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
linet-erp-woocommerce-integration/trunk/classes/class-wc-li-invoice-manager.php
r3773444 r3790681 8 8 { 9 9 10 // Document Timing setting values 11 const TIMING_IMMEDIATE = 'immediate'; 12 const TIMING_DELAYED = 'delayed'; 13 14 const DEFAULT_DELAY = 60; 15 16 // Action Scheduler hook and group for the delayed documents 17 const QUEUE_HOOK = 'linet_auto_send_invoice'; 18 const QUEUE_GROUP = 'linet'; 19 20 // A queued document still pending this long after its due time is reported as stuck 21 const STUCK_GRACE = 600; 22 23 // How far back failed queued documents are reported 24 const FAILED_LOOKBACK = WEEK_IN_SECONDS; 25 26 // Order meta "_linet_auto_doc_{status}" holds the doc id the queue created for that status 27 const AUTO_DOC_META = '_linet_auto_doc_'; 28 29 const NOTICE_TRANSIENT = 'wc_linet_queue_health'; 30 10 31 /** 11 32 * @var WC_LI_Settings … … 49 70 //var_dump($do_doc); 50 71 //var_dump("woocommerce_order_status_$skey"); 51 add_action("woocommerce_order_status_$skey", array($this, 'auto_send_invoice')); 52 } 72 add_action("woocommerce_order_status_$skey", array($this, 'on_order_status'), 10, 2); 73 } 74 } 75 76 // Always registered, so documents queued before the timing was switched back 77 // to immediate still get created 78 add_action(self::QUEUE_HOOK, array($this, 'run_queued_invoice'), 10, 3); 79 80 if (is_admin()) { 81 add_action('admin_notices', array($this, 'queue_health_notice')); 53 82 } 54 83 … … 285 314 286 315 /** 316 * The Document Timing setting: immediate (the original behaviour) or delayed. 317 * 318 * @return string 319 */ 320 public static function doc_timing() 321 { 322 $timing = get_option('wc_linet_doc_timing', self::TIMING_IMMEDIATE); 323 return self::TIMING_DELAYED === $timing ? self::TIMING_DELAYED : self::TIMING_IMMEDIATE; 324 } 325 326 /** 327 * The delay in seconds for delayed documents. 328 * 329 * @return int 330 */ 331 public static function doc_delay() 332 { 333 $delay = get_option('wc_linet_doc_delay', ''); 334 if ('' === $delay || !is_numeric($delay)) { 335 return self::DEFAULT_DELAY; 336 } 337 return max(0, (int) $delay); 338 } 339 340 /** 341 * True when delayed documents can actually be queued. 342 * 343 * @return bool 344 */ 345 public static function queue_available() 346 { 347 return function_exists('as_schedule_single_action') && function_exists('as_has_scheduled_action'); 348 } 349 350 /** 351 * Runs on every mapped woocommerce_order_status_{status} hook. 352 * 353 * Immediate timing behaves exactly like before. Delayed timing queues the 354 * document, so other plugins that set the status first and save their meta 355 * after it are done by the time the document is built. 356 * 357 * @param int $order_id 358 * @param WC_Order|null $order 359 * 360 * @return bool 361 */ 362 public function on_order_status($order_id, $order = null) 363 { 364 if (self::TIMING_DELAYED !== self::doc_timing() || !self::queue_available()) { 365 return $this->auto_send_invoice($order_id); 366 } 367 368 if (!$order instanceof WC_Order) { 369 $order = wc_get_order($order_id); 370 } 371 if (!$order) { 372 return false; 373 } 374 375 // The status is captured now: by the time the job runs the order may have moved on 376 // and the document type must be the one mapped to the status that triggered it. 377 // prev_doc lets the job notice a document created in the meantime (manually for example). 378 $args = array( 379 'order_id' => (int) $order->get_id(), 380 'status' => $order->get_status(), 381 'prev_doc' => (string) $order->get_meta('_linet_doc_id', true), 382 ); 383 384 if (as_has_scheduled_action(self::QUEUE_HOOK, $args, self::QUEUE_GROUP)) { 385 return false; 386 } 387 388 $run_at = time() + self::doc_delay(); 389 as_schedule_single_action($run_at, self::QUEUE_HOOK, $args, self::QUEUE_GROUP); 390 391 $order->add_order_note( 392 sprintf( 393 /* translators: 1: order status, 2: date and time */ 394 __('LINET: Doc. for status %1$s scheduled for %2$s', 'linet-erp-woocommerce-integration'), 395 $args['status'], 396 wp_date(get_option('date_format') . ' ' . get_option('time_format'), $run_at) 397 ) 398 ); 399 400 return true; 401 } 402 403 /** 404 * Action Scheduler callback for a delayed document. 405 * 406 * @param int $order_id 407 * @param string $status status that triggered the document 408 * @param string $prev_doc _linet_doc_id of the order when it was queued 409 * 410 * @throws Exception when Linet answered with an error, so the action shows as failed 411 */ 412 public function run_queued_invoice($order_id, $status, $prev_doc = '') 413 { 414 $order = wc_get_order($order_id); 415 if (!$order) { 416 return; 417 } 418 419 $status = sanitize_key($status); 420 $prev_doc = (string) $prev_doc; 421 422 // Guard: this status already got its document from the queue 423 if ('' !== (string) $order->get_meta(self::AUTO_DOC_META . $status, true)) { 424 $order->add_order_note(sprintf( 425 __('LINET: Skipped scheduled doc. for status %s, it was already created', 'linet-erp-woocommerce-integration'), 426 $status 427 )); 428 return; 429 } 430 431 // Guard: a document was created after this one was queued and not by the queue 432 // (the manual order action for example) 433 $current_doc = (string) $order->get_meta('_linet_doc_id', true); 434 if ('' !== $current_doc && $current_doc !== $prev_doc && !$this->is_queue_doc($order, $current_doc)) { 435 $order->add_order_note(sprintf( 436 __('LINET: Skipped scheduled doc. for status %1$s, doc. %2$s was created after it was scheduled', 'linet-erp-woocommerce-integration'), 437 $status, 438 $current_doc 439 )); 440 return; 441 } 442 443 $doctype = (int) get_option("wc_linet_sync_orders_wc-$status"); 444 if (0 === $doctype) { 445 $order->add_order_note(sprintf( 446 __('LINET: Skipped scheduled doc. for status %s, the status is no longer mapped to a doc. type', 'linet-erp-woocommerce-integration'), 447 $status 448 )); 449 return; 450 } 451 452 $result = $this->auto_send_invoice($order_id, $doctype); 453 454 // Reload: send_invoice saved its meta on its own copy of the order 455 $order = wc_get_order($order_id); 456 $new_doc = (string) $order->get_meta('_linet_doc_id', true); 457 458 if ('' !== $new_doc && $new_doc !== $current_doc) { 459 $order->update_meta_data(self::AUTO_DOC_META . $status, $new_doc); 460 $order->save(); 461 delete_transient(self::NOTICE_TRANSIENT); 462 return; 463 } 464 465 // send_invoice returns true after an error answer from Linet (it already mailed and 466 // noted it). Throwing marks the action failed so it shows up in the queue warning. 467 if (true === $result) { 468 throw new Exception(sprintf('Linet did not create a doc. for order %d (status %s), see the order notes', (int) $order_id, $status)); 469 } 470 } 471 472 /** 473 * True when the doc id was created by the queue for one of the order statuses. 474 * 475 * @param WC_Order $order 476 * @param string $doc_id 477 * 478 * @return bool 479 */ 480 private function is_queue_doc($order, $doc_id) 481 { 482 foreach (array_keys(wc_get_order_statuses()) as $key) { 483 $skey = str_replace('wc-', '', $key); 484 if ($doc_id === (string) $order->get_meta(self::AUTO_DOC_META . $skey, true)) { 485 return true; 486 } 487 } 488 return false; 489 } 490 491 /** 492 * Counts of stuck (pending well past their time) and recently failed queued documents. 493 * Cached for a few minutes, this runs on admin pages. 494 * 495 * @return array{stuck:int,failed:int} 496 */ 497 private static function queue_health() 498 { 499 $health = get_transient(self::NOTICE_TRANSIENT); 500 if (is_array($health)) { 501 return $health; 502 } 503 504 $query = array( 505 'hook' => self::QUEUE_HOOK, 506 'group' => self::QUEUE_GROUP, 507 'per_page' => 100, 508 ); 509 510 $health = array( 511 'stuck' => count(as_get_scheduled_actions($query + array( 512 'status' => 'pending', 513 'date' => time() - self::STUCK_GRACE, 514 'date_compare' => '<=', 515 ), 'ids')), 516 'failed' => count(as_get_scheduled_actions($query + array( 517 'status' => 'failed', 518 'modified' => time() - self::FAILED_LOOKBACK, 519 'modified_compare' => '>=', 520 ), 'ids')), 521 ); 522 523 set_transient(self::NOTICE_TRANSIENT, $health, 5 * MINUTE_IN_SECONDS); 524 525 return $health; 526 } 527 528 /** 529 * Admin warning when delayed documents are not getting created. 530 */ 531 public function queue_health_notice() 532 { 533 if (!current_user_can('manage_woocommerce')) { 534 return; 535 } 536 537 $delayed = self::TIMING_DELAYED === self::doc_timing(); 538 539 if (!function_exists('as_get_scheduled_actions')) { 540 if ($delayed) { 541 echo '<div class="notice notice-warning"><p>' . esc_html__('Linet: Document Timing is set to Delayed but the WooCommerce background queue (Action Scheduler) is not available, documents are created immediately.', 'linet-erp-woocommerce-integration') . '</p></div>'; 542 } 543 return; 544 } 545 546 $health = self::queue_health(); 547 if (0 === $health['stuck'] && 0 === $health['failed']) { 548 return; 549 } 550 551 $messages = array(); 552 553 if ($health['stuck'] > 0) { 554 $url = admin_url('admin.php?page=wc-status&tab=action-scheduler&status=pending&s=' . self::QUEUE_HOOK); 555 $messages[] = sprintf( 556 /* translators: 1: count, 2: minutes */ 557 esc_html__('%1$d Linet documents are waiting more than %2$d minutes past their scheduled time. The WooCommerce background queue is not running, check that WP-Cron (or a server cron) is working.', 'linet-erp-woocommerce-integration'), 558 $health['stuck'], 559 self::STUCK_GRACE / MINUTE_IN_SECONDS 560 ) . ' <a href="' . esc_url($url) . '">' . esc_html__('View pending documents', 'linet-erp-woocommerce-integration') . '</a>'; 561 } 562 563 if ($health['failed'] > 0) { 564 $url = admin_url('admin.php?page=wc-status&tab=action-scheduler&status=failed&s=' . self::QUEUE_HOOK); 565 $messages[] = sprintf( 566 /* translators: %d: count */ 567 esc_html__('%d scheduled Linet documents failed in the last 7 days. See the order notes, then use the "Send Doc. to Linet" order action if needed.', 'linet-erp-woocommerce-integration'), 568 $health['failed'] 569 ) . ' <a href="' . esc_url($url) . '">' . esc_html__('View failed documents', 'linet-erp-woocommerce-integration') . '</a>'; 570 } 571 572 echo '<div class="notice notice-warning"><p>' . implode('</p><p>', $messages) . '</p></div>'; 573 } 574 575 /** 287 576 * Get invoice by order 288 577 * -
linet-erp-woocommerce-integration/trunk/classes/class-wc-li-settings.php
r3773444 r3790681 214 214 ), 215 215 'description' => __('Send orders with a total of 0 (fully covered by a coupon or gift card) to Linet', 'linet-erp-woocommerce-integration'), 216 ), 217 218 'doc_timing' => array( 219 'title' => __('Document Timing', 'linet-erp-woocommerce-integration'), 220 'default' => WC_LI_Invoice_Manager::TIMING_IMMEDIATE, 221 'type' => 'select', 222 'options' => array( 223 WC_LI_Invoice_Manager::TIMING_IMMEDIATE => __('Immediately on status change', 'linet-erp-woocommerce-integration'), 224 WC_LI_Invoice_Manager::TIMING_DELAYED => __('Delayed (background queue)', 'linet-erp-woocommerce-integration'), 225 ), 226 'description' => __('Immediately creates the document the moment the order status changes. Delayed queues it in the WooCommerce background queue (Action Scheduler) and creates it after the delay below, so other plugins (payment gateways for example) can finish saving their order data first', 'linet-erp-woocommerce-integration'), 227 ), 228 229 'doc_delay' => array( 230 'title' => __('Document Delay (seconds)', 'linet-erp-woocommerce-integration'), 231 'default' => (string) WC_LI_Invoice_Manager::DEFAULT_DELAY, 232 'type' => 'text', 233 'description' => __('Used only when Document Timing is Delayed. Seconds to wait after the status change before creating the document. Default 60', 'linet-erp-woocommerce-integration'), 216 234 ), 217 235 -
linet-erp-woocommerce-integration/trunk/readme.txt
r3773444 r3790681 4 4 Requires at least: 6.0 5 5 Tested up to: 7.0 6 Stable tag: 3.6.4 56 Stable tag: 3.6.46 7 7 License: GPLv2 or later 8 8 Requires PHP: 7.4 … … 48 48 49 49 == Changelog == 50 51 = 2026.11.08 - version 3.6.46 = 52 53 * New: "Document Timing" and "Document Delay (seconds)" settings (Order Options). Immediate (the default) creates the Linet doc. on the status change as before. Delayed queues it in the WooCommerce background queue (Action Scheduler) and creates it after the delay (60 seconds by default), so plugins that change the status first and save their order meta after it (payment gateways for example) are done before the doc. is built 54 * New: delayed docs are not created twice for the same status, are skipped when a doc. was created in the meantime (the manual order action for example), and use the doc. type of the status that triggered them 55 * New: admin warning when delayed docs are stuck in the queue (WP-Cron not running) or failed in the last 7 days 50 56 51 57 = 2026.10.29 - version 3.6.45 = -
linet-erp-woocommerce-integration/trunk/woocommerce-linet.php
r3773444 r3790681 8 8 * License: GPL v2 or later 9 9 * License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 * Version: 3.6.4 510 * Version: 3.6.46 11 11 * Tested up to: 7.0 12 12 * Requires at least: 6.0 … … 41 41 class WC_Linet 42 42 { 43 const VERSION = '3.6.4 5';43 const VERSION = '3.6.46'; 44 44 45 45
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)