| @@ -103,16 +103,33 @@ | ||
| 103 | 103 | // Set basic data |
| 104 | 104 | $invoice->setTitle($data['title'] ?? 'New Invoice'); |
| 105 | 105 | |
| 106 | 106 | // Auto-generate invoice number if not provided |
| 107 | - if ((!isset($data['number']) || empty($data['number'])) && | |
| 108 | - (!isset($data['invoice_number']) || empty($data['invoice_number']))) { | |
| 109 | - $invoice_number_service = easy_invoice_get_invoice_number_service(); | |
| 110 | - $data['number'] = $invoice_number_service->generateUniqueNumber(); | |
| 107 | + // A number the form sent is kept only if nobody has it yet (two | |
| 108 | + // builders opened at once are pre-filled with the same one); the | |
| 109 | + // counter moves past it, or a fresh number is issued. | |
| 110 | + $invoice_number_service = easy_invoice_get_invoice_number_service(); | |
| 111 | + $requested = (string) ($data['number'] ?? $data['invoice_number'] ?? ''); | |
| 112 | + $data['number'] = $invoice_number_service->claimOrGenerate($requested); | |
| 113 | + unset($data['invoice_number']); | |
| 114 | + | |
| 115 | + // Discount timing defaults to "before tax", the first choice the builder | |
| 116 | + // offers. Left empty, the model's arithmetic takes the after-tax branch, | |
| 117 | + // which is what API- and import-created invoices used to get. | |
| 118 | + if (empty($data['discount_calculation_method'])) { | |
| 119 | + $data['discount_calculation_method'] = 'before_tax'; | |
| 111 | 120 | } |
| 112 | - | |
| 121 | + | |
| 113 | 122 | // Set invoice data |
| 114 | 123 | $this->setInvoiceData($invoice, $data, false); |
| 124 | + | |
| 125 | + // A client was given but no customer details: take them from the | |
| 126 | + // client record now, so the object handed back (and the meta saved) | |
| 127 | + // already carries the name and email — callers that email straight | |
| 128 | + // after creating would otherwise see "client email is missing". | |
| 129 | + if (is_numeric($data['client_id'] ?? 0) && (int) ($data['client_id'] ?? 0) > 0 && (empty($data['customer_email']) || empty($data['customer_name'])) && method_exists($invoice, 'populateClientInfo')) { | |
| 130 | + $invoice->populateClientInfo(); | |
| 131 | + } | |
| 115 | 132 | |
| 116 | 133 | // Allow plugins to modify the invoice before saving |
| 117 | 134 | do_action('easy_invoice_invoice_before_create', $invoice, $data); |
| 118 | 135 | |
| @@ -185,11 +202,20 @@ | ||
| 185 | 202 | * @param int $customer_id The customer ID |
| 186 | 203 | * @return array Array of Invoice models |
| 187 | 204 | */ |
| 188 | 205 | public function findByCustomer($customer_id) { |
| 206 | + // Invoices record their client as _easy_invoice_client_id; the | |
| 207 | + // _easy_invoice_customer_id key was never written, so this lookup | |
| 208 | + // used to match nothing (the dashboard's "active clients" stayed 0). | |
| 189 | 209 | $args = [ |
| 190 | 210 | 'meta_query' => [ |
| 211 | + 'relation' => 'OR', | |
| 191 | 212 | [ |
| 213 | + 'key' => '_easy_invoice_client_id', | |
| 214 | + 'value' => (int) $customer_id, | |
| 215 | + 'compare' => '=', | |
| 216 | + ], | |
| 217 | + [ | |
| 192 | 218 | 'key' => '_easy_invoice_customer_id', |
| 193 | 219 | 'value' => $customer_id, |
| 194 | 220 | 'compare' => '=', |
| 195 | 221 | ], |
| @@ -286,8 +312,42 @@ | ||
| 286 | 312 | return apply_filters('easy_invoice_invoice_count', $count, $query_args); |
| 287 | 313 | } |
| 288 | 314 | |
| 289 | 315 | /** |
| 316 | + * Find an invoice by its invoice number (stored in post meta) | |
| 317 | + * | |
| 318 | + * @param string $number Invoice number (exact match) | |
| 319 | + * @return Invoice|null | |
| 320 | + */ | |
| 321 | + public function findByNumber(string $number) { | |
| 322 | + $args = [ | |
| 323 | + 'post_type' => $this->post_type, | |
| 324 | + 'post_status' => ['publish', 'draft', 'private', 'pending'], | |
| 325 | + 'posts_per_page' => 1, | |
| 326 | + 'meta_query' => [ | |
| 327 | + [ | |
| 328 | + 'key' => \EasyInvoice\Constants\InvoiceMetaKeys::NUMBER, | |
| 329 | + 'value' => $number, | |
| 330 | + 'compare' => '=', | |
| 331 | + ], | |
| 332 | + ], | |
| 333 | + 'orderby' => 'date', | |
| 334 | + 'order' => 'DESC', | |
| 335 | + ]; | |
| 336 | + | |
| 337 | + $query = new WP_Query($args); | |
| 338 | + if (!empty($query->posts)) { | |
| 339 | + $post = $query->posts[0]; | |
| 340 | + if ($post && $post->post_type === $this->post_type) { | |
| 341 | + $invoice = new Invoice($post); | |
| 342 | + $invoice->ensureProperSlug(); | |
| 343 | + return apply_filters('easy_invoice_invoice_found_by_number', $invoice, $number); | |
| 344 | + } | |
| 345 | + } | |
| 346 | + return null; | |
| 347 | + } | |
| 348 | + | |
| 349 | + /** | |
| 290 | 350 | * Find published invoice by ID |
| 291 | 351 | * |
| 292 | 352 | * @param int $id The invoice ID |
| 293 | 353 | * @return Invoice|null The invoice model or null if not found |
| @@ -361,14 +421,15 @@ | ||
| 361 | 421 | if (isset($data['invoice_title'])) { |
| 362 | 422 | $invoice->setTitle($data['invoice_title']); |
| 363 | 423 | } |
| 364 | 424 | |
| 365 | - if (isset($data['description'])) { | |
| 366 | - $invoice->setDescription($data['description']); | |
| 425 | + // Always update description if it exists in data (even if empty, to allow clearing) | |
| 426 | + if (array_key_exists('description', $data)) { | |
| 427 | + $invoice->setDescription($data['description'] ?? ''); | |
| 367 | 428 | } |
| 368 | 429 | |
| 369 | - if (isset($data['invoice_description'])) { | |
| 370 | - $invoice->setDescription($data['invoice_description']); | |
| 430 | + if (array_key_exists('invoice_description', $data)) { | |
| 431 | + $invoice->setDescription($data['invoice_description'] ?? ''); | |
| 371 | 432 | } |
| 372 | 433 | |
| 373 | 434 | // Set invoice number (only if not preserving existing) |
| 374 | 435 | if (isset($data['invoice_number']) && !empty($data['invoice_number'])) { |
| @@ -477,10 +538,12 @@ | ||
| 477 | 538 | if (isset($data['tax_rate'])) { |
| 478 | 539 | $invoice->setTaxRate($data['tax_rate']); |
| 479 | 540 | } |
| 480 | 541 | |
| 481 | - if (isset($data['calculation_method'])) { | |
| 482 | - $invoice->setCalculationMethod($data['calculation_method']); | |
| 542 | + if (isset($data['discount_calculation_method'])) { | |
| 543 | + $invoice->setDiscountCalculationMethod($data['discount_calculation_method']); | |
| 544 | + } elseif (isset($data['calculation_method'])) { | |
| 545 | + $invoice->setDiscountCalculationMethod($data['calculation_method']); | |
| 483 | 546 | } |
| 484 | 547 | |
| 485 | 548 | if (isset($data['prices_include_tax'])) { |
| 486 | 549 | $invoice->setPricesIncludeTax($data['prices_include_tax']); |