Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/woocommerce-delivery-notes/build/admin.asset.php
+++ b/woocommerce-delivery-notes/build/admin.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-core-data', 'wp-data', 'wp-date', 'wp-element', 'wp-i18n', 'wp-notices', 'wp-primitives'), 'version' => '8eca0dc14f60fcf4093c');
+<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-core-data', 'wp-data', 'wp-date', 'wp-element', 'wp-i18n', 'wp-notices', 'wp-primitives'), 'version' => 'a54b5efc95304ca0b72b');
--- a/woocommerce-delivery-notes/includes/admin/class-backend.php
+++ b/woocommerce-delivery-notes/includes/admin/class-backend.php
@@ -371,13 +371,6 @@
return;
}
- const interval = setInterval(function() {
- if (printWindow.document.readyState === 'complete') {
- clearInterval(interval);
- printWindow.print();
- }
- }, 500);
-
closeModal();
});
--- a/woocommerce-delivery-notes/includes/api/class-api.php
+++ b/woocommerce-delivery-notes/includes/api/class-api.php
@@ -201,6 +201,9 @@
? $value
: array();
+ case 'html':
+ return str_replace( "n", '<br />', wp_kses_post( $value ) );
+
default:
return sanitize_text_field( $value );
--- a/woocommerce-delivery-notes/includes/api/class-settings.php
+++ b/woocommerce-delivery-notes/includes/api/class-settings.php
@@ -91,22 +91,25 @@
return array(
'storeName' => 'text',
'storeLogo' => 'url',
- 'storeAddress' => 'textarea',
+ 'storeAddress' => 'html',
'email' => 'email',
'phone' => 'text',
- 'footerText' => 'textarea',
- 'complimentaryClose' => 'textarea',
- 'policies' => 'textarea',
+ 'footerText' => 'html',
+ 'complimentaryClose' => 'html',
+ 'policies' => 'html',
'printEndpoint' => 'slug',
'defaultDocumentLabel' => 'text',
'textDirection' => 'text',
'enablePDF' => 'bool',
+ 'pdfPaperSize' => 'text',
+ 'pdfUseCurrencyCode' => 'bool',
'showCustomerEmailLink' => 'bool',
'customerEmailText' => 'text',
'showAdminEmailLink' => 'bool',
'adminEmailText' => 'text',
'showViewOrderButton' => 'bool',
'showPrintButtonMyAccountPage' => 'bool',
+ 'autoPrintDialog' => 'bool',
'viewOrderButtonLabel' => 'text',
'invoiceButtonLabel' => 'text',
'deliveryNoteButtonLabel' => 'text',
@@ -317,13 +320,16 @@
'complimentaryClose' => '',
'policies' => '',
'printEndpoint' => 'print-order',
+ 'autoPrintDialog' => false,
'defaultDocumentLabel' => __( 'Document', 'woocommerce-delivery-notes' ),
'textDirection' => 'ltr',
'enablePDF' => true,
+ 'pdfPaperSize' => 'A4',
+ 'pdfUseCurrencyCode' => true,
'showCustomerEmailLink' => true,
- 'customerEmailText' => __( 'View and print your invoice', 'woocommerce-delivery-notes' ),
+ 'customerEmailText' => __( 'View and print your', 'woocommerce-delivery-notes' ),
'showAdminEmailLink' => true,
- 'adminEmailText' => __( 'Print order documents', 'woocommerce-delivery-notes' ),
+ 'adminEmailText' => __( 'Print order', 'woocommerce-delivery-notes' ),
'showViewOrderButton' => true,
'showPrintButtonMyAccountPage' => true,
'ordersListLabel' => __( 'Print', 'woocommerce-delivery-notes' ),
--- a/woocommerce-delivery-notes/includes/api/class-templates.php
+++ b/woocommerce-delivery-notes/includes/api/class-templates.php
@@ -193,13 +193,33 @@
}
}
- return array(
- 'name' => $settings['storeName'] ?? '',
- 'logo' => $logo,
- 'logo_path' => $logo_path,
- 'address' => $settings['storeAddress'] ?? '',
- 'phone' => $settings['phone'] ?? '',
- 'email' => $settings['email'] ?? '',
+ /**
+ * Filter the shop/store data array before it is passed to document templates.
+ *
+ * Use this filter to override individual store fields (name, address, phone,
+ * email, logo) without copying base.php, or to inject extra keys your custom
+ * template expects.
+ *
+ * @param array $shop {
+ * @type string $name Store name.
+ * @type string $logo Logo URL (used in HTML context).
+ * @type string $logo_path Logo absolute file path (used in PDF context).
+ * @type string $address Store address (may contain HTML line breaks).
+ * @type string $phone Store phone number.
+ * @type string $email Store email address.
+ * }
+ * @since 7.1.2
+ */
+ return apply_filters(
+ 'wcdn_shop_data',
+ array(
+ 'name' => $settings['storeName'] ?? '',
+ 'logo' => $logo,
+ 'logo_path' => $logo_path,
+ 'address' => $settings['storeAddress'] ?? '',
+ 'phone' => $settings['phone'] ?? '',
+ 'email' => $settings['email'] ?? '',
+ )
);
}
@@ -218,11 +238,28 @@
array()
);
- return array(
- 'footer' => $settings['footerText'] ?? '',
- 'complimentaryClose' => $settings['complimentaryClose'] ?? '',
- 'policies' => $settings['policies'] ?? '',
- 'isRTL' => 'rtl' === ( $settings['textDirection'] ?? 'ltr' ),
+ /**
+ * Filter the document-level content array before it is passed to templates.
+ *
+ * Use this filter to override or extend the footer, policies, complimentary
+ * close text, or RTL direction flag programmatically.
+ *
+ * @param array $document {
+ * @type string $footer Footer text (HTML allowed via wp_kses_post).
+ * @type string $complimentaryClose Complimentary close text (HTML allowed).
+ * @type string $policies Policies text (HTML allowed).
+ * @type bool $isRTL Whether the site locale is right-to-left.
+ * }
+ * @since 7.1.2
+ */
+ return apply_filters(
+ 'wcdn_document_data',
+ array(
+ 'footer' => $settings['footerText'] ?? '',
+ 'complimentaryClose' => $settings['complimentaryClose'] ?? '',
+ 'policies' => $settings['policies'] ?? '',
+ 'isRTL' => 'rtl' === ( $settings['textDirection'] ?? 'ltr' ),
+ )
);
}
@@ -275,7 +312,9 @@
*/
public static function format_order_data( $order, $is_sample = false, $template = 'invoice' ) {
- add_filter( 'woocommerce_currency_symbol', array( __CLASS__, 'normalize_currency_symbol' ), 10, 2 );
+ if ( Settings::get( 'pdfUseCurrencyCode' ) ) {
+ add_filter( 'woocommerce_currency_symbol', array( __CLASS__, 'normalize_currency_symbol' ), 10, 2 );
+ }
if ( $is_sample ) {
$sample = array(
@@ -396,10 +435,6 @@
$product = apply_filters( 'wcdn_order_item_product', $item->get_product(), $item );
- if ( ! $product ) {
- continue;
- }
-
$qty_refunded = $order->get_qty_refunded_for_item( $item_id );
$_qty = max( 0, $item->get_quantity() + $qty_refunded );
@@ -407,7 +442,7 @@
continue;
}
- $image_id = $product->get_image_id();
+ $image_id = $product ? $product->get_image_id() : false;
$image_file = $image_id ? get_attached_file( $image_id ) : false;
$items[] = array(
@@ -416,7 +451,7 @@
'price' => self::format_order_item( 'price', $product, $order, $item ),
'quantity' => self::format_order_item( 'quantity', $product, $order, $item ),
'total' => self::format_order_item( 'total', $product, $order, $item ),
- 'product_id' => $product->get_id(),
+ 'product_id' => $product ? $product->get_id() : 0,
'order_item_id' => $item_id,
'meta' => self::format_order_item( 'meta', $product, $order, $item ),
'addon' => self::format_order_item( 'addon', $product, $order, $item ),
@@ -425,6 +460,27 @@
);
}
+ /**
+ * Filter the order items array before it is passed to the document template.
+ *
+ * Each item is an associative array with keys: name, sku, price, quantity,
+ * total, product_id, order_item_id, meta, addon, image_url, image_path.
+ * Use this filter to sort, remove, or reorder items.
+ *
+ * Example — sort alphabetically by product name:
+ *
+ * add_filter( 'wcdn_order_items', function( $items ) {
+ * usort( $items, fn( $a, $b ) => strcmp( $a['name'], $b['name'] ) );
+ * return $items;
+ * } );
+ *
+ * @param array $items Formatted order items.
+ * @param WC_Order $order Order object.
+ * @param string $template Template key (e.g. 'invoice', 'receipt').
+ * @since 7.1.2
+ */
+ $items = apply_filters( 'wcdn_order_items', $items, $order, $template );
+
$billing = $order->get_address( 'billing' );
$shipping = $order->get_address( 'shipping' );
@@ -434,9 +490,30 @@
$billing_address = WC()->countries->get_formatted_address( $billing );
$billing_address = array_filter( explode( '<br/>', $billing_address ) );
+ /**
+ * Filter the formatted billing address lines before they appear in the document.
+ *
+ * Each element is one address line (company, street, city/state/postcode, country).
+ * Return a flat array of strings to replace the default output.
+ *
+ * @param array $billing_address Address lines.
+ * @param WC_Order $order Order object.
+ * @since 7.1.2
+ */
+ $billing_address = apply_filters( 'wcdn_billing_address', $billing_address, $order );
+
$shipping_address = WC()->countries->get_formatted_address( $shipping );
$shipping_address = array_filter( explode( '<br/>', $shipping_address ) );
+ /**
+ * Filter the formatted shipping address lines before they appear in the document.
+ *
+ * @param array $shipping_address Address lines.
+ * @param WC_Order $order Order object.
+ * @since 7.1.2
+ */
+ $shipping_address = apply_filters( 'wcdn_shipping_address', $shipping_address, $order );
+
$shipping_method_name = '';
$methods = $order->get_shipping_methods();
@@ -495,6 +572,7 @@
'totals' => self::format_order_totals( $order, $template ),
'refund' => self::generate_refund_preview( $order, $items ),
'status' => $order->get_status(),
+ 'extra_fields' => apply_filters( 'wcdn_order_info_fields', array(), $order ),
);
remove_filter( 'woocommerce_currency_symbol', array( __CLASS__, 'normalize_currency_symbol' ), 10 );
@@ -531,6 +609,24 @@
$totals['discount'] = wc_price( -$order->get_discount_total(), array( 'currency' => $currency ) );
}
+ $fee_items = $order->get_fees();
+ if ( ! empty( $fee_items ) ) {
+ $fee_lines = array();
+ foreach ( $fee_items as $fee ) {
+ $fee_total = (float) $fee->get_total();
+ if ( 0.0 === $fee_total ) {
+ continue;
+ }
+ $fee_lines[] = array(
+ 'label' => $fee->get_name(),
+ 'value' => wc_price( $fee_total, array( 'currency' => $currency ) ),
+ );
+ }
+ if ( ! empty( $fee_lines ) ) {
+ $totals['fee_lines'] = $fee_lines;
+ }
+ }
+
// Tax rows — only when WC taxes are enabled and the order has a non-zero tax amount.
if ( wc_tax_enabled() && $order->get_total_tax() > 0 ) {
@@ -750,12 +846,14 @@
/**
* Extra Product Options (TM EPO / _tmcartepo_data).
*/
- $epo_data = $item->get_meta( '_tmcartepo_data', true );
+ $epo_data = $item->get_meta( '_tmcartepo_data', true );
+ $has_epo_data = false;
if ( ! empty( $epo_data ) && is_array( $epo_data ) ) {
foreach ( $epo_data as $epo ) {
if ( ! empty( $epo['name'] ) && isset( $epo['value'] ) ) {
- $meta[] = array(
+ $has_epo_data = true;
+ $meta[] = array(
'label' => $epo['name'],
'value' => wp_strip_all_tags( $epo['value'] ),
);
@@ -774,8 +872,19 @@
/**
* Variation attributes and remaining visible meta.
+ *
+ * EPO hooks woocommerce_order_item_get_formatted_meta_data to inject the same
+ * fields we already read from _tmcartepo_data. Temporarily toggle its own
+ * wc_epo_no_order_get_items escape hatch so it skips that injection and we
+ * don't render EPO options twice.
*/
+ if ( $has_epo_data ) {
+ add_filter( 'wc_epo_no_order_get_items', '__return_true' );
+ }
$item_meta = apply_filters( 'wcdn_product_meta_data', $item->get_formatted_meta_data( '_' ), $item );
+ if ( $has_epo_data ) {
+ remove_filter( 'wc_epo_no_order_get_items', '__return_true' );
+ }
if ( $item_meta ) {
foreach ( $item_meta as $meta_field ) {
@@ -803,12 +912,23 @@
/**
* Downloadable file count.
+ *
+ * Skip when the item belongs to a WC_Order_Refund: WC's get_item_downloads()
+ * calls $item->get_order()->is_download_permitted() internally, and
+ * WC_Order_Refund does not implement that method (fatal error on refund emails).
*/
- if ( $product && $product->exists() && $product->is_downloadable() && $order->is_download_permitted() ) {
+ $item_order = $item->get_order();
+ if (
+ $product &&
+ $product->exists() &&
+ $product->is_downloadable() &&
+ ! ( $item_order instanceof WC_Order_Refund ) &&
+ $order->is_download_permitted()
+ ) {
$meta[] = array(
'label' => __( 'Download', 'woocommerce-delivery-notes' ),
/* translators: %s: number of files */
- 'value' => sprintf( __( '%s Files', 'woocommerce-delivery-notes' ), count( $item->get_item_downloads() ) ),
+ 'value' => sprintf( __( '%s Files', 'woocommerce-delivery-notes' ), count( (array) $item->get_item_downloads() ) ),
);
}
@@ -933,9 +1053,10 @@
return self::response(
'success',
array(
- 'templates' => $templates,
- 'config' => $config,
- 'preview' => $preview,
+ 'templates' => $templates,
+ 'config' => $config,
+ 'preview' => $preview,
+ 'pdfPaperSize' => Settings::get( 'pdfPaperSize' ),
)
);
}
@@ -1019,17 +1140,61 @@
$defaults
);
+ /*
+ * Copy Page Setup settings to all other templates when requested.
+ */
+ $apply_to_all = ! empty( $templates[ $template_key ]['applyZoomToAll'] );
+
+ if ( $apply_to_all ) {
+ // Collect all fields in the Page Setup (documentZoom) group dynamically
+ // so that any future additions to the group are automatically included.
+ $zoom_fields = array();
+ foreach ( $structure['sections'] ?? array() as $section ) {
+ foreach ( $section['items'] ?? array() as $group ) {
+ if ( isset( $group['id'] ) && 'documentZoom' === $group['id'] ) {
+ foreach ( $group['items'] ?? array() as $field ) {
+ if ( isset( $field['field'] ) && 'applyZoomToAll' !== $field['field'] ) {
+ $zoom_fields[] = $field['field'];
+ }
+ }
+ break 2;
+ }
+ }
+ }
+
+ foreach ( Template_Engine::get_template_keys() as $other_key ) {
+ if ( $other_key === $template_key ) {
+ continue;
+ }
+
+ $other_structure = Template_Engine::get_structure( $other_key );
+ $other_defaults = Template_Engine::build_defaults( $other_key, $other_structure );
+ $other_schema = Template_Engine::build_schema( $other_structure );
+ $other_data = isset( $templates[ $other_key ] ) ? $templates[ $other_key ] : $other_defaults;
+
+ foreach ( $zoom_fields as $zoom_field ) {
+ $other_data[ $zoom_field ] = $templates[ $template_key ][ $zoom_field ];
+ }
+
+ $templates[ $other_key ] = self::sanitize( $other_data, $other_schema, $other_defaults );
+ }
+
+ // Reset the checkbox on the saved template too — it's a one-shot action.
+ $templates[ $template_key ]['applyZoomToAll'] = false;
+ }
+
update_option( self::OPTION_KEY, $templates );
- return self::response(
- 'success',
- array(
- 'message' =>
- __( 'Template saved successfully.', 'woocommerce-delivery-notes' ),
- 'templates' =>
- $templates[ $template_key ],
- )
+ $response_data = array(
+ 'message' => __( 'Template saved successfully.', 'woocommerce-delivery-notes' ),
+ 'template' => $templates[ $template_key ],
);
+
+ if ( $apply_to_all ) {
+ $response_data['allTemplates'] = $templates;
+ }
+
+ return self::response( 'success', $response_data );
}
/**
--- a/woocommerce-delivery-notes/includes/class-woocommerce-delivery-notes.php
+++ b/woocommerce-delivery-notes/includes/class-woocommerce-delivery-notes.php
@@ -26,7 +26,7 @@
*
* @var string
*/
- protected static $plugin_version = '7.1.0';
+ protected static $plugin_version = '7.1.2';
/**
* Minimum version of WordPress required.
--- a/woocommerce-delivery-notes/includes/component/plugin-tracking/class-tyche-plugin-tracking.php
+++ b/woocommerce-delivery-notes/includes/component/plugin-tracking/class-tyche-plugin-tracking.php
@@ -4,10 +4,10 @@
*
* Plugin Tracking Class.
*
- * @author Tyche Softwares
- * @package TycheSoftwares/PluginTracking
- * @category Classes
- * @since 1.2
+ * @author Tyche Softwares
+ * @package TycheSoftwares/PluginTracking
+ * @category Classes
+ * @since 1.2
*/
namespace TycheWCDN;
@@ -259,7 +259,10 @@
src="<?php echo esc_url( $this->api_url . '/assets/plugin-tracking/images/site-logo.jpg?v=' . $this->version ); ?> ">
</div>
<p style="margin: 10px 0 10px 130px; font-size: medium;">
- <?php /* translators: 1. Plugin name, 2. URL to find out more about usage tracking */ print( sprintf( __( 'Want to help make %1$s even more awesome? Allow %1$s to collect non-sensitive diagnostic data and usage information and get 20%% off on your next purchase. <a href="%2$s" target="_blank">Find out more</a>.', 'woocommerce-delivery-notes' ), $this->plugin_name, $this->blog_link ) ); //phpcs:ignore ?>
+ <?php //phpcs:ignore
+ /* translators: 1. Plugin name, 2. URL to find out more about usage tracking */
+ print( $this->safe_sprintf( __( 'Want to help make %1$s even more awesome? Allow %1$s to collect non-sensitive diagnostic data and usage information and get 20%% off on your next purchase. <a href="%2$s" target="_blank">Find out more</a>.', 'woocommerce-delivery-notes' ), $this->plugin_name, $this->blog_link ) ); //phpcs:ignore
+ ?>
</p>
<p class="submit">
<a class="button-primary button button-large"
@@ -272,6 +275,32 @@
}
}
+ /**
+ * Format a translatable sprintf template safely.
+ *
+ * Falls back to str_replace when a translation contains an unescaped %
+ * that would cause a ValueError in sprintf (e.g. Dutch "20% off" translations).
+ *
+ * @param string $template Translated sprintf template.
+ * @param mixed ...$args Substitution values.
+ * @return string
+ */
+ protected function safe_sprintf( $template, ...$args ) {
+ try {
+ return sprintf( $template, ...$args );
+ } catch ( ValueError $e ) {
+ $placeholders = array();
+ $values = array();
+ foreach ( $args as $i => $value ) {
+ $placeholders[] = '%' . ( $i + 1 ) . '$s';
+ $values[] = $value;
+ }
+ $placeholders[] = '%%';
+ $values[] = '%';
+ return str_replace( $placeholders, $values, $template );
+ }
+ }
+
/**
* Generates the Tracking Data.
*
--- a/woocommerce-delivery-notes/includes/core/class-files.php
+++ b/woocommerce-delivery-notes/includes/core/class-files.php
@@ -22,6 +22,32 @@
class Files {
/**
+ * Load a third-party integration once its detection class is available.
+ *
+ * Defers to plugins_loaded priority 20 so third-party plugins that
+ * register their classes at the default priority (10) are already loaded.
+ *
+ * @param string $detect_class Fully-qualified class name used to detect the plugin.
+ * @param string $file Path relative to the includes directory.
+ * @param string $fqcn Fully-qualified class name of the integration to instantiate.
+ * @since 7.1.2
+ */
+ private static function load_integration( string $detect_class, string $file, string $fqcn ): void {
+ $load = static function () use ( $detect_class, $file, $fqcn ) {
+ if ( class_exists( $detect_class ) ) {
+ WCDN()::include_file( $file );
+ new $fqcn();
+ }
+ };
+
+ if ( did_action( 'plugins_loaded' ) ) {
+ $load();
+ } else {
+ add_action( 'plugins_loaded', $load, 20 );
+ }
+ }
+
+ /**
* Include files.
*
* @since 7.0
@@ -94,6 +120,11 @@
WCDN()::include_file( 'integrations/class-emails.php' );
new TycheWCDNIntegrationsEmails();
+ self::load_integration( 'AWCDP_Deposits', 'integrations/class-deposits-partial-payments.php', TycheWCDNIntegrationsDeposits_Partial_Payments::class );
+ self::load_integration( 'DFW_Deposits', 'integrations/class-dfw-deposits.php', TycheWCDNIntegrationsDFW_Deposits::class );
+ self::load_integration( 'WC_Local_Pickup_Plus_Orders', 'integrations/class-local-pickup-plus.php', TycheWCDNIntegrationsLocal_Pickup_Plus::class );
+ self::load_integration( 'Coderockz_Woo_Delivery', 'integrations/class-coderockz-woo-delivery.php', TycheWCDNIntegrationsCoderockz_Woo_Delivery::class );
+
WCDN()::include_file( 'frontend/class-frontend.php' );
WCDN()::include_file( 'admin/class-backend.php' );
--- a/woocommerce-delivery-notes/includes/core/class-migration.php
+++ b/woocommerce-delivery-notes/includes/core/class-migration.php
@@ -59,6 +59,11 @@
$success = self::migrate_to_v71();
}
+ // Update email link text defaults and restore button labels saved as empty strings.
+ if ( $success && version_compare( (string) $from_version, '7.1.2', '<' ) ) {
+ $success = self::migrate_to_v712();
+ }
+
return $success;
}
@@ -253,6 +258,64 @@
}
/**
+ * Run migration to v7.1.2.
+ *
+ * - Updates email link text defaults changed so the document name is
+ * appended at runtime. Only touches values still matching the old defaults.
+ * - Restores button labels saved as empty strings in v7.0 (wp_parse_args
+ * cannot fill in defaults for keys that exist with an empty value).
+ * Only updates labels that are still empty — custom values are preserved.
+ *
+ * @return bool
+ * @since 7.1.2
+ */
+ public static function migrate_to_v712() {
+
+ if ( get_option( 'wcdn_migration_7_1_2_completed' ) ) {
+ return true;
+ }
+
+ $settings = get_option( Settings::OPTION_KEY, array() );
+ $defaults = Settings::default_settings();
+ $updated = false;
+
+ if ( ( $settings['customerEmailText'] ?? '' ) === 'View and print your invoice' ) {
+ $settings['customerEmailText'] = 'View and print your';
+ $updated = true;
+ }
+
+ if ( ( $settings['adminEmailText'] ?? '' ) === 'Print order documents' ) {
+ $settings['adminEmailText'] = 'Print order';
+ $updated = true;
+ }
+
+ $label_keys = array(
+ 'invoiceButtonLabel',
+ 'deliveryNoteButtonLabel',
+ 'receiptButtonLabel',
+ 'creditNoteButtonLabel',
+ 'packingSlipButtonLabel',
+ );
+
+ foreach ( $label_keys as $key ) {
+ if ( isset( $settings[ $key ] ) && '' === $settings[ $key ] && ! empty( $defaults[ $key ] ) ) {
+ $settings[ $key ] = $defaults[ $key ];
+ $updated = true;
+ }
+ }
+
+ if ( $updated ) {
+ update_option( Settings::OPTION_KEY, $settings );
+ }
+
+ // Remove any orphaned cron events from the old file-deletion hook.
+ wp_clear_scheduled_hook( 'wcdn_delete_file' );
+
+ update_option( 'wcdn_migration_7_1_2_completed', true );
+ return true;
+ }
+
+ /**
* Backup old data.
*
* @param array $options Options from database.
@@ -1751,6 +1814,7 @@
delete_option( 'wcdn_migration_7_completed' );
delete_option( 'wcdn_migration_7_0_2_completed' );
delete_option( 'wcdn_migration_7_1_0_completed' );
+ delete_option( 'wcdn_migration_7_1_2_completed' );
return true;
}
--- a/woocommerce-delivery-notes/includes/core/class-uninstall.php
+++ b/woocommerce-delivery-notes/includes/core/class-uninstall.php
@@ -51,6 +51,7 @@
*/
public static function remove_cron_jobs() {
wp_clear_scheduled_hook( 'wcdn_ts_tracker_send_event' );
+ wp_clear_scheduled_hook( 'wcdn_delete_file' );
}
/**
--- a/woocommerce-delivery-notes/includes/frontend/class-frontend.php
+++ b/woocommerce-delivery-notes/includes/frontend/class-frontend.php
@@ -53,10 +53,10 @@
add_filter( 'query_vars', array( $this, 'add_query_vars' ) );
add_action( 'template_redirect', array( $this, 'template_redirect_theme' ) );
add_action( 'wp_ajax_print_order', array( $this, 'template_redirect_admin' ) );
+ add_action( 'wp_ajax_wcdn_generate_bulk_pdf', array( $this, 'ajax_generate_bulk_pdf' ) );
add_filter( 'woocommerce_my_account_my_orders_actions', array( $this, 'create_print_button_account_page' ), 10, 2 );
add_action( 'woocommerce_view_order', array( $this, 'create_print_button_order_page' ) );
add_action( 'woocommerce_thankyou', array( $this, 'create_print_button_order_page' ) );
- add_action( 'wcdn_after_items', array( $this, 'wdn_add_extra_data_after_items' ), 10, 1 );
add_filter( 'woocommerce_get_item_count', array( $this, 'wcdn_order_item_count' ), 20, 3 );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_myaccount_styles' ) );
add_action( 'woocommerce_checkout_create_order', array( $this, 'generate_guest_access_token' ), 10, 1 );
@@ -349,6 +349,15 @@
}
}
+ // AWCDP partial payment child orders — resolve to the parent deposit order so the
+ // invoice shows the original products plus Deposit / Future Payments rows.
+ if ( defined( 'AWCDP_POST_TYPE' ) && $order->get_type() === AWCDP_POST_TYPE ) {
+ $parent = wc_get_order( $order->get_parent_id() );
+ if ( $parent ) {
+ $order = $parent;
+ }
+ }
+
if ( ! $this->can_view_order( $order, $email ) ) {
wp_die( esc_html__( 'Access denied.', 'woocommerce-delivery-notes' ) );
}
@@ -502,29 +511,18 @@
$order_ids = wp_list_pluck( $documents, 'order_id' );
- // Generate merged PDF if multiple.
+ // PDF is generated synchronously on load for single orders only.
+ // For multiple orders, dompdf processing a combined N-page document blocks
+ // the entire HTTP response and causes timeouts beyond ~50 orders.
$pdf_url = '';
- if ( ! empty( $order_ids ) ) {
-
- if ( 1 === count( $order_ids ) ) {
-
- // Single order.
- $pdf_file = Service::pdf()->generate(
- $order_ids[0],
- $template,
- $documents[0]['data']
- );
+ if ( 1 === count( $order_ids ) ) {
- } else {
-
- // Multiple orders.
- $pdf_file = Service::pdf()->generate(
- $order_ids,
- $template,
- $documents
- );
- }
+ $pdf_file = Service::pdf()->generate(
+ $order_ids[0],
+ $template,
+ $documents[0]['data']
+ );
if ( $pdf_file ) {
@@ -541,7 +539,9 @@
<html>
<head>
- <title><?php echo esc_html( Settings::get( 'defaultDocumentLabel' ) ); ?></title>
+ <title>
+ <?php echo esc_html( apply_filters( 'wcdn_print_document_title', Settings::get( 'defaultDocumentLabel' ), $order_ids, $template ) ); ?>
+ </title>
<style>
body {
@@ -665,12 +665,67 @@
.wcdn-toolbar {
display: none;
}
+
+ .wcdn-print-overlay {
+ display: none;
+ }
+ }
+
+ <?php
+ if ( Settings::get( 'autoPrintDialog' ) ) :
+ ?>
+ .wcdn-print-overlay {
+ position: fixed;
+ inset: 0;
+ background: #fff;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 20px;
+ z-index: 9999;
+ }
+
+ .wcdn-print-overlay.is-hidden {
+ display: none;
+ }
+
+ .wcdn-print-spinner {
+ width: 40px;
+ height: 40px;
+ border: 3px solid #e0e0e0;
+ border-top-color: #2271b1;
+ border-radius: 50%;
+ animation: wcdn-spin 0.7s linear infinite;
+ }
+
+ .wcdn-print-overlay p {
+ font-size: 15px;
+ color: #646970;
+ margin: 0;
+ }
+
+ @keyframes wcdn-spin {
+ to {
+ transform: rotate(360deg);
+ }
}
+
+ <?php
+ endif;
+ ?>
</style>
</head>
<body>
+ <?php if ( Settings::get( 'autoPrintDialog' ) ) : ?>
+ <div class="wcdn-print-overlay" id="wcdn-print-overlay">
+ <div class="wcdn-print-spinner"></div>
+ <p><?php esc_html_e( 'Preparing document…', 'woocommerce-delivery-notes' ); ?></p>
+ </div>
+ <?php endif; ?>
+
<div class="wcdn-container">
<?php foreach ( $documents as $doc ) : ?>
@@ -692,11 +747,78 @@
<a href="<?php echo esc_url( $pdf_url ); ?>" target="_blank" class="wcdn-btn wcdn-btn-primary">
<?php esc_html_e( 'Download PDF', 'woocommerce-delivery-notes' ); ?>
</a>
+ <?php elseif ( count( $order_ids ) > 1 && Settings::get( 'enablePDF' ) ) : ?>
+ <button id="wcdn-bulk-pdf-btn" class="wcdn-btn wcdn-btn-primary"
+ data-order-ids="<?php echo esc_attr( implode( ',', $order_ids ) ); ?>"
+ data-template="<?php echo esc_attr( $template ); ?>"
+ data-nonce="<?php echo esc_attr( wp_create_nonce( 'wcdn_bulk_pdf' ) ); ?>"
+ data-ajax-url="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>"
+ data-label="<?php echo esc_attr( __( 'Download PDF', 'woocommerce-delivery-notes' ) ); ?>"
+ data-label-loading="<?php echo esc_attr( __( 'Generating PDF…', 'woocommerce-delivery-notes' ) ); ?>">
+ <?php esc_html_e( 'Download PDF', 'woocommerce-delivery-notes' ); ?>
+ </button>
<?php endif; ?>
</div>
</div>
+ <script>
+ (function() {
+ var btn = document.getElementById('wcdn-bulk-pdf-btn');
+ if (!btn) {
+ return;
+ }
+ btn.addEventListener('click', function() {
+ btn.disabled = true;
+ btn.textContent = btn.dataset.labelLoading;
+ fetch(btn.dataset.ajaxUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ body: new URLSearchParams({
+ action: 'wcdn_generate_bulk_pdf',
+ order_ids: btn.dataset.orderIds,
+ template: btn.dataset.template,
+ _wpnonce: btn.dataset.nonce
+ })
+ })
+ .then(function(r) {
+ return r.json();
+ })
+ .then(function(data) {
+ if (data.success && data.data.url) {
+ window.location.href = data.data.url;
+ } else {
+ alert(data.data && data.data.message ? data.data.message :
+ <?php echo wp_json_encode( __( 'PDF generation failed.', 'woocommerce-delivery-notes' ) ); ?>
+ );
+ btn.disabled = false;
+ btn.textContent = btn.dataset.label;
+ }
+ })
+ .catch(function() {
+ alert(
+ <?php echo wp_json_encode( __( 'PDF generation failed.', 'woocommerce-delivery-notes' ) ); ?>);
+ btn.disabled = false;
+ btn.textContent = btn.dataset.label;
+ });
+ });
+ })();
+ </script>
+
+ <?php if ( Settings::get( 'autoPrintDialog' ) ) : ?>
+ <script>
+ window.addEventListener('DOMContentLoaded', function() {
+ var overlay = document.getElementById('wcdn-print-overlay');
+ if (overlay) {
+ overlay.classList.add('is-hidden');
+ }
+ window.print();
+ });
+ </script>
+ <?php endif; ?>
+
</body>
</html>
@@ -704,65 +826,66 @@
}
/**
- * Add extra data after order items.
+ * AJAX handler: generate a merged PDF for multiple orders on demand.
*
- * @param WC_Order $order Order object.
* @return void
- * @since 7.0
+ * @since 7.1.2
*/
- public function wdn_add_extra_data_after_items( $order ) {
+ public function ajax_generate_bulk_pdf() {
+
+ if ( ! check_ajax_referer( 'wcdn_bulk_pdf', '_wpnonce', false ) || ! current_user_can( 'manage_woocommerce' ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown
+ wp_send_json_error( array( 'message' => __( 'Authentication failed.', 'woocommerce-delivery-notes' ) ) );
+ }
+
+ $order_ids_raw = isset( $_POST['order_ids'] ) ? sanitize_text_field( wp_unslash( $_POST['order_ids'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified above
+ $template = isset( $_POST['template'] ) ? sanitize_text_field( wp_unslash( $_POST['template'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
+
+ if ( ! in_array( $template, Template_Engine::get_template_keys(), true ) ) {
+ wp_send_json_error( array( 'message' => __( 'Invalid template.', 'woocommerce-delivery-notes' ) ) );
+ }
+
+ $order_ids = array_values( array_filter( array_map( 'absint', explode( ',', $order_ids_raw ) ) ) );
+
+ if ( empty( $order_ids ) ) {
+ wp_send_json_error( array( 'message' => __( 'No orders provided.', 'woocommerce-delivery-notes' ) ) );
+ }
+
+ $documents = array();
- /**
- * Local pickup plus plugin is active
- */
- if ( class_exists( 'WC_Local_Pickup_Plus' ) ) {
+ foreach ( $order_ids as $order_id ) {
- $cdn_local_pickup_plugin_plugins_version = wc_local_pickup_plus()->get_version();
+ $order = wc_get_order( $order_id );
- if ( version_compare( $cdn_local_pickup_plugin_plugins_version, '2.0.0', '>=' ) ) {
- $cdn_local_pickup_object = new WC_Local_Pickup_Plus_Orders();
- $local_pickup = wc_local_pickup_plus();
- $cdn_local_pickup_locations = $cdn_local_pickup_object->get_order_pickup_data( $order );
- $cdn_local_pickup__shipping_object = $local_pickup->get_shipping_method_instance();
- $this->cdn_print_local_pickup_address( $cdn_local_pickup_locations, $cdn_local_pickup__shipping_object );
+ if ( ! $order ) {
+ continue;
}
+
+ $documents[] = array(
+ 'order_id' => $order_id,
+ 'data' => array(
+ 'order' => Templates_Api::format_order_data( $order, false, $template ),
+ 'shop' => Templates_Api::get_store_data(),
+ 'document' => Templates_Api::get_document_data(),
+ 'settings' => Templates::template( $template ),
+ 'template' => $template,
+ ),
+ );
}
- }
- /**
- * Print local pickup address.
- *
- * @param array $cdn_local_pickup_locations Pickup locations.
- * @param object $shipping_method Shipping method instance.
- * @return void
- * @since 7.0
- */
- public function cdn_print_local_pickup_address( $cdn_local_pickup_locations, $shipping_method ) {
+ if ( empty( $documents ) ) {
+ wp_send_json_error( array( 'message' => __( 'No valid orders found.', 'woocommerce-delivery-notes' ) ) );
+ }
- $package_number = 1;
- $packages_count = count( $cdn_local_pickup_locations );
- foreach ( $cdn_local_pickup_locations as $pickup_meta ) :
- ?>
-<div>
- <?php if ( $packages_count > 1 ) : ?>
- <h5><?php echo wp_kses_post( sprintf( is_rtl() ? '#%2$s %1$s' : '%1$s #%2$s', esc_html( $shipping_method->get_method_title() ), $package_number ) ); ?>
- </h5>
- <?php endif; ?>
- <ul>
- <?php foreach ( $pickup_meta as $label => $value ) : ?>
- <li>
- <?php if ( is_rtl() ) : ?>
- <?php echo wp_kses_post( $value ); ?> <strong>:<?php echo esc_html( $label ); ?></strong>
- <?php else : ?>
- <strong><?php echo esc_html( $label ); ?>:</strong> <?php echo wp_kses_post( $value ); ?>
- <?php endif; ?>
- </li>
- <?php endforeach; ?>
- </ul>
- <?php ++$package_number; ?>
-</div>
- <?php
- endforeach;
+ $pdf_file = Service::pdf()->generate( $order_ids, $template, $documents );
+
+ if ( ! $pdf_file ) {
+ wp_send_json_error( array( 'message' => __( 'PDF generation failed. Please try with fewer orders or check your server memory limit.', 'woocommerce-delivery-notes' ) ) );
+ }
+
+ $upload_dir = wp_upload_dir();
+ $pdf_url = trailingslashit( $upload_dir['baseurl'] ) . 'wcdn/' . $template . '/' . basename( $pdf_file );
+
+ wp_send_json_success( array( 'url' => $pdf_url ) );
}
/**
--- a/woocommerce-delivery-notes/includes/helpers/class-utils.php
+++ b/woocommerce-delivery-notes/includes/helpers/class-utils.php
@@ -14,6 +14,7 @@
use TycheWCDNHelpersSettings;
use TycheWCDNHelpersTemplates;
+use TycheWCDNServicesTemplate_Engine;
defined( 'ABSPATH' ) || exit;
@@ -510,40 +511,37 @@
}
}
+ // Helper: extract plain status values from get_template_order_statuses(),
+ // which applies the wcdn_allowed_statuses_[template] filter.
+ $allowed = function ( $template ) {
+ return array_column( Template_Engine::get_template_order_statuses( $template ), 'value' );
+ };
+
// Invoice.
- if ( in_array( $status, array( 'pending', 'on-hold', 'processing', 'completed' ), true ) ) {
+ if ( in_array( $status, $allowed( 'invoice' ), true ) ) {
$types[] = 'invoice';
}
- // Receipt (only after payment).
- if ( $order->is_paid() ) {
+ // Receipt.
+ if ( in_array( $status, $allowed( 'receipt' ), true ) ) {
$types[] = 'receipt';
}
// Shipping documents (only for physical products).
if ( $has_physical_items ) {
- $types[] = 'packingslip';
+ if ( in_array( $status, $allowed( 'packingslip' ), true ) ) {
+ $types[] = 'packingslip';
+ }
- // Optional: stricter delivery note logic.
- if ( in_array( $status, array( 'processing', 'completed' ), true ) ) {
+ if ( in_array( $status, $allowed( 'deliverynote' ), true ) ) {
$types[] = 'deliverynote';
}
}
- // Credit note.
- $refunded_total = (float) $order->get_total_refunded();
-
- if ( $refunded_total > 0 ) {
-
- // Partial refund vs full refund.
- if ( $refunded_total < (float) $order->get_total() ) {
- // Partial refund → still show credit note.
- $types[] = 'creditnote';
- } else {
- // Full refund → definitely show credit note.
- $types[] = 'creditnote';
- }
+ // Credit note (refund-based, status acts as secondary gate).
+ if ( (float) $order->get_total_refunded() > 0 ) {
+ $types[] = 'creditnote';
}
return apply_filters( 'wcdn_template_types_from_order', $types, $order );
--- a/woocommerce-delivery-notes/includes/integrations/class-coderockz-woo-delivery.php
+++ b/woocommerce-delivery-notes/includes/integrations/class-coderockz-woo-delivery.php
@@ -0,0 +1,143 @@
+<?php
+/**
+ * Print Invoice & Delivery Notes for WooCommerce.
+ *
+ * Delivery & Pickup Date Time for WooCommerce (Coderockz) Integration.
+ *
+ * @author Tyche Softwares
+ * @package WCDN/Integrations
+ * @category Classes
+ * @since 7.1.2
+ */
+
+namespace TycheWCDNIntegrations;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Coderockz Woo Delivery integration class.
+ *
+ * Appends delivery/pickup date and time fields to the WCDN order info
+ * fields section for orders placed with the Delivery & Pickup Date Time
+ * for WooCommerce plugin (Coderockz). Respects the plugin's own label
+ * and date/time format settings.
+ *
+ * @since 7.1.2
+ */
+class Coderockz_Woo_Delivery {
+
+ /**
+ * Constructor.
+ *
+ * @since 7.1.2
+ */
+ public function __construct() {
+ add_filter( 'wcdn_order_info_fields', array( $this, 'append_delivery_fields' ), 10, 2 );
+ }
+
+ /**
+ * Append delivery/pickup date and time to the order info fields.
+ *
+ * @param array $fields Order info fields.
+ * @param WC_Order $order Order object.
+ * @return array
+ * @since 7.1.2
+ */
+ public function append_delivery_fields( $fields, $order ) {
+
+ if ( ! $order instanceof WC_Order ) {
+ return $fields;
+ }
+
+ $delivery_date_settings = get_option( 'coderockz_woo_delivery_date_settings', array() );
+ $pickup_date_settings = get_option( 'coderockz_woo_delivery_pickup_date_settings', array() );
+ $delivery_time_settings = get_option( 'coderockz_woo_delivery_time_settings', array() );
+ $pickup_time_settings = get_option( 'coderockz_woo_delivery_pickup_settings', array() );
+
+ // Labels — fall back to translatable defaults.
+ $delivery_date_label = ! empty( $delivery_date_settings['field_label'] )
+ ? stripslashes( $delivery_date_settings['field_label'] )
+ : __( 'Delivery Date', 'woocommerce-delivery-notes' );
+
+ $delivery_time_label = ! empty( $delivery_time_settings['field_label'] )
+ ? stripslashes( $delivery_time_settings['field_label'] )
+ : __( 'Delivery Time', 'woocommerce-delivery-notes' );
+
+ $pickup_date_label = ! empty( $pickup_date_settings['pickup_field_label'] )
+ ? stripslashes( $pickup_date_settings['pickup_field_label'] )
+ : __( 'Pickup Date', 'woocommerce-delivery-notes' );
+
+ $pickup_time_label = ! empty( $pickup_time_settings['field_label'] )
+ ? stripslashes( $pickup_time_settings['field_label'] )
+ : __( 'Pickup Time', 'woocommerce-delivery-notes' );
+
+ // Date formats.
+ $delivery_date_format = ! empty( $delivery_date_settings['date_format'] ) ? $delivery_date_settings['date_format'] : 'F j, Y';
+ if ( ! empty( $delivery_date_settings['add_weekday_name'] ) ) {
+ $delivery_date_format = 'l ' . $delivery_date_format;
+ }
+
+ $pickup_date_format = ! empty( $pickup_date_settings['date_format'] ) ? $pickup_date_settings['date_format'] : 'F j, Y';
+ if ( ! empty( $pickup_date_settings['add_weekday_name'] ) ) {
+ $pickup_date_format = 'l ' . $pickup_date_format;
+ }
+
+ // Time formats.
+ $delivery_time_format = ( '24' === ( $delivery_time_settings['time_format'] ?? '12' ) ) ? 'H:i' : 'h:i A';
+ $pickup_time_format = ( '24' === ( $pickup_time_settings['time_format'] ?? '12' ) ) ? 'H:i' : 'h:i A';
+
+ // Delivery date.
+ $delivery_date_raw = $order->get_meta( 'delivery_date', true );
+ if ( ! empty( $delivery_date_raw ) ) {
+ $fields['coderockz_delivery_date'] = array(
+ 'label' => $delivery_date_label,
+ 'value' => date_i18n( $delivery_date_format, strtotime( $delivery_date_raw ) ),
+ );
+ }
+
+ // Delivery time.
+ $delivery_time_raw = $order->get_meta( 'delivery_time', true );
+ if ( ! empty( $delivery_time_raw ) ) {
+ $fields['coderockz_delivery_time'] = array(
+ 'label' => $delivery_time_label,
+ 'value' => self::format_time_slot( $delivery_time_raw, $delivery_time_format ),
+ );
+ }
+
+ // Pickup date.
+ $pickup_date_raw = $order->get_meta( 'pickup_date', true );
+ if ( ! empty( $pickup_date_raw ) ) {
+ $fields['coderockz_pickup_date'] = array(
+ 'label' => $pickup_date_label,
+ 'value' => date_i18n( $pickup_date_format, strtotime( $pickup_date_raw ) ),
+ );
+ }
+
+ // Pickup time.
+ $pickup_time_raw = $order->get_meta( 'pickup_time', true );
+ if ( ! empty( $pickup_time_raw ) ) {
+ $fields['coderockz_pickup_time'] = array(
+ 'label' => $pickup_time_label,
+ 'value' => self::format_time_slot( $pickup_time_raw, $pickup_time_format ),
+ );
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Format a time slot string (e.g. "08:00 - 09:00") using the given PHP time format.
+ *
+ * @param string $raw Raw time value from order meta.
+ * @param string $format PHP date format string.
+ * @return string
+ * @since 7.1.2
+ */
+ private static function format_time_slot( $raw, $format ) {
+ $parts = explode( ' - ', $raw );
+ if ( ! isset( $parts[1] ) ) {
+ return date_i18n( $format, strtotime( $parts[0] ) );
+ }
+ return date_i18n( $format, strtotime( $parts[0] ) ) . ' - ' . date_i18n( $format, strtotime( $parts[1] ) );
+ }
+}
--- a/woocommerce-delivery-notes/includes/integrations/class-deposits-partial-payments.php
+++ b/woocommerce-delivery-notes/includes/integrations/class-deposits-partial-payments.php
@@ -0,0 +1,97 @@
+<?php
+/**
+ * Print Invoice & Delivery Notes for WooCommerce.
+ *
+ * Deposits & Partial Payments for WooCommerce Pro Integration.
+ *
+ * @author Tyche Softwares
+ * @package WCDN/Integrations
+ * @category Classes
+ * @since 7.1.3
+ */
+
+namespace TycheWCDNIntegrations;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Deposits integration class.
+ *
+ * Adds Deposit and Future Payments rows to WCDN document totals
+ * when the Deposits & Partial Payments for WooCommerce Pro plugin is active.
+ *
+ * @since 7.1.2
+ */
+class Deposits_Partial_Payments {
+
+ /**
+ * Constructor.
+ *
+ * @since 7.1.2
+ */
+ public function __construct() {
+ add_filter( 'wcdn_order_totals', array( $this, 'append_deposit_totals' ), 10, 3 );
+ }
+
+ /**
+ * Whether a WC_Order has deposit data from the awcdp plugin.
+ *
+ * @param WC_Order $wc_order Order object.
+ * @return bool
+ */
+ private function has_deposit( $wc_order ) {
+ return 'yes' === $wc_order->get_meta( '_awcdp_deposits_order_has_deposit', true );
+ }
+
+ /**
+ * Append Deposit and Future Payments rows to the totals array,
+ * mirroring the values shown on the Edit Order page.
+ *
+ * @param array $totals Existing totals array.
+ * @param WC_Order|null $wc_order Order object.
+ * @param string $_template Template key — required by hook signature, not used here.
+ * @return array
+ */
+ // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- required by wcdn_order_totals filter signature.
+ public function append_deposit_totals( $totals, $wc_order, $_template ) {
+ if ( ! $wc_order instanceof WC_Order ) {
+ return $totals;
+ }
+
+ if ( ! $this->has_deposit( $wc_order ) ) {
+ return $totals;
+ }
+
+ $currency = $wc_order->get_currency();
+ $deposit = floatval( $wc_order->get_meta( '_awcdp_deposits_deposit_amount', true ) );
+
+ // _awcdp_deposits_second_payment is the scheduled future-payment total written at checkout
+ // and is always persisted. awcdp_deposits_balance_amount is updated post-save via a hook
+ // without a follow-up save(), so it can hold a stale full-order-total value.
+ $balance = floatval( $wc_order->get_meta( '_awcdp_deposits_second_payment', true ) );
+
+ if ( $deposit > 0 ) {
+ $totals['awcdp_deposit'] = wc_price( $deposit, array( 'currency' => $currency ) );
+ }
+
+ if ( $balance > 0 ) {
+ $totals['awcdp_future_payments'] = wc_price( $balance, array( 'currency' => $currency ) );
+ }
+
+ // Remove any "Partial Payment for order X" fee lines AWCDP added to the main order —
+ // they duplicate the deposit/balance rows we just added above.
+ if ( ! empty( $totals['fee_lines'] ) ) {
+ $totals['fee_lines'] = array_values(
+ array_filter(
+ $totals['fee_lines'],
+ function ( $fee ) {
+ return 0 !== strpos( $fee['label'], 'Partial Payment for order' );
+ }
+ )
+ );
+ }
+
+ return $totals;
+ }
+
+}
--- a/woocommerce-delivery-notes/includes/integrations/class-dfw-deposits.php
+++ b/woocommerce-delivery-notes/includes/integrations/class-dfw-deposits.php
@@ -0,0 +1,99 @@
+<?php
+/**
+ * Print Invoice & Delivery Notes for WooCommerce.
+ *
+ * Deposits For WooCommerce Integration.
+ *
+ * @author Tyche Softwares
+ * @package WCDN/Integrations
+ * @category Classes
+ * @since 7.1.3
+ */
+
+namespace TycheWCDNIntegrations;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * DFW Deposits integration class.
+ *
+ * Adds Deposit and Future Payment rows to WCDN document totals
+ * when the Deposits For WooCommerce plugin is active.
+ *
+ * @since 7.1.2
+ */
+class DFW_Deposits {
+
+ /**
+ * Constructor.
+ *
+ * @since 7.1.2
+ */
+ public function __construct() {
+ add_filter( 'wcdn_order_totals', array( $this, 'append_deposit_totals' ), 10, 3 );
+ }
+
+ /**
+ * Append Deposit, Future Payment, and Total Cart Amount rows to the totals array,
+ * mirroring the values shown on the Edit Order page.
+ *
+ * Handles two DFW deposit flows:
+ * - Order-level (cart deposit): reads _deposit and _future_payments order meta.
+ * - Item-level (per-product deposit): sums full_amount − line_subtotal per deposit item
+ * and computes the total cart amount (full_amount + non-deposit subtotals + shipping − discounts).
+ *
+ * @param array $totals Existing totals array.
+ * @param WC_Order|null $wc_order Order object.
+ * @param string $_template Template key — required by hook signature, not used here.
+ * @return array
+ */
+ public function append_deposit_totals( $totals, $wc_order, $_template ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- required by wcdn_order_totals filter signature.
+ if ( ! $wc_order instanceof WC_Order ) {
+ return $totals;
+ }
+
+ $currency = $wc_order->get_currency();
+
+ // Order-level deposit (cart deposit flow saves _has_order_deposit meta).
+ if ( DFW_Manage_Orders::dfw_has_order_deposit( $wc_order ) ) {
+ $deposit = $wc_order->get_meta( '_deposit' );
+ $future = $wc_order->get_meta( '_future_payments' );
+
+ if ( is_numeric( $deposit ) && $deposit > 0 ) {
+ $totals['dfw_deposit'] = wc_price( $deposit, array( 'currency' => $currency ) );
+ }
+
+ if ( is_numeric( $future ) && $future > 0 ) {
+ $totals['dfw_future_payment'] = wc_price( $future, array( 'currency' => $currency ) );
+ }
+
+ return $totals;
+ }
+
+ // Item-level deposit (per-product deposit flow stores has_deposit on each item).
+ if ( DFW_Manage_Orders::has_deposit( $wc_order ) ) {
+ $remaining = 0.0;
+ $total_cart = 0.0;
+
+ foreach ( $wc_order->get_items() as $item ) {
+ if ( ! empty( $item['has_deposit'] ) && isset( $item['full_amount'] ) ) {
+ $full = floatval( $item['full_amount'] );
+ $remaining += $full - $wc_order->get_line_subtotal( $item, false );
+ $total_cart += $full;
+ } else {
+ $total_cart += $wc_order->get_line_subtotal( $item, false );
+ }
+ }
+
+ $total_cart += $wc_order->get_shipping_total();
+ $total_cart -= $wc_order->get_discount_total();
+
+ if ( $remaining > 0 ) {
+ $totals['dfw_future_payment'] = wc_price( $remaining, array( 'currency' => $currency ) );
+ $totals['dfw_total_cart_amount'] = wc_price( $total_cart, array( 'currency' => $currency ) );
+ }
+ }
+
+ return $totals;
+ }
+}
--- a/woocommerce-delivery-notes/includes/integrations/class-emails.php
+++ b/woocommerce-delivery-notes/includes/integrations/class-emails.php
@@ -121,9 +121,6 @@
foreach ( $templates as $template ) {
- $file_name = $order->get_meta( '_wcdn_' . $template . '_pdf' );
- $file_path = $upload_dir['basedir'] . '/wcdn/' . $template . '/' . $file_name;
-
// Template enabled.
if ( ! Templates::get( $template, 'enabled' ) ) {
continue;
@@ -134,7 +131,23 @@
continue;
}
- if ( ! $file_name || ! $filesystem->exists( $file_path ) ) {
+ // Resolve all attachment targets before generating the PDF.
+ // Generating a PDF unconditionally loads fonts and can exhaust
+ // memory on sites with large locale fonts (e.g. CJK scripts).
+ $attach_customer = $this->should_attach_template_for_customer( $template, $email_id, $order );
+ $attach_admin = (bool) Templates::get( $template, 'attachAdminEmail' );
+ $attach_custom = (bool) Templates::get( $template, 'attachCustomEmails' );
+
+ $file_name = $order->get_meta( '_wcdn_' . $template . '_pdf' );
+ $file_path = $upload_dir['basedir'] . '/wcdn/' . $template . '/' . $file_name;
+ $has_cached = $file_name && $filesystem->exists( $file_path );
+
+ // Skip generation entirely when no one needs the PDF.
+ if ( ! $has_cached && ! $attach_customer && ! $attach_admin && ! $attach_custom ) {
+ continue;
+ }
+
+ if ( ! $has_cached ) {
/**
* Build document data.
*/
@@ -156,19 +169,17 @@
);
}
- if ( $this->should_attach_template_for_customer( $template, $email_id, $order ) ) {
- if ( $filesystem->exists( $file_path ) ) {
- $attachments[] = $file_path;
- }
+ if ( $attach_customer && $filesystem->exists( $file_path ) ) {
+ $attachments[] = $file_path;
}
// To all Administrators.
- if ( Templates::get( $template, 'attachAdminEmail' ) ) {
+ if ( $attach_admin ) {
$this->send_to_custom_email( wcdn_get_all_administrator_emails(), $order, $file_path, $template );
}
// Custom Email Addresses.
- if ( Templates::get( $template, 'attachCustomEmails' ) ) {
+ if ( $attach_custom ) {
$this->send_to_custom_email( Templates::get( $template, 'customEmailAddresses' ), $order, $file_path, $template );
}
}
@@ -338,7 +349,8 @@
true
);
- $label = Utils::get_label_for_template_type( $type );
+ $label_data = Utils::template_label( $type );
+ $document_name = $label_data['labels']['name'] ?? $type;
// Customer email.
if ( $show_customer_email_link && ! $sent_to_admin ) {
@@ -348,7 +360,7 @@
$this->print_link_in_email(
$plain_text,
$url,
- Settings::get( 'customerEmailText' ) . ' ' . $label
+ Settings::get( 'customerEmailText' ) . ' ' . $document_name
);
}
}
@@ -359,7 +371,7 @@
$this->print_link_in_email(
$plain_text,
$url,
- Settings::get( 'adminEmailText' ) . ' ' . $label
+ Settings::get( 'adminEmailText' ) . ' ' . $document_name
);
}
}
@@ -385,30 +397,8 @@
return;
}
- ?>
-<p>
- <strong>
- <?php
- echo esc_html(
- apply_filters(
- 'wcdn_print_text_in_email',
- __( 'Print:', 'woocommerce-delivery-notes' )
- )
- );
- ?>
- </strong>
-
- <a href="<?php echo esc_url( $url ); ?>">
- <?php
- echo esc_html(
- apply_filters(
- 'wcdn_print_view_in_browser_text_in_email',
- $link_text
- )
- );
- ?>
- </a>
-</p>
- <?php
+ $text = esc_html( apply_filters( 'wcdn_print_view_in_browser_text_in_email', $link_text ) );
+ $href = esc_url( $url );
+ echo wp_kses_post( '<p><a href="' . $href . '">' . $text . '</a></p>' );
}
}
No newline at end of file
--- a/woocommerce-delivery-notes/includes/integrations/class-local-pickup-plus.php
+++ b/woocommerce-delivery-notes/includes/integrations/class-local-pickup-plus.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Print Invoice & Delivery Notes for WooCommerce.
+ *
+ * WooCommerce Local Pickup Plus Integration.
+ *
+ * @author Tyche Softwares
+ * @package WCDN/Integrations
+ * @category Classes
+ * @since 7.1.3
+ */
+
+namespace TycheWCDNIntegrations;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Local Pickup Plus integration class.
+ *
+ * Adds pickup location details (name, address, phone, appointment)
+ * to the WCDN order info fields section for orders using the
+ * WooCommerce Local Pickup Plus shipping method (v2.0+).
+ *
+ * @since 7.1.2
+ */
+class Local_Pickup_Plus {
+
+ /**
+ * Constructor.
+ *
+ * @since 7.1.2
+ */
+ public function __construct() {
+ add_filter( 'wcdn_order_info_fields', array( $this, 'append_pickup_fields' ), 10, 2 );
+ }
+
+ /**
+ * Append pickup location details to the order info fields.
+ *
+ * Uses the plugin's own Orders handler to retrieve pre-formatted
+ * pickup data and maps each label/value pair into the WCDN fields array.
+ * Supports multiple pickup packages via key suffixes.
+ *
+ * @param array $fields Order info fields.
+ * @param WC_Order $order Order object.
+ * @return array
+ * @since 7.1.2
+ */
+ public function append_pickup_fields( $fields, $order ) {
+
+ if ( ! $order instanceof WC_Order || ! class_exists( 'WC_Local_Pickup_Plus_Orders' ) ) {
+ return $fields;
+ }
+
+ $pickup_handler = new WC_Local_Pickup_Plus_Orders();
+ $pickup_locations = $pickup_handler->get_order_pickup_data( $order );
+
+ if ( empty( $pickup_locations ) ) {
+ return $fields;
+ }
+
+ $package_index = 0;
+
+ foreach ( $pickup_locations as $pickup_meta ) {
+ $suffix = $package_index > 0 ? '_' . $package_index : '';
+ ++$package_index;
+
+ foreach ( $pickup_meta as $label => $value ) {
+ $key = 'pickup_' . sanitize_key( $label ) . $suffix;
+ $fields[ $key ] = array(
+ 'label' => $label,
+ 'value' => wp_strip_all_tags( $value ),
+ );
+ }
+ }
+
+ return $fields;
+ }
+}
--- a/woocommerce-delivery-notes/includes/services/class-pdf.php
+++ b/woocommerce-delivery-notes/includes/services/class-pdf.php
@@ -244,8 +244,8 @@
'600' => 'Inter-SemiBold.ttf',
'700' => 'Inter-Bold.ttf',
);
- foreach ( $inter_weights as $weight => $filename ) {
- $path = $inter_dir . $filename;
+ foreach ( $inter_weights as $weight => $font_file ) {
+ $path = $inter_dir . $font_file;
if ( file_exists( $path ) ) {
$dompdf->getFontMetrics()->registerFont(
array(
@@ -261,7 +261,7 @@
$dompdf->loadHtml( $html, 'UTF-8' );
- $paper_size = apply_filters( 'wcdn_pdf_paper_size', 'A4', $order_id );
+ $paper_size = apply_filters( 'wcdn_pdf_paper_size', Settings::get( 'pdfPaperSize' ), $order_id );
$orientation = apply_filters( 'wcdn_pdf_orientation', 'portrait', $order_id );
$dompdf->setPaper( $paper_size, $orientation );
@@ -308,10 +308,48 @@
true
);
+ $base = preg_replace( '/.pdf$/i', '', $filename );
+ $token = $this->get_secure_token( $order_id, $template, $order );
+ $filename = $base . '-' . $token . '.pdf';
+
return apply_filters( 'wcdn_pdf_filename', $filename, $order_id, $template );
}
/**
+ * Get or create a cryptographic token for a given order and template.
+ *
+ * The token is generated once using random_bytes and stored in order meta
+ * so the filename remains stable across subsequent calls while staying
+ * unguessable even when the order number is known.
+ *
+ * @param int $order_id Order ID.
+ * @param string $template Template key.
+ * @param WC_Order|null $order Optional pre-loaded order object.
+ * @return string 24-character lowercase hex string.
+ * @since 7.1.2
+ */
+ protected function get_secure_token( $order_id, $template, $order = null ) {
+
+ if ( null === $order ) {
+ $order = wc_get_order( $order_id );
+ }
+
+ $meta_key = '_wcdn_' . $template . '_pdf_token';
+ $token = $order ? $order->get_meta( $meta_key ) : '';
+
+ if ( ! $token ) {
+ $token = bin2hex( random_bytes( 12 ) );
+
+ if ( $order ) {
+ $order->update_meta_data( $meta_key, $token );
+ $order->save();
+ }
+ }
+
+ return $token;
+ }
+
+ /**
* Get PDF URL.
*
* @param int $order_id Order ID.
@@ -376,17 +414,21 @@
$filesystem->put_contents( $index_file, '', FS_CHMOD_FILE );
}
- // Apache protection.
+ // Apache protection — prevent directory listing only.
+ // PDF filenames include a cryptographic token so direct URLs are
+ // unguessable; blocking them via FilesMatch would break the download
+ // button on Apache hosts.
$htaccess_file = trailingslashit( $dir ) . '.htaccess';
+ $correct_rules = "Options -Indexesn";
- if ( ! $filesystem->exists( $htaccess_file ) ) {
-
- $rules = "Options -Indexesn";
- $rules .= "<FilesMatch "\.pdf$">n";
- $rules .= "Require all deniedn";
- $rules .= '</FilesMatch>';
-
- $filesystem->put_contents( $htaccess_file, $rules, FS_CHMOD_FILE );
+ if ( $filesystem->exists( $htaccess_file ) ) {
+ // Heal existing files that contain the old deny rule.
+ $current = $filesystem->get_contents( $htaccess_file );
+ if ( false !== strpos( $current, 'Require all denied' ) ) {
+ $filesystem->put_contents( $htaccess_file, $correct_rules, FS_CHMOD_FILE );
+ }
+ } else {
+ $filesystem->put_contents( $htaccess_file, $correct_rules, FS_CHMOD_FILE );
}
}
}
--- a/woocommerce-delivery-notes/includes/services/template/class-template-engine.php
+++ b/woocommerce-delivery-notes/includes/services/template/class-template-engine.php
@@ -389,6 +389,7 @@
self::$layouts_cache = array(
'invoice' => array(
+ 'documentZoom',
'logo',
'documentTitle',
'shopName',
@@ -408,6 +409,7 @@
'shippingMethod',
'showProductCharges',
'showProductImages',
+ 'productName',
'payNow',
'customerNote',
'complimentaryClose',
@@ -415,6 +417,7 @@
'footer',
),
'receipt' => array(
+ 'documentZoom',
'logo',
'documentTitle',
'shopName',
@@ -435,6 +438,7 @@
'shippingMethod',
'showProductCharges',
'showProductImages',
+ 'productName',
'watermark',
'customerNote',
'complimentaryClose',
@@ -442,6 +446,7 @@
'footer',
),
'deliverynote' => array(
+ 'documentZoom',
'logo',
'documentTitle',
'shopName',
@@ -458,15 +463,18 @@
'orderNumber',
'orderDate',
'shippingMethod',
+ 'paymentMethod',
'displayPriceInProductDetailsTable',
'showProductCharges',
'showProductIma