diff --git a/.gitignore b/.gitignore
index b643269b604a..5a86589d7a21 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,3 +34,5 @@ tests/_bootstrap.php
# composer stuff
/c3.php
+
+_ide_helper.php
\ No newline at end of file
diff --git a/app/Events/ExpenseWasArchived.php b/app/Events/ExpenseWasArchived.php
new file mode 100644
index 000000000000..a4b2af4bdf31
--- /dev/null
+++ b/app/Events/ExpenseWasArchived.php
@@ -0,0 +1,22 @@
+expense = $expense;
+ }
+
+}
diff --git a/app/Events/ExpenseWasCreated.php b/app/Events/ExpenseWasCreated.php
new file mode 100644
index 000000000000..ab462fe60253
--- /dev/null
+++ b/app/Events/ExpenseWasCreated.php
@@ -0,0 +1,21 @@
+expense = $expense;
+ }
+}
diff --git a/app/Events/ExpenseWasDeleted.php b/app/Events/ExpenseWasDeleted.php
new file mode 100644
index 000000000000..1549b483b497
--- /dev/null
+++ b/app/Events/ExpenseWasDeleted.php
@@ -0,0 +1,23 @@
+expense = $expense;
+ }
+
+}
diff --git a/app/Events/ExpenseWasRestored.php b/app/Events/ExpenseWasRestored.php
new file mode 100644
index 000000000000..b52a2d119a2d
--- /dev/null
+++ b/app/Events/ExpenseWasRestored.php
@@ -0,0 +1,23 @@
+expense = $expense;
+ }
+
+}
diff --git a/app/Events/ExpenseWasUpdated.php b/app/Events/ExpenseWasUpdated.php
new file mode 100644
index 000000000000..1066d90de4f7
--- /dev/null
+++ b/app/Events/ExpenseWasUpdated.php
@@ -0,0 +1,21 @@
+expense = $expense;
+ }
+}
diff --git a/app/Events/VendorWasArchived.php b/app/Events/VendorWasArchived.php
new file mode 100644
index 000000000000..ca268441f0d4
--- /dev/null
+++ b/app/Events/VendorWasArchived.php
@@ -0,0 +1,22 @@
+vendor = $vendor;
+ }
+}
diff --git a/app/Events/VendorWasCreated.php b/app/Events/VendorWasCreated.php
new file mode 100644
index 000000000000..b2d7e81c9394
--- /dev/null
+++ b/app/Events/VendorWasCreated.php
@@ -0,0 +1,22 @@
+vendor = $vendor;
+ }
+}
diff --git a/app/Events/VendorWasDeleted.php b/app/Events/VendorWasDeleted.php
new file mode 100644
index 000000000000..553bece3ccdc
--- /dev/null
+++ b/app/Events/VendorWasDeleted.php
@@ -0,0 +1,22 @@
+vendor = $vendor;
+ }
+}
diff --git a/app/Events/VendorWasRestored.php b/app/Events/VendorWasRestored.php
new file mode 100644
index 000000000000..88c24693e611
--- /dev/null
+++ b/app/Events/VendorWasRestored.php
@@ -0,0 +1,22 @@
+vendor = $vendor;
+ }
+}
diff --git a/app/Events/VendorWasUpdated.php b/app/Events/VendorWasUpdated.php
new file mode 100644
index 000000000000..eb90a68f46c0
--- /dev/null
+++ b/app/Events/VendorWasUpdated.php
@@ -0,0 +1,21 @@
+vendor = $vendor;
+ }
+}
diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php
index e77bdf0b23ce..cc51a06de32c 100644
--- a/app/Http/Controllers/AccountController.php
+++ b/app/Http/Controllers/AccountController.php
@@ -20,6 +20,7 @@ use App\Models\Account;
use App\Models\Gateway;
use App\Models\InvoiceDesign;
use App\Models\TaxRate;
+use App\Models\PaymentTerm;
use App\Ninja\Repositories\AccountRepository;
use App\Ninja\Repositories\ReferralRepository;
use App\Ninja\Mailers\UserMailer;
@@ -158,6 +159,8 @@ class AccountController extends BaseController
return self::showProducts();
} elseif ($section === ACCOUNT_TAX_RATES) {
return self::showTaxRates();
+ } elseif ($section === ACCOUNT_PAYMENT_TERMS) {
+ return self::showPaymentTerms();
} elseif ($section === ACCOUNT_SYSTEM_SETTINGS) {
return self::showSystemSettings();
} else {
@@ -313,6 +316,17 @@ class AccountController extends BaseController
return View::make('accounts.tax_rates', $data);
}
+ private function showPaymentTerms()
+ {
+ $data = [
+ 'account' => Auth::user()->account,
+ 'title' => trans('texts.payment_terms'),
+ 'taxRates' => PaymentTerm::scope()->get(['id', 'name', 'num_days']),
+ ];
+
+ return View::make('accounts.payment_terms', $data);
+ }
+
private function showInvoiceDesign($section)
{
$account = Auth::user()->account->load('country');
@@ -322,13 +336,15 @@ class AccountController extends BaseController
$invoiceItem = new stdClass();
$client->name = 'Sample Client';
- $client->address1 = '';
- $client->city = '';
- $client->state = '';
- $client->postal_code = '';
- $client->work_phone = '';
- $client->work_email = '';
-
+ $client->address1 = 'address 1';
+ $client->city = ' city';
+ $client->state = 'state';
+ $client->postal_code = 'postal code';
+ $client->work_phone = 'work phone';
+ $client->work_email = 'work email';
+ $client->id_number = 'cleint id';
+ $client->vat_number = 'vat number';
+
$invoice->invoice_number = '0000';
$invoice->invoice_date = Utils::fromSqlDate(date('Y-m-d'));
$invoice->account = json_decode($account->toJson());
@@ -347,7 +363,7 @@ class AccountController extends BaseController
$invoice->client = $client;
$invoice->invoice_items = [$invoiceItem];
-
+
$data['account'] = $account;
$data['invoice'] = $invoice;
$data['invoiceLabels'] = json_decode($account->invoice_labels) ?: [];
@@ -355,7 +371,7 @@ class AccountController extends BaseController
$data['invoiceDesigns'] = InvoiceDesign::getDesigns();
$data['invoiceFonts'] = Cache::get('fonts');
$data['section'] = $section;
-
+
$design = false;
foreach ($data['invoiceDesigns'] as $item) {
if ($item->id == $account->invoice_design_id) {
@@ -444,6 +460,8 @@ class AccountController extends BaseController
return AccountController::saveProducts();
} elseif ($section === ACCOUNT_TAX_RATES) {
return AccountController::saveTaxRates();
+ } elseif ($section === ACCOUNT_PAYMENT_TERMS) {
+ return AccountController::savePaymetTerms();
}
}
@@ -695,7 +713,7 @@ class AccountController extends BaseController
$account->primary_color = Input::get('primary_color');
$account->secondary_color = Input::get('secondary_color');
$account->invoice_design_id = Input::get('invoice_design_id');
-
+
if (Input::has('font_size')) {
$account->font_size = intval(Input::get('font_size'));
}
diff --git a/app/Http/Controllers/BaseAPIController.php b/app/Http/Controllers/BaseAPIController.php
index e5878e22ab55..4d783556022e 100644
--- a/app/Http/Controllers/BaseAPIController.php
+++ b/app/Http/Controllers/BaseAPIController.php
@@ -121,12 +121,15 @@ class BaseAPIController extends Controller
} elseif ($include == 'clients') {
$data[] = 'clients.contacts';
$data[] = 'clients.user';
- } elseif ($include) {
+ } elseif ($include == 'vendors') {
+ $data[] = 'vendors.vendorcontacts';
+ $data[] = 'vendors.user';
+ }
+ elseif ($include) {
$data[] = $include;
}
}
return $data;
}
-
}
diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php
index 27d4ab04af57..e56da3cb8017 100644
--- a/app/Http/Controllers/DashboardController.php
+++ b/app/Http/Controllers/DashboardController.php
@@ -6,11 +6,14 @@ use View;
use App\Models\Activity;
use App\Models\Invoice;
use App\Models\Payment;
+use App\Models\VendorActivity;
+use App\Models\ExpenseActivity;
class DashboardController extends BaseController
{
public function index()
{
+
// total_income, billed_clients, invoice_sent and active_clients
$select = DB::raw('COUNT(DISTINCT CASE WHEN invoices.id IS NOT NULL THEN clients.id ELSE null END) billed_clients,
SUM(CASE WHEN invoices.invoice_status_id >= '.INVOICE_STATUS_SENT.' THEN 1 ELSE 0 END) invoices_sent,
@@ -68,6 +71,19 @@ class DashboardController extends BaseController
->take(50)
->get();
+ $vendoractivities = VendorActivity::where('vendor_activities.account_id', '=', Auth::user()->account_id)
+ ->with('vendor.vendorcontacts', 'user', 'account')
+ ->where('activity_type_id', '>', 0)
+ ->orderBy('created_at', 'desc')
+ ->take(50)
+ ->get();
+
+ $expenseactivities = ExpenseActivity::where('expense_activities.account_id', '=', Auth::user()->account_id)
+ ->where('activity_type_id', '>', 0)
+ ->orderBy('created_at', 'desc')
+ ->take(50)
+ ->get();
+
$pastDue = DB::table('invoices')
->leftJoin('clients', 'clients.id', '=', 'invoices.client_id')
->leftJoin('contacts', 'contacts.client_id', '=', 'clients.id')
@@ -141,6 +157,8 @@ class DashboardController extends BaseController
'payments' => $payments,
'title' => trans('texts.dashboard'),
'hasQuotes' => $hasQuotes,
+ 'vendoractivities' => $vendoractivities,
+ 'expenseactivities' => $expenseactivities,
];
return View::make('dashboard', $data);
diff --git a/app/Http/Controllers/ExpenseActivityController.php b/app/Http/Controllers/ExpenseActivityController.php
new file mode 100644
index 000000000000..0993123129c5
--- /dev/null
+++ b/app/Http/Controllers/ExpenseActivityController.php
@@ -0,0 +1,27 @@
+activityService = $activityService;
+ }
+
+ public function getDatatable($vendorPublicId)
+ {
+ return $this->activityService->getDatatable($vendorPublicId);
+ }
+}
diff --git a/app/Http/Controllers/ExpenseApiController.php b/app/Http/Controllers/ExpenseApiController.php
new file mode 100644
index 000000000000..484538455586
--- /dev/null
+++ b/app/Http/Controllers/ExpenseApiController.php
@@ -0,0 +1,244 @@
+expenseRepo = $expenseRepo;
+ $this->expenseService = $expenseService;
+ }
+
+ /**
+ * Display a listing of the resource.
+ *
+ * @return Response
+ */
+ public function index()
+ {
+ return View::make('list', array(
+ 'entityType' => ENTITY_EXPENSE,
+ 'title' => trans('texts.expenses'),
+ 'sortCol' => '1',
+ 'columns' => Utils::trans([
+ 'checkbox',
+ 'vendor',
+ 'expense_amount',
+ 'expense_date',
+ 'public_notes',
+ 'is_invoiced',
+ 'should_be_invoiced',
+ ''
+ ]),
+ ));
+ }
+
+ public function getDatatable($expensePublicId = null)
+ {
+ return $this->expenseService->getDatatable($expensePublicId, Input::get('sSearch'));
+ }
+
+ public function getDatatableVendor($vendorPublicId = null)
+ {
+ return $this->expenseService->getDatatableVendor($vendorPublicId);
+ }
+
+ public function create($vendorPublicId = 0)
+ {
+ if($vendorPublicId != 0) {
+ $vendor = Vendor::scope($vendorPublicId)->with('vendorcontacts')->firstOrFail();
+ } else {
+ $vendor = null;
+ }
+ $data = array(
+ 'vendorPublicId' => Input::old('vendor') ? Input::old('vendor') : $vendorPublicId,
+ 'expense' => null,
+ 'method' => 'POST',
+ 'url' => 'expenses',
+ 'title' => trans('texts.new_expense'),
+ 'vendors' => Vendor::scope()->with('vendorcontacts')->orderBy('name')->get(),
+ 'vendor' => $vendor,
+ 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(),
+ 'clientPublicId' => null,
+ );
+
+ $data = array_merge($data, self::getViewModel());
+
+ return View::make('expenses.edit', $data);
+ }
+
+ public function edit($publicId)
+ {
+ $expense = Expense::scope($publicId)->firstOrFail();
+ $expense->expense_date = Utils::fromSqlDate($expense->expense_date);
+
+ $data = array(
+ 'vendor' => null,
+ 'expense' => $expense,
+ 'method' => 'PUT',
+ 'url' => 'expenses/'.$publicId,
+ 'title' => 'Edit Expense',
+ 'vendors' => Vendor::scope()->with('vendorcontacts')->orderBy('name')->get(),
+ 'vendorPublicId' => $expense->vendor_id,
+ 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(),
+ 'clientPublicId' => $expense->invoice_client_id,
+ );
+
+ $data = array_merge($data, self::getViewModel());
+
+ if (Auth::user()->account->isNinjaAccount()) {
+ if ($account = Account::whereId($client->public_id)->first()) {
+ $data['proPlanPaid'] = $account['pro_plan_paid'];
+ }
+ }
+
+ return View::make('expenses.edit', $data);
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param int $id
+ * @return Response
+ */
+ public function update(UpdateExpenseRequest $request)
+ {
+ $client = $this->expenseRepo->save($request->input());
+
+ Session::flash('message', trans('texts.updated_expense'));
+
+ return redirect()->to('expenses');
+ }
+
+ public function store(CreateExpenseRequest $request)
+ {
+ $expense = $this->expenseRepo->save($request->input());
+
+ Session::flash('message', trans('texts.created_expense'));
+
+ return redirect()->to('expenses');
+ }
+
+ public function bulk()
+ {
+ $action = Input::get('action');
+ $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
+
+ switch($action)
+ {
+ case 'invoice':
+ $expenses = Expense::scope($ids)->get();
+ $clientPublicId = null;
+ $data = [];
+
+ // Validate that either all expenses do not have a client or if there is a client, it is the same client
+ foreach ($expenses as $expense)
+ {
+ if ($expense->client_id) {
+ if (!$clientPublicId) {
+ $clientPublicId = $expense->client_id;
+ } else if ($clientPublicId != $expense->client_id) {
+ Session::flash('error', trans('texts.expense_error_multiple_clients'));
+ return Redirect::to('expenses');
+ }
+ }
+
+ if ($expense->invoice_id) {
+ Session::flash('error', trans('texts.expense_error_invoiced'));
+ return Redirect::to('expenses');
+ }
+
+ if ($expense->should_be_invoiced == 0) {
+ Session::flash('error', trans('texts.expense_error_should_not_be_invoiced'));
+ return Redirect::to('expenses');
+ }
+
+ $account = Auth::user()->account;
+ $data[] = [
+ 'publicId' => $expense->public_id,
+ 'description' => $expense->public_notes,
+ 'qty' => 1,
+ 'cost' => $expense->amount,
+ ];
+ }
+
+ return Redirect::to("invoices/create/{$clientPublicId}")->with('expenses', $data);
+ break;
+
+ default:
+ $count = $this->expenseService->bulk($ids, $action);
+ }
+
+ if ($count > 0) {
+ $message = Utils::pluralize($action.'d_expense', $count);
+ Session::flash('message', $message);
+ }
+
+ return Redirect::to('expenses');
+ }
+
+ private static function getViewModel()
+ {
+ return [
+ 'data' => Input::old('data'),
+ 'account' => Auth::user()->account,
+ 'sizes' => Cache::get('sizes'),
+ 'paymentTerms' => Cache::get('paymentTerms'),
+ 'industries' => Cache::get('industries'),
+ 'currencies' => Cache::get('currencies'),
+ 'languages' => Cache::get('languages'),
+ 'countries' => Cache::get('countries'),
+ 'customLabel1' => Auth::user()->account->custom_vendor_label1,
+ 'customLabel2' => Auth::user()->account->custom_vendor_label2,
+ ];
+ }
+
+ public function show($publicId)
+ {
+ $expense = Expense::withTrashed()->scope($publicId)->firstOrFail();
+
+ if($expense) {
+ Utils::trackViewed($expense->getDisplayName(), 'expense');
+ }
+
+ $actionLinks = [
+ ['label' => trans('texts.new_expense'), 'url' => '/expenses/create/']
+ ];
+
+ $data = array(
+ 'actionLinks' => $actionLinks,
+ 'showBreadcrumbs' => false,
+ 'expense' => $expense,
+ 'credit' =>0,
+ 'vendor' => $expense->vendor,
+ 'title' => trans('texts.view_expense',['expense' => $expense->public_id]),
+ );
+
+ return View::make('expenses.show', $data);
+ }
+}
diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php
new file mode 100644
index 000000000000..a6d82f17539c
--- /dev/null
+++ b/app/Http/Controllers/ExpenseController.php
@@ -0,0 +1,239 @@
+expenseRepo = $expenseRepo;
+ $this->expenseService = $expenseService;
+ }
+
+ /**
+ * Display a listing of the resource.
+ *
+ * @return Response
+ */
+ public function index()
+ {
+ return View::make('list', array(
+ 'entityType' => ENTITY_EXPENSE,
+ 'title' => trans('texts.expenses'),
+ 'sortCol' => '1',
+ 'columns' => Utils::trans([
+ 'checkbox',
+ 'vendor',
+ 'expense_amount',
+ 'expense_date',
+ 'public_notes',
+ 'is_invoiced',
+ 'should_be_invoiced',
+ ''
+ ]),
+ ));
+ }
+
+ public function getDatatable($expensePublicId = null)
+ {
+ return $this->expenseService->getDatatable($expensePublicId, Input::get('sSearch'));
+ }
+
+ public function create($vendorPublicId = 0)
+ {
+ if($vendorPublicId != 0) {
+ $vendor = Vendor::scope($vendorPublicId)->with('vendorcontacts')->firstOrFail();
+ } else {
+ $vendor = null;
+ }
+ $data = array(
+ 'vendorPublicId' => Input::old('vendor') ? Input::old('vendor') : $vendorPublicId,
+ 'expense' => null,
+ 'method' => 'POST',
+ 'url' => 'expenses',
+ 'title' => trans('texts.new_expense'),
+ 'vendors' => Vendor::scope()->with('vendorcontacts')->orderBy('name')->get(),
+ 'vendor' => $vendor,
+ 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(),
+ 'clientPublicId' => null,
+ );
+
+ $data = array_merge($data, self::getViewModel());
+
+ return View::make('expenses.edit', $data);
+ }
+
+ public function edit($publicId)
+ {
+ $expense = Expense::scope($publicId)->firstOrFail();
+ $expense->expense_date = Utils::fromSqlDate($expense->expense_date);
+
+ $data = array(
+ 'vendor' => null,
+ 'expense' => $expense,
+ 'method' => 'PUT',
+ 'url' => 'expenses/'.$publicId,
+ 'title' => 'Edit Expense',
+ 'vendors' => Vendor::scope()->with('vendorcontacts')->orderBy('name')->get(),
+ 'vendorPublicId' => $expense->vendor_id,
+ 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(),
+ 'clientPublicId' => $expense->invoice_client_id,
+ );
+
+ $data = array_merge($data, self::getViewModel());
+
+ if (Auth::user()->account->isNinjaAccount()) {
+ if ($account = Account::whereId($client->public_id)->first()) {
+ $data['proPlanPaid'] = $account['pro_plan_paid'];
+ }
+ }
+
+ return View::make('expenses.edit', $data);
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param int $id
+ * @return Response
+ */
+ public function update(UpdateExpenseRequest $request)
+ {
+ $client = $this->expenseRepo->save($request->input());
+
+ Session::flash('message', trans('texts.updated_expense'));
+
+ return redirect()->to('expenses');
+ }
+
+ public function store(CreateExpenseRequest $request)
+ {
+ $expense = $this->expenseRepo->save($request->input());
+
+ Session::flash('message', trans('texts.created_expense'));
+
+ return redirect()->to('expenses');
+ }
+
+ public function bulk()
+ {
+ $action = Input::get('action');
+ $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
+
+ switch($action)
+ {
+ case 'invoice':
+ $expenses = Expense::scope($ids)->get();
+ $clientPublicId = null;
+ $data = [];
+
+ // Validate that either all expenses do not have a client or if there is a client, it is the same client
+ foreach ($expenses as $expense)
+ {
+ if ($expense->client_id) {
+ if (!$clientPublicId) {
+ $clientPublicId = $expense->client_id;
+ } else if ($clientPublicId != $expense->client_id) {
+ Session::flash('error', trans('texts.expense_error_multiple_clients'));
+ return Redirect::to('expenses');
+ }
+ }
+
+ if ($expense->invoice_id) {
+ Session::flash('error', trans('texts.expense_error_invoiced'));
+ return Redirect::to('expenses');
+ }
+
+ if ($expense->should_be_invoiced == 0) {
+ Session::flash('error', trans('texts.expense_error_should_not_be_invoiced'));
+ return Redirect::to('expenses');
+ }
+
+ $account = Auth::user()->account;
+ $data[] = [
+ 'publicId' => $expense->public_id,
+ 'description' => $expense->public_notes,
+ 'qty' => 1,
+ 'cost' => $expense->amount,
+ ];
+ }
+
+ return Redirect::to("invoices/create/{$clientPublicId}")->with('expenses', $data);
+ break;
+
+ default:
+ $count = $this->expenseService->bulk($ids, $action);
+ }
+
+ if ($count > 0) {
+ $message = Utils::pluralize($action.'d_expense', $count);
+ Session::flash('message', $message);
+ }
+
+ return Redirect::to('expenses');
+ }
+
+ private static function getViewModel()
+ {
+ return [
+ 'data' => Input::old('data'),
+ 'account' => Auth::user()->account,
+ 'sizes' => Cache::get('sizes'),
+ 'paymentTerms' => Cache::get('paymentTerms'),
+ 'industries' => Cache::get('industries'),
+ 'currencies' => Cache::get('currencies'),
+ 'languages' => Cache::get('languages'),
+ 'countries' => Cache::get('countries'),
+ 'customLabel1' => Auth::user()->account->custom_vendor_label1,
+ 'customLabel2' => Auth::user()->account->custom_vendor_label2,
+ ];
+ }
+
+ public function show($publicId)
+ {
+ $expense = Expense::withTrashed()->scope($publicId)->firstOrFail();
+
+ if($expense) {
+ Utils::trackViewed($expense->getDisplayName(), 'expense');
+ }
+
+ $actionLinks = [
+ ['label' => trans('texts.new_expense'), 'url' => '/expenses/create/']
+ ];
+
+ $data = array(
+ 'actionLinks' => $actionLinks,
+ 'showBreadcrumbs' => false,
+ 'expense' => $expense,
+ 'credit' =>0,
+ 'vendor' => $expense->vendor,
+ 'title' => trans('texts.view_expense',['expense' => $expense->public_id]),
+ );
+
+ return View::make('expenses.show', $data);
+ }
+}
diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php
index 79d5f82fa341..540e38d97296 100644
--- a/app/Http/Controllers/ExportController.php
+++ b/app/Http/Controllers/ExportController.php
@@ -13,6 +13,8 @@ use App\Models\Credit;
use App\Models\Task;
use App\Models\Invoice;
use App\Models\Payment;
+use App\Models\Vendor;
+use App\Models\VendorContact;
class ExportController extends BaseController
{
@@ -155,6 +157,25 @@ class ExportController extends BaseController
->get();
}
+
+ if ($request->input(ENTITY_VENDOR)) {
+ $data['clients'] = Vendor::scope()
+ ->with('user', 'vendorcontacts', 'country')
+ ->withTrashed()
+ ->where('is_deleted', '=', false)
+ ->get();
+
+ $data['vendor_contacts'] = VendorContact::scope()
+ ->with('user', 'vendor.contacts')
+ ->withTrashed()
+ ->get();
+ /*
+ $data['expenses'] = Credit::scope()
+ ->with('user', 'client.contacts')
+ ->get();
+ */
+ }
+
return $data;
}
}
\ No newline at end of file
diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php
index 1d165d0de9cb..44f33b5cc6a4 100644
--- a/app/Http/Controllers/InvoiceController.php
+++ b/app/Http/Controllers/InvoiceController.php
@@ -319,6 +319,7 @@ class InvoiceController extends BaseController
'recurringDueDateHelp' => $recurringDueDateHelp,
'invoiceLabels' => Auth::user()->account->getInvoiceLabels(),
'tasks' => Session::get('tasks') ? json_encode(Session::get('tasks')) : null,
+ 'expenses' => Session::get('expenses') ? json_encode(Session::get('expenses')) : null,
];
}
diff --git a/app/Http/Controllers/PaymentTermController.php b/app/Http/Controllers/PaymentTermController.php
new file mode 100644
index 000000000000..623ca1bf42da
--- /dev/null
+++ b/app/Http/Controllers/PaymentTermController.php
@@ -0,0 +1,103 @@
+paymentTermService = $paymentTermService;
+ }
+
+ public function index()
+ {
+ return Redirect::to('settings/' . ACCOUNT_PAYMENT_TERMS);
+ }
+
+ public function getDatatable()
+ {
+ return $this->paymentTermService->getDatatable();
+ }
+
+ public function edit($publicId)
+ {
+ $data = [
+ 'paymentTerm' => PaymentTerm::scope($publicId)->firstOrFail(),
+ 'method' => 'PUT',
+ 'url' => 'payment_terms/'.$publicId,
+ 'title' => trans('texts.edit_payment_term'),
+ ];
+
+ return View::make('accounts.payment_term', $data);
+ }
+
+ public function create()
+ {
+ $data = [
+ 'paymentTerm' => null,
+ 'method' => 'POST',
+ 'url' => 'payment_terms',
+ 'title' => trans('texts.create_payment_term'),
+ ];
+
+ return View::make('accounts.payment_term', $data);
+ }
+
+ public function store()
+ {
+ return $this->save();
+ }
+
+ public function update($publicId)
+ {
+ return $this->save($publicId);
+ }
+
+ private function save($publicId = false)
+ {
+ if ($publicId) {
+ $paymentTerm = PaymentTerm::scope($publicId)->firstOrFail();
+ } else {
+ $paymentTerm = PaymentTerm::createNew();
+ }
+
+ $paymentTerm->name = trim(Input::get('name'));
+ $paymentTerm->num_days = Utils::parseInt(Input::get('num_days'));
+ $paymentTerm->save();
+
+ $message = $publicId ? trans('texts.updated_payment_term') : trans('texts.created_payment_term');
+ Session::flash('message', $message);
+
+ return Redirect::to('settings/' . ACCOUNT_PAYMENT_TERMS);
+ }
+
+ public function bulk()
+ {
+ $action = Input::get('bulk_action');
+ $ids = Input::get('bulk_public_id');
+ $count = $this->paymentTermService->bulk($ids, $action);
+
+ Session::flash('message', trans('texts.archived_payment_term'));
+
+ return Redirect::to('settings/' . ACCOUNT_PAYMENT_TERMS);
+ }
+
+}
diff --git a/app/Http/Controllers/PublicVendorController.php b/app/Http/Controllers/PublicVendorController.php
new file mode 100644
index 000000000000..32fb7eeb874e
--- /dev/null
+++ b/app/Http/Controllers/PublicVendorController.php
@@ -0,0 +1,198 @@
+activityRepo = $activityRepo;
+ $this->vendor = $activityRepo->vendor;
+ }
+
+ public function dashboard()
+ {
+ if (!$invitation = $this->getInvitation()) {
+ return $this->returnError();
+ }
+
+ $account = $invitation->account;
+ $color = $account->primary_color ? $account->primary_color : '#0b4d78';
+
+ $data = [
+ 'color' => $color,
+ 'account' => $account,
+ 'client' => $this->vendor,
+ 'hideLogo' => $account->isWhiteLabel(),
+ 'clientViewCSS' => $account->clientViewCSS(),
+ ];
+
+ return response()->view('invited.dashboard', $data);
+ }
+
+ public function activityDatatable()
+ {
+ if (!$invitation = $this->getInvitation()) {
+ return false;
+ }
+
+
+ $query = $this->activityRepo->findByClientId($invoice->client_id);
+ $query->where('vendor_activities.adjustment', '!=', 0);
+
+ return Datatable::query($query)
+ ->addColumn('vendor_activities.id', function ($model) { return Utils::timestampToDateTimeString(strtotime($model->created_at)); })
+ ->addColumn('activity_type_id', function ($model) {
+ $data = [
+ 'client' => Utils::getClientDisplayName($model),
+ 'user' => $model->is_system ? ('' . trans('texts.system') . '') : ($model->user_first_name . ' ' . $model->user_last_name),
+ 'invoice' => trans('texts.invoice') . ' ' . $model->invoice,
+ 'contact' => Utils::getClientDisplayName($model),
+ 'payment' => trans('texts.payment') . ($model->payment ? ' ' . $model->payment : ''),
+ ];
+
+ return trans("texts.activity_{$model->activity_type_id}", $data);
+ })
+ ->addColumn('balance', function ($model) { return Utils::formatMoney($model->balance, $model->currency_id, $model->country_id); })
+ ->addColumn('adjustment', function ($model) { return $model->adjustment != 0 ? Utils::wrapAdjustment($model->adjustment, $model->currency_id, $model->country_id) : ''; })
+ ->make();
+ }
+
+ public function invoiceIndex()
+ {
+ if (!$invitation = $this->getInvitation()) {
+ return $this->returnError();
+ }
+ $account = $invitation->account;
+ $color = $account->primary_color ? $account->primary_color : '#0b4d78';
+
+ $data = [
+ 'color' => $color,
+ 'hideLogo' => $account->isWhiteLabel(),
+ 'clientViewCSS' => $account->clientViewCSS(),
+ 'title' => trans('texts.invoices'),
+ 'entityType' => ENTITY_INVOICE,
+ 'columns' => Utils::trans(['invoice_number', 'invoice_date', 'invoice_total', 'balance_due', 'due_date']),
+ ];
+
+ return response()->view('public_list', $data);
+ }
+
+ public function invoiceDatatable()
+ {
+ if (!$invitation = $this->getInvitation()) {
+ return '';
+ }
+
+ return $this->invoiceRepo->getClientDatatable($invitation->contact_id, ENTITY_INVOICE, Input::get('sSearch'));
+ }
+
+
+ public function paymentIndex()
+ {
+ if (!$invitation = $this->getInvitation()) {
+ return $this->returnError();
+ }
+ $account = $invitation->account;
+ $color = $account->primary_color ? $account->primary_color : '#0b4d78';
+
+ $data = [
+ 'color' => $color,
+ 'hideLogo' => $account->isWhiteLabel(),
+ 'clientViewCSS' => $account->clientViewCSS(),
+ 'entityType' => ENTITY_PAYMENT,
+ 'title' => trans('texts.payments'),
+ 'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'payment_amount', 'payment_date'])
+ ];
+
+ return response()->view('public_list', $data);
+ }
+
+ public function paymentDatatable()
+ {
+ if (!$invitation = $this->getInvitation()) {
+ return false;
+ }
+ $payments = $this->paymentRepo->findForContact($invitation->contact->id, Input::get('sSearch'));
+
+ return Datatable::query($payments)
+ ->addColumn('invoice_number', function ($model) { return $model->invitation_key ? link_to('/view/'.$model->invitation_key, $model->invoice_number) : $model->invoice_number; })
+ ->addColumn('transaction_reference', function ($model) { return $model->transaction_reference ? $model->transaction_reference : 'Manual entry'; })
+ ->addColumn('payment_type', function ($model) { return $model->payment_type ? $model->payment_type : ($model->account_gateway_id ? 'Online payment' : ''); })
+ ->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); })
+ ->addColumn('payment_date', function ($model) { return Utils::dateToString($model->payment_date); })
+ ->make();
+ }
+
+ public function quoteIndex()
+ {
+ if (!$invitation = $this->getInvitation()) {
+ return $this->returnError();
+ }
+ $account = $invitation->account;
+ $color = $account->primary_color ? $account->primary_color : '#0b4d78';
+
+ $data = [
+ 'color' => $color,
+ 'hideLogo' => $account->isWhiteLabel(),
+ 'clientViewCSS' => $account->clientViewCSS(),
+ 'title' => trans('texts.quotes'),
+ 'entityType' => ENTITY_QUOTE,
+ 'columns' => Utils::trans(['quote_number', 'quote_date', 'quote_total', 'due_date']),
+ ];
+
+ return response()->view('public_list', $data);
+ }
+
+
+ public function quoteDatatable()
+ {
+ if (!$invitation = $this->getInvitation()) {
+ return false;
+ }
+
+ return $this->invoiceRepo->getClientDatatable($invitation->contact_id, ENTITY_QUOTE, Input::get('sSearch'));
+ }
+
+ private function returnError()
+ {
+ return response()->view('error', [
+ 'error' => trans('texts.invoice_not_found'),
+ 'hideHeader' => true,
+ 'clientViewCSS' => $account->clientViewCSS(),
+ ]);
+ }
+
+ private function getInvitation()
+ {
+ $invitationKey = session('invitation_key');
+
+ if (!$invitationKey) {
+ return false;
+ }
+
+ $invitation = VendorInvitation::where('invitation_key', '=', $invitationKey)->first();
+
+ if (!$invitation || $invitation->is_deleted) {
+ return false;
+ }
+
+ $invoice = $invitation->invoice;
+
+ if (!$invoice || $invoice->is_deleted) {
+ return false;
+ }
+
+ return $invitation;
+ }
+
+}
\ No newline at end of file
diff --git a/app/Http/Controllers/VendorActivityController.php b/app/Http/Controllers/VendorActivityController.php
new file mode 100644
index 000000000000..4c3ddd7d656d
--- /dev/null
+++ b/app/Http/Controllers/VendorActivityController.php
@@ -0,0 +1,27 @@
+activityService = $activityService;
+ }
+
+ public function getDatatable($vendorPublicId)
+ {
+ return $this->activityService->getDatatable($vendorPublicId);
+ }
+}
diff --git a/app/Http/Controllers/VendorApiController.php b/app/Http/Controllers/VendorApiController.php
new file mode 100644
index 000000000000..80236226dda0
--- /dev/null
+++ b/app/Http/Controllers/VendorApiController.php
@@ -0,0 +1,94 @@
+vendorRepo = $vendorRepo;
+ }
+
+ public function ping()
+ {
+ $headers = Utils::getApiHeaders();
+
+ return Response::make('', 200, $headers);
+ }
+
+ /**
+ * @SWG\Get(
+ * path="/vendors",
+ * summary="List of vendors",
+ * tags={"vendor"},
+ * @SWG\Response(
+ * response=200,
+ * description="A list with vendors",
+ * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Vendor"))
+ * ),
+ * @SWG\Response(
+ * response="default",
+ * description="an ""unexpected"" error"
+ * )
+ * )
+ */
+ public function index()
+ {
+ $vendors = Vendor::scope()
+ ->with($this->getIncluded())
+ ->orderBy('created_at', 'desc')
+ ->paginate();
+
+ $transformer = new VendorTransformer(Auth::user()->account, Input::get('serializer'));
+ $paginator = Vendor::scope()->paginate();
+ $data = $this->createCollection($vendors, $transformer, ENTITY_VENDOR, $paginator);
+
+ return $this->response($data);
+ }
+
+ /**
+ * @SWG\Post(
+ * path="/vendors",
+ * tags={"vendor"},
+ * summary="Create a vendor",
+ * @SWG\Parameter(
+ * in="body",
+ * name="body",
+ * @SWG\Schema(ref="#/definitions/Vendor")
+ * ),
+ * @SWG\Response(
+ * response=200,
+ * description="New vendor",
+ * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Vendor"))
+ * ),
+ * @SWG\Response(
+ * response="default",
+ * description="an ""unexpected"" error"
+ * )
+ * )
+ */
+ public function store(CreateVendorRequest $request)
+ {
+ $vendor = $this->vendorRepo->save($request->input());
+
+ $vendor = Vendor::scope($vendor->public_id)
+ ->with('country', 'vendorcontacts', 'industry', 'size', 'currency')
+ ->first();
+
+ $transformer = new VendorTransformer(Auth::user()->account, Input::get('serializer'));
+ $data = $this->createItem($vendor, $transformer, ENTITY_VENDOR);
+ return $this->response($data);
+ }
+}
diff --git a/app/Http/Controllers/VendorController.php b/app/Http/Controllers/VendorController.php
new file mode 100644
index 000000000000..c7eced17f500
--- /dev/null
+++ b/app/Http/Controllers/VendorController.php
@@ -0,0 +1,212 @@
+vendorRepo = $vendorRepo;
+ $this->vendorService = $vendorService;
+
+
+ }
+
+ /**
+ * Display a listing of the resource.
+ *
+ * @return Response
+ */
+ public function index()
+ {
+ return View::make('list', array(
+ 'entityType' => 'vendor',
+ 'title' => trans('texts.vendors'),
+ 'sortCol' => '4',
+ 'columns' => Utils::trans([
+ 'checkbox',
+ 'vendor',
+ 'contact',
+ 'email',
+ 'date_created',
+ 'balance',
+ ''
+ ]),
+ ));
+ }
+
+ public function getDatatable()
+ {
+ return $this->vendorService->getDatatable(Input::get('sSearch'));
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @return Response
+ */
+ public function store(CreateVendorRequest $request)
+ {
+ $vendor = $this->vendorService->save($request->input());
+
+ Session::flash('message', trans('texts.created_vendor'));
+
+ return redirect()->to($vendor->getRoute());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param int $id
+ * @return Response
+ */
+ public function show($publicId)
+ {
+ $vendor = Vendor::withTrashed()->scope($publicId)->with('vendorcontacts', 'size', 'industry')->firstOrFail();
+ Utils::trackViewed($vendor->getDisplayName(), 'vendor');
+
+ $actionLinks = [
+ ['label' => trans('texts.new_vendor'), 'url' => '/vendors/create/' . $vendor->public_id]
+ ];
+
+ $data = array(
+ 'actionLinks' => $actionLinks,
+ 'showBreadcrumbs' => false,
+ 'vendor' => $vendor,
+ 'totalexpense' => $vendor->getTotalExpense(),
+ 'title' => trans('texts.view_vendor'),
+ 'hasRecurringInvoices' => false,
+ 'hasQuotes' => false,
+ 'hasTasks' => false,
+ );
+
+ return View::make('vendors.show', $data);
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return Response
+ */
+ public function create()
+ {
+ if (Vendor::scope()->count() > Auth::user()->getMaxNumVendors()) {
+ return View::make('error', ['hideHeader' => true, 'error' => "Sorry, you've exceeded the limit of ".Auth::user()->getMaxNumVendors()." vendors"]);
+ }
+
+ $data = [
+ 'vendor' => null,
+ 'method' => 'POST',
+ 'url' => 'vendors',
+ 'title' => trans('texts.new_vendor'),
+ ];
+
+ $data = array_merge($data, self::getViewModel());
+
+ return View::make('vendors.edit', $data);
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param int $id
+ * @return Response
+ */
+ public function edit($publicId)
+ {
+ $vendor = Vendor::scope($publicId)->with('vendorcontacts')->firstOrFail();
+ $data = [
+ 'vendor' => $vendor,
+ 'method' => 'PUT',
+ 'url' => 'vendors/'.$publicId,
+ 'title' => trans('texts.edit_vendor'),
+ ];
+
+ $data = array_merge($data, self::getViewModel());
+
+ if (Auth::user()->account->isNinjaAccount()) {
+ if ($account = Account::whereId($vendor->public_id)->first()) {
+ $data['proPlanPaid'] = $account['pro_plan_paid'];
+ }
+ }
+
+ return View::make('vendors.edit', $data);
+ }
+
+ private static function getViewModel()
+ {
+ return [
+ 'data' => Input::old('data'),
+ 'account' => Auth::user()->account,
+ 'sizes' => Cache::get('sizes'),
+ //'paymentTerms' => Cache::get('paymentTerms'),
+ 'paymentTerms' => PaymentTerm::get(),
+ 'industries' => Cache::get('industries'),
+ 'currencies' => Cache::get('currencies'),
+ 'languages' => Cache::get('languages'),
+ 'countries' => Cache::get('countries'),
+ 'customLabel1' => Auth::user()->account->custom_vendor_label1,
+ 'customLabel2' => Auth::user()->account->custom_vendor_label2,
+ ];
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param int $id
+ * @return Response
+ */
+ public function update(UpdateVendorRequest $request)
+ {
+ $vendor = $this->vendorService->save($request->input());
+
+ Session::flash('message', trans('texts.updated_vendor'));
+
+ return redirect()->to($vendor->getRoute());
+ }
+
+ public function bulk()
+ {
+ $action = Input::get('action');
+ $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
+ $count = $this->vendorService->bulk($ids, $action);
+
+ $message = Utils::pluralize($action.'d_vendor', $count);
+ Session::flash('message', $message);
+
+ if ($action == 'restore' && $count == 1) {
+ return Redirect::to('vendors/' . Utils::getFirst($ids));
+ } else {
+ return Redirect::to('vendors');
+ }
+ }
+}
diff --git a/app/Http/Requests/CreateExpenseRequest.php b/app/Http/Requests/CreateExpenseRequest.php
new file mode 100644
index 000000000000..a2c08cdfb8d7
--- /dev/null
+++ b/app/Http/Requests/CreateExpenseRequest.php
@@ -0,0 +1,30 @@
+ 'required|positive',
+ ];
+ }
+}
diff --git a/app/Http/Requests/CreatePaymentTermRequest.php b/app/Http/Requests/CreatePaymentTermRequest.php
new file mode 100644
index 000000000000..d8581793160e
--- /dev/null
+++ b/app/Http/Requests/CreatePaymentTermRequest.php
@@ -0,0 +1,30 @@
+ 'required',
+ 'name' => 'required',
+ ];
+ }
+}
diff --git a/app/Http/Requests/CreateVendorRequest.php b/app/Http/Requests/CreateVendorRequest.php
new file mode 100644
index 000000000000..7186077fc666
--- /dev/null
+++ b/app/Http/Requests/CreateVendorRequest.php
@@ -0,0 +1,44 @@
+ 'valid_contacts',
+ ];
+ }
+
+ public function validator($factory)
+ {
+ // support submiting the form with a single contact record
+ $input = $this->input();
+ if (isset($input['vendor_contact'])) {
+ $input['vendor_contacts'] = [$input['vendor_contact']];
+ unset($input['vendor_contact']);
+ $this->replace($input);
+ }
+
+ return $factory->make(
+ $this->input(), $this->container->call([$this, 'rules']), $this->messages()
+ );
+ }
+}
diff --git a/app/Http/Requests/UpdateExpenseRequest.php b/app/Http/Requests/UpdateExpenseRequest.php
new file mode 100644
index 000000000000..9170e9df02ab
--- /dev/null
+++ b/app/Http/Requests/UpdateExpenseRequest.php
@@ -0,0 +1,32 @@
+ 'required|positive',
+ 'public_notes' => 'required',
+ 'expense_date' => 'required',
+ ];
+ }
+}
diff --git a/app/Http/Requests/UpdatePaymentRequest.php b/app/Http/Requests/UpdatePaymentRequest.php
index 83b192280849..29ac70e85e74 100644
--- a/app/Http/Requests/UpdatePaymentRequest.php
+++ b/app/Http/Requests/UpdatePaymentRequest.php
@@ -2,7 +2,6 @@
use App\Http\Requests\Request;
use Illuminate\Validation\Factory;
-use App\Models\Invoice;
class UpdatePaymentRequest extends Request
{
@@ -24,5 +23,6 @@ class UpdatePaymentRequest extends Request
public function rules()
{
return [];
+
}
}
diff --git a/app/Http/Requests/UpdatePaymentTermRequest.php b/app/Http/Requests/UpdatePaymentTermRequest.php
new file mode 100644
index 000000000000..b3d4f536bc6e
--- /dev/null
+++ b/app/Http/Requests/UpdatePaymentTermRequest.php
@@ -0,0 +1,30 @@
+ 'required|positive',
+ ];
+
+ }
+}
diff --git a/app/Http/Requests/UpdateVendorRequest.php b/app/Http/Requests/UpdateVendorRequest.php
new file mode 100644
index 000000000000..568166735d8c
--- /dev/null
+++ b/app/Http/Requests/UpdateVendorRequest.php
@@ -0,0 +1,29 @@
+ 'valid_contacts',
+ ];
+ }
+}
diff --git a/app/Http/routes.php b/app/Http/routes.php
index d2b7d42a4bf5..3445ced1cbb1 100644
--- a/app/Http/routes.php
+++ b/app/Http/routes.php
@@ -119,6 +119,11 @@ Route::group(['middleware' => 'auth'], function() {
Route::get('settings/{section?}', 'AccountController@showSection');
Route::post('settings/{section?}', 'AccountController@doSection');
+ // Payment term
+ Route::get('api/payment_terms', array('as'=>'api.payment_terms', 'uses'=>'PaymentTermController@getDatatable'));
+ Route::resource('payment_terms', 'PaymentTermController');
+ Route::post('payment_terms/bulk', 'PaymentTermController@bulk');
+
Route::get('account/getSearchData', array('as' => 'getSearchData', 'uses' => 'AccountController@getSearchData'));
Route::post('user/setTheme', 'UserController@setTheme');
Route::post('remove_logo', 'AccountController@removeLogo');
@@ -181,6 +186,21 @@ Route::group(['middleware' => 'auth'], function() {
get('/resend_confirmation', 'AccountController@resendConfirmation');
post('/update_setup', 'AppController@updateSetup');
+
+
+ // vendor
+ Route::resource('vendors', 'VendorController');
+ Route::get('api/vendor', array('as'=>'api.vendors', 'uses'=>'VendorController@getDatatable'));
+ Route::get('api/vendoractivities/{vendor_id?}', array('as'=>'api.vendoractivities', 'uses'=>'VendorActivityController@getDatatable'));
+ Route::post('vendors/bulk', 'VendorController@bulk');
+
+ // Expense
+ Route::resource('expenses', 'ExpenseController');
+ Route::get('expenses/create/{vendor_id?}', 'ExpenseController@create');
+ Route::get('api/expense', array('as'=>'api.expenses', 'uses'=>'ExpenseApiController@getDatatable'));
+ Route::get('api/expenseVendor/{id}', array('as'=>'api.expense', 'uses'=>'ExpenseApiController@getDatatableVendor'));
+ Route::get('api/expenseactivities/{expense_id?}', array('as'=>'api.expenseactivities', 'uses'=>'ExpenseActivityController@getDatatable'));
+ Route::post('expenses/bulk', 'ExpenseController@bulk');
});
// Route groups for API
@@ -202,6 +222,12 @@ Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function()
Route::post('hooks', 'IntegrationController@subscribe');
Route::post('email_invoice', 'InvoiceApiController@emailInvoice');
Route::get('user_accounts','AccountApiController@getUserAccounts');
+
+ // Vendor
+ Route::resource('vendors', 'VendorApiController');
+
+ //Expense
+ Route::resource('expenses', 'ExpenseApiController');
});
// Redirects for legacy links
@@ -242,6 +268,7 @@ if (!defined('CONTACT_EMAIL')) {
define('ENV_STAGING', 'staging');
define('RECENTLY_VIEWED', 'RECENTLY_VIEWED');
+
define('ENTITY_CLIENT', 'client');
define('ENTITY_CONTACT', 'contact');
define('ENTITY_INVOICE', 'invoice');
@@ -258,9 +285,15 @@ if (!defined('CONTACT_EMAIL')) {
define('ENTITY_TAX_RATE', 'tax_rate');
define('ENTITY_PRODUCT', 'product');
define('ENTITY_ACTIVITY', 'activity');
+ define('ENTITY_VENDOR','vendor');
+ define('ENTITY_VENDOR_ACTIVITY','vendor_activity');
+ define('ENTITY_EXPENSE', 'expense');
+ define('ENTITY_PAYMENT_TERM','payment_term');
+ define('ENTITY_EXPENSE_ACTIVITY','expense_activity');
define('PERSON_CONTACT', 'contact');
define('PERSON_USER', 'user');
+ define('PERSON_VENDOR_CONTACT','vendorcontact');
define('BASIC_SETTINGS', 'basic_settings');
define('ADVANCED_SETTINGS', 'advanced_settings');
@@ -287,6 +320,7 @@ if (!defined('CONTACT_EMAIL')) {
define('ACCOUNT_API_TOKENS', 'api_tokens');
define('ACCOUNT_CUSTOMIZE_DESIGN', 'customize_design');
define('ACCOUNT_SYSTEM_SETTINGS', 'system_settings');
+ define('ACCOUNT_PAYMENT_TERMS','payment_terms');
define('ACTION_RESTORE', 'restore');
define('ACTION_ARCHIVE', 'archive');
@@ -327,6 +361,18 @@ if (!defined('CONTACT_EMAIL')) {
define('ACTIVITY_TYPE_RESTORE_CREDIT', 28);
define('ACTIVITY_TYPE_APPROVE_QUOTE', 29);
+ // Vendors
+ define('ACTIVITY_TYPE_CREATE_VENDOR', 30);
+ define('ACTIVITY_TYPE_ARCHIVE_VENDOR', 31);
+ define('ACTIVITY_TYPE_DELETE_VENDOR', 32);
+ define('ACTIVITY_TYPE_RESTORE_VENDOR', 33);
+
+ // expenses
+ define('ACTIVITY_TYPE_CREATE_EXPENSE', 34);
+ define('ACTIVITY_TYPE_ARCHIVE_EXPENSE', 35);
+ define('ACTIVITY_TYPE_DELETE_EXPENSE', 36);
+ define('ACTIVITY_TYPE_RESTORE_EXPENSE', 37);
+
define('DEFAULT_INVOICE_NUMBER', '0001');
define('RECENTLY_VIEWED_LIMIT', 8);
define('LOGGED_ERROR_LIMIT', 100);
@@ -358,6 +404,10 @@ if (!defined('CONTACT_EMAIL')) {
define('LEGACY_CUTOFF', 57800);
define('ERROR_DELAY', 3);
+ define('MAX_NUM_VENDORS', 100);
+ define('MAX_NUM_VENDORS_PRO', 20000);
+ define('MAX_NUM_VENDORS_LEGACY', 500);
+
define('INVOICE_STATUS_DRAFT', 1);
define('INVOICE_STATUS_SENT', 2);
define('INVOICE_STATUS_VIEWED', 3);
@@ -430,6 +480,7 @@ if (!defined('CONTACT_EMAIL')) {
define('EVENT_CREATE_INVOICE', 2);
define('EVENT_CREATE_QUOTE', 3);
define('EVENT_CREATE_PAYMENT', 4);
+ define('EVENT_CREATE_VENDOR',5);
define('REQUESTED_PRO_PLAN', 'REQUESTED_PRO_PLAN');
define('DEMO_ACCOUNT_ID', 'DEMO_ACCOUNT_ID');
diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php
index d86ff218ca70..cf3bbc2e0e41 100644
--- a/app/Libraries/Utils.php
+++ b/app/Libraries/Utils.php
@@ -142,7 +142,7 @@ class Utils
$history = Session::get(RECENTLY_VIEWED);
$last = $history[0];
$penultimate = count($history) > 1 ? $history[1] : $last;
-
+
return Request::url() == $last->url ? $penultimate->url : $last->url;
}
@@ -254,7 +254,7 @@ class Utils
$data = Cache::get($type)->filter(function($item) use ($id) {
return $item->id == $id;
});
-
+
return $data->first();
}
@@ -349,7 +349,7 @@ class Utils
if (!$date) {
return false;
}
-
+
$dateTime = new DateTime($date);
$timestamp = $dateTime->getTimestamp();
$format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT);
@@ -473,7 +473,7 @@ class Utils
}
array_unshift($data, $object);
-
+
if (isset($counts[Auth::user()->account_id]) && $counts[Auth::user()->account_id] > RECENTLY_VIEWED_LIMIT) {
array_pop($data);
}
@@ -582,6 +582,17 @@ class Utils
}
}
+ public static function getVendorDisplayName($model)
+ {
+ if(is_null($model))
+ return '';
+
+ if($model->vendor_name)
+ return $model->vendor_name;
+
+ return 'No vendor name';
+ }
+
public static function getPersonDisplayName($firstName, $lastName, $email)
{
if ($firstName || $lastName) {
@@ -613,7 +624,9 @@ class Utils
return EVENT_CREATE_QUOTE;
} elseif ($eventName == 'create_payment') {
return EVENT_CREATE_PAYMENT;
- } else {
+ } elseif ($eventName == 'create_vendor') {
+ return EVENT_CREATE_VENDOR;
+ }else {
return false;
}
}
@@ -671,7 +684,7 @@ class Utils
if ($publicId) {
$data['id'] = $publicId;
}
-
+
return $data;
}
@@ -717,7 +730,7 @@ class Utils
$str .= 'ENTITY_DELETED ';
}
}
-
+
if ($model->deleted_at && $model->deleted_at != '0000-00-00') {
$str .= 'ENTITY_ARCHIVED ';
}
@@ -737,7 +750,7 @@ class Utils
fwrite($output, "\n");
}
-
+
public static function getFirst($values)
{
if (is_array($values)) {
@@ -902,7 +915,7 @@ class Utils
if (!preg_match("~^(?:f|ht)tps?://~i", $url)) {
$url = "http://" . $url;
}
-
+
return $url;
}
}
diff --git a/app/Listeners/ExpenseActivityListener.php b/app/Listeners/ExpenseActivityListener.php
new file mode 100644
index 000000000000..5e3cf1d9e705
--- /dev/null
+++ b/app/Listeners/ExpenseActivityListener.php
@@ -0,0 +1,57 @@
+activityRepo = $activityRepo;
+ }
+
+ // Expenses
+ public function createdExpense(ExpenseWasCreated $event)
+ {
+ $this->activityRepo->create(
+ $event->expense,
+ ACTIVITY_TYPE_CREATE_EXPENSE
+ );
+ }
+
+ public function deletedExpense(ExpenseWasDeleted $event)
+ {
+ $this->activityRepo->create(
+ $event->expense,
+ ACTIVITY_TYPE_DELETE_EXPENSE
+ );
+ }
+
+ public function archivedExpense(ExpenseWasArchived $event)
+ {
+ /*
+ if ($event->client->is_deleted) {
+ return;
+ }
+ */
+
+ $this->activityRepo->create(
+ $event->expense,
+ ACTIVITY_TYPE_ARCHIVE_EXPENSE
+ );
+ }
+
+ public function restoredExpense(ExpenseWasRestored $event)
+ {
+ $this->activityRepo->create(
+ $event->expense,
+ ACTIVITY_TYPE_RESTORE_EXPENSE
+ );
+ }
+}
diff --git a/app/Listeners/ExpenseListener.php b/app/Listeners/ExpenseListener.php
new file mode 100644
index 000000000000..c8b0e7db5966
--- /dev/null
+++ b/app/Listeners/ExpenseListener.php
@@ -0,0 +1,25 @@
+expenseRepo = $expenseRepo;
+ }
+
+ public function deletedInvoice(InvoiceWasDeleted $event)
+ {
+ // Release any tasks associated with the deleted invoice
+ Expense::where('invoice_id', '=', $event->invoice->id)
+ ->update(['invoice_id' => null]);
+ }
+}
diff --git a/app/Listeners/SubscriptionListener.php b/app/Listeners/SubscriptionListener.php
index 7ef7a1116e74..fab5a2c57493 100644
--- a/app/Listeners/SubscriptionListener.php
+++ b/app/Listeners/SubscriptionListener.php
@@ -9,6 +9,9 @@ use App\Events\InvoiceWasCreated;
use App\Events\CreditWasCreated;
use App\Events\PaymentWasCreated;
+use App\Events\VendorWasCreated;
+use App\Events\ExpenseWasCreated;
+
class SubscriptionListener
{
public function createdClient(ClientWasCreated $event)
@@ -44,4 +47,15 @@ class SubscriptionListener
Utils::notifyZapier($subscription, $entity);
}
}
+
+ public function createdVendor(VendorWasCreated $event)
+ {
+ $this->checkSubscriptions(ACTIVITY_TYPE_CREATE_VENDOR, $event->vendor);
+ }
+
+ public function createdExpense(ExpenseWasCreated $event)
+ {
+ $this->checkSubscriptions(ACTIVITY_TYPE_CREATE_EXPENSE, $event->expense);
+ }
+
}
diff --git a/app/Listeners/VendorActivityListener.php b/app/Listeners/VendorActivityListener.php
new file mode 100644
index 000000000000..181b50feb8bb
--- /dev/null
+++ b/app/Listeners/VendorActivityListener.php
@@ -0,0 +1,55 @@
+activityRepo = $activityRepo;
+ }
+
+ // Vendors
+ public function createdVendor(VendorWasCreated $event)
+ {
+ $this->activityRepo->create(
+ $event->vendor,
+ ACTIVITY_TYPE_CREATE_VENDOR
+ );
+ }
+
+ public function deletedVendor(VendorWasDeleted $event)
+ {
+ $this->activityRepo->create(
+ $event->vendor,
+ ACTIVITY_TYPE_DELETE_VENDOR
+ );
+ }
+
+ public function archivedVendor(VendorWasArchived $event)
+ {
+ if ($event->vendor->is_deleted) {
+ return;
+ }
+
+ $this->activityRepo->create(
+ $event->vendor,
+ ACTIVITY_TYPE_ARCHIVE_VENDOR
+ );
+ }
+
+ public function restoredVendor(VendorWasRestored $event)
+ {
+ $this->activityRepo->create(
+ $event->vendor,
+ ACTIVITY_TYPE_RESTORE_VENDOR
+ );
+ }
+}
diff --git a/app/Models/Account.php b/app/Models/Account.php
index bf57ab820fbc..d774114a3dfa 100644
--- a/app/Models/Account.php
+++ b/app/Models/Account.php
@@ -30,6 +30,7 @@ class Account extends Eloquent
ACCOUNT_PRODUCTS,
ACCOUNT_NOTIFICATIONS,
ACCOUNT_IMPORT_EXPORT,
+ ACCOUNT_PAYMENT_TERMS,
];
public static $advancedSettings = [
diff --git a/app/Models/Expense.php b/app/Models/Expense.php
new file mode 100644
index 000000000000..d97bc5099121
--- /dev/null
+++ b/app/Models/Expense.php
@@ -0,0 +1,101 @@
+belongsTo('App\Models\Account');
+ }
+
+ public function user()
+ {
+ return $this->belongsTo('App\Models\User');
+ }
+
+ public function vendor()
+ {
+ return $this->belongsTo('App\Models\Vendor')->withTrashed();
+ }
+
+ public function getName()
+ {
+ if($this->expense_number)
+ return $this->expense_number;
+
+ return $this->public_id;
+ }
+
+ public function getDisplayName()
+ {
+ return $this->getName();
+ }
+
+ public function getRoute()
+ {
+ return "/expenses/{$this->public_id}";
+ }
+
+ public function getEntityType()
+ {
+ return ENTITY_EXPENSE;
+ }
+
+ public function apply($amount)
+ {
+ if ($amount > $this->balance) {
+ $applied = $this->balance;
+ $this->balance = 0;
+ } else {
+ $applied = $amount;
+ $this->balance = $this->balance - $amount;
+ }
+
+ $this->save();
+
+ return $applied;
+ }
+}
+
+Expense::creating(function ($expense) {
+ $expense->setNullValues();
+});
+
+Expense::created(function ($expense) {
+ event(new ExpenseWasCreated($expense));
+});
+
+Expense::updating(function ($expense) {
+ $expense->setNullValues();
+});
+
+Expense::updated(function ($expense) {
+ event(new ExpenseWasUpdated($expense));
+});
+
+Expense::deleting(function ($expense) {
+ $expense->setNullValues();
+});
+
+Expense::deleted(function ($expense) {
+ event(new ExpenseWasDeleted($expense));
+});
diff --git a/app/Models/ExpenseActivity.php b/app/Models/ExpenseActivity.php
new file mode 100644
index 000000000000..bd72098518c1
--- /dev/null
+++ b/app/Models/ExpenseActivity.php
@@ -0,0 +1,63 @@
+whereAccountId(Auth::user()->account_id);
+ }
+
+ public function account()
+ {
+ return $this->belongsTo('App\Models\Account');
+ }
+
+ public function user()
+ {
+ return $this->belongsTo('App\Models\User')->withTrashed();
+ }
+
+ public function vendor()
+ {
+ return $this->belongsTo('App\Models\Vendor')->withTrashed();
+ }
+
+ public function expense()
+ {
+ return $this->belongsTo('App\Models\Expense')->withTrashed();
+ }
+
+ public function getMessage()
+ {
+ $activityTypeId = $this->activity_type_id;
+ $account = $this->account;
+ $vendor = $this->vendor;
+ $user = $this->user;
+ $contactId = $this->contact_id;
+ $isSystem = $this->is_system;
+ $expense = $this->expense;
+
+ if($expense)
+ {
+ $route = link_to($expense->getRoute(), $expense->getDisplayName());
+ } else {
+ $route ='no expense id';
+ }
+
+ $data = [
+ 'expense' => $route,
+ 'user' => $isSystem ? '' . trans('texts.system') . '' : $user->getDisplayName(),
+ ];
+
+ return trans("texts.activity_{$activityTypeId}", $data);
+ }
+}
diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php
index 3d01216629cf..d486b40c0c22 100644
--- a/app/Models/Invoice.php
+++ b/app/Models/Invoice.php
@@ -28,6 +28,7 @@ class Invoice extends EntityModel implements BalanceAffecting
'is_recurring' => 'boolean',
'has_tasks' => 'boolean',
'auto_bill' => 'boolean',
+ 'has_expenses' => 'boolean',
];
// used for custom invoice numbers
@@ -82,7 +83,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function getDisplayName()
{
- return $this->is_recurring ? trans('texts.recurring') : $this->invoice_number;
+ return $this->is_recurring ? trans('texts.recurring') : $this->invoice_number;
}
public function affectsBalance()
@@ -136,7 +137,7 @@ class Invoice extends EntityModel implements BalanceAffecting
return ($this->amount - $this->balance);
}
-
+
public function trashed()
{
if ($this->client && $this->client->trashed()) {
@@ -212,7 +213,7 @@ class Invoice extends EntityModel implements BalanceAffecting
$invitation->markSent($messageId);
- // if the user marks it as sent rather than acually sending it
+ // if the user marks it as sent rather than acually sending it
// then we won't track it in the activity log
if (!$notify) {
return;
@@ -375,6 +376,7 @@ class Invoice extends EntityModel implements BalanceAffecting
'has_tasks',
'custom_text_value1',
'custom_text_value2',
+ 'has_expenses',
]);
$this->client->setVisible([
@@ -468,7 +470,7 @@ class Invoice extends EntityModel implements BalanceAffecting
// Fix for months with less than 31 days
$transformerConfig = new \Recurr\Transformer\ArrayTransformerConfig();
$transformerConfig->enableLastDayOfMonthFix();
-
+
$transformer = new \Recurr\Transformer\ArrayTransformer();
$transformer->setConfig($transformerConfig);
$dates = $transformer->transform($rule);
@@ -494,7 +496,7 @@ class Invoice extends EntityModel implements BalanceAffecting
if (count($schedule) < 2) {
return null;
}
-
+
return $schedule[1]->getStart();
}
@@ -651,7 +653,7 @@ class Invoice extends EntityModel implements BalanceAffecting
if (!$nextSendDate = $this->getNextSendDate()) {
return false;
}
-
+
return $this->account->getDateTime() >= $nextSendDate;
}
*/
diff --git a/app/Models/PaymentTerm.php b/app/Models/PaymentTerm.php
index de8cced5db72..dbb788aef1c2 100644
--- a/app/Models/PaymentTerm.php
+++ b/app/Models/PaymentTerm.php
@@ -1,8 +1,17 @@
isPro()) {
+ return MAX_NUM_VENDORS_PRO;
+ }
+
+ if ($this->id < LEGACY_CUTOFF) {
+ return MAX_NUM_VENDORS_LEGACY;
+ }
+
+ return MAX_NUM_VENDORS;
+ }
+
+
public function getRememberToken()
{
return $this->remember_token;
diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php
new file mode 100644
index 000000000000..481967232ff6
--- /dev/null
+++ b/app/Models/Vendor.php
@@ -0,0 +1,248 @@
+ 'first_name',
+ 'last' => 'last_name',
+ 'email' => 'email',
+ 'mobile|phone' => 'phone',
+ 'name|organization' => 'name',
+ 'street2|address2' => 'address2',
+ 'street|address|address1' => 'address1',
+ 'city' => 'city',
+ 'state|province' => 'state',
+ 'zip|postal|code' => 'postal_code',
+ 'country' => 'country',
+ 'note' => 'notes',
+ ];
+ }
+
+ public function account()
+ {
+ return $this->belongsTo('App\Models\Account');
+ }
+
+ public function user()
+ {
+ return $this->belongsTo('App\Models\User');
+ }
+
+ public function payments()
+ {
+ return $this->hasMany('App\Models\Payment');
+ }
+
+ public function vendorContacts()
+ {
+ return $this->hasMany('App\Models\VendorContact');
+ }
+
+ public function country()
+ {
+ return $this->belongsTo('App\Models\Country');
+ }
+
+ public function currency()
+ {
+ return $this->belongsTo('App\Models\Currency');
+ }
+
+ public function language()
+ {
+ return $this->belongsTo('App\Models\Language');
+ }
+
+ public function size()
+ {
+ return $this->belongsTo('App\Models\Size');
+ }
+
+ public function industry()
+ {
+ return $this->belongsTo('App\Models\Industry');
+ }
+
+ public function addVendorContact($data, $isPrimary = false)
+ {
+ $publicId = isset($data['public_id']) ? $data['public_id'] : false;
+
+ if ($publicId && $publicId != '-1') {
+ $contact = VendorContact::scope($publicId)->firstOrFail();
+ } else {
+ $contact = VendorContact::createNew();
+ }
+
+ $contact->fill($data);
+ $contact->is_primary = $isPrimary;
+
+ return $this->vendorContacts()->save($contact);
+ }
+
+ public function getRoute()
+ {
+ return "/vendors/{$this->public_id}";
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getDisplayName()
+ {
+ return $this->getName();
+ }
+
+ public function getCityState()
+ {
+ $swap = $this->country && $this->country->swap_postal_code;
+ return Utils::cityStateZip($this->city, $this->state, $this->postal_code, $swap);
+ }
+
+ public function getEntityType()
+ {
+ return 'vendor';
+ }
+
+ public function hasAddress()
+ {
+ $fields = [
+ 'address1',
+ 'address2',
+ 'city',
+ 'state',
+ 'postal_code',
+ 'country_id',
+ ];
+
+ foreach ($fields as $field) {
+ if ($this->$field) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function getDateCreated()
+ {
+ if ($this->created_at == '0000-00-00 00:00:00') {
+ return '---';
+ } else {
+ return $this->created_at->format('m/d/y h:i a');
+ }
+ }
+
+ public function getCurrencyId()
+ {
+ if ($this->currency_id) {
+ return $this->currency_id;
+ }
+
+ if (!$this->account) {
+ $this->load('account');
+ }
+
+ return $this->account->currency_id ?: DEFAULT_CURRENCY;
+ }
+
+ public function getTotalExpense()
+ {
+ return DB::table('expenses')
+ ->where('vendor_id', '=', $this->id)
+ ->whereNull('deleted_at')
+ ->sum('amount');
+ }
+}
+
+Vendor::creating(function ($vendor) {
+ $vendor->setNullValues();
+});
+
+Vendor::created(function ($vendor) {
+ event(new VendorWasCreated($vendor));
+});
+
+Vendor::updating(function ($vendor) {
+ $vendor->setNullValues();
+});
+
+Vendor::updated(function ($vendor) {
+ event(new VendorWasUpdated($vendor));
+});
+
+
+Vendor::deleting(function ($vendor) {
+ $vendor->setNullValues();
+});
+
+Vendor::deleted(function ($vendor) {
+ event(new VendorWasDeleted($vendor));
+});
diff --git a/app/Models/VendorActivity.php b/app/Models/VendorActivity.php
new file mode 100644
index 000000000000..2776a631a677
--- /dev/null
+++ b/app/Models/VendorActivity.php
@@ -0,0 +1,61 @@
+whereAccountId(Auth::user()->account_id);
+ }
+
+ public function account()
+ {
+ return $this->belongsTo('App\Models\Account');
+ }
+
+ public function user()
+ {
+ return $this->belongsTo('App\Models\User')->withTrashed();
+ }
+
+ public function vendorContact()
+ {
+ return $this->belongsTo('App\Models\VendorContact')->withTrashed();
+ }
+
+ public function vendor()
+ {
+ return $this->belongsTo('App\Models\Vendor')->withTrashed();
+ }
+
+ public function getMessage()
+ {
+ $activityTypeId = $this->activity_type_id;
+ $account = $this->account;
+ $vendor = $this->vendor;
+ $user = $this->user;
+ $contactId = $this->contact_id;
+ $isSystem = $this->is_system;
+
+ if($vendor) {
+ $route = $vendor->getRoute();
+
+ $data = [
+ 'vendor' => link_to($route, $vendor->getDisplayName()),
+ 'user' => $isSystem ? '' . trans('texts.system') . '' : $user->getDisplayName(),
+ 'vendorcontact' => $contactId ? $vendor->getDisplayName() : $user->getDisplayName(),
+ ];
+ } else {
+ return trans("texts.invalid_activity");
+ }
+ return trans("texts.activity_{$activityTypeId}", $data);
+ }
+}
diff --git a/app/Models/VendorContact.php b/app/Models/VendorContact.php
new file mode 100644
index 000000000000..5546b27d2adb
--- /dev/null
+++ b/app/Models/VendorContact.php
@@ -0,0 +1,68 @@
+belongsTo('App\Models\Account');
+ }
+
+ public function user()
+ {
+ return $this->belongsTo('App\Models\User');
+ }
+
+ public function vendor()
+ {
+ return $this->belongsTo('App\Models\Vendor')->withTrashed();
+ }
+
+ public function getPersonType()
+ {
+ return PERSON_VENDOR_CONTACT;
+ }
+
+ public function getName()
+ {
+ return $this->getDisplayName();
+ }
+
+ public function getDisplayName()
+ {
+ if ($this->getFullName()) {
+ return $this->getFullName();
+ } else {
+ return $this->email;
+ }
+ }
+
+ public function getFullName()
+ {
+ if ($this->first_name || $this->last_name) {
+ return $this->first_name.' '.$this->last_name;
+ } else {
+ return '';
+ }
+ }
+}
diff --git a/app/Models/VendorInvitation.php b/app/Models/VendorInvitation.php
new file mode 100644
index 000000000000..ffb3776bb05f
--- /dev/null
+++ b/app/Models/VendorInvitation.php
@@ -0,0 +1,90 @@
+belongsTo('App\Models\VendorContact')->withTrashed();
+ }
+
+ public function user()
+ {
+ return $this->belongsTo('App\Models\User')->withTrashed();
+ }
+
+ public function account()
+ {
+ return $this->belongsTo('App\Models\Account');
+ }
+
+ public function getLink($type = 'view')
+ {
+ if (!$this->account) {
+ $this->load('account');
+ }
+
+ $url = SITE_URL;
+ $iframe_url = $this->account->iframe_url;
+
+ if ($this->account->isPro()) {
+ if ($iframe_url) {
+ return "{$iframe_url}/?{$this->invitation_key}";
+ } elseif ($this->account->subdomain) {
+ $url = Utils::replaceSubdomain($url, $this->account->subdomain);
+ }
+ }
+
+ return "{$url}/{$type}/{$this->invitation_key}";
+ }
+
+ public function getStatus()
+ {
+ $hasValue = false;
+ $parts = [];
+ $statuses = $this->message_id ? ['sent', 'opened', 'viewed'] : ['sent', 'viewed'];
+
+ foreach ($statuses as $status) {
+ $field = "{$status}_date";
+ $date = '';
+ if ($this->$field && $this->field != '0000-00-00 00:00:00') {
+ $date = Utils::dateToString($this->$field);
+ $hasValue = true;
+ }
+ $parts[] = trans('texts.invitation_status.' . $status) . ': ' . $date;
+ }
+
+ return $hasValue ? implode($parts, '
') : false;
+ }
+
+ public function getName()
+ {
+ return $this->invitation_key;
+ }
+
+ public function markSent($messageId = null)
+ {
+ $this->message_id = $messageId;
+ $this->email_error = null;
+ $this->sent_date = Carbon::now()->toDateTimeString();
+ $this->save();
+ }
+
+ public function markViewed()
+ {
+ //$invoice = $this->invoice;
+ //$client = $invoice->client;
+
+ $this->viewed_date = Carbon::now()->toDateTimeString();
+ $this->save();
+
+ //$invoice->markViewed();
+ //$client->markLoggedIn();
+ }
+}
diff --git a/app/Ninja/Import/BaseTransformer.php b/app/Ninja/Import/BaseTransformer.php
index ea05584c20d1..8e17bfeec36f 100644
--- a/app/Ninja/Import/BaseTransformer.php
+++ b/app/Ninja/Import/BaseTransformer.php
@@ -87,4 +87,11 @@ class BaseTransformer extends TransformerAbstract
return isset($this->maps[ENTITY_INVOICE.'_'.ENTITY_CLIENT][$invoiceNumber])? $this->maps[ENTITY_INVOICE.'_'.ENTITY_CLIENT][$invoiceNumber] : null;
}
+
+ protected function getVendorId($name)
+ {
+ $name = strtolower($name);
+ return isset($this->maps[ENTITY_VENDOR][$name]) ? $this->maps[ENTITY_VENDOR][$name] : null;
+ }
+
}
\ No newline at end of file
diff --git a/app/Ninja/Import/CSV/VendorTransformer.php b/app/Ninja/Import/CSV/VendorTransformer.php
new file mode 100644
index 000000000000..464274e5a4fa
--- /dev/null
+++ b/app/Ninja/Import/CSV/VendorTransformer.php
@@ -0,0 +1,35 @@
+name) && $this->hasVendor($data->name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $this->getString($data, 'name'),
+ 'work_phone' => $this->getString($data, 'work_phone'),
+ 'address1' => $this->getString($data, 'address1'),
+ 'city' => $this->getString($data, 'city'),
+ 'state' => $this->getString($data, 'state'),
+ 'postal_code' => $this->getString($data, 'postal_code'),
+ 'private_notes' => $this->getString($data, 'notes'),
+ 'contacts' => [
+ [
+ 'first_name' => $this->getString($data, 'first_name'),
+ 'last_name' => $this->getString($data, 'last_name'),
+ 'email' => $this->getString($data, 'email'),
+ 'phone' => $this->getString($data, 'phone'),
+ ],
+ ],
+ 'country_id' => isset($data->country) ? $this->getCountryId($data->country) : null,
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/FreshBooks/VendorTransformer.php b/app/Ninja/Import/FreshBooks/VendorTransformer.php
new file mode 100644
index 000000000000..c083360aa305
--- /dev/null
+++ b/app/Ninja/Import/FreshBooks/VendorTransformer.php
@@ -0,0 +1,36 @@
+hasVendor($data->organization)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->organization,
+ 'work_phone' => $data->busphone,
+ 'address1' => $data->street,
+ 'address2' => $data->street2,
+ 'city' => $data->city,
+ 'state' => $data->province,
+ 'postal_code' => $data->postalcode,
+ 'private_notes' => $data->notes,
+ 'contacts' => [
+ [
+ 'first_name' => $data->firstname,
+ 'last_name' => $data->lastname,
+ 'email' => $data->email,
+ 'phone' => $data->mobphone ?: $data->homephone,
+ ],
+ ],
+ 'country_id' => $this->getCountryId($data->country),
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Harvest/VendorContactTransformer.php b/app/Ninja/Import/Harvest/VendorContactTransformer.php
new file mode 100644
index 000000000000..3aa0b0b36aa2
--- /dev/null
+++ b/app/Ninja/Import/Harvest/VendorContactTransformer.php
@@ -0,0 +1,24 @@
+hasVendor($data->vendor)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'vendor_id' => $this->getVendorId($data->vendor),
+ 'first_name' => $data->first_name,
+ 'last_name' => $data->last_name,
+ 'email' => $data->email,
+ 'phone' => $data->office_phone ?: $data->mobile_phone,
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Harvest/VendorTransformer.php b/app/Ninja/Import/Harvest/VendorTransformer.php
new file mode 100644
index 000000000000..efab1e6b66ad
--- /dev/null
+++ b/app/Ninja/Import/Harvest/VendorTransformer.php
@@ -0,0 +1,20 @@
+hasVendor($data->vendor_name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->vendor_name,
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Hiveage/VendorTransformer.php b/app/Ninja/Import/Hiveage/VendorTransformer.php
new file mode 100644
index 000000000000..dec1b62d1ccb
--- /dev/null
+++ b/app/Ninja/Import/Hiveage/VendorTransformer.php
@@ -0,0 +1,35 @@
+hasVendor($data->name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->name,
+ 'contacts' => [
+ [
+ 'first_name' => $this->getFirstName($data->primary_contact),
+ 'last_name' => $this->getLastName($data->primary_contactk),
+ 'email' => $data->business_email,
+ ],
+ ],
+ 'address1' => $data->address_1,
+ 'address2' => $data->address_2,
+ 'city' => $data->city,
+ 'state' => $data->state_name,
+ 'postal_code' => $data->zip_code,
+ 'work_phone' => $data->phone,
+ 'website' => $data->website,
+ 'country_id' => $this->getCountryId($data->country),
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Invoiceable/VendorTransformer.php b/app/Ninja/Import/Invoiceable/VendorTransformer.php
new file mode 100644
index 000000000000..1ec4a2876884
--- /dev/null
+++ b/app/Ninja/Import/Invoiceable/VendorTransformer.php
@@ -0,0 +1,34 @@
+hasVendor($data->vendor_name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->vendor_name,
+ 'work_phone' => $data->tel,
+ 'website' => $data->website,
+ 'address1' => $data->address,
+ 'city' => $data->city,
+ 'state' => $data->state,
+ 'postal_code' => $data->postcode,
+ 'country_id' => $this->getCountryIdBy2($data->country),
+ 'private_notes' => $data->notes,
+ 'contacts' => [
+ [
+ 'email' => $data->email,
+ 'phone' => $data->mobile,
+ ],
+ ],
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Nutcache/VendorTransformer.php b/app/Ninja/Import/Nutcache/VendorTransformer.php
new file mode 100644
index 000000000000..b97f0811906e
--- /dev/null
+++ b/app/Ninja/Import/Nutcache/VendorTransformer.php
@@ -0,0 +1,35 @@
+hasVendor($data->name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->name,
+ 'city' => isset($data->city) ? $data->city : '',
+ 'state' => isset($data->city) ? $data->stateprovince : '',
+ 'id_number' => isset($data->registration_number) ? $data->registration_number : '',
+ 'postal_code' => isset($data->postalzip_code) ? $data->postalzip_code : '',
+ 'private_notes' => isset($data->notes) ? $data->notes : '',
+ 'work_phone' => isset($data->phone) ? $data->phone : '',
+ 'contacts' => [
+ [
+ 'first_name' => isset($data->contact_name) ? $this->getFirstName($data->contact_name) : '',
+ 'last_name' => isset($data->contact_name) ? $this->getLastName($data->contact_name) : '',
+ 'email' => $data->email,
+ 'phone' => isset($data->mobile) ? $data->mobile : '',
+ ],
+ ],
+ 'country_id' => isset($data->country) ? $this->getCountryId($data->country) : null,
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Ronin/VendorTransformer.php b/app/Ninja/Import/Ronin/VendorTransformer.php
new file mode 100644
index 000000000000..817de03d6647
--- /dev/null
+++ b/app/Ninja/Import/Ronin/VendorTransformer.php
@@ -0,0 +1,28 @@
+hasVendor($data->company)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->company,
+ 'work_phone' => $data->phone,
+ 'contacts' => [
+ [
+ 'first_name' => $this->getFirstName($data->name),
+ 'last_name' => $this->getLastName($data->name),
+ 'email' => $data->email,
+ ],
+ ],
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Wave/VendorTransformer.php b/app/Ninja/Import/Wave/VendorTransformer.php
new file mode 100644
index 000000000000..f2fe2f43e375
--- /dev/null
+++ b/app/Ninja/Import/Wave/VendorTransformer.php
@@ -0,0 +1,38 @@
+hasVendor($data->customer_name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->customer_name,
+ 'id_number' => $data->account_number,
+ 'work_phone' => $data->phone,
+ 'website' => $data->website,
+ 'address1' => $data->address_line_1,
+ 'address2' => $data->address_line_2,
+ 'city' => $data->city,
+ 'state' => $data->provincestate,
+ 'postal_code' => $data->postal_codezip_code,
+ 'private_notes' => $data->delivery_instructions,
+ 'contacts' => [
+ [
+ 'first_name' => $data->contact_first_name,
+ 'last_name' => $data->contact_last_name,
+ 'email' => $data->email,
+ 'phone' => $data->mobile,
+ ],
+ ],
+ 'country_id' => $this->getCountryId($data->country),
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Zoho/VendorTransformer.php b/app/Ninja/Import/Zoho/VendorTransformer.php
new file mode 100644
index 000000000000..811a9f7ff2d9
--- /dev/null
+++ b/app/Ninja/Import/Zoho/VendorTransformer.php
@@ -0,0 +1,37 @@
+hasVendor($data->customer_name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->customer_name,
+ 'id_number' => $data->customer_id,
+ 'work_phone' => $data->phonek,
+ 'address1' => $data->billing_address,
+ 'city' => $data->billing_city,
+ 'state' => $data->billing_state,
+ 'postal_code' => $data->billing_code,
+ 'private_notes' => $data->notes,
+ 'website' => $data->website,
+ 'contacts' => [
+ [
+ 'first_name' => $data->first_name,
+ 'last_name' => $data->last_name,
+ 'email' => $data->emailid,
+ 'phone' => $data->mobilephone,
+ ],
+ ],
+ 'country_id' => $this->getCountryId($data->billing_country),
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Mailers/VendorContactMailer.php b/app/Ninja/Mailers/VendorContactMailer.php
new file mode 100644
index 000000000000..267bb90cec5a
--- /dev/null
+++ b/app/Ninja/Mailers/VendorContactMailer.php
@@ -0,0 +1,152 @@
+vendor;
+ $account = $invoice->account;
+
+ if (Auth::check()) {
+ $user = Auth::user();
+ } else {
+ $user = $invitation->user;
+ if ($invitation->user->trashed()) {
+ $user = $account->users()->orderBy('id')->first();
+ }
+ }
+
+ if (!$user->email || !$user->registered) {
+ return trans('texts.email_errors.user_unregistered');
+ } elseif (!$user->confirmed) {
+ return trans('texts.email_errors.user_unconfirmed');
+ } elseif (!$invitation->contact->email) {
+ return trans('texts.email_errors.invalid_contact_email');
+ } elseif ($invitation->contact->trashed()) {
+ return trans('texts.email_errors.inactive_contact');
+ }
+
+ $variables = [
+ 'account' => $account,
+ 'vendor' => $vendor,
+ 'invitation' => $invitation
+ ];
+
+ $data = [
+ 'body' => $this->processVariables($body, $variables),
+ 'link' => $invitation->getLink(),
+ 'entityType' => $invoice->getEntityType(),
+ 'invitation' => $invitation,
+ 'account' => $account,
+ 'vendor' => $vendor,
+ 'invoice' => $invoice,
+ ];
+
+ if ($account->attatchPDF()) {
+ $data['pdfString'] = $pdfString;
+ $data['pdfFileName'] = $invoice->getFileName();
+ }
+
+ $subject = $this->processVariables($subject, $variables);
+ $fromEmail = $user->email;
+
+ if ($account->email_design_id == EMAIL_DESIGN_PLAIN) {
+ $view = ENTITY_INVOICE;
+ } else {
+ $view = 'design' . ($account->email_design_id - 1);
+ }
+
+ $response = $this->sendTo($invitation->contact->email, $fromEmail, $account->getDisplayName(), $subject, $view, $data);
+
+ if ($response === true) {
+ return true;
+ } else {
+ return $response;
+ }
+ }
+
+ public function sendLicensePaymentConfirmation($name, $email, $amount, $license, $productId)
+ {
+ $view = 'license_confirmation';
+ $subject = trans('texts.payment_subject');
+
+ if ($productId == PRODUCT_ONE_CLICK_INSTALL) {
+ $license = "Softaculous install license: $license";
+ } elseif ($productId == PRODUCT_INVOICE_DESIGNS) {
+ $license = "Invoice designs license: $license";
+ } elseif ($productId == PRODUCT_WHITE_LABEL) {
+ $license = "White label license: $license";
+ }
+
+ $data = [
+ 'vendor' => $name,
+ 'amount' => Utils::formatMoney($amount, DEFAULT_CURRENCY, DEFAULT_COUNTRY),
+ 'license' => $license
+ ];
+
+ $this->sendTo($email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data);
+ }
+
+ private function processVariables($template, $data)
+ {
+ $account = $data['account'];
+ $vendor = $data['vendor'];
+ $invitation = $data['invitation'];
+ $invoice = $invitation->invoice;
+
+ $variables = [
+ '$footer' => $account->getEmailFooter(),
+ '$vendor' => $vendor->getDisplayName(),
+ '$account' => $account->getDisplayName(),
+ '$contact' => $invitation->contact->getDisplayName(),
+ '$firstName' => $invitation->contact->first_name,
+ '$amount' => $account->formatMoney($data['amount'], $vendor),
+ '$invoice' => $invoice->invoice_number,
+ '$quote' => $invoice->invoice_number,
+ '$link' => $invitation->getLink(),
+ '$dueDate' => $account->formatDate($invoice->due_date),
+ '$viewLink' => $invitation->getLink(),
+ '$viewButton' => HTML::emailViewButton($invitation->getLink(), $invoice->getEntityType()),
+ '$paymentLink' => $invitation->getLink('payment'),
+ '$paymentButton' => HTML::emailPaymentButton($invitation->getLink('payment')),
+ '$customClient1' => $account->custom_vendor_label1,
+ '$customClient2' => $account->custom_vendor_label2,
+ '$customInvoice1' => $account->custom_invoice_text_label1,
+ '$customInvoice2' => $account->custom_invoice_text_label2,
+ ];
+
+ // Add variables for available payment types
+ foreach (Gateway::$paymentTypes as $type) {
+ $camelType = Gateway::getPaymentTypeName($type);
+ $type = Utils::toSnakeCase($camelType);
+ $variables["\${$camelType}Link"] = $invitation->getLink() . "/{$type}";
+ $variables["\${$camelType}Button"] = HTML::emailPaymentButton($invitation->getLink('payment') . "/{$type}");
+ }
+
+ $str = str_replace(array_keys($variables), array_values($variables), $template);
+
+ return $str;
+ }
+}
diff --git a/app/Ninja/Presenters/ExpensePresenter.php b/app/Ninja/Presenters/ExpensePresenter.php
new file mode 100644
index 000000000000..3c7237d39d64
--- /dev/null
+++ b/app/Ninja/Presenters/ExpensePresenter.php
@@ -0,0 +1,18 @@
+entity->vendor ? $this->entity->vendor->getDisplayName() : '';
+ }
+
+ public function expense_date()
+ {
+ return Utils::fromSqlDate($this->entity->expense_date);
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Presenters/VendorPresenter.php b/app/Ninja/Presenters/VendorPresenter.php
new file mode 100644
index 000000000000..b3da402bec40
--- /dev/null
+++ b/app/Ninja/Presenters/VendorPresenter.php
@@ -0,0 +1,12 @@
+entity->country ? $this->entity->country->name : '';
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Repositories/ExpenseActivityRepository.php b/app/Ninja/Repositories/ExpenseActivityRepository.php
new file mode 100644
index 000000000000..1406983962b5
--- /dev/null
+++ b/app/Ninja/Repositories/ExpenseActivityRepository.php
@@ -0,0 +1,65 @@
+vendor_id = $entity->vendor_id;
+ $activity->contact_id = $entity->contact_id;
+ $activity->activity_type_id = $activityTypeId;
+ $activity->message = $activity->getMessage();
+ $activity->expense_id = $entity->id;
+ $activity->save();
+
+ return $activity;
+ }
+
+ private function getBlank($entity)
+ {
+ $activity = new ExpenseActivity();
+
+ if (Auth::check() && Auth::user()->account_id == $entity->account_id) {
+ $activity->user_id = Auth::user()->id;
+ $activity->account_id = Auth::user()->account_id;
+ } else {
+ $activity->user_id = $entity->user_id;
+ $activity->account_id = $entity->account_id;
+ }
+
+ $activity->token_id = session('token_id');
+ $activity->ip = Request::getClientIp();
+
+
+ return $activity;
+ }
+
+
+ public function findByExpenseId($expenseId)
+ {
+ return DB::table('expense_activities')
+ ->join('accounts', 'accounts.id', '=', 'expense_activities.account_id')
+ ->join('users', 'users.id', '=', 'expense_activities.user_id')
+ ->join('expenses','expenses.public_id', '=', 'expense_activities.expense_id')
+ ->where('expense_activities.expense_id', '=', $expenseId)
+ ->select('*',
+ 'users.first_name as user_first_name',
+ 'users.last_name as user_last_name',
+ 'users.email as user_email',
+ 'expenses.amount'
+ );
+
+ }
+}
diff --git a/app/Ninja/Repositories/ExpenseRepository.php b/app/Ninja/Repositories/ExpenseRepository.php
new file mode 100644
index 000000000000..d1c6cb86622f
--- /dev/null
+++ b/app/Ninja/Repositories/ExpenseRepository.php
@@ -0,0 +1,160 @@
+with('user')
+ ->withTrashed()
+ ->where('is_deleted', '=', false)
+ ->get();
+ }
+
+ public function findVendor($vendorPublicId)
+ {
+ $accountid = \Auth::user()->account_id;
+ $query = DB::table('expenses')
+ ->join('accounts', 'accounts.id', '=', 'expenses.account_id')
+ ->where('expenses.account_id', '=', $accountid)
+ ->where('expenses.vendor_id','=',$vendorPublicId)
+ ->select('expenses.id',
+ 'expenses.expense_date',
+ 'expenses.amount',
+ 'expenses.public_notes',
+ 'expenses.public_id',
+ 'expenses.deleted_at','expenses.is_invoiced','expenses.should_be_invoiced','expenses.created_at');
+ return $query;
+ }
+
+ public function find($filter = null)
+ {
+ $accountid = \Auth::user()->account_id;
+ $query = DB::table('expenses')
+ ->join('accounts', 'accounts.id', '=', 'expenses.account_id')
+ ->leftjoin('vendors','vendors.public_id','=', 'expenses.vendor_id')
+ ->where('expenses.account_id', '=', $accountid)
+ ->select('expenses.account_id',
+ 'expenses.amount',
+ 'expenses.amount_cur',
+ 'expenses.currency_id',
+ 'expenses.deleted_at',
+ 'expenses.exchange_rate',
+ 'expenses.expense_date',
+ 'expenses.id',
+ 'expenses.is_deleted',
+ 'expenses.is_invoiced',
+ 'expenses.private_notes',
+ 'expenses.public_id',
+ 'expenses.public_notes',
+ 'expenses.should_be_invoiced',
+ 'expenses.vendor_id',
+ 'vendors.name as vendor_name',
+ 'vendors.public_id as vendor_public_id');
+
+ $showTrashed = \Session::get('show_trash:expense');
+
+ if (!$showTrashed) {
+ $query->where('expenses.deleted_at', '=', null);
+ }
+
+ if ($filter) {
+ $query->where(function ($query) use ($filter) {
+ $query->where('expenses.public_notes', 'like', '%'.$filter.'%');
+ });
+ }
+
+ return $query;
+ }
+
+ public function save($input)
+ {
+ $publicId = isset($input['public_id']) ? $input['public_id'] : false;
+
+ if ($publicId) {
+ $expense = Expense::scope($publicId)->firstOrFail();
+ } else {
+ $expense = Expense::createNew();
+ }
+
+ // First auto fill
+ $expense->fill($input);
+
+ // We can have an expense without a vendor
+ if(isset($input['vendor'])) {
+ $expense->vendor_id = $input['vendor'];
+ }
+
+ $expense->expense_date = Utils::toSqlDate($input['expense_date']);
+ $expense->amount = Utils::parseFloat($input['amount']);
+
+ if(isset($input['amount_cur']))
+ $expense->amount_cur = Utils::parseFloat($input['amount_cur']);
+
+ $expense->private_notes = trim($input['private_notes']);
+ $expense->public_notes = trim($input['public_notes']);
+
+ if(isset($input['exchange_rate']))
+ $expense->exchange_rate = Utils::parseFloat($input['exchange_rate']);
+ else
+ $expense->exchange_rate = 100;
+
+ if($expense->exchange_rate == 0)
+ $expense->exchange_rate = 100;
+
+ // set the currency
+ if(isset($input['currency_id']))
+ $expense->currency_id = $input['currency_id'];
+
+ if($expense->currency_id == 0)
+ $expense->currency_id = Session::get(SESSION_CURRENCY, DEFAULT_CURRENCY);
+
+ // Calculate the amount cur
+ $expense->amount_cur = ($expense->amount / 100) * $expense->exchange_rate;
+
+ $expense->should_be_invoiced = isset($input['should_be_invoiced']) ? true : false;
+ if(isset($input['client'])) {
+ $expense->invoice_client_id = $input['client'];
+ }
+ $expense->save();
+
+ return $expense;
+ }
+
+ public function bulk($ids, $action)
+ {
+ $expenses = Expense::withTrashed()->scope($ids)->get();
+
+ foreach ($expenses as $expense) {
+ if ($action == 'restore') {
+ $expense->restore();
+
+ $expense->is_deleted = false;
+ $expense->save();
+ } else {
+ if ($action == 'delete') {
+ $expense->is_deleted = true;
+ $expense->save();
+ }
+
+ $expense->delete();
+ }
+ }
+
+ return count($tasks);
+ }
+
+}
diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php
index 5aa5f6a9b73b..f94bafb97335 100644
--- a/app/Ninja/Repositories/InvoiceRepository.php
+++ b/app/Ninja/Repositories/InvoiceRepository.php
@@ -7,6 +7,7 @@ use App\Models\InvoiceItem;
use App\Models\Invitation;
use App\Models\Product;
use App\Models\Task;
+use App\Models\Expense;
use App\Services\PaymentService;
use App\Ninja\Repositories\BaseRepository;
@@ -177,7 +178,7 @@ class InvoiceRepository extends BaseRepository
$table->addColumn('balance', function ($model) {
return $model->partial > 0 ?
trans('texts.partial_remaining', [
- 'partial' => Utils::formatMoney($model->partial, $model->currency_id, $model->country_id),
+ 'partial' => Utils::formatMoney($model->partial, $model->currency_id, $model->country_id),
'balance' => Utils::formatMoney($model->balance, $model->currency_id, $model->country_id)
]) :
Utils::formatMoney($model->balance, $model->currency_id, $model->country_id);
@@ -206,6 +207,9 @@ class InvoiceRepository extends BaseRepository
if (isset($data['has_tasks']) && filter_var($data['has_tasks'], FILTER_VALIDATE_BOOLEAN)) {
$invoice->has_tasks = true;
}
+ if (isset($data['has_expenses']) && filter_var($data['has_expenses'], FILTER_VALIDATE_BOOLEAN)) {
+ $invoice->has_expenses = true;
+ }
} else {
$invoice = Invoice::scope($publicId)->firstOrFail();
}
@@ -276,7 +280,7 @@ class InvoiceRepository extends BaseRepository
if (isset($data['po_number'])) {
$invoice->po_number = trim($data['po_number']);
}
-
+
$invoice->invoice_design_id = isset($data['invoice_design_id']) ? $data['invoice_design_id'] : $account->invoice_design_id;
if (isset($data['tax_name']) && isset($data['tax_rate']) && $data['tax_name']) {
@@ -398,6 +402,14 @@ class InvoiceRepository extends BaseRepository
$task->save();
}
+ if (isset($item['expense_public_id']) && $item['expense_public_id']) {
+ $expense = Expense::scope($item['expense_public_id'])->where('invoice_id', '=', null)->firstOrFail();
+ $expense->invoice_id = $invoice->id;
+ $expense->invoice_client_id = $invoice->client_id;
+ $expense->is_invoiced = true;
+ $expense->save();
+ }
+
if ($item['product_key']) {
$productKey = trim($item['product_key']);
if (\Auth::user()->account->update_products && ! strtotime($productKey)) {
@@ -406,7 +418,10 @@ class InvoiceRepository extends BaseRepository
$product = Product::createNew();
$product->product_key = trim($item['product_key']);
}
+
$product->notes = $invoice->has_tasks ? '' : $item['notes'];
+ $product->notes = $invoice->has_expenses ? '' : $item['notes'];
+
$product->cost = $item['cost'];
$product->save();
}
@@ -642,7 +657,7 @@ class InvoiceRepository extends BaseRepository
public function findNeedingReminding($account)
{
$dates = [];
-
+
for ($i=1; $i<=3; $i++) {
if ($date = $account->getReminderDate($i)) {
$field = $account->{"field_reminder{$i}"} == REMINDER_FIELD_DUE_DATE ? 'due_date' : 'invoice_date';
diff --git a/app/Ninja/Repositories/PaymentTermRepository.php b/app/Ninja/Repositories/PaymentTermRepository.php
new file mode 100644
index 000000000000..e631e9f1627a
--- /dev/null
+++ b/app/Ninja/Repositories/PaymentTermRepository.php
@@ -0,0 +1,22 @@
+where('payment_terms.account_id', '=', $accountId)
+ ->where('payment_terms.deleted_at', '=', null)
+ ->select('payment_terms.public_id', 'payment_terms.name', 'payment_terms.num_days', 'payment_terms.deleted_at');
+ }
+}
diff --git a/app/Ninja/Repositories/VendorActivityRepository.php b/app/Ninja/Repositories/VendorActivityRepository.php
new file mode 100644
index 000000000000..7c59f80d438d
--- /dev/null
+++ b/app/Ninja/Repositories/VendorActivityRepository.php
@@ -0,0 +1,101 @@
+invoice->vendor;
+ } else {
+ $vendor = $entity->vendor;
+ }
+
+ $this->vendor = $vendor;
+
+ // init activity and copy over context
+ $activity = self::getBlank($altEntity ?: $vendor);
+ $activity = Utils::copyContext($activity, $entity);
+ $activity = Utils::copyContext($activity, $altEntity);
+
+ $activity->vendor_id = $vendor->id;
+ $activity->activity_type_id = $activityTypeId;
+ $activity->adjustment = $balanceChange;
+ $activity->balance = $vendor->balance + $balanceChange;
+
+ $keyField = $entity->getKeyField();
+ $activity->$keyField = $entity->id;
+
+ $activity->ip = Request::getClientIp();
+ $activity->save();
+
+ $vendor->updateBalances($balanceChange, $paidToDateChange);
+
+ return $activity;
+ }
+
+ private function getBlank($entity)
+ {
+ $activity = new VendorActivity();
+
+ if (Auth::check() && Auth::user()->account_id == $entity->account_id) {
+ $activity->user_id = Auth::user()->id;
+ $activity->account_id = Auth::user()->account_id;
+ } else {
+ $activity->user_id = $entity->user_id;
+ $activity->account_id = $entity->account_id;
+
+ if ( ! $entity instanceof Invitation) {
+ $activity->is_system = true;
+ }
+ }
+
+ $activity->token_id = session('token_id');
+
+ return $activity;
+ }
+
+ public function findByVendorId($vendorId)
+ {
+ return DB::table('vendor_activities')
+ ->join('accounts', 'accounts.id', '=', 'vendor_activities.account_id')
+ ->join('users', 'users.id', '=', 'vendor_activities.user_id')
+ ->join('vendors', 'vendors.id', '=', 'vendor_activities.vendor_id')
+ ->leftJoin('vendor_contacts', 'vendor_contacts.vendor_id', '=', 'vendors.id')
+ ->where('vendors.id', '=', $vendorId)
+ ->where('vendor_contacts.is_primary', '=', 1)
+ ->whereNull('vendor_contacts.deleted_at')
+ ->select(
+ DB::raw('COALESCE(vendors.currency_id, accounts.currency_id) currency_id'),
+ DB::raw('COALESCE(vendors.country_id, accounts.country_id) country_id'),
+ 'vendor_activities.id',
+ 'vendor_activities.created_at',
+ 'vendor_activities.contact_id',
+ 'vendor_activities.activity_type_id',
+ 'vendor_activities.is_system',
+ 'vendor_activities.balance',
+ 'vendor_activities.adjustment',
+ 'users.first_name as user_first_name',
+ 'users.last_name as user_last_name',
+ 'users.email as user_email',
+ 'vendors.name as vendor_name',
+ 'vendors.public_id as vendor_public_id',
+ 'vendor_contacts.id as contact',
+ 'vendor_contacts.first_name as first_name',
+ 'vendor_contacts.last_name as last_name',
+ 'vendor_contacts.email as email'
+
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/app/Ninja/Repositories/VendorContactRepository.php b/app/Ninja/Repositories/VendorContactRepository.php
new file mode 100644
index 000000000000..242b1b9d0c54
--- /dev/null
+++ b/app/Ninja/Repositories/VendorContactRepository.php
@@ -0,0 +1,26 @@
+send_invoice = true;
+ $contact->vendor_id = $data['vendor_id'];
+ $contact->is_primary = VendorContact::scope()->where('vendor_id', '=', $contact->vendor_id)->count() == 0;
+ } else {
+ $contact = VendorContact::scope($publicId)->firstOrFail();
+ }
+
+ $contact->fill($data);
+ $contact->save();
+
+ return $contact;
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Repositories/VendorRepository.php b/app/Ninja/Repositories/VendorRepository.php
new file mode 100644
index 000000000000..ae2a7f60a5e5
--- /dev/null
+++ b/app/Ninja/Repositories/VendorRepository.php
@@ -0,0 +1,91 @@
+with('user', 'vendorcontacts', 'country')
+ ->withTrashed()
+ ->where('is_deleted', '=', false)
+ ->get();
+ }
+
+ public function find($filter = null)
+ {
+ $query = DB::table('vendors')
+ ->join('accounts', 'accounts.id', '=', 'vendors.account_id')
+ ->join('vendor_contacts', 'vendor_contacts.vendor_id', '=', 'vendors.id')
+ ->where('vendors.account_id', '=', \Auth::user()->account_id)
+ ->where('vendor_contacts.is_primary', '=', true)
+ ->where('vendor_contacts.deleted_at', '=', null)
+ ->select(
+ DB::raw('COALESCE(vendors.currency_id, accounts.currency_id) currency_id'),
+ DB::raw('COALESCE(vendors.country_id, accounts.country_id) country_id'),
+ 'vendors.public_id',
+ 'vendors.name',
+ 'vendor_contacts.first_name',
+ 'vendor_contacts.last_name',
+ 'vendors.balance',
+ 'vendors.created_at',
+ 'vendors.work_phone',
+ 'vendor_contacts.email',
+ 'vendors.deleted_at',
+ 'vendors.is_deleted'
+ );
+
+ if (!\Session::get('show_trash:vendor')) {
+ $query->where('vendors.deleted_at', '=', null);
+ }
+
+ if ($filter) {
+ $query->where(function ($query) use ($filter) {
+ $query->where('vendors.name', 'like', '%'.$filter.'%')
+ ->orWhere('vendor_contacts.first_name', 'like', '%'.$filter.'%')
+ ->orWhere('vendor_contacts.last_name', 'like', '%'.$filter.'%')
+ ->orWhere('vendor_contacts.email', 'like', '%'.$filter.'%');
+ });
+ }
+
+ return $query;
+ }
+
+ public function save($data)
+ {
+ $publicId = isset($data['public_id']) ? $data['public_id'] : false;
+
+ if (!$publicId || $publicId == '-1') {
+ $vendor = Vendor::createNew();
+ } else {
+ $vendor = Vendor::scope($publicId)->with('vendorcontacts')->firstOrFail();
+ }
+
+ $vendor->fill($data);
+ $vendor->save();
+
+ if ( ! isset($data['vendorcontact']) && ! isset($data['vendorcontacts'])) {
+ return $vendor;
+ }
+
+ $first = true;
+ $vendorcontacts = isset($data['vendorcontact']) ? [$data['vendorcontact']] : $data['vendorcontacts'];
+
+ foreach ($vendorcontacts as $vendorcontact) {
+ $vendorcontact = $vendor->addVendorContact($vendorcontact, $first);
+ $first = false;
+ }
+
+ return $vendor;
+ }
+}
diff --git a/app/Ninja/Transformers/InvoiceTransformer.php b/app/Ninja/Transformers/InvoiceTransformer.php
index 09d24ae83260..8108115b2f01 100644
--- a/app/Ninja/Transformers/InvoiceTransformer.php
+++ b/app/Ninja/Transformers/InvoiceTransformer.php
@@ -24,7 +24,7 @@ class InvoiceTransformer extends EntityTransformer
'invoice_items',
'payments'
];
-
+
public function includeInvoiceItems(Invoice $invoice)
{
$transformer = new InvoiceItemTransformer($this->account, $this->serializer);
@@ -77,6 +77,7 @@ class InvoiceTransformer extends EntityTransformer
'custom_value2' => (float) $invoice->custom_value2,
'custom_taxes1' => (bool) $invoice->custom_taxes1,
'custom_taxes2' => (bool) $invoice->custom_taxes2,
+ 'has_expenses' => (bool) $invoice->has_expenses,
];
}
-}
\ No newline at end of file
+}
diff --git a/app/Ninja/Transformers/VendorContactTransformer.php b/app/Ninja/Transformers/VendorContactTransformer.php
new file mode 100644
index 000000000000..0166883aba4d
--- /dev/null
+++ b/app/Ninja/Transformers/VendorContactTransformer.php
@@ -0,0 +1,24 @@
+ (int) $contact->public_id,
+ 'first_name' => $contact->first_name,
+ 'last_name' => $contact->last_name,
+ 'email' => $contact->email,
+ 'updated_at' => $this->getTimestamp($contact->updated_at),
+ 'archived_at' => $this->getTimestamp($contact->deleted_at),
+ 'is_primary' => (bool) $contact->is_primary,
+ 'phone' => $contact->phone,
+ 'last_login' => $contact->last_login,
+ 'account_key' => $this->account->account_key,
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Transformers/VendorTransformer.php b/app/Ninja/Transformers/VendorTransformer.php
new file mode 100644
index 000000000000..c02750bbaf80
--- /dev/null
+++ b/app/Ninja/Transformers/VendorTransformer.php
@@ -0,0 +1,92 @@
+account, $this->serializer);
+ return $this->includeCollection($vendor->contacts, $transformer, ENTITY_CONTACT);
+ }
+
+ public function includeInvoices(Vendor $vendor)
+ {
+ $transformer = new InvoiceTransformer($this->account, $this->serializer);
+ return $this->includeCollection($vendor->invoices, $transformer, ENTITY_INVOICE);
+ }
+
+ public function transform(Vendor $vendor)
+ {
+ return [
+ 'id' => (int) $vendor->public_id,
+ 'name' => $vendor->name,
+ 'balance' => (float) $vendor->balance,
+ 'paid_to_date' => (float) $vendor->paid_to_date,
+ 'user_id' => (int) $vendor->user->public_id + 1,
+ 'account_key' => $this->account->account_key,
+ 'updated_at' => $this->getTimestamp($vendor->updated_at),
+ 'archived_at' => $this->getTimestamp($vendor->deleted_at),
+ 'address1' => $vendor->address1,
+ 'address2' => $vendor->address2,
+ 'city' => $vendor->city,
+ 'state' => $vendor->state,
+ 'postal_code' => $vendor->postal_code,
+ 'country_id' => (int) $vendor->country_id,
+ 'work_phone' => $vendor->work_phone,
+ 'private_notes' => $vendor->private_notes,
+ 'last_login' => $vendor->last_login,
+ 'website' => $vendor->website,
+ 'industry_id' => (int) $vendor->industry_id,
+ 'size_id' => (int) $vendor->size_id,
+ 'is_deleted' => (bool) $vendor->is_deleted,
+ 'payment_terms' => (int) $vendor->payment_terms,
+ 'vat_number' => $vendor->vat_number,
+ 'id_number' => $vendor->id_number,
+ 'language_id' => (int) $vendor->language_id,
+ 'currency_id' => (int) $vendor->currency_id
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 74098a3966de..a094b01a6079 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -53,7 +53,11 @@ class AppServiceProvider extends ServiceProvider {
$str .= '
Automatically sets a due date for the invoice.
diff --git a/resources/views/accounts/payment_term.blade.php b/resources/views/accounts/payment_term.blade.php new file mode 100644 index 000000000000..a1939995dcbb --- /dev/null +++ b/resources/views/accounts/payment_term.blade.php @@ -0,0 +1,47 @@ +@extends('header') + +@section('content') + @parent + + @include('accounts.nav', ['selected' => ACCOUNT_PAYMENT_TERMS]) + + {!! Former::open($url)->method($method) + ->rules([ + 'name' => 'required', + 'num_days' => 'required' + ]) + ->addClass('warn-on-exit') !!} + + +{{ $expense->public_notes }}
+{{ trans('texts.expense_date') }} | +{{ Utils::fromSqlDate($expense->expense_date) }} | +
{{ trans('texts.expense_amount') }} | +{{ Utils::formatMoney($expense->amount) }} | +
{{ trans('texts.expense_amount_cur') }} | +{{ Utils::formatMoney($$expense->amount_cur, $expense->curency_id) }} | +