Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83caff13cd | ||
|
|
d2736b1c46 | ||
|
|
cdc504f518 | ||
|
|
2c840b2d97 | ||
|
|
34db4b7897 | ||
|
|
beb2a43ed3 | ||
|
|
7a25a15877 | ||
|
|
8eee4bc4f5 | ||
|
|
c34cfeea11 | ||
|
|
f37edbcc92 | ||
|
|
ba581313df | ||
|
|
1c1a180dd5 | ||
|
|
8e815fd887 | ||
|
|
df612bc773 | ||
|
|
10b2cf5af1 | ||
|
|
f140256efd | ||
|
|
1ed8e5b2d0 | ||
|
|
fcb7c96bca | ||
|
|
d587e3fd00 | ||
|
|
1b26d47539 | ||
|
|
340522da19 | ||
|
|
3ecfcede3e | ||
|
|
30f76c044a | ||
|
|
854a8bb50d | ||
|
|
1be3132dc4 | ||
|
|
fefd856cfb | ||
|
|
e5fe62e4de | ||
|
|
129d63c5b1 | ||
|
|
65e9182272 | ||
|
|
3abcba2752 | ||
|
|
6c1e51d126 | ||
|
|
c1b8ba448d | ||
|
|
ab5ea81424 | ||
|
|
befd69fdb2 | ||
|
|
417b92ad9f | ||
|
|
d6b1e102fe | ||
|
|
d7bf942da2 | ||
|
|
5db82db958 | ||
|
|
c63355391a | ||
|
|
de3c8f89fb | ||
|
|
cab4c62c5d | ||
|
|
4b634f96c9 | ||
|
|
9ef0932c6e | ||
|
|
d122c8c95a | ||
|
|
820073e8e0 | ||
|
|
80c14b4b7f | ||
|
|
174acbf70e | ||
|
|
c93b8f0da9 | ||
|
|
2693b0b0f2 | ||
|
|
54d9c57925 | ||
|
|
ba3e94c4ad | ||
|
|
a644653513 | ||
|
|
3391d104f9 | ||
|
|
f17c7be5f0 | ||
|
|
0e9f18d4d1 | ||
|
|
e22050bc71 | ||
|
|
834b53ea40 |
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Admin\Expense;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\DuplicateExpenseRequest;
|
||||
use App\Http\Resources\ExpenseResource;
|
||||
use App\Models\CompanySetting;
|
||||
use App\Models\ExchangeRateLog;
|
||||
use App\Models\Expense;
|
||||
|
||||
class DuplicateExpenseController extends Controller
|
||||
{
|
||||
/**
|
||||
* Duplicate an expense, appending " (copy)" to the note (description).
|
||||
*/
|
||||
public function __invoke(DuplicateExpenseRequest $request, Expense $expense): ExpenseResource
|
||||
{
|
||||
$this->authorize('view', $expense);
|
||||
$this->authorize('create', Expense::class);
|
||||
|
||||
$expense->load('fields');
|
||||
|
||||
$companyCurrency = CompanySetting::getSetting('currency', $request->header('company'));
|
||||
$currentCurrency = $expense->currency_id;
|
||||
$exchangeRate = $companyCurrency != $currentCurrency ? $expense->exchange_rate : 1;
|
||||
|
||||
$notes = trim((string) $expense->notes);
|
||||
$duplicatedNotes = $notes === '' ? '(copy)' : $notes.' (copy)';
|
||||
|
||||
$newExpense = Expense::query()->create([
|
||||
'expense_date' => $request->validated('expense_date'),
|
||||
'expense_number' => null,
|
||||
'expense_category_id' => $expense->expense_category_id,
|
||||
'payment_method_id' => $expense->payment_method_id,
|
||||
'amount' => $expense->amount,
|
||||
'customer_id' => $expense->customer_id,
|
||||
'notes' => $duplicatedNotes,
|
||||
'currency_id' => $expense->currency_id,
|
||||
'creator_id' => $request->user()->id,
|
||||
'company_id' => $request->header('company'),
|
||||
'exchange_rate' => $exchangeRate,
|
||||
'base_amount' => $expense->amount * $exchangeRate,
|
||||
]);
|
||||
|
||||
if ((string) $newExpense->currency_id !== (string) $companyCurrency) {
|
||||
ExchangeRateLog::addExchangeRateLog($newExpense);
|
||||
}
|
||||
|
||||
if ($expense->fields()->exists()) {
|
||||
$customFields = [];
|
||||
|
||||
foreach ($expense->fields as $data) {
|
||||
$customFields[] = [
|
||||
'id' => $data->custom_field_id,
|
||||
'value' => $data->defaultAnswer,
|
||||
];
|
||||
}
|
||||
|
||||
$newExpense->addCustomFields($customFields);
|
||||
}
|
||||
|
||||
return new ExpenseResource($newExpense);
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,12 @@ use App\Models\Company;
|
||||
use App\Models\CompanySetting;
|
||||
use App\Models\Currency;
|
||||
use App\Models\Expense;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use PDF;
|
||||
|
||||
class ExpensesReportController extends Controller
|
||||
{
|
||||
@@ -19,7 +20,7 @@ class ExpensesReportController extends Controller
|
||||
* Handle the incoming request.
|
||||
*
|
||||
* @param string $hash
|
||||
* @return JsonResponse
|
||||
* @return View|Response
|
||||
*/
|
||||
public function __invoke(Request $request, $hash)
|
||||
{
|
||||
@@ -31,14 +32,26 @@ class ExpensesReportController extends Controller
|
||||
|
||||
App::setLocale($locale);
|
||||
|
||||
$expenseCategories = Expense::with('category')
|
||||
// Fetch individual expenses (filtered and ordered by date), then group by category
|
||||
$expenses = Expense::with('category')
|
||||
->whereCompanyId($company->id)
|
||||
->applyFilters($request->only(['from_date', 'to_date']))
|
||||
->expensesAttributes()
|
||||
->applyFilters($request->only(['from_date', 'to_date', 'expense_category_id']))
|
||||
->orderBy('expense_date', 'asc')
|
||||
->get();
|
||||
$totalAmount = 0;
|
||||
foreach ($expenseCategories as $category) {
|
||||
$totalAmount += $category->total_amount;
|
||||
|
||||
$totalAmount = $expenses->sum('base_amount');
|
||||
|
||||
$grouped = $expenses->groupBy(function ($item) {
|
||||
return $item->category ? $item->category->name : trans('expenses.uncategorized');
|
||||
});
|
||||
|
||||
$expenseGroups = collect();
|
||||
foreach ($grouped as $categoryName => $group) {
|
||||
$expenseGroups->push([
|
||||
'name' => $categoryName,
|
||||
'expenses' => $group,
|
||||
'total' => $group->sum('base_amount'),
|
||||
]);
|
||||
}
|
||||
|
||||
$dateFormat = CompanySetting::getSetting('carbon_date_format', $company->id);
|
||||
@@ -62,7 +75,7 @@ class ExpensesReportController extends Controller
|
||||
->get();
|
||||
|
||||
view()->share([
|
||||
'expenseCategories' => $expenseCategories,
|
||||
'expenseGroups' => $expenseGroups,
|
||||
'colorSettings' => $colorSettings,
|
||||
'totalExpense' => $totalAmount,
|
||||
'company' => $company,
|
||||
@@ -70,7 +83,7 @@ class ExpensesReportController extends Controller
|
||||
'to_date' => $to_date,
|
||||
'currency' => $currency,
|
||||
]);
|
||||
$pdf = PDF::loadView('app.pdf.reports.expenses');
|
||||
$pdf = Pdf::loadView('app.pdf.reports.expenses');
|
||||
|
||||
if ($request->has('preview')) {
|
||||
return view('app.pdf.reports.expenses');
|
||||
|
||||
31
app/Http/Requests/DuplicateExpenseRequest.php
Normal file
31
app/Http/Requests/DuplicateExpenseRequest.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class DuplicateExpenseRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, array<int, string>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'expense_date' => [
|
||||
'required',
|
||||
'date_format:Y-m-d',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ use App\Facades\PDF;
|
||||
use App\Mail\SendEstimateMail;
|
||||
use App\Services\SerialNumberFormatter;
|
||||
use App\Space\PdfTemplateUtils;
|
||||
use App\Support\PdfHtmlSanitizer;
|
||||
use App\Traits\GeneratesPdfTrait;
|
||||
use App\Traits\HasCustomFieldsTrait;
|
||||
use Carbon\Carbon;
|
||||
@@ -476,7 +475,7 @@ class Estimate extends Model implements HasMedia
|
||||
|
||||
public function getNotes()
|
||||
{
|
||||
return PdfHtmlSanitizer::sanitize($this->getFormattedString($this->notes));
|
||||
return $this->getFormattedString($this->notes);
|
||||
}
|
||||
|
||||
public function getEmailAttachmentSetting()
|
||||
|
||||
@@ -8,7 +8,6 @@ use App\Facades\PDF;
|
||||
use App\Mail\SendInvoiceMail;
|
||||
use App\Services\SerialNumberFormatter;
|
||||
use App\Space\PdfTemplateUtils;
|
||||
use App\Support\PdfHtmlSanitizer;
|
||||
use App\Traits\GeneratesPdfTrait;
|
||||
use App\Traits\HasCustomFieldsTrait;
|
||||
use Carbon\Carbon;
|
||||
@@ -657,7 +656,7 @@ class Invoice extends Model implements HasMedia
|
||||
|
||||
public function getNotes()
|
||||
{
|
||||
return PdfHtmlSanitizer::sanitize($this->getFormattedString($this->notes));
|
||||
return $this->getFormattedString($this->notes);
|
||||
}
|
||||
|
||||
public function getEmailString($body)
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Facades\Hashids;
|
||||
use App\Jobs\GeneratePaymentPdfJob;
|
||||
use App\Mail\SendPaymentMail;
|
||||
use App\Services\SerialNumberFormatter;
|
||||
use App\Support\PdfHtmlSanitizer;
|
||||
use App\Traits\GeneratesPdfTrait;
|
||||
use App\Traits\HasCustomFieldsTrait;
|
||||
use Barryvdh\DomPDF\Facade\Pdf as PDF;
|
||||
@@ -434,7 +433,7 @@ class Payment extends Model implements HasMedia
|
||||
|
||||
public function getNotes()
|
||||
{
|
||||
return PdfHtmlSanitizer::sanitize($this->getFormattedString($this->notes));
|
||||
return $this->getFormattedString($this->notes);
|
||||
}
|
||||
|
||||
public function getEmailBody($body)
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Traits;
|
||||
use App\Models\Address;
|
||||
use App\Models\CompanySetting;
|
||||
use App\Models\FileDisk;
|
||||
use App\Support\PdfHtmlSanitizer;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
@@ -182,6 +183,10 @@ trait GeneratesPdfTrait
|
||||
|
||||
$str = str_replace('</p>', '<br />', $str);
|
||||
|
||||
return $str;
|
||||
// Sanitize the assembled HTML to strip any SSRF vectors that may have
|
||||
// entered through user-supplied address fields, customer names, or
|
||||
// custom field values. Notes also pass through this method, so they
|
||||
// get the same treatment without needing a separate wrapper.
|
||||
return PdfHtmlSanitizer::sanitize($str);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('tax_types', function (Blueprint $table) {
|
||||
$table->decimal('percent', 5, 3)->nullable()->change();
|
||||
});
|
||||
|
||||
Schema::table('taxes', function (Blueprint $table) {
|
||||
$table->decimal('percent', 5, 3)->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('tax_types', function (Blueprint $table) {
|
||||
$table->decimal('percent', 5, 2)->nullable()->change();
|
||||
});
|
||||
|
||||
Schema::table('taxes', function (Blueprint $table) {
|
||||
$table->decimal('percent', 5, 2)->nullable()->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "تواصل",
|
||||
"category": "الفئة",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "من تاريخ",
|
||||
"to_date": "حتى تاريخ",
|
||||
"expense_date": "التاريخ",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "اجمالي الاداءات",
|
||||
"pdf_tax_types_label": "أنواع الضرائب",
|
||||
"pdf_expenses_label": "النفقات",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "مطلوب من,",
|
||||
"pdf_ship_to": "يشحن إلى,",
|
||||
"pdf_received_from": "تم الاستلام من:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Date",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Date",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Date",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Měna",
|
||||
"contact": "Kontakt",
|
||||
"category": "Kategorie",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Od data",
|
||||
"to_date": "Do data",
|
||||
"expense_date": "Datum",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "DANĚ CELKEM",
|
||||
"pdf_tax_types_label": "Typy daní",
|
||||
"pdf_expenses_label": "Výdaje",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Odběratel",
|
||||
"pdf_ship_to": "Příjemce",
|
||||
"pdf_received_from": "Přijato od:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Date",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Währung",
|
||||
"contact": "Kontakt",
|
||||
"category": "Kategorie",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Von Datum",
|
||||
"to_date": "bis Datum",
|
||||
"expense_date": "Datum",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "Gesamte Umsatzsteuer",
|
||||
"pdf_tax_types_label": "Steuersätze",
|
||||
"pdf_expenses_label": "Ausgaben",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Rechnungsanschrift",
|
||||
"pdf_ship_to": "Lieferanschrift",
|
||||
"pdf_received_from": "Erhalten von:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Νόμισμα",
|
||||
"contact": "Επικοινωνία",
|
||||
"category": "Κατηγορία",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Από Ημερομηνία",
|
||||
"to_date": "Έως ημερομηνία",
|
||||
"expense_date": "Ημερομηνία",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "ΣΥΝΟΛΟ ΦΟΡΟΥ",
|
||||
"pdf_tax_types_label": "Φορολογική κλάση",
|
||||
"pdf_expenses_label": "Έξοδα",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Χρέωση σε,",
|
||||
"pdf_ship_to": "Αποστολή σε,",
|
||||
"pdf_received_from": "Λήψη από",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Date",
|
||||
@@ -676,6 +677,10 @@
|
||||
"no_expenses": "No expenses yet!",
|
||||
"list_of_expenses": "This section will contain the list of expenses.",
|
||||
"confirm_delete": "You will not be able to recover this Expense | You will not be able to recover these Expenses",
|
||||
"duplicate_expense": "Duplicate",
|
||||
"duplicate_expense_title": "Duplicate expense",
|
||||
"duplicate_expense_modal_hint": "Change the date if you need to. (copy) is added to the note.",
|
||||
"duplicated_message": "Expense duplicated successfully",
|
||||
"created_message": "Expense created successfully",
|
||||
"updated_message": "Expense updated successfully",
|
||||
"deleted_message": "Expense deleted successfully | Expenses deleted successfully",
|
||||
@@ -1649,6 +1654,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Divisa",
|
||||
"contact": "Contacto",
|
||||
"category": "Categoría",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Desde la fecha",
|
||||
"to_date": "Hasta la fecha",
|
||||
"expense_date": "Fecha",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "IMPUESTO TOTAL",
|
||||
"pdf_tax_types_label": "Tipos de impuestos",
|
||||
"pdf_expenses_label": "Gastos",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Cobrar a,",
|
||||
"pdf_ship_to": "Enviar a,",
|
||||
"pdf_received_from": "Recibido de:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Date",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Date",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Valuutta",
|
||||
"contact": "Yhteyshenkilö",
|
||||
"category": "Luokka",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Päivästä",
|
||||
"to_date": "Päivään",
|
||||
"expense_date": "Päivämäärä",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "ALV YHTEENSÄ",
|
||||
"pdf_tax_types_label": "ALV Verokannat",
|
||||
"pdf_expenses_label": "Kulut",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Laskutetaan,",
|
||||
"pdf_ship_to": "Toimitetaan,",
|
||||
"pdf_received_from": "Vastaanotettu:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Devise",
|
||||
"contact": "Contact",
|
||||
"category": "Catégorie",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Du",
|
||||
"to_date": "Au",
|
||||
"expense_date": "Date",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAXES",
|
||||
"pdf_tax_types_label": "Taxe",
|
||||
"pdf_expenses_label": "Dépenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Facturer à",
|
||||
"pdf_ship_to": "Expédier à",
|
||||
"pdf_received_from": "Reçu de :",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Date",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "मुद्रा",
|
||||
"contact": "संपर्क",
|
||||
"category": "वर्ग",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "इस तारीख से",
|
||||
"to_date": "इस तारीख तक",
|
||||
"expense_date": "दिनांक",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Valuta",
|
||||
"contact": "Kontakt",
|
||||
"category": "Kategorija",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Datum od",
|
||||
"to_date": "Datum do",
|
||||
"expense_date": "Datum",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "UKUPNO POREZ",
|
||||
"pdf_tax_types_label": "Vrsta Poreza",
|
||||
"pdf_expenses_label": "Rashodi",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Račun za,",
|
||||
"pdf_ship_to": "Isporučiti za,",
|
||||
"pdf_received_from": "Poslat od strane:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Date",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Mata Uang",
|
||||
"contact": "Kontak",
|
||||
"category": "Kategori",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Dari Tanggal",
|
||||
"to_date": "Sampai Tanggal",
|
||||
"expense_date": "Tanggal",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL PAJAK",
|
||||
"pdf_tax_types_label": "Jenis Pajak",
|
||||
"pdf_expenses_label": "Pengeluaran",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Ditagih ke,",
|
||||
"pdf_ship_to": "Dikirim ke,",
|
||||
"pdf_received_from": "Diterima dari:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Valuta",
|
||||
"contact": "Contatto",
|
||||
"category": "Categoria",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Dalla Data",
|
||||
"to_date": "Alla Data",
|
||||
"expense_date": "Data",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTALE IMPOSTA",
|
||||
"pdf_tax_types_label": "Tipi di Tasse",
|
||||
"pdf_expenses_label": "Uscite",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Fattura a,",
|
||||
"pdf_ship_to": "Invia a,",
|
||||
"pdf_received_from": "Ricevuto da:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "通貨",
|
||||
"contact": "連絡先",
|
||||
"category": "カテゴリ",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "開始日",
|
||||
"to_date": "終了日",
|
||||
"expense_date": "日時",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "合計税額",
|
||||
"pdf_tax_types_label": "税の種類",
|
||||
"pdf_expenses_label": "支出",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "請求先",
|
||||
"pdf_ship_to": "配送先",
|
||||
"pdf_received_from": "受信元:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Date",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Data",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Mokesčių tipai",
|
||||
"pdf_expenses_label": "Išlaidos",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Siųsti į,",
|
||||
"pdf_received_from": "Gauta nuo:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Kontakti",
|
||||
"category": "Kategorija",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Datums no",
|
||||
"to_date": "Datums līdz",
|
||||
"expense_date": "Datums",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "NODOKĻI KOPĀ",
|
||||
"pdf_tax_types_label": "Nodokļu veidi",
|
||||
"pdf_expenses_label": "Izdevumi",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Saņēmējs,",
|
||||
"pdf_ship_to": "Piegādes adrese,",
|
||||
"pdf_received_from": "Saņemts no:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Date",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Date",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Valuta",
|
||||
"contact": "Contact",
|
||||
"category": "Categorie",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Van datum",
|
||||
"to_date": "Tot datum",
|
||||
"expense_date": "Datum",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTALE BELASTINGEN",
|
||||
"pdf_tax_types_label": "Belastingtypen",
|
||||
"pdf_expenses_label": "Uitgaven",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Aan,",
|
||||
"pdf_ship_to": "Verzend naar,",
|
||||
"pdf_received_from": "Ontvangen van:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Date",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Waluta",
|
||||
"contact": "Kontakt",
|
||||
"category": "Kategoria",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Od daty",
|
||||
"to_date": "Do daty",
|
||||
"expense_date": "Data",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "CAŁKOWITY PODATEK",
|
||||
"pdf_tax_types_label": "Rodzaje podatku",
|
||||
"pdf_expenses_label": "Wydatki",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Wystawiono dla",
|
||||
"pdf_ship_to": "Wysyłka do",
|
||||
"pdf_received_from": "Otrzymane od:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Moeda",
|
||||
"contact": "Contato",
|
||||
"category": "Categoria",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "A partir da Data",
|
||||
"to_date": "Até a Data",
|
||||
"expense_date": "Data",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "IMPOSTOS TOTAIS",
|
||||
"pdf_tax_types_label": "Tipos de Impostos",
|
||||
"pdf_expenses_label": "Despesas",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Cobrar a,",
|
||||
"pdf_ship_to": "Envie a,",
|
||||
"pdf_received_from": "Remetente:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Moeda",
|
||||
"contact": "Contato",
|
||||
"category": "Categoria",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "A partir da Data",
|
||||
"to_date": "Até a Data",
|
||||
"expense_date": "Data",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "IMPOSTOS TOTAIS",
|
||||
"pdf_tax_types_label": "Tipos de Impostos",
|
||||
"pdf_expenses_label": "Despesas",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Cobrar a,",
|
||||
"pdf_ship_to": "Envie a,",
|
||||
"pdf_received_from": "Remetente:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Date",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Валюта",
|
||||
"contact": "Контакт",
|
||||
"category": "Категория",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "От даты",
|
||||
"to_date": "До даты",
|
||||
"expense_date": "Дата",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "ВСЕГО НАЛОГОВ",
|
||||
"pdf_tax_types_label": "Типы налогов",
|
||||
"pdf_expenses_label": "Расходы",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Адрес счёта,",
|
||||
"pdf_ship_to": "Адрес доставки,",
|
||||
"pdf_received_from": "Получено от:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Kontakt",
|
||||
"category": "Kategória",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Od dátumu",
|
||||
"to_date": "Do dátumu",
|
||||
"expense_date": "Dátum",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "Celkové dane",
|
||||
"pdf_tax_types_label": "Typy daní",
|
||||
"pdf_expenses_label": "Výdaje",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Odberateľ:",
|
||||
"pdf_ship_to": "Doručiť do",
|
||||
"pdf_received_from": "Prijaté od:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Valuta",
|
||||
"contact": "Pišite na",
|
||||
"category": "Kategorija",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Od datuma",
|
||||
"to_date": "Do danes",
|
||||
"expense_date": "Datum",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "SKUPAJ DAVEK",
|
||||
"pdf_tax_types_label": "Vrste davkov",
|
||||
"pdf_expenses_label": "Odhodki",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Račun za,",
|
||||
"pdf_ship_to": "Pošljite v,",
|
||||
"pdf_received_from": "Prejeto od:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Date",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Kontakt",
|
||||
"category": "Kategorija",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Datum od",
|
||||
"to_date": "Datum do",
|
||||
"expense_date": "Datum",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "UKUPNO POREZ",
|
||||
"pdf_tax_types_label": "Tipovi Poreza",
|
||||
"pdf_expenses_label": "Rashodi",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Račun za,",
|
||||
"pdf_ship_to": "Isporučiti za,",
|
||||
"pdf_received_from": "Poslat od strane:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Valuta",
|
||||
"contact": "Kontakt",
|
||||
"category": "Kategori",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Från datum",
|
||||
"to_date": "Till datum",
|
||||
"expense_date": "Datum",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "SUMMA MOMS",
|
||||
"pdf_tax_types_label": "Momssatser",
|
||||
"pdf_expenses_label": "Utgifter",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Faktureras till,",
|
||||
"pdf_ship_to": "Skickas till,",
|
||||
"pdf_received_from": "Från:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Date",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "สกุลเงิน",
|
||||
"contact": "ติดต่อเรา",
|
||||
"category": "ประเภท",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "จากวันที่",
|
||||
"to_date": "ถึงวันที่",
|
||||
"expense_date": "วันที่",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "ภาษีทั้งหมด",
|
||||
"pdf_tax_types_label": "ประเภทภาษี",
|
||||
"pdf_expenses_label": "ค่าใช้จ่าย",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "ที่อยู่เรียกเก็บเงิน,",
|
||||
"pdf_ship_to": "ที่อยู่สำหรับจัดส่ง,",
|
||||
"pdf_received_from": "ได้รับจาก:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "İletişim",
|
||||
"category": "Kategori",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Başlangıç tarihi",
|
||||
"to_date": "Bitiş tarihi",
|
||||
"expense_date": "Tarih",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOPLAM VERGİ",
|
||||
"pdf_tax_types_label": "Vergi Türleri",
|
||||
"pdf_expenses_label": "Giderler",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Fatura Adresi,",
|
||||
"pdf_ship_to": "Teslimat Adresi,",
|
||||
"pdf_received_from": "Alındığı Kişi:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Валюта",
|
||||
"contact": "Контакт",
|
||||
"category": "Категорія",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Від дати",
|
||||
"to_date": "До дати",
|
||||
"expense_date": "Дата",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "Загальний звіт податків",
|
||||
"pdf_tax_types_label": "Типи податків",
|
||||
"pdf_expenses_label": "Витрати",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Рахунок до,",
|
||||
"pdf_ship_to": "Доставити до,",
|
||||
"pdf_received_from": "Отримано від:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Date",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Tiền tệ",
|
||||
"contact": "Tiếp xúc",
|
||||
"category": "Danh mục",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "Từ ngày",
|
||||
"to_date": "Đến nay",
|
||||
"expense_date": "Ngày",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TỔNG THUẾ",
|
||||
"pdf_tax_types_label": "Các loại thuế",
|
||||
"pdf_expenses_label": "Chi phí",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Hoa đơn để,",
|
||||
"pdf_ship_to": "Tàu,",
|
||||
"pdf_received_from": "Nhận được tư:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "Currency",
|
||||
"contact": "Contact",
|
||||
"category": "Category",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "From Date",
|
||||
"to_date": "To Date",
|
||||
"expense_date": "Date",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "TOTAL TAX",
|
||||
"pdf_tax_types_label": "Tax Types",
|
||||
"pdf_expenses_label": "Expenses",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "Bill to,",
|
||||
"pdf_ship_to": "Ship to,",
|
||||
"pdf_received_from": "Received from:",
|
||||
|
||||
@@ -652,6 +652,7 @@
|
||||
"currency": "主要貨幣",
|
||||
"contact": "聯絡",
|
||||
"category": "分類",
|
||||
"uncategorized": "Uncategorized",
|
||||
"from_date": "啟始日",
|
||||
"to_date": "終止日",
|
||||
"expense_date": "日期",
|
||||
@@ -1649,6 +1650,7 @@
|
||||
"pdf_total_tax_label": "稅項總額",
|
||||
"pdf_tax_types_label": "稅收類型",
|
||||
"pdf_expenses_label": "支出",
|
||||
"pdf_expense_group_total_label": "Group total:",
|
||||
"pdf_bill_to": "帳單地址,",
|
||||
"pdf_ship_to": "送貨地址,",
|
||||
"pdf_received_from": "接收自",
|
||||
|
||||
@@ -21,6 +21,18 @@
|
||||
</BaseDropdownItem>
|
||||
</router-link>
|
||||
|
||||
<!-- duplicate expense -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.CREATE_EXPENSE)"
|
||||
@click="onDuplicateExpense(row)"
|
||||
>
|
||||
<BaseIcon
|
||||
name="DocumentDuplicateIcon"
|
||||
class="w-5 h-5 mr-3 text-gray-400 group-hover:text-gray-500"
|
||||
/>
|
||||
{{ $t('expenses.duplicate_expense') }}
|
||||
</BaseDropdownItem>
|
||||
|
||||
<!-- delete expense -->
|
||||
<BaseDropdownItem
|
||||
v-if="userStore.hasAbilities(abilities.DELETE_EXPENSE)"
|
||||
@@ -37,10 +49,10 @@
|
||||
|
||||
<script setup>
|
||||
import { useDialogStore } from '@/scripts/stores/dialog'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useExpenseStore } from '@/scripts/admin/stores/expense'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { inject } from 'vue'
|
||||
import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
@@ -61,15 +73,24 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
const expenseStore = useExpenseStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const $utils = inject('utils')
|
||||
|
||||
function onDuplicateExpense(row) {
|
||||
modalStore.openModal({
|
||||
title: t('expenses.duplicate_expense_title'),
|
||||
componentName: 'DuplicateExpenseModal',
|
||||
data: row,
|
||||
size: 'sm',
|
||||
refreshData: props.loadData,
|
||||
})
|
||||
}
|
||||
|
||||
function removeExpense(id) {
|
||||
dialogStore
|
||||
.openDialog({
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<BaseModal
|
||||
:show="modalActive"
|
||||
:initial-focus="initialFocusRef"
|
||||
@close="closeModal"
|
||||
@open="onModalOpen"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XMarkIcon"
|
||||
class="w-6 h-6 text-gray-500 cursor-pointer"
|
||||
@click="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form action="" @submit.prevent="submitDuplicate">
|
||||
<div
|
||||
ref="initialFocusRef"
|
||||
class="sr-only outline-none focus:outline-none"
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div class="px-8 py-6 sm:p-6">
|
||||
<p class="mb-6 text-sm text-gray-600">
|
||||
{{ $t('expenses.duplicate_expense_modal_hint') }}
|
||||
</p>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('expenses.expense_date')"
|
||||
variant="vertical"
|
||||
required
|
||||
>
|
||||
<BaseDatePicker
|
||||
v-model="selectedExpenseDate"
|
||||
:calendar-button="true"
|
||||
calendar-button-icon="calendar"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="
|
||||
z-0
|
||||
flex
|
||||
justify-end
|
||||
px-4
|
||||
py-4
|
||||
border-t border-gray-200 border-solid
|
||||
"
|
||||
>
|
||||
<BaseButton
|
||||
class="mr-2"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
:loading="isDuplicating"
|
||||
:disabled="isDuplicating || !selectedExpenseDate"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isDuplicating"
|
||||
name="DocumentDuplicateIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('expenses.duplicate_expense') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import moment from 'moment'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useModalStore } from '@/scripts/stores/modal'
|
||||
import { useExpenseStore } from '@/scripts/admin/stores/expense'
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const expenseStore = useExpenseStore()
|
||||
const router = useRouter()
|
||||
|
||||
const selectedExpenseDate = ref('')
|
||||
const isDuplicating = ref(false)
|
||||
const initialFocusRef = ref(null)
|
||||
|
||||
const modalActive = computed(
|
||||
() => modalStore.active && modalStore.componentName === 'DuplicateExpenseModal'
|
||||
)
|
||||
|
||||
function toYmd(value) {
|
||||
if (!value) {
|
||||
return moment().format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
const str = String(value)
|
||||
|
||||
if (str.length >= 10 && /^\d{4}-\d{2}-\d{2}/.test(str)) {
|
||||
return str.slice(0, 10)
|
||||
}
|
||||
|
||||
return moment(value).format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
function onModalOpen() {
|
||||
selectedExpenseDate.value = toYmd(modalStore.data?.expense_date)
|
||||
}
|
||||
|
||||
async function submitDuplicate() {
|
||||
if (!modalStore.data?.id || !selectedExpenseDate.value) {
|
||||
return
|
||||
}
|
||||
|
||||
isDuplicating.value = true
|
||||
|
||||
try {
|
||||
await expenseStore.duplicateExpense({
|
||||
id: modalStore.data.id,
|
||||
expense_date: selectedExpenseDate.value,
|
||||
})
|
||||
|
||||
modalStore.refreshData && modalStore.refreshData()
|
||||
closeModal()
|
||||
router.push('/admin/expenses')
|
||||
} finally {
|
||||
isDuplicating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalStore.closeModal()
|
||||
selectedExpenseDate.value = ''
|
||||
}
|
||||
</script>
|
||||
@@ -58,15 +58,16 @@
|
||||
variant="horizontal"
|
||||
required
|
||||
>
|
||||
<BaseMoney
|
||||
v-model="taxTypeStore.currentTaxType.percent"
|
||||
:currency="{
|
||||
decimal: '.',
|
||||
thousands: ',',
|
||||
symbol: '% ',
|
||||
precision: 2,
|
||||
masked: false,
|
||||
}"
|
||||
<BaseInput
|
||||
:model-value="taxTypeStore.currentTaxType.percent"
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="-100"
|
||||
max="100"
|
||||
inline-addon="%"
|
||||
:invalid="v$.currentTaxType.percent.$error"
|
||||
@update:model-value="onTaxPercentInput"
|
||||
@blur="onTaxPercentBlur"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
@@ -207,7 +208,38 @@ const v$ = useVuelidate(
|
||||
computed(() => taxTypeStore)
|
||||
)
|
||||
|
||||
function onTaxPercentInput(val) {
|
||||
v$.value.currentTaxType.percent.$touch()
|
||||
|
||||
if (val === '' || val === null) {
|
||||
taxTypeStore.currentTaxType.percent = null
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const n = typeof val === 'number' ? val : parseFloat(val)
|
||||
taxTypeStore.currentTaxType.percent = Number.isNaN(n) ? null : n
|
||||
}
|
||||
|
||||
function onTaxPercentBlur() {
|
||||
const p = taxTypeStore.currentTaxType.percent
|
||||
if (p === null || p === undefined || p === '') {
|
||||
return
|
||||
}
|
||||
|
||||
const n = typeof p === 'number' ? p : parseFloat(p)
|
||||
if (Number.isNaN(n)) {
|
||||
return
|
||||
}
|
||||
|
||||
taxTypeStore.currentTaxType.percent = Math.round(n * 1000) / 1000
|
||||
}
|
||||
|
||||
async function submitTaxTypeData() {
|
||||
if (taxTypeStore.currentTaxType.calculation_type === 'percentage') {
|
||||
onTaxPercentBlur()
|
||||
}
|
||||
|
||||
v$.value.currentTaxType.$touch()
|
||||
if (v$.value.currentTaxType.$invalid) {
|
||||
return true
|
||||
|
||||
21
resources/scripts/admin/stores/expense.js
vendored
21
resources/scripts/admin/stores/expense.js
vendored
@@ -91,6 +91,27 @@ export const useExpenseStore = (useWindow = false) => {
|
||||
})
|
||||
},
|
||||
|
||||
duplicateExpense({ id, expense_date }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
http
|
||||
.post(`/api/v1/expenses/${id}/duplicate`, { expense_date })
|
||||
.then((response) => {
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: global.t('expenses.duplicated_message'),
|
||||
})
|
||||
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
addExpense(data) {
|
||||
const formData = utils.toFormData(data)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<BasePage>
|
||||
<DuplicateExpenseModal />
|
||||
|
||||
<!-- Page Header -->
|
||||
<BasePageHeader :title="$t('expenses.title')">
|
||||
<BaseBreadcrumb>
|
||||
@@ -231,6 +233,7 @@ import { useUserStore } from '@/scripts/admin/stores/user'
|
||||
import abilities from '@/scripts/admin/stub/abilities'
|
||||
|
||||
import UFOIcon from '@/scripts/components/icons/empty/UFOIcon.vue'
|
||||
import DuplicateExpenseModal from '@/scripts/admin/components/modal-components/DuplicateExpenseModal.vue'
|
||||
import ExpenseDropdown from '@/scripts/admin/components/dropdowns/ExpenseIndexDropdown.vue'
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
@@ -248,4 +248,5 @@ function openTaxModal() {
|
||||
refreshData: table.value && table.value.refresh,
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
static
|
||||
class="fixed inset-0 z-20 overflow-y-auto"
|
||||
:open="show"
|
||||
:initial-focus="initialFocus"
|
||||
@close="$emit('close')"
|
||||
>
|
||||
<div
|
||||
@@ -106,6 +107,14 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* Optional ref (template ref) for the element that should receive focus when the dialog opens.
|
||||
* When omitted, Headless UI focuses the first focusable control (often an input).
|
||||
*/
|
||||
initialFocus: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
const slots = useSlots()
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
.heading-date-range {
|
||||
font-weight: normal;
|
||||
font-size: 15px;
|
||||
color: #A5ACC1;
|
||||
color: #606060;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
padding: 0px;
|
||||
@@ -41,7 +41,7 @@
|
||||
}
|
||||
|
||||
.sub-heading-text {
|
||||
font-weight: normal;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
color: #595959;
|
||||
padding: 0px;
|
||||
@@ -50,8 +50,7 @@
|
||||
}
|
||||
|
||||
.expenses-title {
|
||||
margin-top: 60px;
|
||||
padding-left: 3px;
|
||||
margin-top: 30px;
|
||||
font-size: 16px;
|
||||
line-height: 21px;
|
||||
color: #040405;
|
||||
@@ -133,6 +132,84 @@
|
||||
line-height: 21px;
|
||||
color: #5851D8;
|
||||
}
|
||||
|
||||
/* -- Items Table -- */
|
||||
|
||||
.items-table {
|
||||
margin-top: 35px;
|
||||
padding: 0px 30px 10px 30px;
|
||||
page-break-before: avoid;
|
||||
page-break-after: auto;
|
||||
}
|
||||
|
||||
.items-table hr {
|
||||
height: 0.1px;
|
||||
}
|
||||
|
||||
.item-table-heading-left {
|
||||
font-size: 13.5;
|
||||
text-align: left;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
padding: 5px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.item-table-heading-right {
|
||||
font-size: 13.5;
|
||||
text-align: right;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
padding: 5px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
tr.item-table-heading-row th {
|
||||
border-bottom: 0.620315px solid #E8E8E8;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.item-table-heading-row {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
tr.item-row td {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.item-cell-left {
|
||||
font-size: 13;
|
||||
color: #040405;
|
||||
text-align: left;
|
||||
padding: 5px;
|
||||
padding-top: 10px;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
|
||||
.item-cell-right {
|
||||
font-size: 13;
|
||||
color: #040405;
|
||||
text-align: right;
|
||||
padding: 5px;
|
||||
padding-top: 10px;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
|
||||
.item-description {
|
||||
color: #595959;
|
||||
font-size: 9px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.item-table-group-total {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
padding: 5px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@if (App::isLocale('th'))
|
||||
@@ -158,33 +235,28 @@
|
||||
</tr>
|
||||
</table>
|
||||
<p class="expenses-title">@lang('pdf_expenses_label')</p>
|
||||
<div class="expenses-table-container">
|
||||
<table class="expenses-table">
|
||||
@foreach ($expenseCategories as $expenseCategory)
|
||||
<tr>
|
||||
<td>
|
||||
<p class="expense-title">
|
||||
{{ $expenseCategory->category->name }}
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
<p class="expense-amount">
|
||||
{!! format_money_pdf($expenseCategory->total_amount, $currency) !!}
|
||||
</p>
|
||||
</td>
|
||||
@foreach ($expenseGroups as $group)
|
||||
<p class="expense-title">{{ $group['name'] }}</p>
|
||||
<table width="100%" style="margin-bottom:18px;">
|
||||
<tr class="item-table-heading-row">
|
||||
<th style="width: 15%;" class="text-left item-table-heading-left">@lang('Date')</th>
|
||||
<th style="width: 70%;" class="text-left item-table-heading-left">@lang('Note')</th>
|
||||
<th style="width: 15%;" class="text-right item-table-heading-right">@lang('Amount')</th>
|
||||
</tr>
|
||||
@foreach ($group['expenses'] as $expense)
|
||||
<tr class="item-row">
|
||||
<td style="width: 15%;" class="text-left item-cell-left">{{ $expense->formatted_expense_date }}</td>
|
||||
<td style="width: 70%;" class="text-left item-cell-left">{{ $expense->notes ? $expense->notes : '-' }}</td>
|
||||
<td style="width: 15%;" class="text-right item-cell-right">{!! format_money_pdf($expense->base_amount, $currency) !!}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="expense-total-table">
|
||||
<tr>
|
||||
<td class="expense-total-cell">
|
||||
<p class="expense-total">{!! format_money_pdf($totalExpense, $currency) !!}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</table>
|
||||
<div class="item-table-group-total">
|
||||
<p>@lang('pdf_expense_group_total_label') <span style="color: #5851D8;">{!! format_money_pdf($group['total'], $currency) !!}</span></p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<table class="report-footer">
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
@@ -23,6 +23,7 @@ use App\Http\Controllers\V1\Admin\ExchangeRate\GetActiveProviderController;
|
||||
use App\Http\Controllers\V1\Admin\ExchangeRate\GetExchangeRateController;
|
||||
use App\Http\Controllers\V1\Admin\ExchangeRate\GetSupportedCurrenciesController;
|
||||
use App\Http\Controllers\V1\Admin\ExchangeRate\GetUsedCurrenciesController;
|
||||
use App\Http\Controllers\V1\Admin\Expense\DuplicateExpenseController;
|
||||
use App\Http\Controllers\V1\Admin\Expense\ExpenseCategoriesController;
|
||||
use App\Http\Controllers\V1\Admin\Expense\ExpensesController;
|
||||
use App\Http\Controllers\V1\Admin\Expense\ShowReceiptController;
|
||||
@@ -312,6 +313,8 @@ Route::prefix('/v1')->group(function () {
|
||||
|
||||
Route::post('/expenses/delete', [ExpensesController::class, 'delete']);
|
||||
|
||||
Route::post('/expenses/{expense}/duplicate', DuplicateExpenseController::class);
|
||||
|
||||
Route::apiResource('expenses', ExpensesController::class);
|
||||
|
||||
Route::apiResource('categories', ExpenseCategoriesController::class);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\V1\Admin\Expense\DuplicateExpenseController;
|
||||
use App\Http\Controllers\V1\Admin\Expense\ExpensesController;
|
||||
use App\Http\Requests\DuplicateExpenseRequest;
|
||||
use App\Http\Requests\ExpenseRequest;
|
||||
use App\Models\Expense;
|
||||
use App\Models\User;
|
||||
@@ -114,6 +116,79 @@ test('search expenses', function () {
|
||||
$response->assertOk();
|
||||
});
|
||||
|
||||
test('duplicate expense', function () {
|
||||
$expense = Expense::factory()->create([
|
||||
'expense_date' => '2019-02-05',
|
||||
'notes' => 'Monthly rent',
|
||||
]);
|
||||
|
||||
$response = postJson("api/v1/expenses/{$expense->id}/duplicate", [
|
||||
'expense_date' => '2019-02-05',
|
||||
]);
|
||||
|
||||
$response->assertStatus(201);
|
||||
|
||||
$newId = $response->json('data.id');
|
||||
|
||||
expect($newId)->not->toBe($expense->id);
|
||||
|
||||
$this->assertDatabaseHas('expenses', [
|
||||
'id' => $newId,
|
||||
'expense_date' => '2019-02-05',
|
||||
'notes' => 'Monthly rent (copy)',
|
||||
'expense_category_id' => $expense->expense_category_id,
|
||||
'amount' => $expense->amount,
|
||||
]);
|
||||
});
|
||||
|
||||
test('duplicate expense with empty note uses copy as note', function () {
|
||||
$expense = Expense::factory()->create([
|
||||
'expense_date' => '2019-02-05',
|
||||
'notes' => null,
|
||||
]);
|
||||
|
||||
postJson("api/v1/expenses/{$expense->id}/duplicate", [
|
||||
'expense_date' => '2019-02-05',
|
||||
])
|
||||
->assertStatus(201)
|
||||
->assertJsonPath('data.notes', '(copy)');
|
||||
});
|
||||
|
||||
test('duplicate expense uses submitted expense date', function () {
|
||||
$expense = Expense::factory()->create([
|
||||
'expense_date' => '2019-02-05',
|
||||
]);
|
||||
|
||||
$response = postJson("api/v1/expenses/{$expense->id}/duplicate", [
|
||||
'expense_date' => '2024-03-10',
|
||||
]);
|
||||
|
||||
$response->assertStatus(201);
|
||||
|
||||
$this->assertDatabaseHas('expenses', [
|
||||
'id' => $response->json('data.id'),
|
||||
'expense_date' => '2024-03-10',
|
||||
]);
|
||||
});
|
||||
|
||||
test('duplicate expense requires expense date', function () {
|
||||
$expense = Expense::factory()->create([
|
||||
'expense_date' => '2019-02-05',
|
||||
]);
|
||||
|
||||
postJson("api/v1/expenses/{$expense->id}/duplicate", [])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['expense_date']);
|
||||
});
|
||||
|
||||
test('duplicate validates using a form request', function () {
|
||||
$this->assertActionUsesFormRequest(
|
||||
DuplicateExpenseController::class,
|
||||
'__invoke',
|
||||
DuplicateExpenseRequest::class
|
||||
);
|
||||
});
|
||||
|
||||
test('delete multiple expenses', function () {
|
||||
$expenses = Expense::factory()->count(3)->create([
|
||||
'expense_date' => '2019-02-05',
|
||||
|
||||
@@ -5,6 +5,7 @@ use App\Http\Requests\TaxTypeRequest;
|
||||
use App\Models\TaxType;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
use function Pest\Laravel\deleteJson;
|
||||
@@ -110,3 +111,22 @@ test('create fixed amount tax type', function () {
|
||||
|
||||
$this->assertDatabaseHas('tax_types', $taxType);
|
||||
});
|
||||
|
||||
test('create percentage tax type with three decimals', function () {
|
||||
$payload = TaxType::factory()->raw([
|
||||
'calculation_type' => 'percentage',
|
||||
'percent' => 6.625,
|
||||
'fixed_amount' => null,
|
||||
]);
|
||||
|
||||
$response = postJson('api/v1/tax-types', $payload)
|
||||
->assertStatus(201);
|
||||
|
||||
$taxTypeId = $response->json('data.id');
|
||||
|
||||
expect($taxTypeId)->not()->toBeNull();
|
||||
|
||||
$rawPercent = DB::table('tax_types')->where('id', $taxTypeId)->value('percent');
|
||||
|
||||
expect((string) $rawPercent)->toBe('6.625');
|
||||
});
|
||||
|
||||
@@ -34,3 +34,39 @@ it('normalizes legacy closing-br markup so lines are not collapsed in PDF output
|
||||
expect($out)->toContain('<br')->toContain('line1')->toContain('line2');
|
||||
expect($out)->not->toBe('line1line2');
|
||||
});
|
||||
|
||||
it('strips SSRF vectors injected via address-template placeholders', function () {
|
||||
// Simulates the output of GeneratesPdfTrait::getFormattedString() after a
|
||||
// malicious customer name like "Acme <img src='http://attacker/probe'>" has
|
||||
// been substituted into an address template via {BILLING_ADDRESS_NAME}.
|
||||
$html = "Acme <img src='http://attacker.test/probe'><br />123 Main St<br />Springfield";
|
||||
|
||||
$out = PdfHtmlSanitizer::sanitize($html);
|
||||
|
||||
expect($out)->not->toContain('<img');
|
||||
expect($out)->not->toContain('src=');
|
||||
expect($out)->not->toContain('attacker.test');
|
||||
expect($out)->toContain('Acme');
|
||||
expect($out)->toContain('123 Main St');
|
||||
expect($out)->toContain('Springfield');
|
||||
});
|
||||
|
||||
it('strips iframe and link tags that could trigger SSRF', function () {
|
||||
$html = '<iframe src="http://attacker/x"></iframe><link rel="stylesheet" href="http://attacker/y.css">Hello';
|
||||
|
||||
$out = PdfHtmlSanitizer::sanitize($html);
|
||||
|
||||
expect($out)->not->toContain('<iframe');
|
||||
expect($out)->not->toContain('<link');
|
||||
expect($out)->not->toContain('attacker');
|
||||
expect($out)->toContain('Hello');
|
||||
});
|
||||
|
||||
it('strips on* event handler attributes from allowed tags', function () {
|
||||
$html = '<p onload="alert(1)" onclick="x">click me</p>';
|
||||
|
||||
$out = PdfHtmlSanitizer::sanitize($html);
|
||||
|
||||
expect($out)->not->toContain('onload')->not->toContain('onclick')->not->toContain('alert');
|
||||
expect($out)->toContain('click me');
|
||||
});
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.3.2
|
||||
2.3.3
|
||||
|
||||
Reference in New Issue
Block a user