Changeset 3752964
- Timestamp:
- 10/18/2026 11:44:47 AM (4 weeks ago)
- Location:
- demfilz-sync-for-quickbooks/trunk
- Files:
-
- 1 added
- 8 edited
-
demfilz-sync-for-quickbooks.php (modified) (2 diffs)
-
includes/admin/class-dwtq-admin.php (modified) (1 diff)
-
includes/admin/views/mapping.php (modified) (2 diffs)
-
includes/class-dwtq-installer.php (modified) (1 diff)
-
includes/providers/class-dwtq-abstract-provider.php (modified) (1 diff)
-
includes/providers/class-dwtq-provider-quickbooks.php (modified) (4 diffs)
-
includes/sync/class-dwtq-gift-cards.php (added)
-
includes/sync/class-dwtq-inventory-sync.php (modified) (1 diff)
-
readme.txt (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
demfilz-sync-for-quickbooks/trunk/demfilz-sync-for-quickbooks.php
r3606810 r3752964 4 4 * Plugin URI: https://demfilz.com/demfilz-woo-to-quickbooks 5 5 * Description: Send WooCommerce orders, customers and stock to QuickBooks Online via the official Intuit API. Free manual sync; Pro adds automation. 6 * Version: 1.2. 06 * Version: 1.2.1 7 7 * Requires at least: 6.2 8 8 * Requires PHP: 7.2 … … 19 19 defined( 'ABSPATH' ) || exit; 20 20 21 define( 'DWTQ_VERSION', '1.2. 0' );21 define( 'DWTQ_VERSION', '1.2.1' ); 22 22 define( 'DWTQ_FILE', __FILE__ ); 23 23 define( 'DWTQ_DIR', plugin_dir_path( __FILE__ ) ); -
demfilz-sync-for-quickbooks/trunk/includes/admin/class-dwtq-admin.php
r3606810 r3752964 209 209 Dwtq_Settings::update( 210 210 array( 211 'default_item' => isset( $_POST['default_item'] ) ? sanitize_text_field( wp_unslash( $_POST['default_item'] ) ) : '', 212 'default_tax_code' => isset( $_POST['default_tax_code'] ) ? sanitize_text_field( wp_unslash( $_POST['default_tax_code'] ) ) : '', 213 'freight_tax_code' => isset( $_POST['freight_tax_code'] ) ? sanitize_text_field( wp_unslash( $_POST['freight_tax_code'] ) ) : '', 214 'sale_type' => isset( $_POST['sale_type'] ) ? sanitize_text_field( wp_unslash( $_POST['sale_type'] ) ) : 'Invoice', 211 'default_item' => isset( $_POST['default_item'] ) ? sanitize_text_field( wp_unslash( $_POST['default_item'] ) ) : '', 212 'default_tax_code' => isset( $_POST['default_tax_code'] ) ? sanitize_text_field( wp_unslash( $_POST['default_tax_code'] ) ) : '', 213 'freight_tax_code' => isset( $_POST['freight_tax_code'] ) ? sanitize_text_field( wp_unslash( $_POST['freight_tax_code'] ) ) : '', 214 'gift_card_item' => isset( $_POST['gift_card_item'] ) ? sanitize_text_field( wp_unslash( $_POST['gift_card_item'] ) ) : '', 215 'gift_card_tax_code' => isset( $_POST['gift_card_tax_code'] ) ? sanitize_text_field( wp_unslash( $_POST['gift_card_tax_code'] ) ) : '', 216 'sale_type' => isset( $_POST['sale_type'] ) ? sanitize_text_field( wp_unslash( $_POST['sale_type'] ) ) : 'Invoice', 215 217 ) 216 218 ); -
demfilz-sync-for-quickbooks/trunk/includes/admin/views/mapping.php
r3606810 r3752964 23 23 $dwtq_tax = Dwtq_Settings::get( 'default_tax_code' ); 24 24 $dwtq_freight = Dwtq_Settings::get( 'freight_tax_code' ); 25 $dwtq_gift_item = Dwtq_Settings::get( 'gift_card_item' ); 26 $dwtq_gift_tax = Dwtq_Settings::get( 'gift_card_tax_code' ); 25 27 26 28 if ( ! function_exists( 'dwtq_render_select' ) ) : … … 73 75 <td><?php dwtq_render_select( 'freight_tax_code', $dwtq_taxes, $dwtq_freight ); ?></td> 74 76 </tr> 77 <tr> 78 <th><?php esc_html_e( 'Gift card item', 'demfilz-sync-for-quickbooks' ); ?></th> 79 <td> 80 <?php dwtq_render_select( 'gift_card_item', $dwtq_items, $dwtq_gift_item ); ?> 81 <p class="description"><?php esc_html_e( 'Only needed if you sell gift cards or accept gift cards or store credit (Demfilz Gift Cards and Store Credit). In QuickBooks, create a Non-inventory or Service item named "Gift card" whose income account is a liability account, for example "Gift card liability". Gift cards sold post to it instead of income, and amounts paid by gift card or store credit post as a negative line for it, so your revenue and tax stay complete and the sale total matches what the customer was charged.', 'demfilz-sync-for-quickbooks' ); ?></p> 82 </td> 83 </tr> 84 <tr> 85 <th><?php esc_html_e( 'Gift card tax code', 'demfilz-sync-for-quickbooks' ); ?></th> 86 <td> 87 <?php dwtq_render_select( 'gift_card_tax_code', $dwtq_taxes, $dwtq_gift_tax ); ?> 88 <p class="description"><?php esc_html_e( 'Choose a code with no tax, such as Out of scope or Not reportable. Leave blank to use the item default.', 'demfilz-sync-for-quickbooks' ); ?></p> 89 </td> 90 </tr> 75 91 </table> 76 92 <p class="description"><?php esc_html_e( 'Free tier maps all lines to one tax code. Pro adds per-product / per-tax-class rules. Leave tax codes blank if your QuickBooks region does not use tax codes on sales.', 'demfilz-sync-for-quickbooks' ); ?></p> -
demfilz-sync-for-quickbooks/trunk/includes/class-dwtq-installer.php
r3606810 r3752964 80 80 public static function default_settings() { 81 81 return array( 82 'environment' => 'production', // Intuit environment: production or sandbox. 83 'sale_type' => 'Invoice', // Sale document type: Invoice, SalesReceipt or Estimate. 84 'default_item' => '', // QuickBooks item id used for line items. 85 'default_tax_code' => '', // QuickBooks tax code id. 86 'freight_tax_code' => '', // QuickBooks tax code id for shipping. 87 'auto_sync_status' => array( 'processing', 'completed' ), // Statuses that trigger auto sync in Pro. 82 'environment' => 'production', // Intuit environment: production or sandbox. 83 'sale_type' => 'Invoice', // Sale document type: Invoice, SalesReceipt or Estimate. 84 'default_item' => '', // QuickBooks item id used for line items. 85 'default_tax_code' => '', // QuickBooks tax code id. 86 'freight_tax_code' => '', // QuickBooks tax code id for shipping. 87 'gift_card_item' => '', // QuickBooks item id (its account is a liability) for gift cards sold and redeemed. 88 'gift_card_tax_code' => '', // QuickBooks tax code id for gift card lines (no tax). 89 'auto_sync_status' => array( 'processing', 'completed' ), // Statuses that trigger auto sync in Pro. 88 90 ); 89 91 } -
demfilz-sync-for-quickbooks/trunk/includes/providers/class-dwtq-abstract-provider.php
r3606810 r3752964 113 113 } 114 114 115 $response = wp_remote_request( $url, $args ); 116 if ( is_wp_error( $response ) ) { 117 return $response; 118 } 115 // QuickBooks throttles to 500 requests/minute per company and replies 429 with 116 // a Retry-After header. Wait and retry a couple of times so a brief throttle does 117 // not fail the sync. Waits are bounded, and larger in background (cron) context 118 // than interactive so a manual click never hangs for long. 119 $max_wait = function_exists( 'wp_doing_cron' ) && wp_doing_cron() ? 60 : 10; 120 $attempts = 0; 121 do { 122 $response = wp_remote_request( $url, $args ); 123 if ( is_wp_error( $response ) ) { 124 return $response; 125 } 126 $status = (int) wp_remote_retrieve_response_code( $response ); 127 if ( 429 !== $status || $attempts >= 2 ) { 128 break; 129 } 130 $wait = (int) wp_remote_retrieve_header( $response, 'retry-after' ); 131 if ( $wait <= 0 ) { 132 $wait = 5; 133 } 134 if ( $wait > $max_wait ) { 135 break; 136 } 137 ++$attempts; 138 sleep( $wait ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration, WordPressVIPMinimum.Performance.LowExpiryCacheTime.sleep -- Deliberate rate-limit backoff, bounded above. 139 } while ( true ); 119 140 120 141 $status = (int) wp_remote_retrieve_response_code( $response ); 121 142 $raw = wp_remote_retrieve_body( $response ); 122 143 $data = '' !== $raw ? json_decode( $raw, true ) : array(); 144 145 if ( 429 === $status ) { 146 $retry = (int) wp_remote_retrieve_header( $response, 'retry-after' ); 147 $msg = $retry > 0 148 /* translators: %d: number of seconds to wait. */ 149 ? sprintf( __( 'QuickBooks is rate-limiting requests (429). Please wait %d seconds and try again.', 'demfilz-sync-for-quickbooks' ), $retry ) 150 : __( 'QuickBooks is rate-limiting requests (429). Please wait a minute and try again.', 'demfilz-sync-for-quickbooks' ); 151 return new WP_Error( 152 'dwtq_api_rate_limit', 153 $msg, 154 array( 155 'status' => 429, 156 'retry_after' => $retry, 157 ) 158 ); 159 } 123 160 124 161 if ( $status < 200 || $status >= 300 ) { -
demfilz-sync-for-quickbooks/trunk/includes/providers/class-dwtq-provider-quickbooks.php
r3606810 r3752964 139 139 */ 140 140 protected function sale_lines( $order ) { 141 $lines = array(); 142 $tax_map = new Dwtq_Tax_Mapper(); 143 $item_ref = Dwtq_Settings::get( 'default_item' ); 141 $lines = array(); 142 $tax_map = new Dwtq_Tax_Mapper(); 143 $item_ref = Dwtq_Settings::get( 'default_item' ); 144 $gift_item = Dwtq_Settings::get( 'gift_card_item' ); 145 $gift_tax = Dwtq_Settings::get( 'gift_card_tax_code' ); 144 146 145 147 foreach ( $order->get_items() as $item ) { 146 $detail = array( 'ItemRef' => array( 'value' => $item_ref ) ); 147 $tax = $tax_map->code_uid_for_item( $order, $item ); 148 // A gift card being sold is money held for the customer: it goes on the gift card 149 // item, whose account is a liability, not on the income item. 150 $gift_sale = Dwtq_Gift_Cards::is_gift_card_sale( $item ); 151 $detail = array( 'ItemRef' => array( 'value' => $gift_sale ? $gift_item : $item_ref ) ); 152 $tax = $gift_sale ? $gift_tax : $tax_map->code_uid_for_item( $order, $item ); 148 153 if ( $tax ) { 149 154 $detail['TaxCodeRef'] = array( 'value' => $tax ); 150 155 } 156 $total = round( (float) $order->get_line_total( $item, false ), 2 ); 157 $face = $gift_sale ? Dwtq_Gift_Cards::face_value( $item, $total ) : $total; 151 158 $lines[] = array( 152 159 'DetailType' => 'SalesItemLineDetail', 153 'Amount' => round( (float) $order->get_line_total( $item, false ), 2 ),160 'Amount' => $face, 154 161 'Description' => $item->get_name(), 155 162 'SalesItemLineDetail' => $detail, 156 163 ); 164 165 // A card sold below its value (on sale or with a coupon): the difference is a 166 // discount, on the income item. 167 if ( $face > $total ) { 168 $amount = -1 * round( $face - $total, 2 ); 169 $detail = array( 170 'ItemRef' => array( 'value' => $item_ref ), 171 'Qty' => 1, 172 'UnitPrice' => $amount, 173 ); 174 if ( $gift_tax ) { 175 $detail['TaxCodeRef'] = array( 'value' => $gift_tax ); 176 } 177 $lines[] = array( 178 'DetailType' => 'SalesItemLineDetail', 179 'Amount' => $amount, 180 /* translators: %s: product name. */ 181 'Description' => sprintf( __( 'Discount on %s', 'demfilz-sync-for-quickbooks' ), $item->get_name() ), 182 'SalesItemLineDetail' => $detail, 183 ); 184 } 157 185 } 158 186 … … 171 199 } 172 200 201 // Fees / surcharges (e.g. a payment surcharge added as a WooCommerce fee). A negative 202 // fee (a discount-style fee) posts as a negative line. 203 $fee_tax = Dwtq_Settings::get( 'default_tax_code' ); 204 foreach ( $order->get_fees() as $fee ) { 205 $amount = round( (float) $fee->get_total(), 2 ); 206 if ( 0.0 === $amount ) { 207 continue; 208 } 209 $detail = array( 210 'ItemRef' => array( 'value' => $item_ref ), 211 'Qty' => 1, 212 'UnitPrice' => $amount, 213 ); 214 if ( $fee_tax ) { 215 $detail['TaxCodeRef'] = array( 'value' => $fee_tax ); 216 } 217 $lines[] = array( 218 'DetailType' => 'SalesItemLineDetail', 219 'Amount' => $amount, 220 'Description' => $fee->get_name() ? $fee->get_name() : __( 'Fee', 'demfilz-sync-for-quickbooks' ), 221 'SalesItemLineDetail' => $detail, 222 ); 223 } 224 225 // Amounts paid by gift card or store credit: a negative line on the gift card item, so 226 // the payment comes out of the gift card liability, the sale keeps its full revenue and 227 // tax, and the sale total matches the order total. 228 foreach ( Dwtq_Gift_Cards::payments( $order ) as $payment ) { 229 $amount = -1 * round( (float) $payment['amount'], 2 ); 230 $detail = array( 231 'ItemRef' => array( 'value' => $gift_item ), 232 'Qty' => 1, 233 'UnitPrice' => $amount, 234 ); 235 if ( $gift_tax ) { 236 $detail['TaxCodeRef'] = array( 'value' => $gift_tax ); 237 } 238 $lines[] = array( 239 'DetailType' => 'SalesItemLineDetail', 240 'Amount' => $amount, 241 'Description' => $payment['label'], 242 'SalesItemLineDetail' => $detail, 243 ); 244 } 245 173 246 return $lines; 174 247 } … … 185 258 if ( '' === (string) Dwtq_Settings::get( 'default_item' ) ) { 186 259 return new WP_Error( 'dwtq_no_item', __( 'Choose a default QuickBooks product/service on the Mapping tab before syncing.', 'demfilz-sync-for-quickbooks' ) ); 260 } 261 if ( '' === (string) Dwtq_Settings::get( 'gift_card_item' ) && Dwtq_Gift_Cards::order_uses_gift_cards( $order ) ) { 262 return new WP_Error( 'dwtq_no_gift_card_item', __( 'This order sells a gift card or was paid partly by gift card or store credit. Choose a gift card item (one whose account is a liability account) on the Mapping tab, then sync the order again.', 'demfilz-sync-for-quickbooks' ) ); 187 263 } 188 264 … … 281 357 282 358 /** 359 * Fetch every stock-tracked QuickBooks item in a few paginated calls and return 360 * a SKU => quantity-on-hand map. Lets a bulk stock sync match locally instead of 361 * one API query per product (which blows QuickBooks' per-minute throttle). 362 * 363 * @return array|WP_Error Map of SKU (string) => QtyOnHand (float). 364 */ 365 public function get_stock_map() { 366 $map = array(); 367 $start = 1; 368 $page = 1000; 369 // Cap pages so a runaway/huge catalogue cannot loop forever. 370 for ( $i = 0; $i < 50; $i++ ) { 371 $data = $this->query( "SELECT * FROM Item WHERE Type = 'Inventory' STARTPOSITION " . $start . ' MAXRESULTS ' . $page ); 372 if ( is_wp_error( $data ) ) { 373 return $data; 374 } 375 $items = isset( $data['Item'] ) && is_array( $data['Item'] ) ? $data['Item'] : array(); 376 foreach ( $items as $item ) { 377 if ( empty( $item['Sku'] ) || ! isset( $item['QtyOnHand'] ) ) { 378 continue; 379 } 380 $map[ (string) $item['Sku'] ] = (float) $item['QtyOnHand']; 381 } 382 if ( count( $items ) < $page ) { 383 break; 384 } 385 $start += $page; 386 } 387 return $map; 388 } 389 390 /** 283 391 * List tax codes (id => name). 284 392 * -
demfilz-sync-for-quickbooks/trunk/includes/sync/class-dwtq-inventory-sync.php
r3606810 r3752964 66 66 return $qty; 67 67 } 68 69 /** 70 * Bulk-pull stock for many products from ONE QuickBooks item map (no per-product 71 * API call). Fetches the SKU => quantity map once (unless passed in) and matches 72 * each product locally. Used by the Pro scheduled sweep and the "Sync all stock" 73 * button to stay under QuickBooks' per-minute throttle. 74 * 75 * @param int[]|null $product_ids Product ids to update (null = all published). 76 * @param array|null $map Pre-fetched SKU => qty map (null = fetch now). 77 * @return array|WP_Error { updated, skipped, missing } on success. 78 */ 79 public function pull_bulk( $product_ids = null, $map = null ) { 80 if ( null === $map ) { 81 $provider = Dwtq_Provider_Factory::make(); 82 if ( is_wp_error( $provider ) ) { 83 return $provider; 84 } 85 $map = $provider->get_stock_map(); 86 if ( is_wp_error( $map ) ) { 87 return $map; 88 } 89 } 90 91 if ( null === $product_ids ) { 92 $product_ids = wc_get_products( 93 array( 94 'status' => 'publish', 95 'limit' => -1, 96 'return' => 'ids', 97 ) 98 ); 99 } 100 101 $updated = 0; 102 $skipped = 0; 103 $missing = 0; 104 foreach ( (array) $product_ids as $pid ) { 105 $product = wc_get_product( $pid ); 106 if ( ! $product ) { 107 continue; 108 } 109 $sku = $product->get_sku(); 110 if ( '' === $sku ) { 111 ++$skipped; 112 continue; 113 } 114 if ( ! array_key_exists( $sku, $map ) ) { 115 ++$missing; 116 continue; 117 } 118 $product->set_manage_stock( true ); 119 $product->set_stock_quantity( $map[ $sku ] ); 120 $product->save(); 121 ++$updated; 122 } 123 124 return array( 125 'updated' => $updated, 126 'skipped' => $skipped, 127 'missing' => $missing, 128 ); 129 } 68 130 } -
demfilz-sync-for-quickbooks/trunk/readme.txt
r3644269 r3752964 6 6 Requires PHP: 7.2 7 7 Requires Plugins: woocommerce 8 Stable tag: 1.2. 08 Stable tag: 1.2.1 9 9 License: GPLv2 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 74 74 No. The free version handles connecting, pushing an order, customer sync and stock pull. You only need a QuickBooks Online company to connect to (a free trial or the Intuit sandbox company works). Pro is an optional separate add-on that unlocks automation. 75 75 76 = Does it handle gift cards and store credit? = 77 Yes, with Demfilz Gift Cards and Store Credit for WooCommerce. A gift card is a payment, not a discount. In QuickBooks, create a Non-inventory or Service item called "Gift card" whose income account is a liability account (for example "Gift card liability"), then choose it on the Mapping tab with a tax code that has no tax. Gift cards sold post to that item instead of income. When an order is paid partly by gift card or store credit, the sale keeps every product and shipping line with its tax, and the amount paid by card is a negative line for the gift card item, so the sale total matches what the customer paid at checkout. Until the item is chosen, orders that use gift cards are not synced and the log says why. 78 76 79 == Screenshots == 77 80 … … 82 85 83 86 == Changelog == 87 88 = 1.2.1 = 89 * New: gift card and store credit support for Demfilz Gift Cards and Store Credit for WooCommerce. Gift cards sold post to a gift card item whose account is a liability, and amounts paid by gift card or store credit post as a negative line for that item, so revenue and tax stay complete and the sale total matches the order total. Choose the item on the Mapping tab. Developers can report another gift card plugin's payments with the dwtq_gift_card_payments filter. 90 * A gift card sold below its value (on sale or with a coupon) posts its full value to the gift card account and the difference as a discount on income, so the account always holds what customers can spend. 91 * Fix: order fees (for example a payment surcharge) are now synced as lines on the default item with the default tax code, so an order with a fee no longer syncs below its order total. Negative (discount-style) fees post as negative lines. 92 * QuickBooks rate limits (429): a throttled request now waits for the time QuickBooks asks for and tries again, up to twice (up to 60 seconds in background jobs, 10 seconds when you click a button), then shows a clear "wait and try again" message instead of a raw API error. 93 * Stock can now be read for the whole catalogue from one paginated QuickBooks item list instead of one request per product (used by the Pro scheduled stock sync), which keeps large catalogues under the QuickBooks request limit. 84 94 85 95 = 1.2.0 = … … 110 120 == Upgrade Notice == 111 121 122 = 1.2.1 = 123 Adds gift card and store credit support. If you use Demfilz Gift Cards and Store Credit, choose a gift card item on the Mapping tab. 124 112 125 = 1.1.0 = 113 126 Adds supplier-bill sync (with Demfilz Auto Order) and connects Pro to live billing.
Note:
See TracChangeset
for help on using the changeset viewer.