| @@ -177,8 +177,18 @@ | ||
| 177 | 177 | return apply_filters('easy_invoice_quotes_by_client', $filtered_quotes, $client_id); |
| 178 | 178 | } |
| 179 | 179 | |
| 180 | 180 | /** |
| 181 | + * Quotes for a client — the name QuoteService calls; it did not exist. | |
| 182 | + * | |
| 183 | + * @param int $customer_id Client id. | |
| 184 | + * @return array | |
| 185 | + */ | |
| 186 | + public function findByCustomer($customer_id): array { | |
| 187 | + return $this->findByClient((int) $customer_id); | |
| 188 | + } | |
| 189 | + | |
| 190 | + /** | |
| 181 | 191 | * Create new quote |
| 182 | 192 | * |
| 183 | 193 | * @since 1.0.0 |
| 184 | 194 | * @param array $data Quote data |
| @@ -188,9 +198,26 @@ | ||
| 188 | 198 | // Allow plugins to modify the data before creation |
| 189 | 199 | $data = apply_filters('easy_invoice_quote_create_data', $data); |
| 190 | 200 | |
| 191 | 201 | $quote = new Quote(); |
| 202 | + // Same default as invoices: discount before tax unless chosen otherwise. | |
| 203 | + if (empty($data['discount_calculation_method'])) { | |
| 204 | + $data['discount_calculation_method'] = 'before_tax'; | |
| 205 | + } | |
| 206 | + // Reserve a number under the number lock. Left to the form default, | |
| 207 | + // the model peeked at the next number without taking it, so quotes | |
| 208 | + // created at the same moment (or by two people with the builder | |
| 209 | + // open) shared one number. | |
| 210 | + $requested = (string) ($data['number'] ?? $data['quote_number'] ?? ''); | |
| 211 | + $data['number'] = (new \EasyInvoice\Services\QuoteNumberService())->claimOrGenerate($requested); | |
| 212 | + unset($data['quote_number']); | |
| 192 | 213 | $this->setQuoteData($quote, $data); |
| 214 | + | |
| 215 | + // Same as invoices: a client id without customer details is filled in | |
| 216 | + // from the client record before the first save. | |
| 217 | + if (($data['client_id'] ?? 0) > 0 && (empty($data['customer_email']) || empty($data['customer_name'])) && method_exists($quote, 'populateClientInfo')) { | |
| 218 | + $quote->populateClientInfo(); | |
| 219 | + } | |
| 193 | 220 | |
| 194 | 221 | // Allow plugins to modify the quote before saving |
| 195 | 222 | do_action('easy_invoice_quote_before_create', $quote, $data); |
| 196 | 223 | |
| @@ -263,8 +290,42 @@ | ||
| 263 | 290 | } |
| 264 | 291 | |
| 265 | 292 | return $result !== false; |
| 266 | 293 | } |
| 294 | + | |
| 295 | + /** | |
| 296 | + * Find a quote by its number (stored in post meta) | |
| 297 | + * | |
| 298 | + * @param string $number Quote number | |
| 299 | + * @return Quote|null | |
| 300 | + */ | |
| 301 | + public function findByNumber(string $number): ?Quote { | |
| 302 | + $args = [ | |
| 303 | + 'post_type' => PostTypes::EASY_INVOICE_QUOTE_POST_TYPE, | |
| 304 | + 'post_status' => ['publish', 'draft', 'private', 'pending'], | |
| 305 | + 'posts_per_page' => 1, | |
| 306 | + 'meta_query' => [ | |
| 307 | + [ | |
| 308 | + 'key' => '_easy_invoice_quote_number', | |
| 309 | + 'value' => $number, | |
| 310 | + 'compare' => '=', | |
| 311 | + ], | |
| 312 | + ], | |
| 313 | + 'orderby' => 'date', | |
| 314 | + 'order' => 'DESC', | |
| 315 | + ]; | |
| 316 | + | |
| 317 | + $posts = get_posts($args); | |
| 318 | + if (!empty($posts)) { | |
| 319 | + $post = $posts[0]; | |
| 320 | + if ($post && $post->post_type === PostTypes::EASY_INVOICE_QUOTE_POST_TYPE) { | |
| 321 | + $quote = new Quote($post); | |
| 322 | + $quote->ensureProperSlug(); | |
| 323 | + return apply_filters('easy_invoice_quote_found_by_number', $quote, $number); | |
| 324 | + } | |
| 325 | + } | |
| 326 | + return null; | |
| 327 | + } | |
| 267 | 328 | |
| 268 | 329 | /** |
| 269 | 330 | * Force publish quote (for URL fixes) |
| 270 | 331 | * |
| @@ -459,10 +520,11 @@ | ||
| 459 | 520 | if (isset($data['notes'])) { |
| 460 | 521 | $quote->setNotes($data['notes']); |
| 461 | 522 | } |
| 462 | 523 | |
| 463 | - if (isset($data['description'])) { | |
| 464 | - $quote->setDescription($data['description']); | |
| 524 | + // Always update description if it exists in data (even if empty, to allow clearing) | |
| 525 | + if (array_key_exists('description', $data)) { | |
| 526 | + $quote->setDescription($data['description'] ?? ''); | |
| 465 | 527 | } |
| 466 | 528 | |
| 467 | 529 | if (isset($data['terms'])) { |
| 468 | 530 | $quote->setTerms($data['terms']); |
| @@ -501,8 +563,17 @@ | ||
| 501 | 563 | if (isset($data['discount_value'])) { |
| 502 | 564 | $quote->setDiscountValue($data['discount_value']); |
| 503 | 565 | } |
| 504 | 566 | |
| 567 | + // Per-quote tax switch. The invoice repository picks this up through the | |
| 568 | + // form processor's pass-through; quotes set every field by hand, and this | |
| 569 | + // one was missing, so a quote built with tax on lost it (and passed a | |
| 570 | + // tax-free invoice on conversion) whenever the site's global tax was off. | |
| 571 | + if (array_key_exists('tax_enabled', $data)) { | |
| 572 | + $enabled = $data['tax_enabled']; | |
| 573 | + $quote->setTaxEnabled(($enabled === true || $enabled === 1 || in_array(strtolower((string) $enabled), ['1', 'yes', 'true', 'on'], true)) ? 'yes' : 'no'); | |
| 574 | + } | |
| 575 | + | |
| 505 | 576 | if (isset($data['tax_rate'])) { |
| 506 | 577 | $quote->setTaxRate($data['tax_rate']); |
| 507 | 578 | } |
| 508 | 579 | |
| @@ -555,10 +626,10 @@ | ||
| 555 | 626 | $quote->setCustomFields($data['custom_fields']); |
| 556 | 627 | } |
| 557 | 628 | |
| 558 | 629 | // Populate client information if we have a client_id |
| 559 | - if ($quote->getClientId() > 0) { | |
| 560 | - $this->populateCustomerFromClient($quote, $quote->getClientId()); | |
| 630 | + if (is_numeric($quote->getClientId()) && (int) $quote->getClientId() > 0) { | |
| 631 | + $this->populateCustomerFromClient($quote, (int) $quote->getClientId()); | |
| 561 | 632 | } |
| 562 | 633 | |
| 563 | 634 | // Allow plugins to modify the data setting process |
| 564 | 635 | do_action('easy_invoice_quote_set_data_after', $quote, $data); |