diff --git a/app/Export/CSV/ActivityExport.php b/app/Export/CSV/ActivityExport.php index 09d555fc1954..21aa0ecb0979 100644 --- a/app/Export/CSV/ActivityExport.php +++ b/app/Export/CSV/ActivityExport.php @@ -24,8 +24,7 @@ use App\Transformers\ActivityTransformer; class ActivityExport extends BaseExport { - private Company $company; - + private $entity_transformer; public string $date_key = 'created_at'; diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index ccbf10b45057..d11c9ef72c7e 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -13,14 +13,14 @@ namespace App\Export\CSV; use App\Utils\Number; use App\Models\Client; +use App\Models\Company; use App\Models\Expense; use App\Models\Invoice; -use App\Models\GatewayType; use App\Models\Payment; use League\Fractal\Manager; use Illuminate\Support\Carbon; use App\Utils\Traits\MakesHash; -use App\Transformers\ClientTransformer; +use App\Transformers\TaskTransformer; use App\Transformers\PaymentTransformer; use Illuminate\Database\Eloquent\Builder; use League\Fractal\Serializer\ArraySerializer; @@ -29,6 +29,8 @@ class BaseExport { use MakesHash; + public Company $company; + public array $input; public string $date_key = ''; @@ -136,6 +138,35 @@ class BaseExport "user" => "invoice.user_id", ]; + protected array $recurring_invoice_report_keys = [ + "invoice_number" => "recurring_invoice.number", + "amount" => "recurring_invoice.amount", + "balance" => "recurring_invoice.balance", + "paid_to_date" => "recurring_invoice.paid_to_date", + "po_number" => "recurring_invoice.po_number", + "date" => "recurring_invoice.date", + "due_date" => "recurring_invoice.due_date", + "terms" => "recurring_invoice.terms", + "footer" => "recurring_invoice.footer", + "status" => "recurring_invoice.status", + "public_notes" => "recurring_invoice.public_notes", + "private_notes" => "recurring_invoice.private_notes", + "uses_inclusive_taxes" => "recurring_invoice.uses_inclusive_taxes", + "is_amount_discount" => "recurring_invoice.is_amount_discount", + "partial" => "recurring_invoice.partial", + "partial_due_date" => "recurring_invoice.partial_due_date", + "surcharge1" => "recurring_invoice.custom_surcharge1", + "surcharge2" => "recurring_invoice.custom_surcharge2", + "surcharge3" => "recurring_invoice.custom_surcharge3", + "surcharge4" => "recurring_invoice.custom_surcharge4", + "exchange_rate" => "recurring_invoice.exchange_rate", + "tax_amount" => "recurring_invoice.total_taxes", + "assigned_user" => "recurring_invoice.assigned_user_id", + "user" => "recurring_invoice.user_id", + "frequency_id" => "recurring_invoice.frequency_id", + "next_send_date" => "recurring_invoice.next_send_date" + ]; + protected array $purchase_order_report_keys = [ 'amount' => 'purchase_order.amount', 'balance' => 'purchase_order.balance', @@ -193,13 +224,17 @@ class BaseExport ]; protected array $quote_report_keys = [ - "quote_number" => "quote.number", + 'custom_value1' => 'quote.custom_value1', + 'custom_value2' => 'quote.custom_value2', + 'custom_value3' => 'quote.custom_value3', + 'custom_value4' => 'quote.custom_value4', + "number" => "quote.number", "amount" => "quote.amount", "balance" => "quote.balance", "paid_to_date" => "quote.paid_to_date", "po_number" => "quote.po_number", "date" => "quote.date", - "due_date" => "quote.due_date", + "valid_until" => "quote.due_date", "terms" => "quote.terms", "footer" => "quote.footer", "status" => "quote.status", @@ -314,8 +349,6 @@ class BaseExport 'custom_value4' => 'task.custom_value4', 'status' => 'task.status_id', 'project' => 'task.project_id', - 'invoice' => 'task.invoice_id', - 'client' => 'task.client_id', ]; protected function filterByClients($query) @@ -347,8 +380,11 @@ class BaseExport 'vendor' => $value = $this->resolveVendorKey($parts[1], $entity, $transformer), 'vendor_contact' => $value = $this->resolveVendorContactKey($parts[1], $entity, $transformer), 'invoice' => $value = $this->resolveInvoiceKey($parts[1], $entity, $transformer), + 'recurring_invoice' => $value = $this->resolveInvoiceKey($parts[1], $entity, $transformer), + 'quote' => $value = $this->resolveQuoteKey($parts[1], $entity, $transformer), 'purchase_order' => $value = $this->resolvePurchaseOrderKey($parts[1], $entity, $transformer), 'payment' => $value = $this->resolvePaymentKey($parts[1], $entity, $transformer), + 'task' => $value = $this->resolveTaskKey($parts[1], $entity, $transformer), default => $value = '' }; @@ -414,6 +450,22 @@ class BaseExport } + private function resolveTaskKey($column, $entity, $transformer) + { + nlog("searching for {$column}"); + + $transformed_entity = $transformer->transform($entity); + + if(array_key_exists($column, $transformed_entity)) { + return $transformed_entity[$column]; + } + + return ''; + + } + + + private function resolveVendorKey($column, $entity, $transformer) { @@ -511,6 +563,20 @@ class BaseExport return ''; } + private function resolveQuoteKey($column, $entity, $transformer) + { + nlog("searching for {$column}"); + + $transformed_entity = $transformer->transform($entity); + + if(array_key_exists($column, $transformed_entity)) { + return $transformed_entity[$column]; + } + + return ''; + + } + private function resolveInvoiceKey($column, $entity, $transformer) { nlog("searching for {$column}"); @@ -537,7 +603,23 @@ class BaseExport } - $transformed_invoice = $transformer->transform($entity); + if($transformer instanceof TaskTransformer) { + $transformed_invoice = $transformer->includeInvoice($entity); + + if(!$transformed_invoice) + return ''; + + $manager = new Manager(); + $manager->setSerializer(new ArraySerializer()); + $transformed_invoice = $manager->createData($transformed_invoice)->toArray(); + + } + + if(array_key_exists($column, $transformed_invoice)) { + return $transformed_invoice[$column]; + } elseif (array_key_exists(str_replace("invoice.", "", $column), $transformed_invoice)) { + return $transformed_invoice[$column]; + } if($column == 'status') return $entity->stringStatus($entity->status_id); @@ -707,8 +789,27 @@ class BaseExport $this->end_date = now()->startOfDay()->format('Y-m-d'); return $query->whereBetween($this->date_key, [now()->subDays(365), now()])->orderBy($this->date_key, 'ASC'); case 'this_year': - $this->start_date = now()->startOfYear()->format('Y-m-d'); - $this->end_date = now()->format('Y-m-d'); + + $first_month_of_year = $this->company->getSetting('first_month_of_year') ?? 1; + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + if(now()->lt($fin_year_start)) + $fin_year_start->subYearNoOverflow(); + + $this->start_date = $fin_year_start->format('Y-m-d'); + $this->end_date = $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d'); + return $query->whereBetween($this->date_key, [now()->startOfYear(), now()])->orderBy($this->date_key, 'ASC'); + case 'last_year': + + $first_month_of_year = $this->company->getSetting('first_month_of_year') ?? 1; + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + $fin_year_start->subYearNoOverflow(); + + if(now()->subYear()->lt($fin_year_start)) + $fin_year_start->subYearNoOverflow(); + + $this->start_date = $fin_year_start->format('Y-m-d'); + $this->end_date = $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d'); return $query->whereBetween($this->date_key, [now()->startOfYear(), now()])->orderBy($this->date_key, 'ASC'); case 'custom': $this->start_date = $custom_start_date->format('Y-m-d'); @@ -743,6 +844,11 @@ class BaseExport $key = array_search($value, $this->invoice_report_keys); } + if(!$key) { + $prefix = ctrans('texts.recurring_invoice')." "; + $key = array_search($value, $this->recurring_invoice_report_keys); + } + if(!$key) { $prefix = ctrans('texts.payment')." "; $key = array_search($value, $this->payment_report_keys); @@ -790,6 +896,7 @@ class BaseExport $key = str_replace('item.', '', $key); $key = str_replace('recurring_invoice.', '', $key); + $key = str_replace('purchase_order.', '', $key); $key = str_replace('invoice.', '', $key); $key = str_replace('quote.', '', $key); $key = str_replace('credit.', '', $key); @@ -800,9 +907,18 @@ class BaseExport $key = str_replace('payment.', '', $key); $key = str_replace('expense.', '', $key); - $header[] = "{$prefix}" . ctrans("texts.{$key}"); + if(in_array($key, ['quote1','quote2','quote3','quote4','credit1','credit2','credit3','credit4','purchase_order1','purchase_order2','purchase_order3','purchase_order4'])) + { + $number = substr($key, -1); + $header[] = ctrans('texts.item') . " ". ctrans("texts.custom_value{$number}"); + } + else + { + $header[] = "{$prefix}" . ctrans("texts.{$key}"); + } } -// nlog($header); + + // nlog($header); return $header; } diff --git a/app/Export/CSV/ClientExport.php b/app/Export/CSV/ClientExport.php index bcf93bf5897f..32f42ba807b7 100644 --- a/app/Export/CSV/ClientExport.php +++ b/app/Export/CSV/ClientExport.php @@ -22,8 +22,6 @@ use League\Csv\Writer; class ClientExport extends BaseExport { - private $company; - private $client_transformer; private $contact_transformer; @@ -172,8 +170,8 @@ class ClientExport extends BaseExport $entity['shipping_country'] = $client->shipping_country ? ctrans("texts.country_{$client->shipping_country->name}") : ''; } - if (in_array('client.currency', $this->input['report_keys'])) { - $entity['currency'] = $client->currency() ? $client->currency()->code : $client->company->currency()->code; + if (in_array('client.currency_id', $this->input['report_keys'])) { + $entity['client.currency_id'] = $client->currency() ? $client->currency()->code : $client->company->currency()->code; } if (in_array('client.industry_id', $this->input['report_keys'])) { diff --git a/app/Export/CSV/ContactExport.php b/app/Export/CSV/ContactExport.php index e960a572fa05..04ca04797d82 100644 --- a/app/Export/CSV/ContactExport.php +++ b/app/Export/CSV/ContactExport.php @@ -23,7 +23,6 @@ use League\Csv\Writer; class ContactExport extends BaseExport { - private Company $company; private ClientTransformer $client_transformer; diff --git a/app/Export/CSV/CreditExport.php b/app/Export/CSV/CreditExport.php index 67b259971003..1cadfea5b7dd 100644 --- a/app/Export/CSV/CreditExport.php +++ b/app/Export/CSV/CreditExport.php @@ -21,7 +21,6 @@ use League\Csv\Writer; class CreditExport extends BaseExport { - private Company $company; private CreditTransformer $credit_transformer; diff --git a/app/Export/CSV/DocumentExport.php b/app/Export/CSV/DocumentExport.php index 3b7d06ede415..b43a759b5254 100644 --- a/app/Export/CSV/DocumentExport.php +++ b/app/Export/CSV/DocumentExport.php @@ -21,7 +21,6 @@ use League\Csv\Writer; class DocumentExport extends BaseExport { - private Company $company; private $entity_transformer; diff --git a/app/Export/CSV/ExpenseExport.php b/app/Export/CSV/ExpenseExport.php index 9f542fea2316..70bb8c841edb 100644 --- a/app/Export/CSV/ExpenseExport.php +++ b/app/Export/CSV/ExpenseExport.php @@ -21,7 +21,6 @@ use League\Csv\Writer; class ExpenseExport extends BaseExport { - private Company $company; private $expense_transformer; diff --git a/app/Export/CSV/InvoiceExport.php b/app/Export/CSV/InvoiceExport.php index 98ed8498a6c3..853da158d23d 100644 --- a/app/Export/CSV/InvoiceExport.php +++ b/app/Export/CSV/InvoiceExport.php @@ -23,8 +23,6 @@ use App\Transformers\InvoiceTransformer; class InvoiceExport extends BaseExport { - private Company $company; - private $invoice_transformer; public string $date_key = 'date'; diff --git a/app/Export/CSV/InvoiceItemExport.php b/app/Export/CSV/InvoiceItemExport.php index 1f82c3407da8..943844bc125a 100644 --- a/app/Export/CSV/InvoiceItemExport.php +++ b/app/Export/CSV/InvoiceItemExport.php @@ -22,7 +22,6 @@ use League\Csv\Writer; class InvoiceItemExport extends BaseExport { - private Company $company; private $invoice_transformer; diff --git a/app/Export/CSV/PaymentExport.php b/app/Export/CSV/PaymentExport.php index e7aaf1abb9ed..dab8054c132f 100644 --- a/app/Export/CSV/PaymentExport.php +++ b/app/Export/CSV/PaymentExport.php @@ -21,8 +21,6 @@ use League\Csv\Writer; class PaymentExport extends BaseExport { - private Company $company; - private $entity_transformer; public string $date_key = 'date'; diff --git a/app/Export/CSV/ProductExport.php b/app/Export/CSV/ProductExport.php index de1b41e031fc..f1a24d49b48b 100644 --- a/app/Export/CSV/ProductExport.php +++ b/app/Export/CSV/ProductExport.php @@ -22,8 +22,6 @@ use League\Csv\Writer; class ProductExport extends BaseExport { - private Company $company; - private $entity_transformer; public string $date_key = 'created_at'; diff --git a/app/Export/CSV/ProductSalesExport.php b/app/Export/CSV/ProductSalesExport.php index a3228dfcdb2f..72017220d501 100644 --- a/app/Export/CSV/ProductSalesExport.php +++ b/app/Export/CSV/ProductSalesExport.php @@ -23,8 +23,6 @@ use League\Csv\Writer; class ProductSalesExport extends BaseExport { - private Company $company; - public string $date_key = 'created_at'; protected Collection $products; diff --git a/app/Export/CSV/PurchaseOrderExport.php b/app/Export/CSV/PurchaseOrderExport.php index 0b4202ce5af0..bfba71830f3c 100644 --- a/app/Export/CSV/PurchaseOrderExport.php +++ b/app/Export/CSV/PurchaseOrderExport.php @@ -22,7 +22,6 @@ use League\Csv\Writer; class PurchaseOrderExport extends BaseExport { - private Company $company; private $purchase_order_transformer; @@ -31,39 +30,39 @@ class PurchaseOrderExport extends BaseExport public Writer $csv; public array $entity_keys = [ - 'amount' => 'amount', - 'balance' => 'balance', - 'vendor' => 'vendor_id', - 'custom_surcharge1' => 'custom_surcharge1', - 'custom_surcharge2' => 'custom_surcharge2', - 'custom_surcharge3' => 'custom_surcharge3', - 'custom_surcharge4' => 'custom_surcharge4', - 'custom_value1' => 'custom_value1', - 'custom_value2' => 'custom_value2', - 'custom_value3' => 'custom_value3', - 'custom_value4' => 'custom_value4', - 'date' => 'date', - 'discount' => 'discount', - 'due_date' => 'due_date', - 'exchange_rate' => 'exchange_rate', - 'footer' => 'footer', - 'number' => 'number', - 'paid_to_date' => 'paid_to_date', - 'partial' => 'partial', - 'partial_due_date' => 'partial_due_date', - 'po_number' => 'po_number', - 'private_notes' => 'private_notes', - 'public_notes' => 'public_notes', - 'status' => 'status_id', - 'tax_name1' => 'tax_name1', - 'tax_name2' => 'tax_name2', - 'tax_name3' => 'tax_name3', - 'tax_rate1' => 'tax_rate1', - 'tax_rate2' => 'tax_rate2', - 'tax_rate3' => 'tax_rate3', - 'terms' => 'terms', - 'total_taxes' => 'total_taxes', - 'currency_id' => 'currency_id', + 'amount' => 'purchase_order.amount', + 'balance' => 'purchase_order.balance', + 'vendor' => 'purchase_order.vendor_id', + // 'custom_surcharge1' => 'purchase_order.custom_surcharge1', + // 'custom_surcharge2' => 'purchase_order.custom_surcharge2', + // 'custom_surcharge3' => 'purchase_order.custom_surcharge3', + // 'custom_surcharge4' => 'purchase_order.custom_surcharge4', + 'custom_value1' => 'purchase_order.custom_value1', + 'custom_value2' => 'purchase_order.custom_value2', + 'custom_value3' => 'purchase_order.custom_value3', + 'custom_value4' => 'purchase_order.custom_value4', + 'date' => 'purchase_order.date', + 'discount' => 'purchase_order.discount', + 'due_date' => 'purchase_order.due_date', + 'exchange_rate' => 'purchase_order.exchange_rate', + 'footer' => 'purchase_order.footer', + 'number' => 'purchase_order.number', + 'paid_to_date' => 'purchase_order.paid_to_date', + 'partial' => 'purchase_order.partial', + 'partial_due_date' => 'purchase_order.partial_due_date', + 'po_number' => 'purchase_order.po_number', + 'private_notes' => 'purchase_order.private_notes', + 'public_notes' => 'purchase_order.public_notes', + 'status' => 'purchase_order.status_id', + 'tax_name1' => 'purchase_order.tax_name1', + 'tax_name2' => 'purchase_order.tax_name2', + 'tax_name3' => 'purchase_order.tax_name3', + 'tax_rate1' => 'purchase_order.tax_rate1', + 'tax_rate2' => 'purchase_order.tax_rate2', + 'tax_rate3' => 'purchase_order.tax_rate3', + 'terms' => 'purchase_order.terms', + 'total_taxes' => 'purchase_order.total_taxes', + 'currency_id' => 'purchase_order.currency_id', ]; private array $decorate_keys = [ @@ -130,7 +129,7 @@ class PurchaseOrderExport extends BaseExport $keyval = array_search($key, $this->entity_keys); if(!$keyval) { - $keyval = array_search(str_replace("invoice.", "", $key), $this->entity_keys) ?? $key; + $keyval = array_search(str_replace("purchase_order.", "", $key), $this->entity_keys) ?? $key; } if(!$keyval) { diff --git a/app/Export/CSV/PurchaseOrderItemExport.php b/app/Export/CSV/PurchaseOrderItemExport.php index 4847a162f9c0..2bb1eef01591 100644 --- a/app/Export/CSV/PurchaseOrderItemExport.php +++ b/app/Export/CSV/PurchaseOrderItemExport.php @@ -21,7 +21,6 @@ use League\Csv\Writer; class PurchaseOrderItemExport extends BaseExport { - private Company $company; private $purchase_order_transformer; @@ -37,10 +36,10 @@ class PurchaseOrderItemExport extends BaseExport 'vendor' => 'vendor_id', 'vendor_number' => 'vendor.number', 'vendor_id_number' => 'vendor.id_number', - 'custom_surcharge1' => 'custom_surcharge1', - 'custom_surcharge2' => 'custom_surcharge2', - 'custom_surcharge3' => 'custom_surcharge3', - 'custom_surcharge4' => 'custom_surcharge4', + // 'custom_surcharge1' => 'custom_surcharge1', + // 'custom_surcharge2' => 'custom_surcharge2', + // 'custom_surcharge3' => 'custom_surcharge3', + // 'custom_surcharge4' => 'custom_surcharge4', // 'custom_value1' => 'custom_value1', // 'custom_value2' => 'custom_value2', // 'custom_value3' => 'custom_value3', @@ -82,10 +81,10 @@ class PurchaseOrderItemExport extends BaseExport 'tax_name3' => 'item.tax_name3', 'line_total' => 'item.line_total', 'gross_line_total' => 'item.gross_line_total', - // 'invoice1' => 'item.custom_value1', - // 'invoice2' => 'item.custom_value2', - // 'invoice3' => 'item.custom_value3', - // 'invoice4' => 'item.custom_value4', + 'purchase_order1' => 'item.custom_value1', + 'purchase_order2' => 'item.custom_value2', + 'purchase_order3' => 'item.custom_value3', + 'purchase_order4' => 'item.custom_value4', 'tax_category' => 'item.tax_id', 'type' => 'item.type_id', ]; @@ -139,7 +138,7 @@ class PurchaseOrderItemExport extends BaseExport private function iterateItems(PurchaseOrder $purchase_order) { - $transformed_invoice = $this->buildRow($purchase_order); + $transformed_purchase_order = $this->buildRow($purchase_order); $transformed_items = []; @@ -154,7 +153,7 @@ class PurchaseOrderItemExport extends BaseExport $keyval = $key; - $keyval = str_replace("custom_value", "invoice", $key); + $keyval = str_replace("custom_value", "purchase_order", $key); if($key == 'type_id') { $keyval = 'type'; @@ -184,7 +183,7 @@ class PurchaseOrderItemExport extends BaseExport } } - $transformed_items = array_merge($transformed_invoice, $item_array); + $transformed_items = array_merge($transformed_purchase_order, $item_array); $entity = $this->decorateAdvancedFields($purchase_order, $transformed_items); $this->csv->insertOne($entity); @@ -193,7 +192,7 @@ class PurchaseOrderItemExport extends BaseExport private function buildRow(PurchaseOrder $purchase_order) :array { - $transformed_invoice = $this->purchase_order_transformer->transform($purchase_order); + $transformed_purchase_order = $this->purchase_order_transformer->transform($purchase_order); $entity = []; @@ -201,17 +200,17 @@ class PurchaseOrderItemExport extends BaseExport $keyval = array_search($key, $this->entity_keys); if(!$keyval) { - $keyval = array_search(str_replace("invoice.", "", $key), $this->entity_keys) ?? $key; + $keyval = array_search(str_replace("purchase_order.", "", $key), $this->entity_keys) ?? $key; } if(!$keyval) { $keyval = $key; } - if (array_key_exists($key, $transformed_invoice)) { - $entity[$keyval] = $transformed_invoice[$key]; - } elseif (array_key_exists($keyval, $transformed_invoice)) { - $entity[$keyval] = $transformed_invoice[$keyval]; + if (array_key_exists($key, $transformed_purchase_order)) { + $entity[$keyval] = $transformed_purchase_order[$key]; + } elseif (array_key_exists($keyval, $transformed_purchase_order)) { + $entity[$keyval] = $transformed_purchase_order[$keyval]; } else { $entity[$keyval] = $this->resolveKey($keyval, $purchase_order, $this->purchase_order_transformer); } diff --git a/app/Export/CSV/QuoteExport.php b/app/Export/CSV/QuoteExport.php index 8031f01f05e2..2f7d5d736c44 100644 --- a/app/Export/CSV/QuoteExport.php +++ b/app/Export/CSV/QuoteExport.php @@ -21,7 +21,6 @@ use League\Csv\Writer; class QuoteExport extends BaseExport { - private Company $company; private $quote_transformer; @@ -43,7 +42,7 @@ class QuoteExport extends BaseExport 'custom_value4' => 'custom_value4', 'date' => 'date', 'discount' => 'discount', - 'due_date' => 'due_date', + 'valid_until' => 'due_date', 'exchange_rate' => 'exchange_rate', 'footer' => 'footer', 'number' => 'number', @@ -115,17 +114,28 @@ class QuoteExport extends BaseExport private function buildRow(Quote $quote) :array { - $transformed_quote = $this->quote_transformer->transform($quote); + $transformed_entity = $this->quote_transformer->transform($quote); $entity = []; foreach (array_values($this->input['report_keys']) as $key) { $keyval = array_search($key, $this->entity_keys); - if (array_key_exists($key, $transformed_quote)) { - $entity[$keyval] = $transformed_quote[$key]; - } else { - $entity[$keyval] = ''; + if(!$keyval) { + $keyval = array_search(str_replace("invoice.", "", $key), $this->entity_keys) ?? $key; + } + + if(!$keyval) { + $keyval = $key; + } + + if (array_key_exists($key, $transformed_entity)) { + $entity[$keyval] = $transformed_entity[$key]; + } elseif (array_key_exists($keyval, $transformed_entity)) { + $entity[$keyval] = $transformed_entity[$keyval]; + } + else { + $entity[$keyval] = $this->resolveKey($keyval, $quote, $this->quote_transformer); } } diff --git a/app/Export/CSV/QuoteItemExport.php b/app/Export/CSV/QuoteItemExport.php index 4059174a8af8..9af20df7b5ca 100644 --- a/app/Export/CSV/QuoteItemExport.php +++ b/app/Export/CSV/QuoteItemExport.php @@ -21,7 +21,6 @@ use League\Csv\Writer; class QuoteItemExport extends BaseExport { - private Company $company; private $quote_transformer; @@ -63,10 +62,11 @@ class QuoteItemExport extends BaseExport 'terms' => 'terms', 'total_taxes' => 'total_taxes', 'currency' => 'currency_id', - 'qty' => 'item.quantity', - 'unit_cost' => 'item.cost', + 'quantity' => 'item.quantity', + 'cost' => 'item.cost', 'product_key' => 'item.product_key', - 'cost' => 'item.product_cost', + 'buy_price' => 'item.product_cost', + 'cost' => 'item.cost', 'notes' => 'item.notes', 'discount' => 'item.discount', 'is_amount_discount' => 'item.is_amount_discount', @@ -77,11 +77,13 @@ class QuoteItemExport extends BaseExport 'tax_name2' => 'item.tax_name2', 'tax_name3' => 'item.tax_name3', 'line_total' => 'item.line_total', - // 'gross_line_total' => 'item.gross_line_total', - 'custom_value1' => 'item.custom_value1', - 'custom_value2' => 'item.custom_value2', - 'custom_value3' => 'item.custom_value3', - 'custom_value4' => 'item.custom_value4', + 'gross_line_total' => 'item.gross_line_total', + 'quote1' => 'item.custom_value1', + 'quote2' => 'item.custom_value2', + 'quote3' => 'item.custom_value3', + 'quote4' => 'item.custom_value4', + 'tax_category' => 'item.tax_id', + 'type' => 'item.type_id', ]; private array $decorate_keys = [ @@ -135,25 +137,44 @@ class QuoteItemExport extends BaseExport $transformed_items = []; - foreach ($quote->line_items as $item) { - $item_array = []; + $transformed_items = []; - foreach (array_values($this->input['report_keys']) as $key) { - if (str_contains($key, 'item.')) { - $key = str_replace('item.', '', $key); - $item_array[$key] = $item->{$key}; + foreach ($quote->line_items as $item) { + $item_array = []; + + foreach (array_values($this->input['report_keys']) as $key) { //items iterator produces item array + + if (str_contains($key, "item.")) { + + $key = str_replace("item.", "", $key); + + $keyval = $key; + + $keyval = str_replace("custom_value", "quote", $key); + + if($key == 'type_id') + $keyval = 'type'; + + if($key == 'tax_id') + $keyval = 'tax_category'; + + if (property_exists($item, $key)) { + $item_array[$keyval] = $item->{$key}; + } else { + $item_array[$keyval] = ''; + } } } $entity = []; - foreach (array_values($this->input['report_keys']) as $key) { - $keyval = array_search($key, $this->entity_keys); + foreach (array_values($this->input['report_keys']) as $key) { //create an array of report keys only + $keyval = array_search($key, $this->entity_keys); if (array_key_exists($key, $transformed_items)) { $entity[$keyval] = $transformed_items[$key]; } else { - $entity[$keyval] = ''; + $entity[$keyval] = ""; } } @@ -173,16 +194,26 @@ class QuoteItemExport extends BaseExport foreach (array_values($this->input['report_keys']) as $key) { $keyval = array_search($key, $this->entity_keys); + if(!$keyval) { + $keyval = array_search(str_replace("quote.", "", $key), $this->entity_keys) ?? $key; + } + + if(!$keyval) { + $keyval = $key; + } + if (array_key_exists($key, $transformed_quote)) { $entity[$keyval] = $transformed_quote[$key]; - } else { - $entity[$keyval] = ''; + } elseif (array_key_exists($keyval, $transformed_quote)) { + $entity[$keyval] = $transformed_quote[$keyval]; + } + else { + $entity[$keyval] = $this->resolveKey($keyval, $quote, $this->quote_transformer); } } return $this->decorateAdvancedFields($quote, $entity); } - private function decorateAdvancedFields(Quote $quote, array $entity) :array { if (in_array('currency_id', $this->input['report_keys'])) { diff --git a/app/Export/CSV/RecurringInvoiceExport.php b/app/Export/CSV/RecurringInvoiceExport.php index dfce445df17b..6df397c64855 100644 --- a/app/Export/CSV/RecurringInvoiceExport.php +++ b/app/Export/CSV/RecurringInvoiceExport.php @@ -21,7 +21,6 @@ use League\Csv\Writer; class RecurringInvoiceExport extends BaseExport { - private Company $company; private $invoice_transformer; @@ -33,10 +32,10 @@ class RecurringInvoiceExport extends BaseExport 'amount' => 'amount', 'balance' => 'balance', 'client' => 'client_id', - 'custom_surcharge1' => 'custom_surcharge1', - 'custom_surcharge2' => 'custom_surcharge2', - 'custom_surcharge3' => 'custom_surcharge3', - 'custom_surcharge4' => 'custom_surcharge4', + // 'custom_surcharge1' => 'custom_surcharge1', + // 'custom_surcharge2' => 'custom_surcharge2', + // 'custom_surcharge3' => 'custom_surcharge3', + // 'custom_surcharge4' => 'custom_surcharge4', 'custom_value1' => 'custom_value1', 'custom_value2' => 'custom_value2', 'custom_value3' => 'custom_value3', @@ -66,7 +65,8 @@ class RecurringInvoiceExport extends BaseExport 'currency' => 'currency_id', 'vendor' => 'vendor_id', 'project' => 'project_id', - 'frequency' => 'frequency_id' + 'frequency_id' => 'frequency_id', + 'next_send_date' => 'next_send_date' ]; private array $decorate_keys = [ @@ -127,11 +127,22 @@ class RecurringInvoiceExport extends BaseExport foreach (array_values($this->input['report_keys']) as $key) { $keyval = array_search($key, $this->entity_keys); + if(!$keyval) { + $keyval = array_search(str_replace("recurring_invoice.", "", $key), $this->entity_keys) ?? $key; + } + + if(!$keyval) { + $keyval = $key; + } + if (array_key_exists($key, $transformed_invoice)) { $entity[$keyval] = $transformed_invoice[$key]; + } elseif (array_key_exists($keyval, $transformed_invoice)) { + $entity[$keyval] = $transformed_invoice[$keyval]; } else { - $entity[$keyval] = ''; + $entity[$keyval] = $this->resolveKey($keyval, $invoice, $this->invoice_transformer); } + } return $this->decorateAdvancedFields($invoice, $entity); @@ -163,7 +174,9 @@ class RecurringInvoiceExport extends BaseExport $entity['vendor'] = $invoice->vendor ? $invoice->vendor->name : ''; } - $entity['frequency'] = $invoice->frequencyForKey($invoice->frequency_id); + if (in_array('recurring_invoice.frequency_id', $this->input['report_keys']) || in_array('frequency_id', $this->input['report_keys'])) { + $entity['frequency_id'] = $invoice->frequencyForKey($invoice->frequency_id); + } return $entity; } diff --git a/app/Export/CSV/TaskExport.php b/app/Export/CSV/TaskExport.php index 3f51da154851..387bda902043 100644 --- a/app/Export/CSV/TaskExport.php +++ b/app/Export/CSV/TaskExport.php @@ -24,7 +24,6 @@ use League\Csv\Writer; class TaskExport extends BaseExport { - private Company $company; private $entity_transformer; @@ -47,9 +46,7 @@ class TaskExport extends BaseExport 'custom_value4' => 'custom_value4', 'status' => 'status_id', 'project' => 'project_id', - 'invoice' => 'invoice_id', - 'client' => 'client_id', - ]; + ]; private array $decorate_keys = [ 'status', @@ -110,38 +107,39 @@ class TaskExport extends BaseExport $entity = []; $transformed_entity = $this->entity_transformer->transform($task); + foreach (array_values($this->input['report_keys']) as $key) { + $keyval = array_search($key, $this->entity_keys); + + if(!$keyval) { + $keyval = array_search(str_replace("task.", "", $key), $this->entity_keys) ?? $key; + } + + if(!$keyval) { + $keyval = $key; + } + + if (array_key_exists($key, $transformed_entity)) { + $entity[$keyval] = $transformed_entity[$key]; + } elseif (array_key_exists($keyval, $transformed_entity)) { + $entity[$keyval] = $transformed_entity[$keyval]; + } + else { + $entity[$keyval] = $this->resolveKey($keyval, $task, $this->entity_transformer); + } + } + + $entity['start_date'] = ''; + $entity['end_date'] = ''; + $entity['duration'] = ''; + + if (is_null($task->time_log) || (is_array(json_decode($task->time_log, 1)) && count(json_decode($task->time_log, 1)) == 0)) { - foreach (array_values($this->input['report_keys']) as $key) { - $keyval = array_search($key, $this->entity_keys); - - if (array_key_exists($key, $transformed_entity)) { - $entity[$keyval] = $transformed_entity[$key]; - } else { - $entity[$keyval] = ''; - } - } - - $entity['start_date'] = ''; - $entity['end_date'] = ''; - $entity['duration'] = ''; - - $entity = $this->decorateAdvancedFields($task, $entity); - - ksort($entity); $this->csv->insertOne($entity); - } elseif (is_array(json_decode($task->time_log, 1)) && count(json_decode($task->time_log, 1)) > 0) { - foreach (array_values($this->input['report_keys']) as $key) { - $keyval = array_search($key, $this->entity_keys); - - if (array_key_exists($key, $transformed_entity)) { - $entity[$keyval] = $transformed_entity[$key]; - } else { - $entity[$keyval] = ''; - } - } - + } else { $this->iterateLogs($task, $entity); } + + } private function iterateLogs(Task $task, array $entity) @@ -164,39 +162,26 @@ class TaskExport extends BaseExport } foreach ($logs as $key => $item) { - if (in_array('start_date', $this->input['report_keys'])) { + if (in_array('task.start_date', $this->input['report_keys']) || in_array('start_date', $this->input['report_keys'])) { $entity['start_date'] = Carbon::createFromTimeStamp($item[0])->setTimezone($timezone_name)->format($date_format_default); } - if (in_array('end_date', $this->input['report_keys']) && $item[1] > 0) { + if ((in_array('task.end_date', $this->input['report_keys']) || in_array('end_date', $this->input['report_keys'])) && $item[1] > 0) { $entity['end_date'] = Carbon::createFromTimeStamp($item[1])->setTimezone($timezone_name)->format($date_format_default); } - if (in_array('end_date', $this->input['report_keys']) && $item[1] == 0) { + if ((in_array('task.end_date', $this->input['report_keys']) || in_array('end_date', $this->input['report_keys'])) && $item[1] == 0) { $entity['end_date'] = ctrans('texts.is_running'); } - if (in_array('duration', $this->input['report_keys'])) { + if (in_array('task.duration', $this->input['report_keys']) || in_array('duration', $this->input['report_keys'])) { $entity['duration'] = $task->calcDuration(); } - - if (! array_key_exists('duration', $entity)) { - $entity['duration'] = ''; - } - - if (! array_key_exists('start_date', $entity)) { - $entity['start_date'] = ''; - } - - if (! array_key_exists('end_date', $entity)) { - $entity['end_date'] = ''; - } - + $entity = $this->decorateAdvancedFields($task, $entity); - - ksort($entity); + $this->csv->insertOne($entity); - + unset($entity['start_date']); unset($entity['end_date']); unset($entity['duration']); @@ -213,14 +198,6 @@ class TaskExport extends BaseExport $entity['project'] = $task->project()->exists() ? $task->project->name : ''; } - if (in_array('client_id', $this->input['report_keys'])) { - $entity['client'] = $task->client ? $task->client->present()->name() : ''; - } - - if (in_array('invoice_id', $this->input['report_keys'])) { - $entity['invoice'] = $task->invoice ? $task->invoice->number : ''; - } - return $entity; } } diff --git a/app/Export/CSV/VendorExport.php b/app/Export/CSV/VendorExport.php index e978c36e1432..efc8bf32134e 100644 --- a/app/Export/CSV/VendorExport.php +++ b/app/Export/CSV/VendorExport.php @@ -22,7 +22,6 @@ use League\Csv\Writer; class VendorExport extends BaseExport { - private $company; private $vendor_transformer; @@ -115,7 +114,7 @@ class VendorExport extends BaseExport private function buildRow(Vendor $vendor) :array { - $transformed_contact = false; + $transformed_contact = []; $transformed_vendor = $this->vendor_transformer->transform($vendor); diff --git a/app/Factory/CreditFactory.php b/app/Factory/CreditFactory.php index bb3643d469d2..ec1d8341a39b 100644 --- a/app/Factory/CreditFactory.php +++ b/app/Factory/CreditFactory.php @@ -49,7 +49,7 @@ class CreditFactory $credit->user_id = $user_id; $credit->company_id = $company_id; $credit->recurring_id = null; - + $credit->exchange_rate = 1; return $credit; } } diff --git a/app/Factory/PurchaseOrderFactory.php b/app/Factory/PurchaseOrderFactory.php index 31a082ac83b7..4e68f9ad2f0f 100644 --- a/app/Factory/PurchaseOrderFactory.php +++ b/app/Factory/PurchaseOrderFactory.php @@ -49,7 +49,8 @@ class PurchaseOrderFactory $purchase_order->user_id = $user_id; $purchase_order->company_id = $company_id; $purchase_order->recurring_id = null; - + $purchase_order->exchange_rate = 1; + return $purchase_order; } } diff --git a/app/Factory/QuoteFactory.php b/app/Factory/QuoteFactory.php index 64a4b42f244f..d312a6215646 100644 --- a/app/Factory/QuoteFactory.php +++ b/app/Factory/QuoteFactory.php @@ -46,6 +46,7 @@ class QuoteFactory $quote->user_id = $user_id; $quote->company_id = $company_id; $quote->paid_to_date = 0; + $quote->exchange_rate = 1; return $quote; } diff --git a/app/Filters/ExpenseFilters.php b/app/Filters/ExpenseFilters.php index b057ad3f27f9..4475973e3e8f 100644 --- a/app/Filters/ExpenseFilters.php +++ b/app/Filters/ExpenseFilters.php @@ -32,11 +32,12 @@ class ExpenseFilters extends QueryFilters } return $this->builder->where(function ($query) use ($filter) { - $query->where('public_notes', 'like', '%'.$filter.'%') - ->orWhere('custom_value1', 'like', '%'.$filter.'%') - ->orWhere('custom_value2', 'like', '%'.$filter.'%') - ->orWhere('custom_value3', 'like', '%'.$filter.'%') - ->orWhere('custom_value4', 'like', '%'.$filter.'%'); + $query->where('number', 'like', '%'.$filter.'%') + ->orWhere('public_notes', 'like', '%'.$filter.'%') + ->orWhere('custom_value1', 'like', '%'.$filter.'%') + ->orWhere('custom_value2', 'like', '%'.$filter.'%') + ->orWhere('custom_value3', 'like', '%'.$filter.'%') + ->orWhere('custom_value4', 'like', '%'.$filter.'%'); }); } diff --git a/app/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index fe5c075b99ac..3d419a66ae9d 100644 --- a/app/Helpers/Invoice/InvoiceItemSum.php +++ b/app/Helpers/Invoice/InvoiceItemSum.php @@ -238,14 +238,20 @@ class InvoiceItemSum { $this->rule->tax($this->item); + $precision = strlen(substr(strrchr($this->rule->tax_rate1, "."), 1)); + $this->item->tax_name1 = $this->rule->tax_name1; - $this->item->tax_rate1 = $this->rule->tax_rate1; + $this->item->tax_rate1 = round($this->rule->tax_rate1, $precision); + + $precision = strlen(substr(strrchr($this->rule->tax_rate2, "."), 1)); $this->item->tax_name2 = $this->rule->tax_name2; - $this->item->tax_rate2 = $this->rule->tax_rate2; + $this->item->tax_rate2 = round($this->rule->tax_rate2, $precision); + + $precision = strlen(substr(strrchr($this->rule->tax_rate3, "."), 1)); $this->item->tax_name3 = $this->rule->tax_name3; - $this->item->tax_rate3 = $this->rule->tax_rate3; + $this->item->tax_rate3 = round($this->rule->tax_rate3, $precision); return $this; } diff --git a/app/Http/Controllers/ActivityController.php b/app/Http/Controllers/ActivityController.php index cf69f531cbfd..b201296e0db4 100644 --- a/app/Http/Controllers/ActivityController.php +++ b/app/Http/Controllers/ActivityController.php @@ -11,24 +11,23 @@ namespace App\Http\Controllers; -use App\Http\Requests\Activity\DownloadHistoricalEntityRequest; -use App\Models\Activity; -use App\Transformers\ActivityTransformer; -use App\Utils\HostedPDF\NinjaPdf; -use App\Utils\Ninja; -use App\Utils\PhantomJS\Phantom; -use App\Utils\Traits\Pdf\PageNumbering; -use App\Utils\Traits\Pdf\PdfMaker; -use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; -use Illuminate\Http\Response; -use Illuminate\Support\Facades\Storage; use stdClass; -use Symfony\Component\HttpFoundation\StreamedResponse; +use App\Utils\Ninja; +use App\Models\Activity; +use Illuminate\Http\Request; +use App\Utils\Traits\MakesHash; +use App\Utils\PhantomJS\Phantom; +use App\Utils\HostedPDF\NinjaPdf; +use App\Utils\Traits\Pdf\PdfMaker; +use App\Utils\Traits\Pdf\PageNumbering; +use Illuminate\Support\Facades\Storage; +use App\Transformers\ActivityTransformer; +use App\Http\Requests\Activity\ShowActivityRequest; +use App\Http\Requests\Activity\DownloadHistoricalEntityRequest; class ActivityController extends BaseController { - use PdfMaker, PageNumbering; + use PdfMaker, PageNumbering, MakesHash; protected $entity_type = Activity::class; @@ -39,50 +38,6 @@ class ActivityController extends BaseController parent::__construct(); } - /** - * @OA\Get( - * path="/api/v1/activities", - * operationId="getActivities", - * tags={"actvities"}, - * summary="Gets a list of actvities", - * description="Lists all activities", - * @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"), - * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), - * @OA\Parameter(ref="#/components/parameters/include"), - * @OA\Parameter(ref="#/components/parameters/index"), - * @OA\Parameter( - * name="rows", - * in="query", - * description="The number of activities to return", - * example="50", - * required=false, - * @OA\Schema( - * type="number", - * format="integer", - * ), - * ), - * @OA\Response( - * response=200, - * description="A list of actvities", - * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), - * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), - * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), - * @OA\JsonContent(ref="#/components/schemas/Activity"), - * ), - * @OA\Response( - * response=422, - * description="Validation error", - * @OA\JsonContent(ref="#/components/schemas/ValidationError"), - * ), - * @OA\Response( - * response="default", - * description="Unexpected Error", - * @OA\JsonContent(ref="#/components/schemas/Error"), - * ), - * ) - * @param Request $request - * @return Response|mixed - */ public function index(Request $request) { $default_activities = $request->has('rows') ? $request->input('rows') : 75; @@ -115,47 +70,36 @@ class ActivityController extends BaseController return $this->listResponse($activities); } - /** - * @OA\Get( - * path="/api/v1/actvities/download_entity/{activity_id}", - * operationId="getActivityHistoricalEntityPdf", - * tags={"actvities"}, - * summary="Gets a PDF for the given activity", - * description="Gets a PDF for the given activity", - * @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"), - * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), - * @OA\Parameter( - * name="activity_id", - * in="path", - * description="The Activity Hashed ID", - * example="D2J234DFA", - * required=true, - * @OA\Schema( - * type="string", - * format="string", - * ), - * ), - * @OA\Response( - * response=200, - * description="PDF File", - * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), - * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), - * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), - * ), - * @OA\Response( - * response=404, - * description="No file exists for the given record", - * ), - * @OA\Response( - * response="default", - * description="Unexpected Error", - * @OA\JsonContent(ref="#/components/schemas/Error"), - * ), - * ) - * @param DownloadHistoricalEntityRequest $request - * @param Activity $activity - * @return JsonResponse|StreamedResponse - */ + public function entityActivity(ShowActivityRequest $request) + { + + $default_activities = request()->has('rows') ? request()->input('rows') : 75; + + $activities = Activity::with('user') + ->orderBy('created_at', 'DESC') + ->company() + ->where("{$request->entity}_id", $request->entity_id) + ->take($default_activities); + + /** @var \App\Models\User auth()->user() */ + $user = auth()->user(); + + if (!$user->isAdmin()) { + $activities->where('user_id', auth()->user()->id); + } + + $system = ctrans('texts.system'); + + $data = $activities->cursor()->map(function ($activity) use ($system) { + + return $activity->activity_string(); + + }); + + return response()->json(['data' => $data->toArray()], 200); + + } + public function downloadHistoricalEntity(DownloadHistoricalEntityRequest $request, Activity $activity) { $backup = $activity->backup; diff --git a/app/Http/Controllers/ClientPortal/InvoiceController.php b/app/Http/Controllers/ClientPortal/InvoiceController.php index 6e04f4a7e938..2e432e964210 100644 --- a/app/Http/Controllers/ClientPortal/InvoiceController.php +++ b/app/Http/Controllers/ClientPortal/InvoiceController.php @@ -86,7 +86,7 @@ class InvoiceController extends Controller public function showBlob($hash) { - $data = Cache::pull($hash); + $data = Cache::get($hash); match($data['entity_type']){ 'invoice' => $invitation = InvoiceInvitation::withTrashed()->find($data['invitation_id']), diff --git a/app/Http/Controllers/ProtectedDownloadController.php b/app/Http/Controllers/ProtectedDownloadController.php index d7277d69f383..c333539b99ab 100644 --- a/app/Http/Controllers/ProtectedDownloadController.php +++ b/app/Http/Controllers/ProtectedDownloadController.php @@ -29,8 +29,10 @@ class ProtectedDownloadController extends BaseController throw new SystemError('File no longer available', 404); abort(404, 'File no longer available'); } - - return response()->download($hashed_path, basename($hashed_path), [])->deleteFileAfterSend(true); + + return response()->streamDownload(function () use ($hashed_path) { + echo Storage::get($hashed_path); + }, basename($hashed_path), []); } diff --git a/app/Http/Requests/Activity/ShowActivityRequest.php b/app/Http/Requests/Activity/ShowActivityRequest.php index d7c745cb5d10..219844f35dbe 100644 --- a/app/Http/Requests/Activity/ShowActivityRequest.php +++ b/app/Http/Requests/Activity/ShowActivityRequest.php @@ -12,10 +12,12 @@ namespace App\Http\Requests\Activity; use App\Http\Requests\Request; -use App\Models\Activity; +use App\Utils\Traits\MakesHash; class ShowActivityRequest extends Request { + use MakesHash; + /** * Determine if the user is authorized to make this request. * @@ -23,7 +25,25 @@ class ShowActivityRequest extends Request */ public function authorize() : bool { - // return auth()->user()->isAdmin(); - return auth()->user()->can('view', Activity::class); + return true; + } + + public function rules() + { + return [ + 'entity' => 'bail|required|in:invoice,quote,credit,purchase_order,payment,client,vendor,expense,task,project,subscription,recurring_invoice,', + 'entity_id' => 'bail|required|exists:'.$this->entity.'s,id,company_id,'.auth()->user()->company()->id, + ]; + } + + public function prepareForValidation() + { + $input = $this->all(); + + if(isset($input['entity_id'])) + $input['entity_id'] = $this->decodePrimaryKey($input['entity_id']); + + $this->replace($input); + } } diff --git a/app/Http/Requests/Chart/ShowChartRequest.php b/app/Http/Requests/Chart/ShowChartRequest.php index f7b4f63ba7e0..39517cd29db2 100644 --- a/app/Http/Requests/Chart/ShowChartRequest.php +++ b/app/Http/Requests/Chart/ShowChartRequest.php @@ -45,7 +45,7 @@ class ShowChartRequest extends Request $input = $this->all(); if(isset($input['date_range'])) { - $dates = $this->calculateStartAndEndDates($input); + $dates = $this->calculateStartAndEndDates($input, auth()->user()->company()); $input['start_date'] = $dates[0]; $input['end_date'] = $dates[1]; } diff --git a/app/Http/Requests/Credit/StoreCreditRequest.php b/app/Http/Requests/Credit/StoreCreditRequest.php index 13eee9c59852..8616ab177bc0 100644 --- a/app/Http/Requests/Credit/StoreCreditRequest.php +++ b/app/Http/Requests/Credit/StoreCreditRequest.php @@ -67,7 +67,8 @@ class StoreCreditRequest extends Request $rules['tax_name1'] = 'bail|sometimes|string|nullable'; $rules['tax_name2'] = 'bail|sometimes|string|nullable'; $rules['tax_name3'] = 'bail|sometimes|string|nullable'; - + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; + if ($this->invoice_id) { $rules['invoice_id'] = new ValidInvoiceCreditRule(); } @@ -88,7 +89,11 @@ class StoreCreditRequest extends Request $input = $this->decodePrimaryKeys($input); $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; - //$input['line_items'] = json_encode($input['line_items']); + + if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } + $this->replace($input); } } diff --git a/app/Http/Requests/Credit/UpdateCreditRequest.php b/app/Http/Requests/Credit/UpdateCreditRequest.php index 929ad49e443a..a397d41a8862 100644 --- a/app/Http/Requests/Credit/UpdateCreditRequest.php +++ b/app/Http/Requests/Credit/UpdateCreditRequest.php @@ -67,7 +67,8 @@ class UpdateCreditRequest extends Request $rules['tax_name1'] = 'bail|sometimes|string|nullable'; $rules['tax_name2'] = 'bail|sometimes|string|nullable'; $rules['tax_name3'] = 'bail|sometimes|string|nullable'; - + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; + return $rules; } @@ -81,6 +82,10 @@ class UpdateCreditRequest extends Request $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; } + if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } + $input['id'] = $this->credit->id; $this->replace($input); diff --git a/app/Http/Requests/Invoice/StoreInvoiceRequest.php b/app/Http/Requests/Invoice/StoreInvoiceRequest.php index 327aa8b96a04..37ff17758412 100644 --- a/app/Http/Requests/Invoice/StoreInvoiceRequest.php +++ b/app/Http/Requests/Invoice/StoreInvoiceRequest.php @@ -72,7 +72,8 @@ class StoreInvoiceRequest extends Request $rules['tax_name1'] = 'bail|sometimes|string|nullable'; $rules['tax_name2'] = 'bail|sometimes|string|nullable'; $rules['tax_name3'] = 'bail|sometimes|string|nullable'; - + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; + return $rules; } diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index 0a6992192e93..971de8712cd3 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -72,7 +72,8 @@ class UpdateInvoiceRequest extends Request $rules['tax_name2'] = 'bail|sometimes|string|nullable'; $rules['tax_name3'] = 'bail|sometimes|string|nullable'; $rules['status_id'] = 'bail|sometimes|not_in:5'; //do not all cancelled invoices to be modfified. - + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; + // not needed. // $rules['partial_due_date'] = 'bail|sometimes|required_unless:partial,0,null'; @@ -95,6 +96,10 @@ class UpdateInvoiceRequest extends Request unset($input['documents']); } + if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } + $this->replace($input); } diff --git a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php index 91a312203354..71d8fe32de99 100644 --- a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php @@ -60,7 +60,8 @@ class StorePurchaseOrderRequest extends Request } $rules['status_id'] = 'nullable|integer|in:1,2,3,4,5'; - + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; + return $rules; } @@ -77,6 +78,10 @@ class StorePurchaseOrderRequest extends Request $input['amount'] = 0; $input['balance'] = 0; + if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } + $this->replace($input); } } diff --git a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php index e23fa91520b8..f772944ddd53 100644 --- a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php @@ -63,6 +63,7 @@ class UpdatePurchaseOrderRequest extends Request } $rules['status_id'] = 'sometimes|integer|in:1,2,3,4,5'; + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; return $rules; } @@ -79,6 +80,10 @@ class UpdatePurchaseOrderRequest extends Request $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; } + if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } + $this->replace($input); } } diff --git a/app/Http/Requests/Quote/StoreQuoteRequest.php b/app/Http/Requests/Quote/StoreQuoteRequest.php index 387f165de110..39e03dc37e33 100644 --- a/app/Http/Requests/Quote/StoreQuoteRequest.php +++ b/app/Http/Requests/Quote/StoreQuoteRequest.php @@ -55,6 +55,7 @@ class StoreQuoteRequest extends Request $rules['discount'] = 'sometimes|numeric'; $rules['is_amount_discount'] = ['boolean']; + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; // $rules['number'] = new UniqueQuoteNumberRule($this->all()); $rules['line_items'] = 'array'; @@ -72,6 +73,10 @@ class StoreQuoteRequest extends Request $input['amount'] = 0; $input['balance'] = 0; + if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } + $this->replace($input); } } diff --git a/app/Http/Requests/Quote/UpdateQuoteRequest.php b/app/Http/Requests/Quote/UpdateQuoteRequest.php index 07b463936892..b692e82663d4 100644 --- a/app/Http/Requests/Quote/UpdateQuoteRequest.php +++ b/app/Http/Requests/Quote/UpdateQuoteRequest.php @@ -57,6 +57,7 @@ class UpdateQuoteRequest extends Request $rules['line_items'] = 'array'; $rules['discount'] = 'sometimes|numeric'; $rules['is_amount_discount'] = ['boolean']; + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; return $rules; } @@ -75,6 +76,10 @@ class UpdateQuoteRequest extends Request unset($input['documents']); } + if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } + $input['id'] = $this->quote->id; $this->replace($input); diff --git a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php index 420afb7c09f7..11c266ac8f46 100644 --- a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php @@ -67,7 +67,8 @@ class StoreRecurringInvoiceRequest extends Request $rules['tax_name2'] = 'bail|sometimes|string|nullable'; $rules['tax_name3'] = 'bail|sometimes|string|nullable'; $rules['due_date_days'] = 'bail|sometimes|string'; - + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; + return $rules; } @@ -143,6 +144,10 @@ class StoreRecurringInvoiceRequest extends Request unset($input['number']); } + if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } + $this->replace($input); } diff --git a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php index f268cbe84288..26cc4e2f44de 100644 --- a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php @@ -61,7 +61,8 @@ class UpdateRecurringInvoiceRequest extends Request $rules['tax_name1'] = 'bail|sometimes|string|nullable'; $rules['tax_name2'] = 'bail|sometimes|string|nullable'; $rules['tax_name3'] = 'bail|sometimes|string|nullable'; - + $rules['exchange_rate'] = 'bail|sometimes|gt:0'; + return $rules; } @@ -121,6 +122,10 @@ class UpdateRecurringInvoiceRequest extends Request unset($input['documents']); } + if (array_key_exists('exchange_rate', $input) && is_null($input['exchange_rate'])) { + $input['exchange_rate'] = 1; + } + $this->replace($input); } diff --git a/app/Import/Transformer/BaseTransformer.php b/app/Import/Transformer/BaseTransformer.php index 0bdf3051decc..ddc190b029ed 100644 --- a/app/Import/Transformer/BaseTransformer.php +++ b/app/Import/Transformer/BaseTransformer.php @@ -319,6 +319,21 @@ class BaseTransformer // return Number::parseFloat($number); } + /** + * @param $data + * @param $field + * + * @return float + */ + public function getFloatOrOne($data, $field) + { + if (array_key_exists($field, $data)) + return Number::parseStringFloat($data[$field]) > 0 ? Number::parseStringFloat($data[$field]) : 1; + + return 1; + + } + /** * @param $name * diff --git a/app/Import/Transformer/Csv/InvoiceTransformer.php b/app/Import/Transformer/Csv/InvoiceTransformer.php index 807a4c2eff43..1399990a2a9e 100644 --- a/app/Import/Transformer/Csv/InvoiceTransformer.php +++ b/app/Import/Transformer/Csv/InvoiceTransformer.php @@ -114,7 +114,7 @@ class InvoiceTransformer extends BaseTransformer $invoice_data, 'invoice.custom_surcharge4' ), - 'exchange_rate' => $this->getString( + 'exchange_rate' => $this->getFloatOrOne( $invoice_data, 'invoice.exchange_rate' ), diff --git a/app/Import/Transformer/Csv/PaymentTransformer.php b/app/Import/Transformer/Csv/PaymentTransformer.php index 3ea61e845d47..44fcc9de52e4 100644 --- a/app/Import/Transformer/Csv/PaymentTransformer.php +++ b/app/Import/Transformer/Csv/PaymentTransformer.php @@ -46,7 +46,7 @@ class PaymentTransformer extends BaseTransformer $data, 'payment.transaction_reference ' ), - 'date' => $this->getString($data, 'payment.date'), + 'date' => isset($data['payment.date']) ? $this->parseDate($data['payment.date']) : date('y-m-d'), 'private_notes' => $this->getString($data, 'payment.private_notes'), 'custom_value1' => $this->getString($data, 'payment.custom_value1'), 'custom_value2' => $this->getString($data, 'payment.custom_value2'), diff --git a/app/Import/Transformer/Csv/QuoteTransformer.php b/app/Import/Transformer/Csv/QuoteTransformer.php index a61014e3a22d..93e24120d0bf 100644 --- a/app/Import/Transformer/Csv/QuoteTransformer.php +++ b/app/Import/Transformer/Csv/QuoteTransformer.php @@ -114,7 +114,7 @@ class QuoteTransformer extends BaseTransformer $quote_data, 'quote.custom_surcharge4' ), - 'exchange_rate' => $this->getString( + 'exchange_rate' => $this->getFloatOrOne( $quote_data, 'quote.exchange_rate' ), diff --git a/app/Import/Transformer/Csv/RecurringInvoiceTransformer.php b/app/Import/Transformer/Csv/RecurringInvoiceTransformer.php index e19331d36151..d4c2a17bdffd 100644 --- a/app/Import/Transformer/Csv/RecurringInvoiceTransformer.php +++ b/app/Import/Transformer/Csv/RecurringInvoiceTransformer.php @@ -122,7 +122,7 @@ class RecurringInvoiceTransformer extends BaseTransformer $invoice_data, 'invoice.custom_surcharge4' ), - 'exchange_rate' => $this->getString( + 'exchange_rate' => $this->getFloat( $invoice_data, 'invoice.exchange_rate' ), diff --git a/app/Jobs/Util/ReminderJob.php b/app/Jobs/Util/ReminderJob.php index 80f2676d2ab5..1c10a0a3d5c4 100644 --- a/app/Jobs/Util/ReminderJob.php +++ b/app/Jobs/Util/ReminderJob.php @@ -129,12 +129,10 @@ class ReminderJob implements ShouldQueue $invoice->service()->touchReminder($reminder_template)->save(); $fees = $this->calcLateFee($invoice, $reminder_template); - if(in_array($invoice->client->getSetting('lock_invoices'), ['when_sent','when_paid'])) { + if($invoice->isLocked()) return $this->addFeeToNewInvoice($invoice, $reminder_template, $fees); - } - else - $invoice = $this->setLateFee($invoice, $fees[0], $fees[1]); - + + $invoice = $this->setLateFee($invoice, $fees[0], $fees[1]); //20-04-2022 fixes for endless reminders - generic template naming was wrong $enabled_reminder = 'enable_'.$reminder_template; diff --git a/app/Models/Account.php b/app/Models/Account.php index e6a5701f558c..46ba1c330ace 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -554,7 +554,7 @@ class Account extends BaseModel $nmo->to_user = $this->companies()->first()->owner(); NinjaMailerJob::dispatch($nmo, true); - Cache::put("throttle_notified:{$this->key}", true, 60 * 24); + Cache::put("throttle_notified:{$this->key}", true, 60 * 60 * 24); if (config('ninja.notification.slack')) { $this->companies()->first()->notification(new EmailQuotaNotification($this))->ninja(); diff --git a/app/Models/Quote.php b/app/Models/Quote.php index ae0d4b2333d2..04c3cee2cd94 100644 --- a/app/Models/Quote.php +++ b/app/Models/Quote.php @@ -455,7 +455,7 @@ class Quote extends BaseModel case self::STATUS_DRAFT: return ctrans('texts.draft'); case self::STATUS_SENT: - return ctrans('texts.pending'); + return ctrans('texts.sent'); case self::STATUS_APPROVED: return ctrans('texts.approved'); case self::STATUS_EXPIRED: diff --git a/app/PaymentDrivers/CheckoutComPaymentDriver.php b/app/PaymentDrivers/CheckoutComPaymentDriver.php index e54d76dd6881..77b258661c39 100644 --- a/app/PaymentDrivers/CheckoutComPaymentDriver.php +++ b/app/PaymentDrivers/CheckoutComPaymentDriver.php @@ -440,6 +440,9 @@ class CheckoutComPaymentDriver extends BaseDriver $request->query('cko-session-id') ); + nlog("checkout3ds"); + nlog($payment); + if (isset($payment['approved']) && $payment['approved']) { return $this->processSuccessfulPayment($payment); } else { diff --git a/app/Repositories/BaseRepository.php b/app/Repositories/BaseRepository.php index 15c93b5fe556..0350cb00b95a 100644 --- a/app/Repositories/BaseRepository.php +++ b/app/Repositories/BaseRepository.php @@ -180,6 +180,8 @@ class BaseRepository unset($tmp_data['client_contacts']); } + nlog($tmp_data); + $model->fill($tmp_data); $model->custom_surcharge_tax1 = $client->company->custom_surcharge_taxes1; diff --git a/app/Repositories/InvoiceRepository.php b/app/Repositories/InvoiceRepository.php index 83b6980f1567..c7347da552b0 100644 --- a/app/Repositories/InvoiceRepository.php +++ b/app/Repositories/InvoiceRepository.php @@ -97,6 +97,10 @@ class InvoiceRepository extends BaseRepository // reversed delete invoice actions $invoice = $invoice->service()->handleRestore()->save(); + /* If the reverse did not succeed due to rules, then do not restore / unarchive */ + if($invoice->is_deleted) + return $invoice; + parent::restore($invoice); return $invoice; diff --git a/app/Repositories/TaskRepository.php b/app/Repositories/TaskRepository.php index 387979342aeb..be1f17e4911d 100644 --- a/app/Repositories/TaskRepository.php +++ b/app/Repositories/TaskRepository.php @@ -101,9 +101,6 @@ class TaskRepository extends BaseRepository $key_values = array_column($time_log, 0); array_multisort($key_values, SORT_ASC, $time_log); - // array_multisort($time_log); - // ksort($time_log); - if (isset($data['action'])) { if ($data['action'] == 'start') { $task->is_running = true; @@ -121,8 +118,12 @@ class TaskRepository extends BaseRepository $task->is_running = $data['is_running'] ? 1 : 0; } + $task->calculated_start_date = $this->harvestStartDate($time_log); + $task->time_log = json_encode($time_log); + + $task->saveQuietly(); if (array_key_exists('documents', $data)) { @@ -132,6 +133,17 @@ class TaskRepository extends BaseRepository return $task; } + private function harvestStartDate($time_log) + { + + if(isset($time_log[0][0])){ + return \Carbon\Carbon::createFromTimestamp($time_log[0][0]); + } + + return null; + + } + /** * Store tasks in bulk. * @@ -199,8 +211,12 @@ class TaskRepository extends BaseRepository if (strlen($task->time_log) < 5) { $log = []; - $log = array_merge($log, [[time(), 0]]); + $start_time = time(); + + $log = array_merge($log, [[$start_time, 0]]); $task->time_log = json_encode($log); + $task->calculated_start_date = \Carbon\Carbon::createFromTimestamp($start_time); + $task->saveQuietly(); } diff --git a/app/Repositories/VendorRepository.php b/app/Repositories/VendorRepository.php index 4c4fc5fa6b23..bb9266014701 100644 --- a/app/Repositories/VendorRepository.php +++ b/app/Repositories/VendorRepository.php @@ -44,7 +44,9 @@ class VendorRepository extends BaseRepository public function save(array $data, Vendor $vendor) : ?Vendor { $vendor->fill($data); - + + nlog($data); + $vendor->saveQuietly(); if ($vendor->number == '' || ! $vendor->number) { diff --git a/app/Services/Invoice/HandleRestore.php b/app/Services/Invoice/HandleRestore.php index c3e2947e8732..d5bf6c8275ba 100644 --- a/app/Services/Invoice/HandleRestore.php +++ b/app/Services/Invoice/HandleRestore.php @@ -44,6 +44,7 @@ class HandleRestore extends AbstractService //cannot restore an invoice with a deleted payment foreach ($this->invoice->payments as $payment) { if (($this->invoice->paid_to_date == 0) && $payment->is_deleted) { + $this->invoice->delete(); return $this->invoice; } } diff --git a/app/Transformers/TaskTransformer.php b/app/Transformers/TaskTransformer.php index 028e2bf44ef4..d2080b6a4500 100644 --- a/app/Transformers/TaskTransformer.php +++ b/app/Transformers/TaskTransformer.php @@ -37,6 +37,7 @@ class TaskTransformer extends EntityTransformer 'status', 'project', 'user', + 'invoice', ]; public function includeDocuments(Task $task) @@ -46,6 +47,17 @@ class TaskTransformer extends EntityTransformer return $this->includeCollection($task->documents, $transformer, Document::class); } + public function includeInvoice(Task $task): ?Item + { + $transformer = new InvoiceTransformer($this->serializer); + + if (!$task->user) { + return null; + } + + return $this->includeItem($task->invoice, $transformer, Invoice::class); + } + public function includeUser(Task $task): ?Item { $transformer = new UserTransformer($this->serializer); diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index 27451e68d1f6..621568d2d0f2 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -583,8 +583,11 @@ class HtmlEngine if ($this->settings->signature_on_pdf) { $data['$contact.signature'] = ['value' => $this->invitation->signature_base64, 'label' => ctrans('texts.signature')]; + $data['$contact.signature_date'] = ['value' => $this->translateDate($this->invitation->signature_date, $this->client->date_format(), $this->client->locale()), 'label' => ctrans('texts.date')]; + } else { $data['$contact.signature'] = ['value' => '', 'label' => '']; + $data['$contact.signature_date'] = ['value' => '', 'label' => ctrans('texts.date')]; } $data['$thanks'] = ['value' => '', 'label' => ctrans('texts.thanks')]; diff --git a/app/Utils/Traits/MakesDates.php b/app/Utils/Traits/MakesDates.php index caea8e926d64..69ccb7a8b3c4 100644 --- a/app/Utils/Traits/MakesDates.php +++ b/app/Utils/Traits/MakesDates.php @@ -14,6 +14,7 @@ namespace App\Utils\Traits; use DateTime; use DateTimeZone; use Carbon\Carbon; +use App\Models\Company; use App\DataMapper\Schedule\EmailStatement; /** @@ -119,8 +120,32 @@ trait MakesDates * * @return array [$start_date, $end_date]; */ - public function calculateStartAndEndDates(array $data): array + public function calculateStartAndEndDates(array $data, ?Company $company = null): array { + //override for financial years + if($data['date_range'] == 'this_year') { + $first_month_of_year = $company ? $company?->first_month_of_year : 1; + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + if(now()->lt($fin_year_start)) + $fin_year_start->subYearNoOverflow(); + + } + + //override for financial years + if($data['date_range'] == 'last_year') { + $first_month_of_year = $company ? $company?->first_month_of_year : 1; + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + $fin_year_start->subYearNoOverflow(); + + if(now()->subYear()->lt($fin_year_start)) { + $fin_year_start->subYearNoOverflow(); + } + + } + + return match ($data['date_range']) { EmailStatement::LAST7 => [now()->startOfDay()->subDays(7)->format('Y-m-d'), now()->startOfDay()->format('Y-m-d')], EmailStatement::LAST30 => [now()->startOfDay()->subDays(30)->format('Y-m-d'), now()->startOfDay()->format('Y-m-d')], @@ -129,8 +154,8 @@ trait MakesDates EmailStatement::LAST_MONTH => [now()->startOfDay()->subMonthNoOverflow()->firstOfMonth()->format('Y-m-d'), now()->startOfDay()->subMonthNoOverflow()->lastOfMonth()->format('Y-m-d')], EmailStatement::THIS_QUARTER => [now()->startOfDay()->firstOfQuarter()->format('Y-m-d'), now()->startOfDay()->lastOfQuarter()->format('Y-m-d')], EmailStatement::LAST_QUARTER => [now()->startOfDay()->subQuarterNoOverflow()->firstOfQuarter()->format('Y-m-d'), now()->startOfDay()->subQuarterNoOverflow()->lastOfQuarter()->format('Y-m-d')], - EmailStatement::THIS_YEAR => [now()->startOfDay()->firstOfYear()->format('Y-m-d'), now()->startOfDay()->lastOfYear()->format('Y-m-d')], - EmailStatement::LAST_YEAR => [now()->startOfDay()->subYearNoOverflow()->firstOfYear()->format('Y-m-d'), now()->startOfDay()->subYearNoOverflow()->lastOfYear()->format('Y-m-d')], + EmailStatement::THIS_YEAR => [$fin_year_start->format('Y-m-d'), $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d')], + EmailStatement::LAST_YEAR => [$fin_year_start->format('Y-m-d'), $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d')], EmailStatement::CUSTOM_RANGE => [$data['start_date'], $data['end_date']], default => [now()->startOfDay()->firstOfMonth()->format('Y-m-d'), now()->startOfDay()->lastOfMonth()->format('Y-m-d')], }; diff --git a/database/migrations/2023_07_18_214607_add_start_date_column_to_tasks.php b/database/migrations/2023_07_18_214607_add_start_date_column_to_tasks.php new file mode 100644 index 000000000000..ad988bd747ef --- /dev/null +++ b/database/migrations/2023_07_18_214607_add_start_date_column_to_tasks.php @@ -0,0 +1,29 @@ +date('calculated_start_date')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + } +}; diff --git a/phpstan.neon b/phpstan.neon index cebc91d3270e..2372c3d259ae 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,9 +4,9 @@ includes: parameters: treatPhpDocTypesAsCertain: false parallel: - jobSize: 5 - maximumNumberOfProcesses: 16 - processTimeout: 600.0 + jobSize: 10 + maximumNumberOfProcesses: 1 + processTimeout: 60.0 ignoreErrors: - '#Call to an undefined method .*badMethod\(\)#' - '#Call to an undefined method Illuminate\Database\Eloquent\Builder::exclude#' diff --git a/routes/api.php b/routes/api.php index 97c8550b3417..0331edea846f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -143,6 +143,7 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] Route::get('health_check', [PingController::class, 'health'])->name('health_check'); Route::get('activities', [ActivityController::class, 'index']); + Route::post('activities/entity', [ActivityController::class, 'entityActivity']); Route::get('activities/download_entity/{activity}', [ActivityController::class, 'downloadHistoricalEntity']); Route::post('charts/totals', [ChartController::class, 'totals'])->name('chart.totals'); @@ -406,6 +407,6 @@ Route::post('api/v1/yodlee/data_updates', [YodleeController::class, 'dataUpdates Route::post('api/v1/yodlee/refresh_updates', [YodleeController::class, 'refreshUpdatesWebhook'])->middleware('throttle:100,1'); Route::post('api/v1/yodlee/balance', [YodleeController::class, 'balanceWebhook'])->middleware('throttle:100,1'); -Route::get('api/v1/protected_download/{hash}', [ProtectedDownloadController::class, 'index'])->name('protected_download')->middleware('signed')->middleware('throttle:300,1'); +Route::get('api/v1/protected_download/{hash}', [ProtectedDownloadController::class, 'index'])->name('protected_download')->middleware('throttle:300,1'); Route::fallback([BaseController::class, 'notFound'])->middleware('throttle:404'); \ No newline at end of file diff --git a/tests/Feature/ActivityApiTest.php b/tests/Feature/ActivityApiTest.php index 271b54d2ad0c..60aeb5e5a503 100644 --- a/tests/Feature/ActivityApiTest.php +++ b/tests/Feature/ActivityApiTest.php @@ -11,10 +11,11 @@ namespace Tests\Feature; -use Illuminate\Foundation\Testing\DatabaseTransactions; -use Illuminate\Routing\Middleware\ThrottleRequests; -use Tests\MockAccountData; use Tests\TestCase; +use Tests\MockAccountData; +use Illuminate\Validation\ValidationException; +use Illuminate\Routing\Middleware\ThrottleRequests; +use Illuminate\Foundation\Testing\DatabaseTransactions; /** * @test @@ -34,6 +35,38 @@ class ActivityApiTest extends TestCase $this->withoutMiddleware( ThrottleRequests::class ); + + $this->withoutExceptionHandling(); + + } + + public function testActivityEntity() + { + + $invoice = $this->company->invoices()->first(); + + $invoice->service()->markSent()->markPaid()->markDeleted()->handleRestore()->save(); + + $data = [ + 'entity' => 'invoice', + 'entity_id' => $invoice->hashed_id + ]; + + $response = false; + + try { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/activities/entity', $data); + } catch (ValidationException $e) { + $message = json_decode($e->validator->getMessageBag(), 1); + nlog($message); + } + + $response->assertStatus(200); + + } public function testActivityGet() diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index 918a8e78d8b7..e11adfb447f4 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -26,6 +26,7 @@ use App\Utils\Traits\MakesHash; use App\DataMapper\CompanySettings; use App\Factory\CompanyUserFactory; use App\Factory\InvoiceItemFactory; +use App\Models\Expense; use App\Services\Report\ARDetailReport; use Illuminate\Routing\Middleware\ThrottleRequests; @@ -142,10 +143,13 @@ class ReportCsvGenerationTest extends TestCase $this->client = Client::factory()->create([ 'user_id' => $this->user->id, + // 'assigned_user_id', $this->user->id, 'company_id' => $this->company->id, 'is_deleted' => 0, 'name' => 'bob', - 'address1' => '1234' + 'address1' => '1234', + 'balance' => 100, + 'paid_to_date' => 50, ]); ClientContact::factory()->create([ @@ -160,6 +164,353 @@ class ReportCsvGenerationTest extends TestCase } + + public function testVendorCsvGeneration() + { + + $vendor = + \App\Models\Vendor::factory()->create( + [ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'name' => 'Vendor 1', + 'city' => 'city', + 'address1' => 'address1', + 'address2' => 'address2', + 'postal_code' => 'postal_code', + 'phone' => 'work_phone', + 'private_notes' => 'private_notes', + 'public_notes' => 'public_notes', + 'website' => 'website', + 'number' => '1234', + ] + ); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + // 'report_keys' => ["vendor.name","purchase_order.number","purchase_order.amount", "item.quantity", "item.cost", "item.line_total", "item.discount", "item.notes", "item.product_key", "item.custom_value1", "item.tax_name1", "item.tax_rate1",], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/vendors', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('Vendor 1', $this->getFirstValueByColumn($csv, 'Name')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Number')); + $this->assertEquals('city', $this->getFirstValueByColumn($csv, 'City')); + $this->assertEquals('address1', $this->getFirstValueByColumn($csv, 'Street')); + $this->assertEquals('address2', $this->getFirstValueByColumn($csv, 'Apt/Suite')); + $this->assertEquals('postal_code', $this->getFirstValueByColumn($csv, 'Postal Code')); + $this->assertEquals('work_phone', $this->getFirstValueByColumn($csv, 'Phone')); + $this->assertEquals('private_notes', $this->getFirstValueByColumn($csv, 'Private Notes')); + $this->assertEquals('public_notes', $this->getFirstValueByColumn($csv, 'Public Notes')); + $this->assertEquals('website', $this->getFirstValueByColumn($csv, 'Website')); + + } + + public function testVendorCustomColumnCsvGeneration() + { + + \App\Models\Vendor::query()->cursor()->each(function ($t) { + $t->forceDelete(); + }); + + $vendor = + \App\Models\Vendor::factory()->create( + [ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'name' => 'Vendor 1', + 'city' => 'city', + 'address1' => 'address1', + 'address2' => 'address2', + 'postal_code' => 'postal_code', + 'phone' => 'work_phone', + 'private_notes' => 'private_notes', + 'public_notes' => 'public_notes', + 'website' => 'website', + 'number' => '1234', + ] + ); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["vendor.name", "vendor.city", "vendor.number"], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/vendors', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('Vendor 1', $this->getFirstValueByColumn($csv, 'Name')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Number')); + $this->assertEquals('city', $this->getFirstValueByColumn($csv, 'City')); + } + + + public function testTaskCustomColumnsCsvGeneration() + { + + $invoice = \App\Models\Invoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'date' => '2023-01-01', + 'amount' => 1000, + 'balance' => 1000, + 'number' => '123456', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '12345', + 'public_notes' => 'Public5', + 'private_notes' => 'Private5', + 'terms' => 'Terms5', + ]); + + + $log = '[[1689547165,1689550765,"sumtin",true]]'; + + \App\Models\Task::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'invoice_id' => $invoice->id, + 'description' => 'test1', + 'time_log' => $log, + 'custom_value1' => 'Custom 11', + 'custom_value2' => 'Custom 22', + 'custom_value3' => 'Custom 33', + 'custom_value4' => 'Custom 44', + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [ + 'client.name', + 'invoice.number', + 'invoice.amount', + 'task.start_date', + 'task.end_date', + 'task.duration', + 'task.description', + 'task.custom_value1', + 'task.custom_value2', + 'task.custom_value3', + 'task.custom_value4', + ], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/tasks', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals(3600, $this->getFirstValueByColumn($csv, 'Task Duration')); + $this->assertEquals('test1', $this->getFirstValueByColumn($csv, 'Task Description')); + $this->assertEquals('16/Jul/2023', $this->getFirstValueByColumn($csv, 'Task Start Date')); + $this->assertEquals('16/Jul/2023', $this->getFirstValueByColumn($csv, 'Task End Date')); + $this->assertEquals('Custom 11', $this->getFirstValueByColumn($csv, 'Task Custom Value 1')); + $this->assertEquals('Custom 22', $this->getFirstValueByColumn($csv, 'Task Custom Value 2')); + $this->assertEquals('Custom 33', $this->getFirstValueByColumn($csv, 'Task Custom Value 3')); + $this->assertEquals('Custom 44', $this->getFirstValueByColumn($csv, 'Task Custom Value 4')); + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals('123456', $this->getFirstValueByColumn($csv, 'Invoice Invoice Number')); + $this->assertEquals(1000, $this->getFirstValueByColumn($csv, 'Invoice Amount')); + + } + + + + + public function testTasksCsvGeneration() + { + + \App\Models\Task::query()->cursor()->each(function ($t) { + $t->forceDelete(); + }); + + $log = '[[1689547165,1689550765,"sumtin",true]]'; + + \App\Models\Task::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'description' => 'test', + 'time_log' => $log, + 'custom_value1' => 'Custom 1', + 'custom_value2' => 'Custom 2', + 'custom_value3' => 'Custom 3', + 'custom_value4' => 'Custom 4', + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/tasks', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals(3600, $this->getFirstValueByColumn($csv, 'Duration')); + $this->assertEquals('test', $this->getFirstValueByColumn($csv, 'Description')); + $this->assertEquals('16/Jul/2023', $this->getFirstValueByColumn($csv, 'Start Date')); + $this->assertEquals('16/Jul/2023', $this->getFirstValueByColumn($csv, 'End Date')); + $this->assertEquals('Custom 1', $this->getFirstValueByColumn($csv, 'Custom Value 1')); + $this->assertEquals('Custom 2', $this->getFirstValueByColumn($csv, 'Custom Value 2')); + $this->assertEquals('Custom 3', $this->getFirstValueByColumn($csv, 'Custom Value 3')); + $this->assertEquals('Custom 4', $this->getFirstValueByColumn($csv, 'Custom Value 4')); + + } + + public function testProductsCsvGeneration() + { + + \App\Models\Product::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'product_key' => 'product_key', + 'notes' => 'notes', + 'cost' => 100, + 'quantity' => 1, + 'custom_value1' => 'Custom 1', + 'custom_value2' => 'Custom 2', + 'custom_value3' => 'Custom 3', + 'custom_value4' => 'Custom 4', + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/products', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('product_key', $this->getFirstValueByColumn($csv, 'Product')); + $this->assertEquals('notes', $this->getFirstValueByColumn($csv, 'Notes')); + $this->assertEquals(100, $this->getFirstValueByColumn($csv, 'Cost')); + $this->assertEquals(1, $this->getFirstValueByColumn($csv, 'Quantity')); + $this->assertEquals('Custom 1', $this->getFirstValueByColumn($csv, 'Custom Value 1')); + $this->assertEquals('Custom 2', $this->getFirstValueByColumn($csv, 'Custom Value 2')); + $this->assertEquals('Custom 3', $this->getFirstValueByColumn($csv, 'Custom Value 3')); + $this->assertEquals('Custom 4', $this->getFirstValueByColumn($csv, 'Custom Value 4')); + + } + + + public function testPaymentCsvGeneration() + { + + $invoice = \App\Models\Invoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'date' => '2023-01-01', + 'amount' => 100, + 'balance' => 100, + 'number' => '12345', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + ]); + + $invoice->client->balance = 100; + $invoice->client->paid_to_date = 0; + $invoice->push(); + + $invoice->service()->markPaid()->save(); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [ + "payment.date", + "payment.amount", + "invoice.number", + "invoice.amount", + "client.name", + "client.balance", + "client.paid_to_date" + ], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/payments', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals(100, $this->getFirstValueByColumn($csv, 'Payment Amount')); + $this->assertEquals(now()->addSeconds($this->company->timezone()->utc_offset)->format('Y-m-d'), $this->getFirstValueByColumn($csv, 'Payment Date')); + $this->assertEquals('12345', $this->getFirstValueByColumn($csv, 'Invoice Invoice Number')); + $this->assertEquals(100, $this->getFirstValueByColumn($csv, 'Invoice Amount')); + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals(0, $this->getFirstValueByColumn($csv, 'Client Balance')); + $this->assertEquals(100, $this->getFirstValueByColumn($csv, 'Client Paid to Date')); + + } + + + public function testPaymentCustomFieldsCsvGeneration() + { + + \App\Models\Payment::factory()->create([ + 'amount' => 500, + 'date' => '2020-01-01', + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'transaction_reference' => '1234', + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/payments', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals(500, $this->getFirstValueByColumn($csv, 'Amount')); + $this->assertEquals(0, $this->getFirstValueByColumn($csv, 'Applied')); + $this->assertEquals(0, $this->getFirstValueByColumn($csv, 'Refunded')); + $this->assertEquals('2020-01-01', $this->getFirstValueByColumn($csv, 'Date')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Transaction Reference')); + + } + + public function testClientCsvGeneration() { @@ -191,6 +542,482 @@ class ReportCsvGenerationTest extends TestCase } + public function testClientCustomColumnsCsvGeneration() + { + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["client.name","client.user","client.assigned_user","client.balance","client.paid_to_date","client.currency_id"], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/clients', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Name')); + $this->assertEquals(100, $this->getFirstValueByColumn($csv, 'Balance')); + $this->assertEquals(50, $this->getFirstValueByColumn($csv, 'Paid to Date')); + $this->assertEquals($this->user->present()->name(), $this->getFirstValueByColumn($csv, 'Client User')); + $this->assertEquals('', $this->getFirstValueByColumn($csv, 'Client Assigned User')); + $this->assertEquals('USD', $this->getFirstValueByColumn($csv, 'Client Currency')); + + } + + public function testCreditCustomColumnsCsvGeneration() + { + + Credit::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'number' => '1234', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["client.name","credit.number","credit.amount","payment.date", "payment.amount"], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/credits', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Credit Credit Number')); + $this->assertEquals('Unpaid', $this->getFirstValueByColumn($csv, 'Payment Amount')); + $this->assertEquals('', $this->getFirstValueByColumn($csv, 'Payment Date')); + + } + + public function testInvoiceCustomColumnsCsvGeneration() + { + + \App\Models\Invoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'number' => '1234', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["client.name","invoice.number","invoice.amount","payment.date", "payment.amount"], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/invoices', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Invoice Invoice Number')); + $this->assertEquals('Unpaid', $this->getFirstValueByColumn($csv, 'Payment Amount')); + $this->assertEquals('', $this->getFirstValueByColumn($csv, 'Payment Date')); + + } + + public function testRecurringInvoiceCustomColumnsCsvGeneration() + { + + \App\Models\RecurringInvoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'number' => '1234', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + 'frequency_id' => 1, + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["client.name","recurring_invoice.number","recurring_invoice.amount", "recurring_invoice.frequency_id"], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/recurring_invoices', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Recurring Invoice Invoice Number')); + $this->assertEquals('Daily', $this->getFirstValueByColumn($csv, 'Recurring Invoice How Often')); + + } + + + public function testInvoiceItemsCustomColumnsCsvGeneration() + { + + \App\Models\Invoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'number' => '1234', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + 'line_items' => [ + [ + 'quantity' => 10, + 'cost' => 100, + 'line_total' => 1000, + 'is_amount_discount' => true, + 'discount' => 0, + 'notes' => 'item notes', + 'product_key' => 'product key', + 'custom_value1' => 'custom 1', + 'custom_value2' => 'custom 2', + 'custom_value3' => 'custom 3', + 'custom_value4' => 'custom 4', + 'tax_name1' => 'GST', + 'tax_rate1' => 10.00, + 'type_id' => '1', + ], + ] + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["client.name","invoice.number","invoice.amount","payment.date", "payment.amount", "item.quantity", "item.cost", "item.line_total", "item.discount", "item.notes", "item.product_key", "item.custom_value1", "item.tax_name1", "item.tax_rate1",], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/invoice_items', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Invoice Invoice Number')); + $this->assertEquals('Unpaid', $this->getFirstValueByColumn($csv, 'Payment Amount')); + $this->assertEquals('', $this->getFirstValueByColumn($csv, 'Payment Date')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Quantity')); + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Cost')); + $this->assertEquals('1000', $this->getFirstValueByColumn($csv, 'Line Total')); + $this->assertEquals('0', $this->getFirstValueByColumn($csv, 'Discount')); + $this->assertEquals('item notes', $this->getFirstValueByColumn($csv, 'Notes')); + $this->assertEquals('product key', $this->getFirstValueByColumn($csv, 'Product')); + $this->assertEquals('custom 1', $this->getFirstValueByColumn($csv, 'Custom Invoice 1')); + $this->assertEquals('GST', $this->getFirstValueByColumn($csv, 'Tax Name 1')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Tax Rate 1')); + + } + + + public function testQuoteItemsCustomColumnsCsvGeneration() + { + + \App\Models\Quote::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'number' => '1234', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + 'line_items' => [ + [ + 'quantity' => 10, + 'cost' => 100, + 'line_total' => 1000, + 'is_amount_discount' => true, + 'discount' => 0, + 'notes' => 'item notes', + 'product_key' => 'product key', + 'custom_value1' => 'custom 1', + 'custom_value2' => 'custom 2', + 'custom_value3' => 'custom 3', + 'custom_value4' => 'custom 4', + 'tax_name1' => 'GST', + 'tax_rate1' => 10.00, + 'type_id' => '1', + ], + ] + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["client.name","quote.number","quote.amount", "item.quantity", "item.cost", "item.line_total", "item.discount", "item.notes", "item.product_key", "item.custom_value1", "item.tax_name1", "item.tax_rate1",], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/quote_items', $data); + + $csv = $response->streamedContent(); + + + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Quote Number')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Quantity')); + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Cost')); + $this->assertEquals('1000', $this->getFirstValueByColumn($csv, 'Line Total')); + $this->assertEquals('0', $this->getFirstValueByColumn($csv, 'Discount')); + $this->assertEquals('item notes', $this->getFirstValueByColumn($csv, 'Notes')); + $this->assertEquals('product key', $this->getFirstValueByColumn($csv, 'Product')); + $this->assertEquals('custom 1', $this->getFirstValueByColumn($csv, 'Item Custom Value 1')); + $this->assertEquals('GST', $this->getFirstValueByColumn($csv, 'Tax Name 1')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Tax Rate 1')); + + } + + + public function testPurchaseOrderCsvGeneration() + { + + $vendor = + \App\Models\Vendor::factory()->create( + [ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'name' => 'Vendor 1', + ] + ); + + \App\Models\PurchaseOrder::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'vendor_id' => $vendor->id, + 'amount' => 100, + 'balance' => 50, + 'status_id' => 2, + 'discount' => 10, + 'number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/purchase_orders', $data); + + $response->assertStatus(200); + + $csv = $response->streamedContent(); + + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Amount')); + $this->assertEquals('50', $this->getFirstValueByColumn($csv, 'Balance')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Discount')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Number')); + $this->assertEquals('Public', $this->getFirstValueByColumn($csv, 'Public Notes')); + $this->assertEquals('Private', $this->getFirstValueByColumn($csv, 'Private Notes')); + $this->assertEquals('Terms', $this->getFirstValueByColumn($csv, 'Terms')); + } + + + public function testPurchaseOrderItemsCustomColumnsCsvGeneration() + { + + $vendor = + \App\Models\Vendor::factory()->create( + [ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'name' => 'Vendor 1', + ] + ); + + + \App\Models\PurchaseOrder::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'vendor_id' => $vendor->id, + 'amount' => 100, + 'balance' => 50, + 'number' => '1234', + 'po_number' => '1234', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + 'line_items' => [ + [ + 'quantity' => 10, + 'cost' => 100, + 'line_total' => 1000, + 'is_amount_discount' => true, + 'discount' => 0, + 'notes' => 'item notes', + 'product_key' => 'product key', + 'custom_value1' => 'custom 1', + 'custom_value2' => 'custom 2', + 'custom_value3' => 'custom 3', + 'custom_value4' => 'custom 4', + 'tax_name1' => 'GST', + 'tax_rate1' => 10.00, + 'type_id' => '1', + ], + ] + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["vendor.name","purchase_order.number","purchase_order.amount", "item.quantity", "item.cost", "item.line_total", "item.discount", "item.notes", "item.product_key", "item.custom_value1", "item.tax_name1", "item.tax_rate1",], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/purchase_order_items', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('Vendor 1', $this->getFirstValueByColumn($csv, 'Vendor Name')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Purchase Order Number')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Quantity')); + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Cost')); + $this->assertEquals('1000', $this->getFirstValueByColumn($csv, 'Line Total')); + $this->assertEquals('0', $this->getFirstValueByColumn($csv, 'Discount')); + $this->assertEquals('item notes', $this->getFirstValueByColumn($csv, 'Notes')); + $this->assertEquals('product key', $this->getFirstValueByColumn($csv, 'Product')); + $this->assertEquals('custom 1', $this->getFirstValueByColumn($csv, 'Item Custom Value 1')); + $this->assertEquals('GST', $this->getFirstValueByColumn($csv, 'Tax Name 1')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Tax Rate 1')); + + } + + public function testQuoteCustomColumnsCsvGeneration() + { + + \App\Models\Quote::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'number' => '1234', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["client.name","quote.number","quote.amount"], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/quotes', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'Quote Number')); + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Quote Amount')); + + } + + + public function testInvoicePaidCustomColumnsCsvGeneration() + { + + $invoice = \App\Models\Invoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'date' => '2023-01-01', + 'amount' => 100, + 'balance' => 100, + 'number' => '12345', + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + ]); + + $invoice->service()->markPaid()->save(); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ["client.name","invoice.number","invoice.amount","payment.date", "payment.amount"], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/invoices', $data); + + $csv = $response->streamedContent(); + + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals('12345', $this->getFirstValueByColumn($csv, 'Invoice Invoice Number')); + $this->assertEquals('$100.00', $this->getFirstValueByColumn($csv, 'Payment Amount')); + $this->assertEquals(now()->addSeconds($this->company->timezone()->utc_offset)->format('Y-m-d'), $this->getFirstValueByColumn($csv, 'Payment Date')); + + + } + public function testClientContactCsvGeneration() { @@ -227,6 +1054,17 @@ class ReportCsvGenerationTest extends TestCase } + private function getFirstValueByColumn($csv, $column) + { + $reader = Reader::createFromString($csv); + $reader->setHeaderOffset(0); + + $res = $reader->fetchColumnByName($column); + $res = iterator_to_array($res, true); + + return $res[1]; + } + public function testCreditCsvGeneration() { @@ -256,6 +1094,322 @@ class ReportCsvGenerationTest extends TestCase ])->post('/api/v1/reports/credits', $data); $response->assertStatus(200); - + + $csv = $response->streamedContent(); + + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Amount')); + $this->assertEquals('50', $this->getFirstValueByColumn($csv, 'Balance')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Discount')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'PO Number')); + $this->assertEquals('Public', $this->getFirstValueByColumn($csv, 'Public Notes')); + $this->assertEquals('Private', $this->getFirstValueByColumn($csv, 'Private Notes')); + $this->assertEquals('Terms', $this->getFirstValueByColumn($csv, 'Terms')); } + + public function testInvoiceCsvGeneration() + { + + Invoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + 'date' => '2020-01-01', + 'due_date' => '2021-01-02', + 'partial_due_date' => '2021-01-03', + 'partial' => 10, + 'discount' => 10, + 'custom_value1' => 'Custom 1', + 'custom_value2' => 'Custom 2', + 'custom_value3' => 'Custom 3', + 'custom_value4' => 'Custom 4', + 'footer' => 'Footer', + 'tax_name1' => 'Tax 1', + 'tax_rate1' => 10, + 'tax_name2' => 'Tax 2', + 'tax_rate2' => 20, + 'tax_name3' => 'Tax 3', + 'tax_rate3' => 30, + + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/invoices', $data); + + $response->assertStatus(200); + + $csv = $response->streamedContent(); + + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Amount')); + $this->assertEquals('50', $this->getFirstValueByColumn($csv, 'Balance')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Discount')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'PO Number')); + $this->assertEquals('Public', $this->getFirstValueByColumn($csv, 'Public Notes')); + $this->assertEquals('Private', $this->getFirstValueByColumn($csv, 'Private Notes')); + $this->assertEquals('Terms', $this->getFirstValueByColumn($csv, 'Terms')); + $this->assertEquals('2020-01-01', $this->getFirstValueByColumn($csv, 'Date')); + $this->assertEquals('2021-01-02', $this->getFirstValueByColumn($csv, 'Due Date')); + $this->assertEquals('2021-01-03', $this->getFirstValueByColumn($csv, 'Partial Due Date')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Partial/Deposit')); + $this->assertEquals('Custom 1', $this->getFirstValueByColumn($csv, 'Custom Value 1')); + $this->assertEquals('Custom 2', $this->getFirstValueByColumn($csv, 'Custom Value 2')); + $this->assertEquals('Custom 3', $this->getFirstValueByColumn($csv, 'Custom Value 3')); + $this->assertEquals('Custom 4', $this->getFirstValueByColumn($csv, 'Custom Value 4')); + $this->assertEquals('Footer', $this->getFirstValueByColumn($csv, 'Footer')); + $this->assertEquals('Tax 1', $this->getFirstValueByColumn($csv, 'Tax Name 1')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Tax Rate 1')); + $this->assertEquals('Tax 2', $this->getFirstValueByColumn($csv, 'Tax Name 2')); + $this->assertEquals('20', $this->getFirstValueByColumn($csv, 'Tax Rate 2')); + $this->assertEquals('Tax 3', $this->getFirstValueByColumn($csv, 'Tax Name 3')); + $this->assertEquals('30', $this->getFirstValueByColumn($csv, 'Tax Rate 3')); + $this->assertEquals('Sent', $this->getFirstValueByColumn($csv, 'Status')); + + } + + public function testRecurringInvoiceCsvGeneration() + { + + \App\Models\RecurringInvoice::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + 'date' => '2020-01-01', + 'due_date' => '2021-01-02', + 'partial_due_date' => '2021-01-03', + 'partial' => 10, + 'discount' => 10, + 'custom_value1' => 'Custom 1', + 'custom_value2' => 'Custom 2', + 'custom_value3' => 'Custom 3', + 'custom_value4' => 'Custom 4', + 'footer' => 'Footer', + 'tax_name1' => 'Tax 1', + 'tax_rate1' => 10, + 'tax_name2' => 'Tax 2', + 'tax_rate2' => 20, + 'tax_name3' => 'Tax 3', + 'tax_rate3' => 30, + 'frequency_id' => 1, + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/recurring_invoices', $data); + + $response->assertStatus(200); + + $csv = $response->streamedContent(); + + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Amount')); + $this->assertEquals('50', $this->getFirstValueByColumn($csv, 'Balance')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Discount')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'PO Number')); + $this->assertEquals('Public', $this->getFirstValueByColumn($csv, 'Public Notes')); + $this->assertEquals('Private', $this->getFirstValueByColumn($csv, 'Private Notes')); + $this->assertEquals('Terms', $this->getFirstValueByColumn($csv, 'Terms')); + $this->assertEquals('2020-01-01', $this->getFirstValueByColumn($csv, 'Date')); + $this->assertEquals('2021-01-02', $this->getFirstValueByColumn($csv, 'Due Date')); + $this->assertEquals('2021-01-03', $this->getFirstValueByColumn($csv, 'Partial Due Date')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Partial/Deposit')); + $this->assertEquals('Custom 1', $this->getFirstValueByColumn($csv, 'Custom Value 1')); + $this->assertEquals('Custom 2', $this->getFirstValueByColumn($csv, 'Custom Value 2')); + $this->assertEquals('Custom 3', $this->getFirstValueByColumn($csv, 'Custom Value 3')); + $this->assertEquals('Custom 4', $this->getFirstValueByColumn($csv, 'Custom Value 4')); + $this->assertEquals('Footer', $this->getFirstValueByColumn($csv, 'Footer')); + $this->assertEquals('Tax 1', $this->getFirstValueByColumn($csv, 'Tax Name 1')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Tax Rate 1')); + $this->assertEquals('Tax 2', $this->getFirstValueByColumn($csv, 'Tax Name 2')); + $this->assertEquals('20', $this->getFirstValueByColumn($csv, 'Tax Rate 2')); + $this->assertEquals('Tax 3', $this->getFirstValueByColumn($csv, 'Tax Name 3')); + $this->assertEquals('30', $this->getFirstValueByColumn($csv, 'Tax Rate 3')); + $this->assertEquals('Daily', $this->getFirstValueByColumn($csv, 'How Often')); + + } + + + public function testQuoteCsvGeneration() + { + + \App\Models\Quote::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'balance' => 50, + 'status_id' => 2, + 'discount' => 10, + 'po_number' => '1234', + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'terms' => 'Terms', + 'date' => '2020-01-01', + 'due_date' => '2020-01-01', + 'partial_due_date' => '2021-01-03', + 'partial' => 10, + 'discount' => 10, + 'custom_value1' => 'Custom 1', + 'custom_value2' => 'Custom 2', + 'custom_value3' => 'Custom 3', + 'custom_value4' => 'Custom 4', + 'footer' => 'Footer', + 'tax_name1' => 'Tax 1', + 'tax_rate1' => 10, + 'tax_name2' => 'Tax 2', + 'tax_rate2' => 20, + 'tax_name3' => 'Tax 3', + 'tax_rate3' => 30, + + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/quotes', $data); + + $response->assertStatus(200); + + $csv = $response->streamedContent(); + + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Amount')); + $this->assertEquals('50', $this->getFirstValueByColumn($csv, 'Balance')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Discount')); + $this->assertEquals('1234', $this->getFirstValueByColumn($csv, 'PO Number')); + $this->assertEquals('Public', $this->getFirstValueByColumn($csv, 'Public Notes')); + $this->assertEquals('Private', $this->getFirstValueByColumn($csv, 'Private Notes')); + $this->assertEquals('Terms', $this->getFirstValueByColumn($csv, 'Terms')); + $this->assertEquals('2020-01-01', $this->getFirstValueByColumn($csv, 'Date')); + $this->assertEquals('2020-01-01', $this->getFirstValueByColumn($csv, 'Valid Until')); + $this->assertEquals('2021-01-03', $this->getFirstValueByColumn($csv, 'Partial Due Date')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Partial/Deposit')); + $this->assertEquals('Custom 1', $this->getFirstValueByColumn($csv, 'Custom Value 1')); + $this->assertEquals('Custom 2', $this->getFirstValueByColumn($csv, 'Custom Value 2')); + $this->assertEquals('Custom 3', $this->getFirstValueByColumn($csv, 'Custom Value 3')); + $this->assertEquals('Custom 4', $this->getFirstValueByColumn($csv, 'Custom Value 4')); + $this->assertEquals('Footer', $this->getFirstValueByColumn($csv, 'Footer')); + $this->assertEquals('Tax 1', $this->getFirstValueByColumn($csv, 'Tax Name 1')); + $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Tax Rate 1')); + $this->assertEquals('Tax 2', $this->getFirstValueByColumn($csv, 'Tax Name 2')); + $this->assertEquals('20', $this->getFirstValueByColumn($csv, 'Tax Rate 2')); + $this->assertEquals('Tax 3', $this->getFirstValueByColumn($csv, 'Tax Name 3')); + $this->assertEquals('30', $this->getFirstValueByColumn($csv, 'Tax Rate 3')); + $this->assertEquals('Expired', $this->getFirstValueByColumn($csv, 'Status')); + + } + + + public function testExpenseCsvGeneration() + { + Expense::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'amount' => 100, + 'public_notes' => 'Public', + 'private_notes' => 'Private', + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => [], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/expenses', $data); + + $response->assertStatus(200); + + $csv = $response->streamedContent(); + + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Amount')); + $this->assertEquals('Public', $this->getFirstValueByColumn($csv, 'Public Notes')); + $this->assertEquals('Private', $this->getFirstValueByColumn($csv, 'Private Notes')); + $this->assertEquals($this->user->present()->name(), $this->getFirstValueByColumn($csv, 'User')); + + } + + public function testExpenseCustomColumnsCsvGeneration() + { + $vendor = + \App\Models\Vendor::factory()->create( + [ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'name' => 'Vendor 1', + ] + + ); + + Expense::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + 'vendor_id' => $vendor->id, + 'amount' => 100, + 'public_notes' => 'Public', + 'private_notes' => 'Private', + 'currency_id' => 1, + ]); + + $data = [ + 'date_range' => 'all', + 'report_keys' => ['client.name','vendor.name','expense.amount','expense.currency_id'], + 'send_email' => false, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/reports/expenses', $data); + + $response->assertStatus(200); + + $csv = $response->streamedContent(); + + $this->assertEquals('bob', $this->getFirstValueByColumn($csv, 'Client Name')); + $this->assertEquals('Vendor 1', $this->getFirstValueByColumn($csv, 'Vendor Name')); + $this->assertEquals('100', $this->getFirstValueByColumn($csv, 'Amount')); + $this->assertEquals('USD', $this->getFirstValueByColumn($csv, 'Currency')); + + } + + } \ No newline at end of file diff --git a/tests/Feature/PaymentV2Test.php b/tests/Feature/PaymentV2Test.php index 761c00b03361..5f1f386ef173 100644 --- a/tests/Feature/PaymentV2Test.php +++ b/tests/Feature/PaymentV2Test.php @@ -11,20 +11,21 @@ namespace Tests\Feature; +use Tests\TestCase; use App\Models\Client; -use App\Models\ClientContact; use App\Models\Credit; use App\Models\Invoice; use App\Models\Payment; +use Tests\MockAccountData; +use App\Models\ClientContact; use App\Utils\Traits\MakesHash; +use App\Factory\InvoiceItemFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Foundation\Testing\DatabaseTransactions; -use Illuminate\Foundation\Testing\WithoutEvents; -use Illuminate\Routing\Middleware\ThrottleRequests; use Illuminate\Support\Facades\Session; use Illuminate\Validation\ValidationException; -use Tests\MockAccountData; -use Tests\TestCase; +use Illuminate\Foundation\Testing\WithoutEvents; +use Illuminate\Routing\Middleware\ThrottleRequests; +use Illuminate\Foundation\Testing\DatabaseTransactions; /** * @test @@ -189,4 +190,172 @@ class PaymentV2Test extends TestCase $this->assertEquals(20, $client->fresh()->paid_to_date); } + +public function testStorePaymentWithCreditsThenDeletingInvoicesAndThenPayments() + { + $client = Client::factory()->create(['company_id' =>$this->company->id, 'user_id' => $this->user->id, 'balance' => 100, 'paid_to_date' => 0]); + ClientContact::factory()->create([ + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'company_id' => $this->company->id, + 'is_primary' => 1, + ]); + + $line_items = []; + + $item = InvoiceItemFactory::create(); + $item->quantity = 1; + $item->cost = 100; + + $line_items[] = $item; + + + $invoice = Invoice::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'status_id' => Invoice::STATUS_SENT, + 'uses_inclusive_taxes' => false, + 'amount' => 100, + 'balance' => 100, + 'discount' => 0, + 'number' => uniqid("st", true), + 'line_items' => $line_items + ]); + + $this->assertEquals(100, $client->balance); + $this->assertEquals(0, $client->paid_to_date); + $this->assertEquals(100, $invoice->amount); + $this->assertEquals(100, $invoice->balance); + + $credit = Credit::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $client->id, + 'status_id' => Invoice::STATUS_SENT, + 'uses_inclusive_taxes' => false, + 'amount' => 20, + 'balance' => 20, + 'discount' => 0, + 'number' => uniqid("st", true), + 'line_items' => [] + ]); + + $this->assertEquals(20, $credit->amount); + $this->assertEquals(20, $credit->balance); + + $data = [ + 'client_id' => $client->hashed_id, + 'invoices' => [ + [ + 'invoice_id' => $invoice->hashed_id, + 'amount' => 100, + ], + ], + 'credits' => [ + [ + 'credit_id' => $credit->hashed_id, + 'amount' => 20, + ], + ], + 'date' => '2020/12/12', + + ]; + + $response = null; + + try { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/payments?include=invoices', $data); + } catch (ValidationException $e) { + $message = json_decode($e->validator->getMessageBag(), 1); + nlog($message); + $this->assertNotNull($message); + } + + $arr = $response->json(); + $response->assertStatus(200); + + $payment_id = $arr['data']['id']; + + $payment = Payment::find($this->decodePrimaryKey($payment_id)); + $credit = $credit->fresh(); + + $this->assertNotNull($payment); + $this->assertNotNull($payment->invoices()); + $this->assertEquals(1, $payment->invoices()->count()); + $this->assertEquals(80, $payment->amount); + $this->assertEquals(0, $client->fresh()->balance); + $this->assertEquals(100, $client->fresh()->paid_to_date); + $this->assertEquals(0, $credit->balance); + + $invoice = $invoice->fresh(); + + //delete the invoice + + $data = [ + 'action' => 'delete', + 'ids' => [ + $invoice->hashed_id, + ], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/invoices/bulk', $data); + + $response->assertStatus(200); + + $payment = $payment->fresh(); + $invoice = $invoice->fresh(); + + $this->assertTrue($invoice->is_deleted); + $this->assertFalse($payment->is_deleted); + + $data = [ + 'action' => 'delete', + 'ids' => [ + $payment->hashed_id, + ], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/payments/bulk', $data); + + $payment = $payment->fresh(); + $this->assertTrue($payment->is_deleted); + + $data = [ + 'action' => 'restore', + 'ids' => [ + $invoice->hashed_id, + ], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/invoices/bulk', $data); + + $response->assertStatus(200); + $invoice = $invoice->fresh(); + + $this->assertTrue($invoice->is_deleted); + $this->assertTrue($invoice->trashed()); + + $client = $client->fresh(); + $credit = $credit->fresh(); + + $this->assertEquals(0, $client->balance); + $this->assertEquals(0, $client->paid_to_date); + // $this->assertEquals(20, $client->credit_balance); + $this->assertEquals(20, $credit->balance); + + } + } \ No newline at end of file diff --git a/tests/Feature/TaskApiTest.php b/tests/Feature/TaskApiTest.php index 236f2fa66291..1651954bb121 100644 --- a/tests/Feature/TaskApiTest.php +++ b/tests/Feature/TaskApiTest.php @@ -31,6 +31,8 @@ class TaskApiTest extends TestCase use DatabaseTransactions; use MockAccountData; + private $faker; + protected function setUp() :void { parent::setUp(); @@ -100,6 +102,20 @@ class TaskApiTest extends TestCase } } + public function testStartDate() + { + $x = []; + + $this->assertFalse(isset($x[0][0])); + + $x[0][0] = 'a'; + + $this->assertTrue(isset($x[0][0])); + + $this->assertNotNull(\Carbon\Carbon::createFromTimestamp($x[0][0])); + + } + public function testMultiSortArray() { diff --git a/tests/Unit/DatesTest.php b/tests/Unit/DatesTest.php index 0fbaf15987d1..56cda7834226 100644 --- a/tests/Unit/DatesTest.php +++ b/tests/Unit/DatesTest.php @@ -31,6 +31,150 @@ class DatesTest extends TestCase // $this->makeTestData(); } + public function testLastFinancialYear3() + { + $this->travelTo(now()->createFromDate(2020, 6, 30)); + + //override for financial years + $first_month_of_year = 7; + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + $fin_year_start->subYearNoOverflow(); + + if(now()->subYear()->lt($fin_year_start)) { + $fin_year_start->subYearNoOverflow(); + } + + $this->assertEquals('2018-07-01', $fin_year_start->format('Y-m-d')); + $this->assertEquals('2019-06-30', $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d')); + + $this->travelBack(); + + } + + public function testLastFinancialYear2() + { + $this->travelTo(now()->createFromDate(2020, 7, 1)); + + //override for financial years + $first_month_of_year = 7; + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + $fin_year_start->subYearNoOverflow(); + + if(now()->subYear()->lt($fin_year_start)) { + $fin_year_start->subYearNoOverflow(); + } + + $this->assertEquals('2019-07-01', $fin_year_start->format('Y-m-d')); + $this->assertEquals('2020-06-30', $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d')); + + $this->travelBack(); + + } + + public function testLastFinancialYear() + { + $this->travelTo(now()->createFromDate(2020, 12, 1)); + + //override for financial years + $first_month_of_year = 7; + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + $fin_year_start->subYearNoOverflow(); + + if(now()->subYear()->lt($fin_year_start)) { + $fin_year_start->subYearNoOverflow(); + } + + $this->assertEquals('2019-07-01', $fin_year_start->format('Y-m-d')); + $this->assertEquals('2020-06-30', $fin_year_start->copy()->addYear()->subDay()->format('Y-m-d')); + + $this->travelBack(); + + } + + public function testFinancialYearDates4() + { + $this->travelTo(now()->createFromDate(2020, 12, 1)); + + $first_month_of_year = 7; + + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + if(now()->lt($fin_year_start)) + $fin_year_start->subYear(); + + $fin_year_end = $fin_year_start->copy()->addYear()->subDay(); + + $this->assertEquals('2020-07-01', $fin_year_start->format('Y-m-d')); + $this->assertEquals('2021-06-30', $fin_year_end->format('Y-m-d')); + + $this->travelBack(); + + } + + public function testFinancialYearDates3() + { + $this->travelTo(now()->createFromDate(2021, 12, 1)); + + $first_month_of_year = 7; + + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + if(now()->lt($fin_year_start)) + $fin_year_start->subYear(); + + $fin_year_end = $fin_year_start->copy()->addYear()->subDay(); + + $this->assertEquals('2021-07-01', $fin_year_start->format('Y-m-d')); + $this->assertEquals('2022-06-30', $fin_year_end->format('Y-m-d')); + + $this->travelBack(); + + } + + public function testFinancialYearDates2() + { + $this->travelTo(now()->createFromDate(2021, 8, 1)); + + $first_month_of_year = 7; + + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + if(now()->lt($fin_year_start)) + $fin_year_start->subYear(); + + $fin_year_end = $fin_year_start->copy()->addYear()->subDay(); + + $this->assertEquals('2021-07-01', $fin_year_start->format('Y-m-d')); + $this->assertEquals('2022-06-30', $fin_year_end->format('Y-m-d')); + + $this->travelBack(); + + } + + + public function testFinancialYearDates() + { + $this->travelTo(now()->createFromDate(2021, 1, 1)); + + $first_month_of_year = 7; + + $fin_year_start = now()->createFromDate(now()->year, $first_month_of_year, 1); + + if(now()->lt($fin_year_start)) + $fin_year_start->subYear(); + + $fin_year_end = $fin_year_start->copy()->addYear()->subDay(); + + $this->assertEquals('2020-07-01', $fin_year_start->format('Y-m-d')); + $this->assertEquals('2021-06-30', $fin_year_end->format('Y-m-d')); + + $this->travelBack(); + + } + public function testDaysDiff() { $string_date = '2021-06-01';