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,