getDefaultTabsArray(); // Apply filter to allow pro plugins to modify tabs $all_tabs = apply_filters('easy_invoice_invoice_form_tabs', $all_tabs); // Register all tabs from the array foreach ($all_tabs as $tab_id => $tab_config) { $this->registerTab($tab_id, $tab_config); } } /** * Get default tabs array * * @since 1.0.0 * @return array */ private function getDefaultTabsArray(): array { return [ 'invoice-tab' => [ 'label' => __('Invoice Details', 'easy-invoice'), 'icon' => 'fas fa-file-invoice', 'active' => true, 'order' => 1 ], 'items-tab' => [ 'label' => __('Items', 'easy-invoice'), 'icon' => 'fas fa-list', 'active' => false, 'order' => 2 ], 'client-tab' => [ 'label' => __('Client', 'easy-invoice'), 'icon' => 'fas fa-user', 'active' => false, 'order' => 3 ], 'discounts-taxes-tab' => [ 'label' => __('Discounts & Taxes', 'easy-invoice'), 'icon' => 'fas fa-percentage', 'active' => false, 'order' => 4 ], 'payments-tab' => [ 'label' => __('Payment', 'easy-invoice'), 'icon' => 'fas fa-credit-card', 'active' => false, 'order' => 5 ] ]; } /** * Register default fields * * @since 1.0.0 * @return void */ public function registerDefaultFields(): void { // Define all fields in a single array for easy modification by pro plugins $all_fields = $this->getDefaultFieldsArray(); // Gate the basic tax fields on the global Settings → Tax → // Enable Tax setting. When tax is globally off, the entire // tax block (override checkbox, tax rate, prices include tax) // is hidden from the invoice builder — the merchant has to // turn tax on globally before any tax controls surface here. // discount_calculation_method stays (it lives in the discount // section, controls discount timing, not tax). if (get_option('easy_invoice_tax_enabled', 'no') !== 'yes') { $tax_field_names = ['tax_enabled', 'tax_rate', 'prices_include_tax']; $all_fields = array_values(array_filter( $all_fields, static fn($f) => !in_array($f['name'] ?? '', $tax_field_names, true) )); } // Apply filter to allow pro plugins to modify fields $all_fields = apply_filters('easy_invoice_invoice_fields', $all_fields); // Register all fields from the array foreach ($all_fields as $field_config) { $this->registerField($field_config['tab_id'], $field_config); } } /** * Get default fields array * * @since 1.0.0 * @return array */ private function getDefaultFieldsArray(): array { return [ // Invoice Tab Fields [ 'tab_id' => 'invoice-tab', 'name' => 'invoice_template', 'type' => 'hidden', 'label' => __('Invoice Template', 'easy-invoice'), 'required' => false, 'grid_cols' => 'sm:col-span-6', 'order' => 0, 'default_value' => 'standard', 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => function($value) { return !empty(trim($value)) ? true : __('Invoice template is required.', 'easy-invoice'); } ], [ 'tab_id' => 'invoice-tab', 'name' => 'title', 'type' => 'text', 'label' => __('Invoice Title', 'easy-invoice'), 'placeholder' => __('Enter invoice title', 'easy-invoice'), 'required' => true, 'grid_cols' => 'sm:col-span-6', 'order' => 1, 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => function($value) { return !empty(trim($value)) ? true : __('Invoice title is required.', 'easy-invoice'); } ], [ 'tab_id' => 'invoice-tab', 'name' => 'description', 'type' => 'textarea', 'label' => __('Invoice Description', 'easy-invoice'), 'placeholder' => __('Enter invoice description or additional details', 'easy-invoice'), 'required' => false, 'grid_cols' => 'sm:col-span-6', 'rows' => 3, 'order' => 2, 'description' => __('Optional description or additional details about this invoice.', 'easy-invoice'), 'sanitize_callback' => 'sanitize_textarea_field', ], [ 'tab_id' => 'invoice-tab', 'name' => 'number', 'type' => 'text', 'label' => __('Invoice Number', 'easy-invoice'), 'placeholder' => __('INV-001', 'easy-invoice'), 'required' => true, 'readonly' => true, 'description' => __('This is the unique number for your invoice. It cannot be changed.', 'easy-invoice'), 'grid_cols' => 'sm:col-span-3', 'order' => 3, 'default_value' => function() { // For new invoices, show the next number without incrementing // For existing invoices, this will be overridden by the invoice object's getNumber() method if (class_exists('\\EasyInvoice\\Services\\InvoiceNumberService')) { $service = new \EasyInvoice\Services\InvoiceNumberService(); return $service->getNextNumber(); } return 'INV-' . str_pad(time(), 6, '0', STR_PAD_LEFT); }, 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => function($value) { return !empty(trim($value)) ? true : __('Invoice number is required.', 'easy-invoice'); } ], [ 'tab_id' => 'invoice-tab', 'name' => 'issue_date', 'type' => 'date', 'label' => __('Issue Date', 'easy-invoice'), 'required' => true, 'grid_cols' => 'sm:col-span-3', 'order' => 4, // Both date fields are `required` but shipped with no default, so a // brand-new invoice always failed its first save with "Issue Date is // required / Due Date is required" — while the live preview beside the // form was already showing dates, which made the error look wrong. // The quote form has always defaulted its date this way. // // current_time() rather than gmdate(): gmdate() uses the server timezone, // so a site ahead of or behind UTC could get yesterday's or tomorrow's // date on a new invoice. 'default_value' => current_time('Y-m-d'), 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => function($value) { return !empty($value) && strtotime($value) ? true : __('Please enter a valid issue date.', 'easy-invoice'); } ], [ 'tab_id' => 'invoice-tab', 'name' => 'due_date', 'type' => 'date', 'label' => __('Due Date', 'easy-invoice'), 'required' => true, 'grid_cols' => 'sm:col-span-3', 'order' => 5, // Defaults to issue date + the configured payment term // (Settings → Invoice → "Due days", default 30) rather than a // hardcoded value, so the prefilled date matches the terms the site // actually bills on. See the note on issue_date above. 'default_value' => gmdate('Y-m-d', strtotime( '+' . max(0, (int) get_option('easy_invoice_invoice_due_days', 30)) . ' days', strtotime(current_time('Y-m-d')) )), 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => function($value) { return !empty($value) && strtotime($value) ? true : __('Please enter a valid due date.', 'easy-invoice'); } ], [ 'tab_id' => 'invoice-tab', 'name' => 'status', 'type' => 'select', 'label' => __('Status', 'easy-invoice'), 'required' => true, 'grid_cols' => 'sm:col-span-3', 'options' => [ 'available' => __('Available', 'easy-invoice'), 'draft' => __('Draft', 'easy-invoice'), 'overdue' => __('Overdue', 'easy-invoice'), 'paid' => __('Paid', 'easy-invoice'), 'unpaid' => __('Unpaid', 'easy-invoice'), 'cancelled' => __('Cancelled', 'easy-invoice') ], 'order' => 6 ], // Customer tax identity. FormProcessor stores a field as // '_easy_invoice_' . $name, so these land on the meta keys // Services\TaxTreatment reads when deciding whether an invoice is a // cross-border reverse charge or an export. [ 'tab_id' => 'invoice-tab', 'name' => 'customer_vat_number', 'type' => 'text', 'label' => __('Customer VAT number', 'easy-invoice'), 'placeholder' => __('e.g. FR12345678901', 'easy-invoice'), 'grid_cols' => 'sm:col-span-3', 'section' => 'notes', 'order' => 4, 'description' => __('Needed to invoice a VAT-registered business in another country without charging VAT.', 'easy-invoice'), ], [ 'tab_id' => 'invoice-tab', 'name' => 'customer_country', 'type' => 'text', 'label' => __('Customer country', 'easy-invoice'), 'placeholder' => __('e.g. FR', 'easy-invoice'), 'grid_cols' => 'sm:col-span-3', 'section' => 'notes', 'order' => 5, 'description' => __('Two-letter country code.', 'easy-invoice'), ], [ 'tab_id' => 'invoice-tab', 'name' => 'notes', 'type' => 'textarea', 'label' => __('Notes', 'easy-invoice'), 'placeholder' => __('Additional notes for the client', 'easy-invoice'), 'grid_cols' => 'sm:col-span-6', 'rows' => 3, 'section' => 'notes', 'order' => 6, 'description' => __('These notes will be visible to your client on the invoice.', 'easy-invoice'), ], [ 'tab_id' => 'invoice-tab', 'name' => 'internal_notes', 'type' => 'textarea', 'label' => __('Internal Notes', 'easy-invoice'), 'placeholder' => __('Internal notes (not visible to client)', 'easy-invoice'), 'grid_cols' => 'sm:col-span-6', 'rows' => 3, 'section' => 'notes', 'order' => 7, 'description' => __('Only you and your team can see these notes. Clients will not see them.', 'easy-invoice'), ], [ 'tab_id' => 'invoice-tab', 'name' => 'terms', 'type' => 'textarea', 'label' => __('Payment Terms', 'easy-invoice'), 'placeholder' => __('Payment terms and conditions', 'easy-invoice'), 'grid_cols' => 'sm:col-span-6', 'rows' => 3, 'section' => 'notes', 'order' => 8, 'default_value' => function() { return \EasyInvoice\Controllers\SettingsController::getInvoiceTermsConditions(); }, 'description' => __('Specify the payment terms and conditions for this invoice.', 'easy-invoice'), ], // Client Tab Fields [ 'tab_id' => 'client-tab', 'name' => 'client_id', 'type' => 'select', 'label' => __('Select Client', 'easy-invoice'), 'required' => false, 'grid_cols' => 'sm:col-span-6', 'options' => $this->getClientOptions(), 'order' => 1, 'description' => __('Choose an existing client or add a new one.', 'easy-invoice'), ], // Discounts & Taxes Tab Fields [ 'tab_id' => 'discounts-taxes-tab', 'name' => 'discount_type', 'type' => 'select', 'label' => __('Discount Type', 'easy-invoice'), 'grid_cols' => 'sm:col-span-2', 'section' => 'discount', 'options' => [ 'none' => __('No Discount', 'easy-invoice'), 'percentage' => __('Percentage', 'easy-invoice'), 'fixed' => __('Fixed Amount', 'easy-invoice') ], 'order' => 1, 'description' => __('Choose how the discount should be applied to this invoice.', 'easy-invoice'), ], [ 'tab_id' => 'discounts-taxes-tab', 'name' => 'discount_value', 'type' => 'number', 'label' => __('Discount Value', 'easy-invoice'), 'placeholder' => '0.00', 'grid_cols' => 'sm:col-span-2', 'section' => 'discount', 'min' => '0', 'step' => '0.01', 'order' => 2, 'sanitize_callback' => 'floatval', 'validate_callback' => function($value) { return is_numeric($value) && $value >= 0 ? true : __('Discount value must be a non-negative number.', 'easy-invoice'); }, 'description' => __('Enter the discount amount or percentage for this invoice.', 'easy-invoice'), ], [ // Per-invoice "Enable Tax" override. // // Defaults to the global Settings → Tax → Enable Tax // checkbox value. The user can flip it for a specific // invoice — handy for tax-exempt customers (override // "on" globally → "off" here) or one-off taxable // invoices when tax is globally off. // // calculateTotals() in the Invoice model treats the // tax_rate as 0 when this is unchecked, regardless of // what value sits in the tax_rate field — so the rest // of this section can stay visible without applying // tax until the user opts in. 'tab_id' => 'discounts-taxes-tab', 'name' => 'tax_enabled', 'type' => 'checkbox', 'label' => __('Apply Tax to This Invoice', 'easy-invoice'), 'grid_cols' => 'sm:col-span-6', 'section' => 'tax', 'order' => 2.5, 'default_value' => function() { // Default to global setting. Saved values override. return get_option('easy_invoice_tax_enabled', 'no') === 'yes' ? 'yes' : 'no'; }, 'sanitize_callback' => function($value) { // Coerce any truthy input ('yes', '1', 1, true, 'on') // to 'yes'; anything else to 'no'. Stored as a // string so the field-save loop persists '' as 'no' // (PHP checkbox unchecked = field absent from POST). if ($value === 'yes' || $value === '1' || $value === 1 || $value === true || $value === 'on') return 'yes'; return 'no'; }, 'description' => __('Override the global tax setting for this invoice. Defaults to whatever is configured in Settings → Tax.', 'easy-invoice'), ], // Discount calculation timing — belongs in the discount // section (it controls when the discount applies relative // to tax, not how tax is calculated). Pinned next to the // Discount Type / Value fields above so the grouping is // intuitive. [ 'tab_id' => 'discounts-taxes-tab', 'name' => 'discount_calculation_method', 'type' => 'select', 'label' => __('Discount Timing', 'easy-invoice'), 'grid_cols' => 'sm:col-span-2', 'section' => 'discount', 'options' => [ 'before_tax' => __('Before Tax', 'easy-invoice'), 'after_tax' => __('After Tax', 'easy-invoice') ], 'order' => 3, 'description' => __('Apply discount before tax (reduces taxable amount) or after tax (tax on full amount)', 'easy-invoice'), ], // Tax sub-fields below sit on one row (2 + 2 + 2 = 6 cols) // so the user sees Tax Rate and Price Includes Tax // side-by-side under the Apply Tax checkbox. [ 'tab_id' => 'discounts-taxes-tab', 'name' => 'tax_rate', 'type' => 'number', 'label' => __('Tax Rate (%)', 'easy-invoice'), 'placeholder' => '0.00', 'grid_cols' => 'sm:col-span-3', 'section' => 'tax', 'min' => '0', 'max' => '100', 'step' => '0.01', 'order' => 4, 'default_value' => function() { return easy_invoice_get_tax_rate(); }, 'sanitize_callback' => 'floatval', 'validate_callback' => function($value) { return is_numeric($value) && $value >= 0 && $value <= 100 ? true : __('Tax rate must be between 0 and 100.', 'easy-invoice'); }, 'description' => __('The tax rate that will be applied to taxable items in this invoice.', 'easy-invoice'), ], [ 'tab_id' => 'discounts-taxes-tab', 'name' => 'prices_include_tax', 'type' => 'select', 'label' => __('Price Includes Tax', 'easy-invoice'), 'grid_cols' => 'sm:col-span-3', 'section' => 'tax', 'options' => [ 'no' => __('No', 'easy-invoice'), 'yes' => __('Yes', 'easy-invoice') ], 'order' => 5, 'description' => __('Choose whether the prices in this invoice include tax or not.', 'easy-invoice'), ], // Payments Tab Fields [ 'tab_id' => 'payments-tab', 'name' => 'payment_gateways', 'type' => 'payment_gateways', 'label' => __('Payment Gateways', 'easy-invoice'), 'grid_cols' => 'sm:col-span-12', 'section' => 'payment', 'order' => 1 ], [ 'tab_id' => 'payments-tab', 'name' => 'payment_gateways_toggle_state', 'type' => 'hidden', 'label' => __('Payment Gateways Toggle State', 'easy-invoice'), 'default_value' => '0', 'section' => 'payment', 'order' => 2 ], // Currency Section in Payment Tab [ 'tab_id' => 'payments-tab', 'name' => 'currency_code', 'type' => 'select', 'label' => __('Currency', 'easy-invoice'), 'grid_cols' => 'sm:col-span-3', 'section' => 'currency', 'options' => array_merge( ['global' => __('Use from Global Settings', 'easy-invoice')], \EasyInvoice\Helpers\CurrencyHelper::getCurrencyOptions() ), 'default_value' => 'global', 'order' => 2, 'description' => __('Select the currency for this invoice or use global settings.', 'easy-invoice'), ], [ 'tab_id' => 'payments-tab', 'name' => 'currency_position', 'type' => 'select', 'label' => __('Currency Symbol Position', 'easy-invoice'), 'grid_cols' => 'sm:col-span-3', 'section' => 'currency', 'options' => [ 'global' => __('Use from Global Settings', 'easy-invoice'), 'before' => __('Before Amount', 'easy-invoice'), 'after' => __('After Amount', 'easy-invoice'), ], 'default_value' => 'global', 'order' => 3, 'description' => __('Choose where to display the currency symbol or use global setting.', 'easy-invoice'), ], ]; } /** * Register default templates * * @since 1.0.0 * @return void */ public function registerDefaultTemplates(): void { // Define all templates in a single array for easy modification by pro plugins $all_templates = $this->getDefaultTemplatesArray(); // Apply filter to allow pro plugins to modify templates $all_templates = apply_filters('easy_invoice_invoice_templates', $all_templates); // Register all templates from the array foreach ($all_templates as $template_id => $template_config) { $this->registerTemplate($template_id, $template_config); } } /** * Get default templates array * * @since 1.0.0 * @return array */ private function getDefaultTemplatesArray(): array { // Check if this is the free version (no pro plugin active) $is_free_version = !easy_invoice_has_pro(); return [ 'standard' => [ 'name' => __('Standard', 'easy-invoice'), 'icon' => 'fas fa-file-invoice', 'description' => __('Clean and professional standard template', 'easy-invoice'), 'order' => 1, 'is_free' => true ], 'legacy' => [ 'name' => __('Legacy', 'easy-invoice'), 'icon' => 'fas fa-file-alt', 'description' => __('Clean, minimalist design with traditional business layout', 'easy-invoice'), 'order' => 2, 'is_free' => true ], 'professional' => [ 'name' => __('Professional', 'easy-invoice'), 'icon' => 'fas fa-briefcase', 'description' => __('Professional business template with modern design', 'easy-invoice'), 'order' => 3, 'is_free' => !$is_free_version ], 'modern' => [ 'name' => __('Modern', 'easy-invoice'), 'icon' => 'fas fa-rocket', 'description' => __('Modern and sleek design template', 'easy-invoice'), 'order' => 4, 'is_free' => !$is_free_version ] ]; } /** * Register default item fields * * @since 1.0.0 * @return void */ public function registerDefaultItemFields(): void { // Define all item fields in a single array for easy modification by pro plugins $all_item_fields = $this->getDefaultItemFieldsArray(); // Apply filter to allow pro plugins to modify item fields $all_item_fields = apply_filters('easy_invoice_invoice_item_fields', $all_item_fields); // Register all item fields from the array foreach ($all_item_fields as $field_config) { $this->registerItemField($field_config); } } /** * Get default item fields array * * @since 1.0.0 * @return array */ private function getDefaultItemFieldsArray(): array { $fields = [ [ 'name' => 'title', 'type' => 'text', 'label' => __('Item Title', 'easy-invoice'), 'placeholder' => __('Enter item title', 'easy-invoice'), 'required' => true, 'container_class' => 'sm:col-span-6', 'order' => 1, 'description' => __('A short name for this item (e.g. Service Fee, Product Name).', 'easy-invoice'), ], [ 'name' => 'description', 'type' => 'textarea', 'label' => __('Description', 'easy-invoice'), 'placeholder' => __('Enter item description', 'easy-invoice'), 'container_class' => 'sm:col-span-6', 'rows' => 3, 'order' => 2, 'description' => __('Details about this item for your client.', 'easy-invoice'), ], [ 'name' => 'quantity', 'type' => 'number', 'label' => __('Quantity', 'easy-invoice'), 'placeholder' => '0', 'required' => true, 'container_class' => 'sm:col-span-1', 'min' => '0', 'step' => '0.01', 'default_value' => '0', 'order' => 3, 'description' => __('How many units of this item are being billed?', 'easy-invoice'), ], [ 'name' => 'price', 'type' => 'number', 'label' => __('Price', 'easy-invoice'), 'placeholder' => '0.00', 'required' => true, 'container_class' => 'sm:col-span-1', 'min' => '0', 'step' => '0.01', 'default_value' => '0.00', 'order' => 4, 'description' => __('Unit price for this item.', 'easy-invoice'), ] ]; // Add adjust field only if setting allows it if (\EasyInvoice\Controllers\SettingsController::shouldShowInvoiceAdjustField()) { $fields[] = [ 'name' => 'adjust_percentage', 'type' => 'number', 'label' => __('Adjust (%)', 'easy-invoice'), 'placeholder' => '0.00', 'container_class' => 'sm:col-span-1', 'min' => '-100', 'max' => '1000', 'step' => '0.01', 'default_value' => '0.00', 'order' => 5, 'description' => __('Percentage adjustment to apply to this item (positive for markup, negative for discount).', 'easy-invoice'), ]; } // Add total field $fields[] = [ 'name' => 'total', 'type' => 'number', 'label' => __('Total', 'easy-invoice'), 'placeholder' => '0.00', 'container_class' => 'sm:col-span-1', 'min' => '0', 'step' => '0.01', 'default_value' => '0.00', 'readonly' => true, 'order' => \EasyInvoice\Controllers\SettingsController::shouldShowInvoiceAdjustField() ? 6 : 5, 'description' => __('Total amount for this item (Quantity x Price) with adjustment applied.', 'easy-invoice'), ]; // Add taxable field $fields[] = [ 'name' => 'taxable', 'type' => 'checkbox', 'label' => __('Taxable', 'easy-invoice'), 'container_class' => 'sm:col-span-6', 'default_value' => '1', 'order' => \EasyInvoice\Controllers\SettingsController::shouldShowInvoiceAdjustField() ? 7 : 6, 'description' => __('Check if this item is subject to tax.', 'easy-invoice'), ]; return $fields; } /** * Get client options for select field * * @since 1.0.0 * @return array */ private function getClientOptions(): array { $options = ['' => __('Select a client', 'easy-invoice')]; // Get clients from repository $client_repository = new \EasyInvoice\Repositories\ClientRepository(); $clients = $client_repository->all(); foreach ($clients as $client) { $client_name = $client->getBusinessClientName() ?: ($client->getFirstName() . ' ' . $client->getLastName()); $options[$client->getId()] = $client_name; } return $options; } /** * Get the filter name for tabs * * @since 1.0.0 * @return string */ protected function getTabFilterName(): string { return 'easy_invoice_invoice_register_tab'; } /** * Get the filter name for fields * * @since 1.0.0 * @return string */ protected function getFieldFilterName(): string { return 'easy_invoice_invoice_register_field'; } /** * Get the filter name for templates * * @since 1.0.0 * @return string */ protected function getTemplateFilterName(): string { return 'easy_invoice_invoice_register_template'; } /** * Get the filter name for item fields * * @since 1.0.0 * @return string */ protected function getItemFieldFilterName(): string { return 'easy_invoice_invoice_register_item_field'; } }