diff --git a/.env.example b/.env.example index d54053ff..839da9ec 100644 --- a/.env.example +++ b/.env.example @@ -23,7 +23,7 @@ DB_PASSWORD="invoiceshelf" BROADCAST_CONNECTION=log CACHE_STORE=file -QUEUE_DRIVER=sync +QUEUE_CONNECTION=sync SESSION_DRIVER=cookie SESSION_LIFETIME=1440 SESSION_ENCRYPT=false diff --git a/app/Http/Controllers/V1/Admin/Estimate/CloneEstimateController.php b/app/Http/Controllers/V1/Admin/Estimate/CloneEstimateController.php new file mode 100644 index 00000000..0f35f7a8 --- /dev/null +++ b/app/Http/Controllers/V1/Admin/Estimate/CloneEstimateController.php @@ -0,0 +1,131 @@ +authorize('create', Estimate::class); + + $date = Carbon::now(); + + $serial = (new SerialNumberFormatter()) + ->setModel($estimate) + ->setCompany($estimate->company_id) + ->setCustomer($estimate->customer_id) + ->setNextNumbers(); + + $due_date = null; + $dueDateEnabled = CompanySetting::getSetting( + 'estimate_set_expiry_date_automatically', + $request->header('company') + ); + + if ($dueDateEnabled === 'YES') { + $dueDateDays = intval(CompanySetting::getSetting( + 'estimate_expiry_date_days', + $request->header('company') + )); + $due_date = Carbon::now()->addDays($dueDateDays)->format('Y-m-d'); + } + + $exchange_rate = $estimate->exchange_rate; + + $newEstimate = Estimate::create([ + 'estimate_date' => $date->format('Y-m-d'), + 'expiry_date' => $due_date, + 'estimate_number' => $serial->getNextNumber(), + 'sequence_number' => $serial->nextSequenceNumber, + 'customer_sequence_number' => $serial->nextCustomerSequenceNumber, + 'reference_number' => $estimate->reference_number, + 'customer_id' => $estimate->customer_id, + 'company_id' => $request->header('company'), + 'template_name' => $estimate->template_name, + 'status' => Estimate::STATUS_DRAFT, + 'sub_total' => $estimate->sub_total, + 'discount' => $estimate->discount, + 'discount_type' => $estimate->discount_type, + 'discount_val' => $estimate->discount_val, + 'total' => $estimate->total, + 'due_amount' => $estimate->total, + 'tax_per_item' => $estimate->tax_per_item, + 'discount_per_item' => $estimate->discount_per_item, + 'tax' => $estimate->tax, + 'notes' => $estimate->notes, + 'exchange_rate' => $exchange_rate, + 'base_total' => $estimate->total * $exchange_rate, + 'base_discount_val' => $estimate->discount_val * $exchange_rate, + 'base_sub_total' => $estimate->sub_total * $exchange_rate, + 'base_tax' => $estimate->tax * $exchange_rate, + 'base_due_amount' => $estimate->total * $exchange_rate, + 'currency_id' => $estimate->currency_id, + 'sales_tax_type' => $estimate->sales_tax_type, + 'sales_tax_address_type' => $estimate->sales_tax_address_type, + ]); + + $newEstimate->unique_hash = Hashids::connection(Estimate::class)->encode($newEstimate->id); + $newEstimate->save(); + $estimate->load('items.taxes'); + + $estimateItems = $estimate->items->toArray(); + + foreach ($estimateItems as $estimateItem) { + $estimateItem['company_id'] = $request->header('company'); + $estimateItem['name'] = $estimateItem['name']; + $estimateItem['exchange_rate'] = $exchange_rate; + $estimateItem['base_price'] = $estimateItem['price'] * $exchange_rate; + $estimateItem['base_discount_val'] = $estimateItem['discount_val'] * $exchange_rate; + $estimateItem['base_tax'] = $estimateItem['tax'] * $exchange_rate; + $estimateItem['base_total'] = $estimateItem['total'] * $exchange_rate; + + $item = $newEstimate->items()->create($estimateItem); + + if (array_key_exists('taxes', $estimateItem) && $estimateItem['taxes']) { + foreach ($estimateItem['taxes'] as $tax) { + $tax['company_id'] = $request->header('company'); + + if ($tax['amount']) { + $item->taxes()->create($tax); + } + } + } + } + + if ($estimate->taxes) { + foreach ($estimate->taxes->toArray() as $tax) { + $tax['company_id'] = $request->header('company'); + $newEstimate->taxes()->create($tax); + } + } + + if ($estimate->fields()->exists()) { + $customFields = []; + + foreach ($estimate->fields as $data) { + $customFields[] = [ + 'id' => $data->custom_field_id, + 'value' => $data->defaultAnswer, + ]; + } + + $newEstimate->addCustomFields($customFields); + } + + return new EstimateResource($newEstimate); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index d4eb3eb3..f140cb13 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -35,7 +35,16 @@ class AppServiceProvider extends ServiceProvider * * @var string */ - public const HOME = '/home'; + public const HOME = '/admin/dashboard'; + + /** + * The path to the "customer home" route for your application. + * + * This is used by Laravel authentication to redirect customers after login. + * + * @var string + */ + public const CUSTOMER_HOME = '/customer/dashboard'; /** * Bootstrap any application services. diff --git a/lang/ar.json b/lang/ar.json index 9c9749ff..c6ed2909 100644 --- a/lang/ar.json +++ b/lang/ar.json @@ -304,6 +304,9 @@ "record_payment": "تسجيل مدفوات", "add_estimate": "إضافة تقدير", "save_estimate": "حفظ التقدير", + "cloned_successfully": "تم استنساخ العرض بنجاح", + "clone_estimate": "استنساخ العرض", + "confirm_clone": "سيتم استنساخ هذا العرض إلى عرض جديد", "confirm_conversion": "هل تريد تحويل هذا التقدير إلى فاتورة؟", "conversion_message": "تم إنشاء الفاتورة بنجاح", "confirm_send_estimate": "سيتم إرسال هذا التقدير بالبريد الإلكتروني إلى العميل", diff --git a/lang/cs.json b/lang/cs.json index c7577fa6..64c9c9bb 100644 --- a/lang/cs.json +++ b/lang/cs.json @@ -304,6 +304,9 @@ "record_payment": "Zaznamenat platbu", "add_estimate": "Přidat nabídku", "save_estimate": "Uložit nabídku", + "cloned_successfully": "Devis úspěšně zkopírován", + "clone_estimate": "Klonovat devis", + "confirm_clone": "Tento devis bude zkopírován do nového devisu", "confirm_conversion": "Tento odhad bude použit k vytvoření nové faktury.", "conversion_message": "Faktura byla úspěšně vytvořena", "confirm_send_estimate": "Tento odhad bude zaslán e-mailem zákazníkovi", diff --git a/lang/de.json b/lang/de.json index 985fc411..f57caec7 100644 --- a/lang/de.json +++ b/lang/de.json @@ -304,6 +304,9 @@ "record_payment": "Zahlung erfassen", "add_estimate": "Angebote hinzufügen", "save_estimate": "Angebot speichern", + "cloned_successfully": "Angebot erfolgreich geklont", + "clone_estimate": "Angebot klonen", + "confirm_clone": "Dieses Angebot wird in ein neues Angebot kopiert", "confirm_conversion": "Dieses Angebot wird verwendet, um eine neue Rechnung zu erstellen.", "conversion_message": "Rechnung erfolgreich erstellt", "confirm_send_estimate": "Das Angebot wird per E-Mail an den Kunden gesendet", diff --git a/lang/el.json b/lang/el.json index 73216823..12375cf9 100644 --- a/lang/el.json +++ b/lang/el.json @@ -304,6 +304,9 @@ "record_payment": "Καταγραφή Πληρωμής", "add_estimate": "Νέα Εκτίμηση", "save_estimate": "Νέα Εκτίμηση", + "cloned_successfully": "Η προσφορά κλωνοποιήθηκε με επιτυχία", + "clone_estimate": "Κλωνοποίηση προσφοράς", + "confirm_clone": "Αυτή η προσφορά θα κλωνοποιηθεί σε μια νέα προσφορά", "confirm_conversion": "Αυτή η εκτίμηση θα χρησιμοποιηθεί για τη δημιουργία ενός νέου τιμολογίου.", "conversion_message": "Το τιμολόγιο κλωνοποιήθηκε επιτυχώς", "confirm_send_estimate": "Αυτό το τιμολόγιο θα αποσταλεί μέσω email στον πελάτη", diff --git a/lang/en.json b/lang/en.json index 3162e19e..fc7d30f9 100644 --- a/lang/en.json +++ b/lang/en.json @@ -323,6 +323,9 @@ "record_payment": "Record Payment", "add_estimate": "Add Estimate", "save_estimate": "Save Estimate", + "cloned_successfully": "Estimate cloned successfully", + "clone_estimate": "Clone Estimate", + "confirm_clone": "This Estimate will be cloned into a new Estimate", "confirm_conversion": "This estimate will be used to create a new Invoice.", "conversion_message": "Invoice created successful", "confirm_send_estimate": "This estimate will be sent via email to the customer", diff --git a/lang/es.json b/lang/es.json index 0800673d..5b547018 100644 --- a/lang/es.json +++ b/lang/es.json @@ -304,6 +304,9 @@ "record_payment": "Registro de pago", "add_estimate": "Agregar presupuesto", "save_estimate": "Guardar presupuesto", + "cloned_successfully": "Presupuesto clonado con éxito", + "clone_estimate": "Clonar presupuesto", + "confirm_clone": "Este presupuesto será clonado en un nuevo presupuesto", "confirm_conversion": "¿Quiere convertir este presupuesto en una factura?", "conversion_message": "Conversión exitosa", "confirm_send_estimate": "Este presupuesto se enviará por correo electrónico al cliente", diff --git a/lang/fr.json b/lang/fr.json index d0bf8983..c544e72e 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -323,6 +323,9 @@ "record_payment": "Enregistrer un paiement", "add_estimate": "Nouveau devis", "save_estimate": "Enregistrer", + "cloned_successfully": "Devis dupliqué avec succès", + "clone_estimate": "Dupliquer le devis", + "confirm_clone": "Ce devis sera dupliqué dans un nouveau devis", "confirm_conversion": "Ce devis sera utilisé pour créer une nouvelle facture.", "conversion_message": "Conversion réussie", "confirm_send_estimate": "Ce devis sera envoyée par email au client", diff --git a/lang/it.json b/lang/it.json index 02df0b52..3ddaef01 100644 --- a/lang/it.json +++ b/lang/it.json @@ -304,6 +304,9 @@ "record_payment": "Registra Pagamento", "add_estimate": "Aggiungi Preventivo", "save_estimate": "Salva Preventivo", + "cloned_successfully": "Preventivo clonato con successo", + "clone_estimate": "Clonare preventivo", + "confirm_clone": "Questo preventivo sarà clonato in un nuovo preventivo", "confirm_conversion": "Questo preventivo verrà usato per generare una nuova fattura.", "conversion_message": "Fattura creata", "confirm_send_estimate": "Questo preventivo verrà inviato al cliente via mail", diff --git a/lang/ja.json b/lang/ja.json index 7a232d10..d57ac936 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -304,6 +304,9 @@ "record_payment": "Record Payment", "add_estimate": "Add Estimate", "save_estimate": "Save Estimate", + "cloned_successfully": "見積もりが正常にクローンされました", + "clone_estimate": "見積もりをクローン", + "confirm_clone": "この見積もりは新しい見積もりにクローンされます", "confirm_conversion": "This estimate will be used to create a new Invoice.", "conversion_message": "Invoice created successful", "confirm_send_estimate": "This estimate will be sent via email to the customer", diff --git a/lang/ko.json b/lang/ko.json index c2625302..b34c19f0 100644 --- a/lang/ko.json +++ b/lang/ko.json @@ -258,6 +258,9 @@ "record_payment": "기록 지불", "add_estimate": "견적 추가", "save_estimate": "견적 저장", + "cloned_successfully": "견적이 성공적으로 복제되었습니다", + "clone_estimate": "견적 복제", + "confirm_clone": "이 견적은 새로운 견적으로 복제될 것입니다", "confirm_conversion": "이 견적은 새 인보이스를 만드는 데 사용됩니다.", "conversion_message": "인보이스가 성공적으로 생성되었습니다.", "confirm_send_estimate": "이 견적은 이메일을 통해 고객에게 전송됩니다.", diff --git a/lang/lv.json b/lang/lv.json index c0f43e82..45b393c6 100644 --- a/lang/lv.json +++ b/lang/lv.json @@ -304,6 +304,9 @@ "record_payment": "Izveidot maksājumu", "add_estimate": "Pievienot aprēķinu", "save_estimate": "Saglabāt aprēķinu", + "cloned_successfully": "Piedāvājums veiksmīgi klonēts", + "clone_estimate": "Klonēt piedāvājumu", + "confirm_clone": "Šis piedāvājums tiks klonēts jaunā piedāvājumā", "confirm_conversion": "Šis aprēķins tiks izmantots, lai izveidotu jaunu rēķinu.", "conversion_message": "Rēķins izveidots veiksmīgi", "confirm_send_estimate": "Šis aprēķins tiks nosūtīts klientam e-pastā", diff --git a/lang/nl.json b/lang/nl.json index ab77bc9b..97ea6b37 100644 --- a/lang/nl.json +++ b/lang/nl.json @@ -304,6 +304,9 @@ "record_payment": "Betaling registreren", "add_estimate": "Offerte toevoegen", "save_estimate": "Bewaar offerte", + "cloned_successfully": "Offerte succesvol gekloond", + "clone_estimate": "Offerte klonen", + "confirm_clone": "Deze offerte zal worden gekopieerd naar een nieuwe offerte", "confirm_conversion": "Deze offerte wordt gebruikt om een nieuwe factuur te maken.", "conversion_message": "Factuur gemaakt", "confirm_send_estimate": "Deze offerte wordt via e-mail naar de klant gestuurd", diff --git a/lang/pl.json b/lang/pl.json index 56f5183d..9c070f2b 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -304,6 +304,9 @@ "record_payment": "Zarejestruj płatność", "add_estimate": "Dodaj ofertę", "save_estimate": "Zapisz ofertę", + "cloned_successfully": "Oferta została pomyślnie sklonowana", + "clone_estimate": "Sklonuj ofertę", + "confirm_clone": "Ta oferta zostanie sklonowana do nowej oferty", "confirm_conversion": "Ta oferta zostanie użyta do utworzenia nowej faktury.", "conversion_message": "Faktura została utworzona pomyślnie", "confirm_send_estimate": "Ta oferta zostanie wysłana pocztą elektroniczną do kontrahenta", diff --git a/lang/pt-br.json b/lang/pt-br.json index a666641e..4a394d62 100644 --- a/lang/pt-br.json +++ b/lang/pt-br.json @@ -237,6 +237,9 @@ "record_payment": "Registro de pago", "add_estimate": "Adicionar orçamento", "save_estimate": "Salvar Orçamento", + "cloned_successfully": "Orçamento clonado com sucesso", + "clone_estimate": "Clonar orçamento", + "confirm_clone": "Este orçamento será clonado em um novo orçamento", "confirm_conversion": "Deseja converter este orçamento em uma fatura?", "conversion_message": "Converção realizada com sucesso", "confirm_send_estimate": "Este orçamento será enviado por email ao cliente", diff --git a/lang/sk.json b/lang/sk.json index 812763aa..308540f9 100644 --- a/lang/sk.json +++ b/lang/sk.json @@ -304,6 +304,9 @@ "record_payment": "Zaznamenať Platbu", "add_estimate": "Vytvoriť Cenový odhad", "save_estimate": "Uložiť Cenový odhad", + "cloned_successfully": "Ponuka úspešne skopírovaná", + "clone_estimate": "Klonovať ponuku", + "confirm_clone": "Táto ponuka bude skopírovaná do novej ponuky", "confirm_conversion": "Tento cenový odhad bude použitý k vytvoreniu novej Faktúry.", "conversion_message": "Faktúra úspešne vytvorená", "confirm_send_estimate": "Tento Cenový odhad bude odoslaný zákazníkovi prostredníctvom e-mailu", diff --git a/lang/sr.json b/lang/sr.json index 1b4e762e..af1a37f6 100644 --- a/lang/sr.json +++ b/lang/sr.json @@ -304,6 +304,9 @@ "record_payment": "Unesi uplatu", "add_estimate": "Dodaj Profakturu", "save_estimate": "Sačuvaj Profakturu", + "cloned_successfully": "Ponuda uspešno klonirana", + "clone_estimate": "Kloniraj ponudu", + "confirm_clone": "Ova ponuda će biti klonirana u novu ponudu", "confirm_conversion": "Detalji ove Profakture će biti iskorišćeni za pravljenje Fakture.", "conversion_message": "Faktura uspešno kreirana", "confirm_send_estimate": "Ova Profaktura će biti poslata putem Email-a klijentu", diff --git a/lang/sv.json b/lang/sv.json index 92d79a6e..434bf9e2 100644 --- a/lang/sv.json +++ b/lang/sv.json @@ -304,6 +304,9 @@ "record_payment": "Registrera betalning", "add_estimate": "Lägg till kostnadsförslag", "save_estimate": "Spara kostnadsförslag", + "cloned_successfully": "Offert klonades framgångsrikt", + "clone_estimate": "Klona offert", + "confirm_clone": "Denna offert kommer att klonas till en ny offert", "confirm_conversion": "Detta kostnadsförslag används för att skapa ny faktura.", "conversion_message": "Faktura skapades", "confirm_send_estimate": "Detta kostnadsförslag skickas via epost till kund", diff --git a/lang/th.json b/lang/th.json index 32449b09..6ae04fe6 100644 --- a/lang/th.json +++ b/lang/th.json @@ -303,6 +303,9 @@ "record_payment": "บันทึกการชำระเงิน", "add_estimate": "เพิ่มค่าใบเสนอราคา", "save_estimate": "บันทึกใบเสนอราคา", + "cloned_successfully": "โคลนข้อเสนอสำเร็จ", + "clone_estimate": "โคลนข้อเสนอ", + "confirm_clone": "ข้อเสนอนี้จะถูกโคลนเป็นข้อเสนอใหม่", "confirm_conversion": "ใบเสนอราคานี้จะใช้ในการสร้างใบวางบิลใหม่", "conversion_message": "ใบวางบิลที่สร้างเสร็จสมบูรณ์", "confirm_send_estimate": "ใบเสนอราคานี้จะถูกส่งผ่านทางอีเมลถึงลูกค้า", diff --git a/resources/scripts/admin/components/dropdowns/EstimateIndexDropdown.vue b/resources/scripts/admin/components/dropdowns/EstimateIndexDropdown.vue index 15bb3ceb..e9549c06 100644 --- a/resources/scripts/admin/components/dropdowns/EstimateIndexDropdown.vue +++ b/resources/scripts/admin/components/dropdowns/EstimateIndexDropdown.vue @@ -62,6 +62,18 @@ + + + + {{ $t('estimates.clone_estimate') }} + + { + if (res) { + estimateStore.cloneEstimate(data).then((res) => { + router.push(`/admin/estimates/${res.data.data.id}/edit`) + }) + } + }) +} diff --git a/resources/scripts/admin/stores/estimate.js b/resources/scripts/admin/stores/estimate.js index 5384249b..e017e170 100644 --- a/resources/scripts/admin/stores/estimate.js +++ b/resources/scripts/admin/stores/estimate.js @@ -329,6 +329,25 @@ export const useEstimateStore = (useWindow = false) => { }) }, + cloneEstimate(data) { + return new Promise((resolve, reject) => { + axios + .post(`/api/v1/estimates/${data.id}/clone`, data) + .then((response) => { + const notificationStore = useNotificationStore() + notificationStore.showNotification({ + type: 'success', + message: global.t('estimates.cloned_successfully'), + }) + resolve(response) + }) + .catch((err) => { + handleError(err) + reject(err) + }) + }) + }, + markAsAccepted(data) { return new Promise((resolve, reject) => { axios diff --git a/routes/api.php b/routes/api.php index 19c9b4a8..41738328 100644 --- a/routes/api.php +++ b/routes/api.php @@ -12,6 +12,7 @@ use App\Http\Controllers\V1\Admin\Customer\CustomerStatsController; use App\Http\Controllers\V1\Admin\CustomField\CustomFieldsController; use App\Http\Controllers\V1\Admin\Dashboard\DashboardController; use App\Http\Controllers\V1\Admin\Estimate\ChangeEstimateStatusController; +use App\Http\Controllers\V1\Admin\Estimate\CloneEstimateController; use App\Http\Controllers\V1\Admin\Estimate\ConvertEstimateController; use App\Http\Controllers\V1\Admin\Estimate\EstimatesController; use App\Http\Controllers\V1\Admin\Estimate\EstimateTemplatesController; @@ -285,6 +286,8 @@ Route::prefix('/v1')->group(function () { Route::post('/estimates/{estimate}/send', SendEstimateController::class); + Route::post('/estimates/{estimate}/clone', CloneEstimateController::class); + Route::post('/estimates/{estimate}/status', ChangeEstimateStatusController::class); Route::post('/estimates/{estimate}/convert-to-invoice', ConvertEstimateController::class); diff --git a/tests/Feature/Admin/EstimateTest.php b/tests/Feature/Admin/EstimateTest.php index 7c9863c2..9d1d504a 100644 --- a/tests/Feature/Admin/EstimateTest.php +++ b/tests/Feature/Admin/EstimateTest.php @@ -65,6 +65,18 @@ test('create estimate', function () { ]); }); +test('clone estimate', function () { + + $estimate = Estimate::factory()->create(); + + $beforeCount = Estimate::count(); + + $response = $this->post("/api/v1/estimates/{$estimate->id}/clone"); + + $this->assertDatabaseCount('estimates', $beforeCount + 1); + +}); + test('store validates using a form request', function () { $this->assertActionUsesFormRequest( EstimatesController::class,