← All changes
|
includes/Forms/Invoice/InvoiceFieldRegistration.php
+112
-9
2.1.10
→
2.4.0
View file →
| @@ -90,12 +90,27 @@ | ||
| 90 | 90 | */ |
| 91 | 91 | public function registerDefaultFields(): void { |
| 92 | 92 | // Define all fields in a single array for easy modification by pro plugins |
| 93 | 93 | $all_fields = $this->getDefaultFieldsArray(); |
| 94 | - | |
| 94 | + | |
| 95 | + // Gate the basic tax fields on the global Settings → Tax → | |
| 96 | + // Enable Tax setting. When tax is globally off, the entire | |
| 97 | + // tax block (override checkbox, tax rate, prices include tax) | |
| 98 | + // is hidden from the invoice builder — the merchant has to | |
| 99 | + // turn tax on globally before any tax controls surface here. | |
| 100 | + // discount_calculation_method stays (it lives in the discount | |
| 101 | + // section, controls discount timing, not tax). | |
| 102 | + if (get_option('easy_invoice_tax_enabled', 'no') !== 'yes') { | |
| 103 | + $tax_field_names = ['tax_enabled', 'tax_rate', 'prices_include_tax']; | |
| 104 | + $all_fields = array_values(array_filter( | |
| 105 | + $all_fields, | |
| 106 | + static fn($f) => !in_array($f['name'] ?? '', $tax_field_names, true) | |
| 107 | + )); | |
| 108 | + } | |
| 109 | + | |
| 95 | 110 | // Apply filter to allow pro plugins to modify fields |
| 96 | 111 | $all_fields = apply_filters('easy_invoice_invoice_fields', $all_fields); |
| 97 | - | |
| 112 | + | |
| 98 | 113 | // Register all fields from the array |
| 99 | 114 | foreach ($all_fields as $field_config) { |
| 100 | 115 | $this->registerField($field_config['tab_id'], $field_config); |
| 101 | 116 | } |
| @@ -183,8 +198,18 @@ | ||
| 183 | 198 | 'label' => __('Issue Date', 'easy-invoice'), |
| 184 | 199 | 'required' => true, |
| 185 | 200 | 'grid_cols' => 'sm:col-span-3', |
| 186 | 201 | 'order' => 4, |
| 202 | + // Both date fields are `required` but shipped with no default, so a | |
| 203 | + // brand-new invoice always failed its first save with "Issue Date is | |
| 204 | + // required / Due Date is required" — while the live preview beside the | |
| 205 | + // form was already showing dates, which made the error look wrong. | |
| 206 | + // The quote form has always defaulted its date this way. | |
| 207 | + // | |
| 208 | + // current_time() rather than gmdate(): gmdate() uses the server timezone, | |
| 209 | + // so a site ahead of or behind UTC could get yesterday's or tomorrow's | |
| 210 | + // date on a new invoice. | |
| 211 | + 'default_value' => current_time('Y-m-d'), | |
| 187 | 212 | 'sanitize_callback' => 'sanitize_text_field', |
| 188 | 213 | 'validate_callback' => function($value) { |
| 189 | 214 | return !empty($value) && strtotime($value) ? true : __('Please enter a valid issue date.', 'easy-invoice'); |
| 190 | 215 | } |
| @@ -196,8 +221,16 @@ | ||
| 196 | 221 | 'label' => __('Due Date', 'easy-invoice'), |
| 197 | 222 | 'required' => true, |
| 198 | 223 | 'grid_cols' => 'sm:col-span-3', |
| 199 | 224 | 'order' => 5, |
| 225 | + // Defaults to issue date + the configured payment term | |
| 226 | + // (Settings → Invoice → "Due days", default 30) rather than a | |
| 227 | + // hardcoded value, so the prefilled date matches the terms the site | |
| 228 | + // actually bills on. See the note on issue_date above. | |
| 229 | + 'default_value' => gmdate('Y-m-d', strtotime( | |
| 230 | + '+' . max(0, (int) get_option('easy_invoice_invoice_due_days', 30)) . ' days', | |
| 231 | + strtotime(current_time('Y-m-d')) | |
| 232 | + )), | |
| 200 | 233 | 'sanitize_callback' => 'sanitize_text_field', |
| 201 | 234 | 'validate_callback' => function($value) { |
| 202 | 235 | return !empty($value) && strtotime($value) ? true : __('Please enter a valid due date.', 'easy-invoice'); |
| 203 | 236 | } |
| @@ -218,10 +251,36 @@ | ||
| 218 | 251 | 'cancelled' => __('Cancelled', 'easy-invoice') |
| 219 | 252 | ], |
| 220 | 253 | 'order' => 6 |
| 221 | 254 | ], |
| 255 | + // Customer tax identity. FormProcessor stores a field as | |
| 256 | + // '_easy_invoice_' . $name, so these land on the meta keys | |
| 257 | + // Services\TaxTreatment reads when deciding whether an invoice is a | |
| 258 | + // cross-border reverse charge or an export. | |
| 222 | 259 | [ |
| 223 | 260 | 'tab_id' => 'invoice-tab', |
| 261 | + 'name' => 'customer_vat_number', | |
| 262 | + 'type' => 'text', | |
| 263 | + 'label' => __('Customer VAT number', 'easy-invoice'), | |
| 264 | + 'placeholder' => __('e.g. FR12345678901', 'easy-invoice'), | |
| 265 | + 'grid_cols' => 'sm:col-span-3', | |
| 266 | + 'section' => 'notes', | |
| 267 | + 'order' => 4, | |
| 268 | + 'description' => __('Needed to invoice a VAT-registered business in another country without charging VAT.', 'easy-invoice'), | |
| 269 | + ], | |
| 270 | + [ | |
| 271 | + 'tab_id' => 'invoice-tab', | |
| 272 | + 'name' => 'customer_country', | |
| 273 | + 'type' => 'text', | |
| 274 | + 'label' => __('Customer country', 'easy-invoice'), | |
| 275 | + 'placeholder' => __('e.g. FR', 'easy-invoice'), | |
| 276 | + 'grid_cols' => 'sm:col-span-3', | |
| 277 | + 'section' => 'notes', | |
| 278 | + 'order' => 5, | |
| 279 | + 'description' => __('Two-letter country code.', 'easy-invoice'), | |
| 280 | + ], | |
| 281 | + [ | |
| 282 | + 'tab_id' => 'invoice-tab', | |
| 224 | 283 | 'name' => 'notes', |
| 225 | 284 | 'type' => 'textarea', |
| 226 | 285 | 'label' => __('Notes', 'easy-invoice'), |
| 227 | 286 | 'placeholder' => __('Additional notes for the client', 'easy-invoice'), |
| @@ -306,14 +365,54 @@ | ||
| 306 | 365 | }, |
| 307 | 366 | 'description' => __('Enter the discount amount or percentage for this invoice.', 'easy-invoice'), |
| 308 | 367 | ], |
| 309 | 368 | [ |
| 369 | + // Per-invoice "Enable Tax" override. | |
| 370 | + // | |
| 371 | + // Defaults to the global Settings → Tax → Enable Tax | |
| 372 | + // checkbox value. The user can flip it for a specific | |
| 373 | + // invoice — handy for tax-exempt customers (override | |
| 374 | + // "on" globally → "off" here) or one-off taxable | |
| 375 | + // invoices when tax is globally off. | |
| 376 | + // | |
| 377 | + // calculateTotals() in the Invoice model treats the | |
| 378 | + // tax_rate as 0 when this is unchecked, regardless of | |
| 379 | + // what value sits in the tax_rate field — so the rest | |
| 380 | + // of this section can stay visible without applying | |
| 381 | + // tax until the user opts in. | |
| 310 | 382 | 'tab_id' => 'discounts-taxes-tab', |
| 383 | + 'name' => 'tax_enabled', | |
| 384 | + 'type' => 'checkbox', | |
| 385 | + 'label' => __('Apply Tax to This Invoice', 'easy-invoice'), | |
| 386 | + 'grid_cols' => 'sm:col-span-6', | |
| 387 | + 'section' => 'tax', | |
| 388 | + 'order' => 2.5, | |
| 389 | + 'default_value' => function() { | |
| 390 | + // Default to global setting. Saved values override. | |
| 391 | + return get_option('easy_invoice_tax_enabled', 'no') === 'yes' ? 'yes' : 'no'; | |
| 392 | + }, | |
| 393 | + 'sanitize_callback' => function($value) { | |
| 394 | + // Coerce any truthy input ('yes', '1', 1, true, 'on') | |
| 395 | + // to 'yes'; anything else to 'no'. Stored as a | |
| 396 | + // string so the field-save loop persists '' as 'no' | |
| 397 | + // (PHP checkbox unchecked = field absent from POST). | |
| 398 | + if ($value === 'yes' || $value === '1' || $value === 1 || $value === true || $value === 'on') return 'yes'; | |
| 399 | + return 'no'; | |
| 400 | + }, | |
| 401 | + 'description' => __('Override the global tax setting for this invoice. Defaults to whatever is configured in Settings → Tax.', 'easy-invoice'), | |
| 402 | + ], | |
| 403 | + // Discount calculation timing — belongs in the discount | |
| 404 | + // section (it controls when the discount applies relative | |
| 405 | + // to tax, not how tax is calculated). Pinned next to the | |
| 406 | + // Discount Type / Value fields above so the grouping is | |
| 407 | + // intuitive. | |
| 408 | + [ | |
| 409 | + 'tab_id' => 'discounts-taxes-tab', | |
| 311 | 410 | 'name' => 'discount_calculation_method', |
| 312 | 411 | 'type' => 'select', |
| 313 | - 'label' => __('Calculation Method', 'easy-invoice'), | |
| 412 | + 'label' => __('Discount Timing', 'easy-invoice'), | |
| 314 | 413 | 'grid_cols' => 'sm:col-span-2', |
| 315 | - 'section' => 'tax', | |
| 414 | + 'section' => 'discount', | |
| 316 | 415 | 'options' => [ |
| 317 | 416 | 'before_tax' => __('Before Tax', 'easy-invoice'), |
| 318 | 417 | 'after_tax' => __('After Tax', 'easy-invoice') |
| 319 | 418 | ], |
| @@ -319,8 +418,12 @@ | ||
| 319 | 418 | ], |
| 320 | 419 | 'order' => 3, |
| 321 | 420 | 'description' => __('Apply discount before tax (reduces taxable amount) or after tax (tax on full amount)', 'easy-invoice'), |
| 322 | 421 | ], |
| 422 | + | |
| 423 | + // Tax sub-fields below sit on one row (2 + 2 + 2 = 6 cols) | |
| 424 | + // so the user sees Tax Rate and Price Includes Tax | |
| 425 | + // side-by-side under the Apply Tax checkbox. | |
| 323 | 426 | [ |
| 324 | 427 | 'tab_id' => 'discounts-taxes-tab', |
| 325 | 428 | 'name' => 'tax_rate', |
| 326 | 429 | 'type' => 'number', |
| @@ -340,9 +443,9 @@ | ||
| 340 | 443 | return is_numeric($value) && $value >= 0 && $value <= 100 ? true : __('Tax rate must be between 0 and 100.', 'easy-invoice'); |
| 341 | 444 | }, |
| 342 | 445 | 'description' => __('The tax rate that will be applied to taxable items in this invoice.', 'easy-invoice'), |
| 343 | 446 | ], |
| 344 | - | |
| 447 | + | |
| 345 | 448 | [ |
| 346 | 449 | 'tab_id' => 'discounts-taxes-tab', |
| 347 | 450 | 'name' => 'prices_include_tax', |
| 348 | 451 | 'type' => 'select', |
| @@ -352,9 +455,9 @@ | ||
| 352 | 455 | 'options' => [ |
| 353 | 456 | 'no' => __('No', 'easy-invoice'), |
| 354 | 457 | 'yes' => __('Yes', 'easy-invoice') |
| 355 | 458 | ], |
| 356 | - 'order' => 4, | |
| 459 | + 'order' => 5, | |
| 357 | 460 | 'description' => __('Choose whether the prices in this invoice include tax or not.', 'easy-invoice'), |
| 358 | 461 | ], |
| 359 | 462 | |
| 360 | 463 | // Payments Tab Fields |
| @@ -548,9 +651,9 @@ | ||
| 548 | 651 | ] |
| 549 | 652 | ]; |
| 550 | 653 | |
| 551 | 654 | // Add adjust field only if setting allows it |
| 552 | - if (\EasyInvoice\Controllers\SettingsController::shouldShowQuoteAdjustField()) { | |
| 655 | + if (\EasyInvoice\Controllers\SettingsController::shouldShowInvoiceAdjustField()) { | |
| 553 | 656 | $fields[] = [ |
| 554 | 657 | 'name' => 'adjust_percentage', |
| 555 | 658 | 'type' => 'number', |
| 556 | 659 | 'label' => __('Adjust (%)', 'easy-invoice'), |
| @@ -575,9 +678,9 @@ | ||
| 575 | 678 | 'min' => '0', |
| 576 | 679 | 'step' => '0.01', |
| 577 | 680 | 'default_value' => '0.00', |
| 578 | 681 | 'readonly' => true, |
| 579 | - 'order' => \EasyInvoice\Controllers\SettingsController::shouldShowQuoteAdjustField() ? 6 : 5, | |
| 682 | + 'order' => \EasyInvoice\Controllers\SettingsController::shouldShowInvoiceAdjustField() ? 6 : 5, | |
| 580 | 683 | 'description' => __('Total amount for this item (Quantity x Price) with adjustment applied.', 'easy-invoice'), |
| 581 | 684 | ]; |
| 582 | 685 | |
| 583 | 686 | // Add taxable field |
| @@ -586,9 +689,9 @@ | ||
| 586 | 689 | 'type' => 'checkbox', |
| 587 | 690 | 'label' => __('Taxable', 'easy-invoice'), |
| 588 | 691 | 'container_class' => 'sm:col-span-6', |
| 589 | 692 | 'default_value' => '1', |
| 590 | - 'order' => \EasyInvoice\Controllers\SettingsController::shouldShowQuoteAdjustField() ? 7 : 6, | |
| 693 | + 'order' => \EasyInvoice\Controllers\SettingsController::shouldShowInvoiceAdjustField() ? 7 : 6, | |
| 591 | 694 | 'description' => __('Check if this item is subject to tax.', 'easy-invoice'), |
| 592 | 695 | ]; |
| 593 | 696 | |
| 594 | 697 | return $fields; |