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 697c100129e5..f526fef67825 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; @@ -160,6 +161,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 { @@ -330,6 +333,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'); @@ -339,13 +353,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 = trans('texts.address1'); + $client->city = trans('texts.city'); + $client->state = trans('texts.state'); + $client->postal_code = trans('texts.postal_code'); + $client->work_phone = trans('texts.work_phone'); + $client->work_email = trans('texts.work_id'); + $client->id_number = trans('texts.id_number'); + $client->vat_number = trans('texts.var_number'); + $invoice->invoice_number = '0000'; $invoice->invoice_date = Utils::fromSqlDate(date('Y-m-d')); $invoice->account = json_decode($account->toJson()); @@ -364,7 +380,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) ?: []; @@ -372,7 +388,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) { @@ -461,6 +477,8 @@ class AccountController extends BaseController return AccountController::saveProducts(); } elseif ($section === ACCOUNT_TAX_RATES) { return AccountController::saveTaxRates(); + } elseif ($section === ACCOUNT_PAYMENT_TERMS) { + return AccountController::savePaymetTerms(); } } @@ -713,7 +731,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/AppController.php b/app/Http/Controllers/AppController.php index 6e3236896b8a..35f1f9b0a5be 100644 --- a/app/Http/Controllers/AppController.php +++ b/app/Http/Controllers/AppController.php @@ -241,12 +241,12 @@ class AppController extends BaseController { if (!Utils::isNinjaProd()) { try { + Cache::flush(); + Session::flush(); + Artisan::call('optimize', array('--force' => true)); Artisan::call('migrate', array('--force' => true)); Artisan::call('db:seed', array('--force' => true, '--class' => 'PaymentLibrariesSeeder')); Artisan::call('db:seed', array('--force' => true, '--class' => 'FontsSeeder')); - Artisan::call('optimize', array('--force' => true)); - Cache::flush(); - Session::flush(); Event::fire(new UserSettingsChanged()); Session::flash('message', trans('texts.processed_updates')); } catch (Exception $e) { 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..483b73a6c2ef 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -11,6 +11,7 @@ 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, diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php new file mode 100644 index 000000000000..4746e1ac95c3 --- /dev/null +++ b/app/Http/Controllers/ExpenseController.php @@ -0,0 +1,226 @@ +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_date', + 'expense_amount', + 'public_notes', + 'status', + '' + ]), + )); + } + + 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 ? $expense->vendor->public_id : null, + 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), + 'clientPublicId' => $expense->client ? $expense->client->public_id : null, + ); + + $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) + { + $expense = $this->expenseRepo->save($request->input()); + + Session::flash('message', trans('texts.updated_expense')); + + return redirect()->to("expenses/{$expense->public_id}/edit"); + } + + public function store(CreateExpenseRequest $request) + { + $expense = $this->expenseRepo->save($request->input()); + + Session::flash('message', trans('texts.created_expense')); + + return redirect()->to("expenses/{$expense->public_id}/edit"); + } + + 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) + { + Session::reflash(); + + return Redirect::to("expenses/{$publicId}/edit"); + } +} 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 68abcc92f2c3..a17453be8a1e 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/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..bbd69ed23300 --- /dev/null +++ b/app/Http/Controllers/VendorController.php @@ -0,0 +1,204 @@ +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', + '' + ]), + )); + } + + 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, + 'currencies' => Cache::get('currencies'), + 'countries' => Cache::get('countries'), + ]; + } + + /** + * 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..78f6eeee77ed --- /dev/null +++ b/app/Http/Requests/CreateExpenseRequest.php @@ -0,0 +1,30 @@ + '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..7b67ca89230e --- /dev/null +++ b/app/Http/Requests/UpdateExpenseRequest.php @@ -0,0 +1,31 @@ + 'positive', + '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 e31d463bfb03..216365ffc7e8 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'); @@ -186,6 +191,19 @@ 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::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'=>'ExpenseController@getDatatable')); + Route::get('api/expenseVendor/{id}', array('as'=>'api.expense', 'uses'=>'ExpenseController@getDatatableVendor')); + Route::post('expenses/bulk', 'ExpenseController@bulk'); }); // Route groups for API @@ -207,6 +225,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 @@ -247,6 +271,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'); @@ -264,9 +289,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'); @@ -294,6 +325,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'); @@ -334,6 +366,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); @@ -365,6 +409,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); @@ -438,6 +486,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 7f1134d5fc31..cfeb191bb8e8 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(); } @@ -385,7 +385,7 @@ class Utils if (!$date) { return false; } - + $dateTime = new DateTime($date); $timestamp = $dateTime->getTimestamp(); $format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT); @@ -433,7 +433,10 @@ class Utils $format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT); $dateTime = DateTime::createFromFormat('Y-m-d', $date); - return $formatResult ? $dateTime->format($format) : $dateTime; + if(!$dateTime) + return $date; + else + return $formatResult ? $dateTime->format($format) : $dateTime; } public static function fromSqlDateTime($date, $formatResult = true) @@ -509,7 +512,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); } @@ -618,6 +621,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) { @@ -649,7 +663,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; } } @@ -707,7 +723,7 @@ class Utils if ($publicId) { $data['id'] = $publicId; } - + return $data; } @@ -753,7 +769,7 @@ class Utils $str .= 'ENTITY_DELETED '; } } - + if ($model->deleted_at && $model->deleted_at != '0000-00-00') { $str .= 'ENTITY_ARCHIVED '; } @@ -773,7 +789,7 @@ class Utils fwrite($output, "\n"); } - + public static function getFirst($values) { if (is_array($values)) { @@ -938,7 +954,7 @@ class Utils if (!preg_match("~^(?:f|ht)tps?://~i", $url)) { $url = "http://" . $url; } - + return $url; } } 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/Models/Client.php b/app/Models/Client.php index 7a83493ba2c2..6655a9ef2e67 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -93,7 +93,7 @@ class Client extends EntityModel public function user() { - return $this->belongsTo('App\Models\User'); + return $this->belongsTo('App\Models\User')->withTrashed(); } public function invoices() diff --git a/app/Models/Expense.php b/app/Models/Expense.php new file mode 100644 index 000000000000..e79c348ceaeb --- /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/Invoice.php b/app/Models/Invoice.php index 5fb63c00d806..39fedd489731 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; @@ -383,6 +384,7 @@ class Invoice extends EntityModel implements BalanceAffecting 'has_tasks', 'custom_text_value1', 'custom_text_value2', + 'has_expenses', ]); $this->client->setVisible([ @@ -476,7 +478,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); @@ -502,7 +504,7 @@ class Invoice extends EntityModel implements BalanceAffecting if (count($schedule) < 2) { return null; } - + return $schedule[1]->getStart(); } @@ -659,7 +661,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..bc34fbd2b1dc --- /dev/null +++ b/app/Models/Vendor.php @@ -0,0 +1,242 @@ + '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/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/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/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/ExpenseRepository.php b/app/Ninja/Repositories/ExpenseRepository.php new file mode 100644 index 000000000000..213e24dae310 --- /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.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.foreign_amount', + 'expenses.currency_id', + 'expenses.deleted_at', + 'expenses.exchange_rate', + 'expenses.expense_date', + 'expenses.id', + 'expenses.is_deleted', + 'expenses.private_notes', + 'expenses.public_id', + 'expenses.invoice_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['foreign_amount'])) + $expense->foreign_amount = Utils::parseFloat($input['foreign_amount']); + + $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->foreign_amount = ($expense->amount / 100) * $expense->exchange_rate; + + $expense->should_be_invoiced = isset($input['should_be_invoiced']) ? true : false; + if(isset($input['client'])) { + $expense->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..9c1a6dcc0356 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,13 @@ 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->client_id = $invoice->client_id; + $expense->save(); + } + if ($item['product_key']) { $productKey = trim($item['product_key']); if (\Auth::user()->account->update_products && ! strtotime($productKey)) { @@ -406,7 +417,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 +656,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/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..c7fc5bb90b5f --- /dev/null +++ b/app/Ninja/Repositories/VendorRepository.php @@ -0,0 +1,90 @@ +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.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..c1714b27a120 --- /dev/null +++ b/app/Ninja/Transformers/VendorTransformer.php @@ -0,0 +1,82 @@ +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, + 'is_deleted' => (bool) $vendor->is_deleted, + 'vat_number' => $vendor->vat_number, + 'id_number' => $vendor->id_number, + '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 .= '
  • '.trans("texts.credits").'
  • '.trans("texts.new_credit").'
  • '; - } + } else if ($type == ENTITY_EXPENSE) { + $str .= '
  • +
  • '.trans("texts.vendors").'
  • +
  • '.trans("texts.new_vendor").'
  • '; + } $str .= ' '; diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 37a09fbc44ca..bb4e94d79546 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -137,7 +137,7 @@ class EventServiceProvider extends ServiceProvider { 'App\Events\UserSettingsChanged' => [ 'App\Listeners\HandleUserSettingsChanged', ], - + ]; /** diff --git a/app/Services/ExpenseService.php b/app/Services/ExpenseService.php new file mode 100644 index 000000000000..c6de78685273 --- /dev/null +++ b/app/Services/ExpenseService.php @@ -0,0 +1,161 @@ +expenseRepo = $expenseRepo; + $this->datatableService = $datatableService; + } + + protected function getRepo() + { + return $this->expenseRepo; + } + + public function save($data) + { + return $this->expenseRepo->save($data); + } + + public function getDatatable($search) + { + $query = $this->expenseRepo->find($search); + + return $this->createDatatable(ENTITY_EXPENSE, $query); + } + + public function getDatatableVendor($vendorPublicId) + { + $query = $this->expenseRepo->findVendor($vendorPublicId); + return $this->datatableService->createDatatable(ENTITY_EXPENSE, + $query, + $this->getDatatableColumnsVendor(ENTITY_EXPENSE,false), + $this->getDatatableActionsVendor(ENTITY_EXPENSE), + false); + } + + protected function getDatatableColumns($entityType, $hideClient) + { + return [ + [ + 'vendor_name', + function ($model) + { + if($model->vendor_public_id) { + return link_to("vendors/{$model->vendor_public_id}", $model->vendor_name); + } else { + return 'No vendor' ; + } + } + ], + [ + 'expense_date', + function ($model) { + return Utils::fromSqlDate($model->expense_date); + } + ], + [ + 'amount', + function ($model) { + return Utils::formatMoney($model->amount, false, false); + } + ], + [ + 'public_notes', + function ($model) { + return $model->public_notes != null ? substr($model->public_notes, 0, 100) : ''; + } + ], + [ + 'invoice_id', + function ($model) { + return self::getStatusLabel($model->invoice_id, $model->should_be_invoiced); + } + ], + ]; + } + + protected function getDatatableColumnsVendor($entityType, $hideClient) + { + return [ + [ + 'expense_date', + function ($model) { + return $model->expense_date; + } + ], + [ + 'amount', + function ($model) { + return Utils::formatMoney($model->amount, false, false); + } + ], + [ + 'public_notes', + function ($model) { + return $model->public_notes != null ? $model->public_notes : ''; + } + ], + [ + 'invoice_id', + function ($model) { + return ''; + } + ], + ]; + } + + protected function getDatatableActions($entityType) + { + return [ + [ + trans('texts.edit_expense'), + function ($model) { + return URL::to("expenses/{$model->public_id}/edit") ; + } + ], + /* + [ + trans('texts.invoice_expense'), + function ($model) { + return URL::to("expense/invoice/{$model->public_id}") . '?client=1'; + } + ], + */ + ]; + } + + protected function getDatatableActionsVendor($entityType) + { + return []; + } + + private function getStatusLabel($invoiceId, $shouldBeInvoiced) + { + if ($invoiceId) { + $label = trans('texts.invoiced'); + $class = 'success'; + } elseif ($shouldBeInvoiced) { + $label = trans('texts.pending'); + $class = 'warning'; + } else { + $label = trans('texts.logged'); + $class = 'primary'; + } + + return "

    $label

    "; + } + +} diff --git a/app/Services/PaymentTermService.php b/app/Services/PaymentTermService.php new file mode 100644 index 000000000000..bfcf670475b9 --- /dev/null +++ b/app/Services/PaymentTermService.php @@ -0,0 +1,60 @@ +paymentTermRepo = $paymentTermRepo; + $this->datatableService = $datatableService; + } + + protected function getRepo() + { + return $this->paymentTermRepo; + } + + public function getDatatable($accountId = 0) + { + $query = $this->paymentTermRepo->find(); + + return $this->createDatatable(ENTITY_PAYMENT_TERM, $query, false); + } + + protected function getDatatableColumns($entityType, $hideClient) + { + return [ + [ + 'name', + function ($model) { + return link_to("payment_terms/{$model->public_id}/edit", $model->name); + } + ], + [ + 'days', + function ($model) { + return $model->num_days; + } + ] + ]; + } + + protected function getDatatableActions($entityType) + { + return [ + [ + uctrans('texts.edit_payment_terms'), + function ($model) { + return URL::to("payment_terms/{$model->public_id}/edit"); + } + ] + ]; + } +} \ No newline at end of file diff --git a/app/Services/VendorService.php b/app/Services/VendorService.php new file mode 100644 index 000000000000..fccd9c83c0a7 --- /dev/null +++ b/app/Services/VendorService.php @@ -0,0 +1,91 @@ +vendorRepo = $vendorRepo; + $this->ninjaRepo = $ninjaRepo; + $this->datatableService = $datatableService; + } + + protected function getRepo() + { + return $this->vendorRepo; + } + + public function save($data) + { + if (Auth::user()->account->isNinjaAccount() && isset($data['pro_plan_paid'])) { + $this->ninjaRepo->updateProPlanPaid($data['public_id'], $data['pro_plan_paid']); + } + + return $this->vendorRepo->save($data); + } + + public function getDatatable($search) + { + $query = $this->vendorRepo->find($search); + + return $this->createDatatable(ENTITY_VENDOR, $query); + } + + protected function getDatatableColumns($entityType, $hideVendor) + { + return [ + [ + 'name', + function ($model) { + return link_to("vendors/{$model->public_id}", $model->name ?: ''); + } + ], + [ + 'first_name', + function ($model) { + return link_to("vendors/{$model->public_id}", $model->first_name.' '.$model->last_name); + } + ], + [ + 'email', + function ($model) { + return link_to("vendors/{$model->public_id}", $model->email ?: ''); + } + ], + [ + 'vendors.created_at', + function ($model) { + return Utils::timestampToDateString(strtotime($model->created_at)); + } + ], + ]; + } + + protected function getDatatableActions($entityType) + { + return [ + [ + trans('texts.edit_vendor'), + function ($model) { + return URL::to("vendors/{$model->public_id}/edit"); + } + ], + [], + [ + trans('texts.enter_expense'), + function ($model) { + return URL::to("expenses/create/{$model->public_id}"); + } + ] + ]; + } +} diff --git a/composer.json b/composer.json index 21a91f6e18a7..cac5f1780f21 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "omnipay/omnipay": "~2.3.0", "intervention/image": "dev-master", "webpatser/laravel-countries": "dev-master", - "barryvdh/laravel-ide-helper": "2.0.x", + "barryvdh/laravel-ide-helper": "^2.1", "doctrine/dbal": "2.5.x", "jsanc623/phpbenchtime": "2.x", "lokielse/omnipay-alipay": "dev-master", diff --git a/composer.lock b/composer.lock index abef59303432..cb674180854c 100644 --- a/composer.lock +++ b/composer.lock @@ -8851,4 +8851,4 @@ "prefer-lowest": false, "platform": [], "platform-dev": [] -} +} \ No newline at end of file diff --git a/config/app.php b/config/app.php index a7e071e62898..cd55e8ee965a 100644 --- a/config/app.php +++ b/config/app.php @@ -161,6 +161,8 @@ return [ 'App\Providers\ConfigServiceProvider', 'App\Providers\EventServiceProvider', 'App\Providers\RouteServiceProvider', + + 'Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider', ], /* diff --git a/config/debugbar.php b/config/debugbar.php index a389dd17da36..57ec1876f5fc 100644 --- a/config/debugbar.php +++ b/config/debugbar.php @@ -89,7 +89,7 @@ return array( 'files' => false, // Show the included files 'config' => false, // Display config settings 'auth' => false, // Display Laravel authentication status - 'session' => false, // Display session data in a separate tab + 'session' => true, // Display session data in a separate tab ), /* diff --git a/database/.gitignore b/database/.gitignore index 9b1dffd90fdc..bb55a4b89e24 100644 --- a/database/.gitignore +++ b/database/.gitignore @@ -1 +1,2 @@ *.sqlite +*-komodoproject \ No newline at end of file diff --git a/database/migrations/2016_01_04_175228_create_vendors_table.php b/database/migrations/2016_01_04_175228_create_vendors_table.php new file mode 100644 index 000000000000..68cc58c3dc80 --- /dev/null +++ b/database/migrations/2016_01_04_175228_create_vendors_table.php @@ -0,0 +1,135 @@ +increments('id'); + $table->timestamps(); + $table->softDeletes(); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('currency_id')->nullable(); + $table->string('name')->nullable(); + $table->string('address1'); + $table->string('address2'); + $table->string('city'); + $table->string('state'); + $table->string('postal_code'); + $table->unsignedInteger('country_id')->nullable(); + $table->string('work_phone'); + $table->text('private_notes'); + $table->string('website'); + $table->tinyInteger('is_deleted')->default(0); + $table->integer('public_id')->default(0); + $table->string('vat_number')->nullable(); + $table->string('id_number')->nullable(); + + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('country_id')->references('id')->on('countries'); + $table->foreign('currency_id')->references('id')->on('currencies'); + }); + + Schema::create('vendor_contacts', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('vendor_id')->index(); + $table->timestamps(); + $table->softDeletes(); + + $table->boolean('is_primary')->default(0); + $table->string('first_name')->nullable(); + $table->string('last_name')->nullable(); + $table->string('email')->nullable(); + $table->string('phone')->nullable(); + + $table->foreign('vendor_id')->references('id')->on('vendors')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + + $table->unsignedInteger('public_id')->nullable(); + $table->unique(array('account_id', 'public_id')); + }); + + Schema::create('expenses', function (Blueprint $table) { + $table->increments('id'); + $table->timestamps(); + $table->softDeletes(); + + $table->unsignedInteger('account_id')->index(); + $table->unsignedInteger('vendor_id')->nullable(); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('invoice_id')->nullable(); + $table->unsignedInteger('client_id')->nullable(); + $table->boolean('is_deleted')->default(false); + $table->decimal('amount', 13, 2); + $table->decimal('foreign_amount', 13, 2); + $table->decimal('exchange_rate', 13, 2); + $table->date('expense_date')->nullable(); + $table->text('private_notes'); + $table->text('public_notes'); + $table->integer('currency_id', false, true)->nullable(); + $table->boolean('should_be_invoiced')->default(true); + + // Relations + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + + // Indexes + $table->unsignedInteger('public_id')->index(); + $table->unique(array('account_id', 'public_id')); + }); + + Schema::table('payment_terms', function (Blueprint $table) { + $table->timestamps(); + $table->softDeletes(); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('account_id'); + $table->integer('public_id')->default(0); + + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + + $table->unsignedInteger('public_id')->index(); + $table->unique(array('account_id', 'public_id')); + }); + + // Update public id + $paymentTerms = DB::table('payment_terms') + ->where('public_id', '=', 0) + ->select('id', 'public_id') + ->get(); + $i = 1; + foreach ($paymentTerms as $pTerm) { + $data = ['public_id' => $i]; + DB::table('paymet_terms')->where('id', $pTerm->id)->update($data); + } + + Schema::table('invoices', function (Blueprint $table) { + $table->boolean('has_expenses')->default(false); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('expenses'); + Schema::drop('vendor_contacts'); + Schema::drop('vendors'); + } +} diff --git a/invoiceninja.komodoproject b/invoiceninja.komodoproject new file mode 100644 index 000000000000..16111c8ee84d --- /dev/null +++ b/invoiceninja.komodoproject @@ -0,0 +1,43 @@ + + + + + + + + + + + + + PHP + + + + application/x-www-form-urlencoded + GET + 1 + 0 + 0 + + + + + PHP + %25d/%25m/%25Y %25H:%25M:%25S + 1 + + default + default + PHP + Project + None + None + None + c:\wamp\bin\php\php5.6.15\php.exe + vendor;C:/webdev/invoiceninja/app + 1 + None + None + + diff --git a/public/js/built.js b/public/js/built.js index a26e6f108f0e..22b404a0efeb 100644 --- a/public/js/built.js +++ b/public/js/built.js @@ -30298,6 +30298,17 @@ function getClientDisplayName(client) return ''; } +function getVendorDisplayName(vendor) +{ + var contact = vendor.contacts ? vendor.vendorcontacts[0] : false; + if (vendor.name) { + return vendor.name; + } else if (contact) { + return getContactDisplayName(contact); + } + return ''; +} + function populateInvoiceComboboxes(clientId, invoiceId) { var clientMap = {}; var invoiceMap = {}; diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 4ede9c4f6322..9110ad91dc79 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -256,6 +256,14 @@ return array( 'deleted_credits' => 'Successfully deleted :count credits', 'imported_file' => 'Successfully imported file', + 'updated_vendor' => 'Successfully updated vendor', + 'created_vendor' => 'Successfully created vendor', + 'archived_vendor' => 'Successfully archived vendor', + 'archived_vendors' => 'Successfully archived :count vendors', + 'deleted_vendor' => 'Successfully deleted vendor', + 'deleted_vendors' => 'Successfully deleted :count vendors', + + // Emails 'confirmation_subject' => 'Invoice Ninja Account Confirmation', 'confirmation_header' => 'Account Confirmation', @@ -884,6 +892,14 @@ return array( 'activity_27' => ':user restored payment :payment', 'activity_28' => ':user restored :credit credit', 'activity_29' => ':contact approved quote :quote', + 'activity_30' => ':user created :vendor', + 'activity_31' => ':user created :vendor', + 'activity_32' => ':user created :vendor', + 'activity_33' => ':user created :vendor', + 'activity_34' => ':user created expense :expense', + 'activity_35' => ':user created :vendor', + 'activity_36' => ':user created :vendor', + 'activity_37' => ':user created :vendor', 'payment' => 'Payment', 'system' => 'System', @@ -997,6 +1013,54 @@ return array( 'white_label_custom_css' => ':link for $'.WHITE_LABEL_PRICE.' to enable custom styling and help support our project.', 'white_label_purchase_link' => 'Purchase a white label license', + // Expense / vendor + 'expense' => 'Expense', + 'expenses' => 'Expenses', + 'new_expense' => 'Create Expense', + 'vendors' => 'Vendors', + 'new_vendor' => 'Create Vendor', + 'payment_terms_net' => 'Net', + 'vendor' => 'Vendor', + 'edit_vendor' => 'Edit Vendor', + 'archive_vendor' => 'Archive Vendor', + 'delete_vendor' => 'Delete Vendor', + 'view_vendor' => 'View Vendor', + 'deleted_expense' => 'Successfully deleted expense', + 'archived_expense' => 'Successfully archived expense', + + // Expenses + 'expense_amount' => 'Expense Amount', + 'expense_balance' => 'Expense Balance', + 'expense_date' => 'Expense Date', + 'expense_should_be_invoiced' => 'Should this expense be invoiced?', + 'public_notes' => 'Public Notes', + 'foreign_amount' => 'Foreign Amount', + 'exchange_rate' => 'Exchange Rate', + 'yes' => 'Yes', + 'no' => 'No', + 'should_be_invoiced' => 'Should be invoiced', + 'view_expense' => 'View expense # :expense', + 'edit_expense' => 'Edit Expense', + 'archive_expense' => 'Archive Expense', + 'delete_expense' => 'Delete Expense', + 'view_expense_num' => 'Expense # :expense', + 'updated_expense' => 'Successfully updated expense', + 'created_expense' => 'Successfully created expense', + 'enter_expense' => 'Enter Expense', + 'view' => 'View', + 'restore_expense' => 'Restore Expense', + 'invoice_expense' => 'Invoice Expense', + 'expense_error_multiple_clients' =>'The expenses can\'t belong to different clients', + 'expense_error_invoiced' => 'Expense have already been invoiced', + 'expense_error_should_not_be_invoiced' => 'Expense maked not to be invoiced', + + // Payment terms + 'num_days' => 'Number of days', + 'create_payment_term' => 'Create Payment Term', + 'edit_payment_terms' => 'Edit Payment Term', + 'edit_payment_term' => 'Edit Payment Term', + 'archive_payment_term' => 'Archive Payment Term', + // recurring due dates 'recurring_due_dates' => 'Recurring Invoice Due Dates', 'recurring_due_date_help' => '

    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') !!} + + +
    +
    +

    {!! $title !!}

    +
    +
    + + @if ($paymentTerm) + {{ Former::populate($paymentTerm) }} + @endif + + {!! Former::text('name')->label('texts.name') !!} + {!! Former::text('num_days')->label('texts.num_days') !!} + +
    +
    + + {!! Former::actions( + Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/settings/payment_terms'))->appendIcon(Icon::create('remove-circle')), + Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) + ) !!} + + {!! Former::close() !!} + + + +@stop \ No newline at end of file diff --git a/resources/views/accounts/payment_terms.blade.php b/resources/views/accounts/payment_terms.blade.php new file mode 100644 index 000000000000..f88016991f13 --- /dev/null +++ b/resources/views/accounts/payment_terms.blade.php @@ -0,0 +1,33 @@ +@extends('header') + +@section('content') + @parent + + @include('accounts.nav', ['selected' => ACCOUNT_PAYMENT_TERMS]) + + {!! Button::primary(trans('texts.create_payment_term')) + ->asLinkTo(URL::to('/payment_terms/create')) + ->withAttributes(['class' => 'pull-right']) + ->appendIcon(Icon::create('plus-sign')) !!} + + @include('partials.bulk_form', ['entityType' => ENTITY_PAYMENT_TERM]) + + {!! Datatable::table() + ->addColumn( + trans('texts.name'), + trans('texts.num_days'), + trans('texts.action')) + ->setUrl(url('api/payment_terms/')) + ->setOptions('sPaginationType', 'bootstrap') + ->setOptions('bFilter', false) + ->setOptions('bAutoWidth', false) + ->setOptions('aoColumns', [[ "sWidth"=> "40%" ], [ "sWidth"=> "40%" ], ["sWidth"=> "20%"]]) + ->setOptions('aoColumnDefs', [['bSortable'=>false, 'aTargets'=>[2]]]) + ->render('datatable') !!} + + + + +@stop \ No newline at end of file diff --git a/resources/views/expenses/edit.blade.php b/resources/views/expenses/edit.blade.php new file mode 100644 index 000000000000..1d6d2892969e --- /dev/null +++ b/resources/views/expenses/edit.blade.php @@ -0,0 +1,113 @@ +@extends('header') + +@section('head') + @parent + + +@stop + +@section('content') + + {!! Former::open($url)->addClass('warn-on-exit')->method($method) !!} + + @if ($expense) + {!! Former::populate($expense) !!} + {!! Former::populateField('should_be_invoiced', intval($expense->should_be_invoiced)) !!} + {!! Former::hidden('public_id') !!} + @endif + +
    +
    +
    +
    + {!! Former::select('vendor')->addOption('', '') + ->addGroupClass('vendor-select') !!} + {!! Former::select('client') + ->addOption('', '') + ->addGroupClass('client-select') + ->append(Former::checkbox('should_be_invoiced')->raw() . + trans('texts.invoice')) !!} + {!! Former::text('expense_date') + ->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT)) + ->addGroupClass('expense_date')->label(trans('texts.expense_date')) + ->append('') !!} + {!! Former::select('currency_id')->addOption('','') + ->placeholder($account->currency ? $account->currency->name : '') + ->fromQuery($currencies, 'name', 'id') !!} + + {!! Former::text('amount')->label(trans('texts.expense_amount')) !!} + {!! Former::text('foreign_amount') !!} + {!! Former::text('exchange_rate') !!} +
    +
    + + {!! Former::textarea('public_notes')->rows(8) !!} + {!! Former::textarea('private_notes')->rows(8) !!} +
    +
    +
    +
    + +
    + {!! Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/expenses'))->appendIcon(Icon::create('remove-circle')) !!} + {!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!} +
    + + {!! Former::close() !!} + + +@stop \ No newline at end of file diff --git a/resources/views/header.blade.php b/resources/views/header.blade.php index eb7416f1bc2c..f20244b6416c 100644 --- a/resources/views/header.blade.php +++ b/resources/views/header.blade.php @@ -375,6 +375,7 @@ {!! HTML::menu_link('task') !!} {!! HTML::menu_link('invoice') !!} {!! HTML::menu_link('payment') !!} + {!! HTML::menu_link('expense') !!} - +
    @endif @@ -72,7 +72,7 @@
    @@ -88,20 +88,20 @@    @if (Utils::isConfirmed()) - - @endif
    - +
    @@ -109,7 +109,7 @@ ->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))->appendIcon('calendar')->addGroupClass('invoice_date') !!} {!! Former::text('due_date')->data_bind("datePicker: due_date, valueUpdate: 'afterkeydown'")->label(trans("texts.{$entityType}_due_date")) ->data_date_format(Session::get(SESSION_DATE_PICKER_FORMAT, DEFAULT_DATE_PICKER_FORMAT))->appendIcon('calendar')->addGroupClass('due_date') !!} - + {!! Former::text('partial')->data_bind("value: partial, valueUpdate: 'afterkeydown'")->onchange('onPartialChange()') ->rel('tooltip')->data_toggle('tooltip')->data_placement('bottom')->title(trans('texts.partial_value')) !!}
    @@ -127,6 +127,26 @@ @if ($account->showCustomField('custom_invoice_text_label1', $invoice)) {!! Former::text('custom_text_value1')->label($account->custom_invoice_text_label1)->data_bind("value: custom_text_value1, valueUpdate: 'afterkeydown'") !!} + @endif + + @if ($entityType == ENTITY_INVOICE) +
    +
    + @if ($invoice->recurring_invoice) + {!! trans('texts.created_by_invoice', ['invoice' => link_to('/invoices/'.$invoice->recurring_invoice->public_id, trans('texts.recurring_invoice'))]) !!} + @elseif ($invoice->id) + + @if (isset($lastSent) && $lastSent) + {!! trans('texts.last_sent_on', ['date' => link_to('/invoices/'.$lastSent->public_id, $invoice->last_sent_date, ['id' => 'lastSent'])]) !!}
    + @endif + @if ($invoice->is_recurring && $invoice->getNextSendDate()) + {!! trans('texts.next_send_on', ['date' => ''.$account->formatDate($invoice->getNextSendDate()). + '']) !!} + @endif +
    + @endif +
    +
    @endif
    @@ -135,7 +155,7 @@ {!! Former::text('invoice_number') ->label(trans("texts.{$entityType}_number_short")) ->data_bind("value: invoice_number, valueUpdate: 'afterkeydown'") !!} - + {!! Former::checkbox('auto_bill') ->label(trans('texts.auto_bill')) @@ -147,15 +167,15 @@ ->addGroupClass('discount-group')->type('number')->min('0')->step('any')->append( Former::select('is_amount_discount')->addOption(trans('texts.discount_percent'), '0') ->addOption(trans('texts.discount_amount'), '1')->data_bind("value: is_amount_discount")->raw() - ) !!} + ) !!} @if ($account->showCustomField('custom_invoice_text_label2', $invoice)) {!! Former::text('custom_text_value2')->label($account->custom_invoice_text_label2)->data_bind("value: custom_text_value2, valueUpdate: 'afterkeydown'") !!} @endif - + @if ($entityType == ENTITY_INVOICE)
    -
    +
    @if ($invoice->recurring_invoice) {!! trans('texts.created_by_invoice', ['invoice' => link_to('/invoices/'.$invoice->recurring_invoice->public_id, trans('texts.recurring_invoice'))]) !!} @elseif ($invoice->id) @@ -207,16 +227,17 @@ !!} - + - - @@ -228,7 +249,7 @@
    - @@ -252,7 +273,7 @@
    {!! Former::textarea('public_notes')->data_bind("value: wrapped_notes, valueUpdate: 'afterkeydown'") - ->label(null)->style('resize: none; min-width: 450px;')->rows(3) !!} + ->label(null)->style('resize: none; min-width: 450px;')->rows(3) !!}
    {!! Former::textarea('terms')->data_bind("value:wrapped_terms, placeholder: terms_placeholder, valueUpdate: 'afterkeydown'") @@ -323,7 +344,7 @@ - + @if (!$account->hide_quantity) {{ trans('texts.tax') }} @endif @@ -376,7 +397,7 @@
    - +

     

    @@ -390,6 +411,7 @@ {!! Former::text('is_quote')->data_bind('value: is_quote') !!} {!! Former::text('has_tasks')->data_bind('value: has_tasks') !!} {!! Former::text('data')->data_bind('value: ko.mapping.toJSON(model)') !!} + {!! Former::text('has_expenses')->data_bind('value: has_expenses') !!} {!! Former::text('pdfupload') !!}
    @@ -401,12 +423,12 @@ @if (!Utils::isPro() || \App\Models\InvoiceDesign::count() == COUNT_FREE_DESIGNS_SELF_HOST) {!! Former::select('invoice_design_id')->style('display:inline;width:150px;background-color:white !important')->raw()->fromQuery($invoiceDesigns, 'name', 'id')->data_bind("value: invoice_design_id")->addOption(trans('texts.more_designs') . '...', '-1') !!} - @else + @else {!! Former::select('invoice_design_id')->style('display:inline;width:150px;background-color:white !important')->raw()->fromQuery($invoiceDesigns, 'name', 'id')->data_bind("value: invoice_design_id") !!} @endif - {!! Button::primary(trans('texts.download_pdf'))->withAttributes(array('onclick' => 'onDownloadClick()'))->appendIcon(Icon::create('download-alt')) !!} - + {!! Button::primary(trans('texts.download_pdf'))->withAttributes(array('onclick' => 'onDownloadClick()'))->appendIcon(Icon::create('download-alt')) !!} + @if ($invoice->isClientTrashed()) @elseif ($invoice->trashed()) @@ -454,22 +476,24 @@ ->label('client_name') !!} + {!! Former::text('client[id_number]') ->label('id_number') ->data_bind("value: id_number, valueUpdate: 'afterkeydown'") !!} {!! Former::text('client[vat_number]') ->label('vat_number') ->data_bind("value: vat_number, valueUpdate: 'afterkeydown'") !!} - + {!! Former::text('client[website]') ->label('website') ->data_bind("value: website, valueUpdate: 'afterkeydown'") !!} {!! Former::text('client[work_phone]') ->label('work_phone') ->data_bind("value: work_phone, valueUpdate: 'afterkeydown'") !!} + - @if (Auth::user()->isPro()) + @if (Auth::user()->isPro()) @if ($account->custom_client_label1) {!! Former::text('client[custom_value1]') ->label($account->custom_client_label1) @@ -515,11 +539,11 @@ {!! Former::hidden('public_id')->data_bind("value: public_id, valueUpdate: 'afterkeydown', attr: {name: 'client[contacts][' + \$index() + '][public_id]'}") !!} - {!! Former::text('first_name')->data_bind("value: first_name, valueUpdate: 'afterkeydown', + {!! Former::text('first_name')->data_bind("value: first_name, valueUpdate: 'afterkeydown', attr: {name: 'client[contacts][' + \$index() + '][first_name]'}") !!} {!! Former::text('last_name')->data_bind("value: last_name, valueUpdate: 'afterkeydown', attr: {name: 'client[contacts][' + \$index() + '][last_name]'}") !!} - {!! Former::text('email')->data_bind("value: email, valueUpdate: 'afterkeydown', + {!! Former::text('email')->data_bind("value: email, valueUpdate: 'afterkeydown', attr: {name: 'client[contacts][' + \$index() + '][email]', id:'email'+\$index()}") ->addClass('client-email') !!} {!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown', @@ -529,7 +553,7 @@
    {!! link_to('#', trans('texts.remove_contact').' -', array('data-bind'=>'click: $parent.removeContact')) !!} - + {!! link_to('#', trans('texts.add_contact').' +', array('data-bind'=>'click: $parent.addContact')) !!} @@ -552,7 +576,7 @@ ->placeholder($account->language ? $account->language->name : '') ->label(trans('texts.language_id')) ->data_bind('value: language_id') - ->fromQuery($languages, 'name', 'id') !!} + ->fromQuery($languages, 'name', 'id') !!} {!! Former::select('client[payment_terms]')->addOption('','')->data_bind('value: payment_terms') ->fromQuery($paymentTerms, 'name', 'num_days') ->label(trans('texts.payment_terms')) @@ -577,9 +601,9 @@   - +
    - +
    @@ -599,11 +623,11 @@ - + - + @@ -639,16 +663,16 @@ @include('invoices.knockout') \ No newline at end of file + diff --git a/resources/views/list.blade.php b/resources/views/list.blade.php index 1b7ac5b630c9..98c309a5db36 100644 --- a/resources/views/list.blade.php +++ b/resources/views/list.blade.php @@ -12,6 +12,9 @@ @if ($entityType == ENTITY_TASK) {!! Button::primary(trans('texts.invoice'))->withAttributes(['class'=>'invoice', 'onclick' =>'submitForm("invoice")'])->appendIcon(Icon::create('check')) !!} @endif + @if ($entityType == ENTITY_EXPENSE) + {!! Button::primary(trans('texts.invoice'))->withAttributes(['class'=>'invoice', 'onclick' =>'submitForm("invoice")'])->appendIcon(Icon::create('check')) !!} + @endif {!! DropdownButton::normal(trans('texts.archive'))->withContents([ ['label' => trans('texts.archive_'.$entityType), 'url' => 'javascript:submitForm("archive")'], diff --git a/resources/views/vendor.blade.php b/resources/views/vendor.blade.php new file mode 100644 index 000000000000..3248b39f4d3b --- /dev/null +++ b/resources/views/vendor.blade.php @@ -0,0 +1,99 @@ +{!!-- // vendor --!!} +
    +
    + + {!! Former::legend('Organization') !!} + {!! Former::text('name') !!} + {!! Former::text('id_number') !!} + {!! Former::text('vat_number') !!} + {!! Former::text('work_phone')->label('Phone') !!} + {!! Former::textarea('notes') !!} + + + {!! Former::legend('Address') !!} + {!! Former::text('address1')->label('Street') !!} + {!! Former::text('address2')->label('Apt/Floor') !!} + {!! Former::text('city') !!} + {!! Former::text('state') !!} + {!! Former::text('postal_code') !!} + {!! Former::select('country_id')->addOption('','')->label('Country') + ->fromQuery($countries, 'name', 'id') !!} + + +
    +
    + + {!! Former::legend('VendorContacts') !!} +
    + {!! Former::hidden('public_id')->data_bind("value: public_id, valueUpdate: 'afterkeydown'") !!} + {!! Former::text('first_name')->data_bind("value: first_name, valueUpdate: 'afterkeydown'") !!} + {!! Former::text('last_name')->data_bind("value: last_name, valueUpdate: 'afterkeydown'") !!} + {!! Former::text('email')->data_bind("value: email, valueUpdate: 'afterkeydown'") !!} + {!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown'") !!} + +
    +
    + + {!! link_to('#', 'Remove contact', array('data-bind'=>'click: $parent.removeContact')) !!} + + + {!! link_to('#', 'Add contact', array('onclick'=>'return addContact()')) !!} + +
    +
    + +
    + +
    +
    + + +{!! Former::hidden('data')->data_bind("value: ko.toJSON(model)") !!} + + + diff --git a/resources/views/vendors/edit.blade.php b/resources/views/vendors/edit.blade.php new file mode 100644 index 000000000000..e0286c8d5366 --- /dev/null +++ b/resources/views/vendors/edit.blade.php @@ -0,0 +1,218 @@ +@extends('header') + + +@section('onReady') + $('input#name').focus(); +@stop + +@section('content') + +@if ($errors->first('vendorcontacts')) +
    {{ trans($errors->first('vendorcontacts')) }}
    +@endif + +
    + + {!! Former::open($url) + ->autocomplete('off') + ->rules( + ['email' => 'email'] + )->addClass('col-md-12 warn-on-exit') + ->method($method) !!} + + @include('partials.autocomplete_fix') + + @if ($vendor) + {!! Former::populate($vendor) !!} + {!! Former::hidden('public_id') !!} + @endif + +
    +
    + + +
    +
    +

    {!! trans('texts.organization') !!}

    +
    +
    + + {!! Former::text('name')->data_bind("attr { placeholder: placeholderName }") !!} + {!! Former::text('id_number') !!} + {!! Former::text('vat_number') !!} + {!! Former::text('website') !!} + {!! Former::text('work_phone') !!} + +
    +
    + +
    +
    +

    {!! trans('texts.address') !!}

    +
    +
    + + {!! Former::text('address1') !!} + {!! Former::text('address2') !!} + {!! Former::text('city') !!} + {!! Former::text('state') !!} + {!! Former::text('postal_code') !!} + {!! Former::select('country_id')->addOption('','') + ->fromQuery($countries, 'name', 'id') !!} + +
    +
    +
    +
    + + +
    +
    +

    {!! trans('texts.contacts') !!}

    +
    +
    + +
    + {!! Former::hidden('public_id')->data_bind("value: public_id, valueUpdate: 'afterkeydown', + attr: {name: 'vendorcontacts[' + \$index() + '][public_id]'}") !!} + {!! Former::text('first_name')->data_bind("value: first_name, valueUpdate: 'afterkeydown', + attr: {name: 'vendorcontacts[' + \$index() + '][first_name]'}") !!} + {!! Former::text('last_name')->data_bind("value: last_name, valueUpdate: 'afterkeydown', + attr: {name: 'vendorcontacts[' + \$index() + '][last_name]'}") !!} + {!! Former::text('email')->data_bind("value: email, valueUpdate: 'afterkeydown', + attr: {name: 'vendorcontacts[' + \$index() + '][email]', id:'email'+\$index()}") !!} + {!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown', + attr: {name: 'vendorcontacts[' + \$index() + '][phone]'}") !!} + +
    +
    + + {!! link_to('#', trans('texts.remove_contact').' -', array('data-bind'=>'click: $parent.removeContact')) !!} + + + {!! link_to('#', trans('texts.add_contact').' +', array('onclick'=>'return addContact()')) !!} + +
    +
    +
    +
    +
    + + +
    +
    +

    {!! trans('texts.additional_info') !!}

    +
    +
    + + {!! Former::select('currency_id')->addOption('','') + ->placeholder($account->currency ? $account->currency->name : '') + ->fromQuery($currencies, 'name', 'id') !!} + {!! Former::textarea('private_notes')->rows(6) !!} + + + @if (isset($proPlanPaid)) + {!! Former::populateField('pro_plan_paid', $proPlanPaid) !!} + {!! Former::text('pro_plan_paid') + ->data_date_format('yyyy-mm-dd') + ->addGroupClass('pro_plan_paid_date') + ->append('') !!} + + @endif + +
    +
    + +
    +
    + + + {!! Former::hidden('data')->data_bind("value: ko.toJSON(model)") !!} + + + +
    + {!! Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/vendors/' . ($vendor ? $vendor->public_id : '')))->appendIcon(Icon::create('remove-circle')) !!} + {!! Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk')) !!} +
    + + {!! Former::close() !!} +
    +@stop diff --git a/resources/views/vendors/show.blade.php b/resources/views/vendors/show.blade.php new file mode 100644 index 000000000000..1cff74275f69 --- /dev/null +++ b/resources/views/vendors/show.blade.php @@ -0,0 +1,258 @@ +@extends('header') + +@section('head') + @parent + + @if ($vendor->hasAddress()) + + + + @endif +@stop + + +@section('content') + +
    + {!! Former::open('vendors/bulk')->addClass('mainForm') !!} +
    + {!! Former::text('action') !!} + {!! Former::text('public_id')->value($vendor->public_id) !!} +
    + + @if ($vendor->trashed()) + {!! Button::primary(trans('texts.restore_vendor'))->withAttributes(['onclick' => 'onRestoreClick()']) !!} + @else + {!! DropdownButton::normal(trans('texts.edit_vendor')) + ->withAttributes(['class'=>'normalDropDown']) + ->withContents([ + ['label' => trans('texts.archive_vendor'), 'url' => "javascript:onArchiveClick()"], + ['label' => trans('texts.delete_vendor'), 'url' => "javascript:onDeleteClick()"], + ] + )->split() !!} + + {!! DropdownButton::primary(trans('texts.new_expense')) + ->withAttributes(['class'=>'primaryDropDown']) + ->withContents($actionLinks)->split() !!} + @endif + {!! Former::close() !!} + +
    + + +

    {{ $vendor->getDisplayName() }}

    +
    +
    +
    +
    +

    {{ trans('texts.details') }}

    + @if ($vendor->id_number) +

    {{ trans('texts.id_number').': '.$vendor->id_number }}

    + @endif + @if ($vendor->vat_number) +

    {{ trans('texts.vat_number').': '.$vendor->vat_number }}

    + @endif + + @if ($vendor->address1) + {{ $vendor->address1 }}
    + @endif + @if ($vendor->address2) + {{ $vendor->address2 }}
    + @endif + @if ($vendor->getCityState()) + {{ $vendor->getCityState() }}
    + @endif + @if ($vendor->country) + {{ $vendor->country->name }}
    + @endif + + @if ($vendor->account->custom_vendor_label1 && $vendor->custom_value1) + {{ $vendor->account->custom_vendor_label1 . ': ' . $vendor->custom_value1 }}
    + @endif + @if ($vendor->account->custom_vendor_label2 && $vendor->custom_value2) + {{ $vendor->account->custom_vendor_label2 . ': ' . $vendor->custom_value2 }}
    + @endif + + @if ($vendor->work_phone) + {{ $vendor->work_phone }} + @endif + + @if ($vendor->private_notes) +

    {{ $vendor->private_notes }}

    + @endif + + @if ($vendor->vendor_industry) + {{ $vendor->vendor_industry->name }}
    + @endif + @if ($vendor->vendor_size) + {{ $vendor->vendor_size->name }}
    + @endif + + @if ($vendor->website) +

    {!! Utils::formatWebsite($vendor->website) !!}

    + @endif + + @if ($vendor->language) +

    {{ $vendor->language->name }}

    + @endif + +

    {{ $vendor->payment_terms ? trans('texts.payment_terms') . ": " . trans('texts.payment_terms_net') . " " . $vendor->payment_terms : '' }}

    +
    + +
    +

    {{ trans('texts.contacts') }}

    + @foreach ($vendor->vendorcontacts as $contact) + @if ($contact->first_name || $contact->last_name) + {{ $contact->first_name.' '.$contact->last_name }}
    + @endif + @if ($contact->email) + {!! HTML::mailto($contact->email, $contact->email) !!}
    + @endif + @if ($contact->phone) + {{ $contact->phone }}
    + @endif + @endforeach +
    + +
    +

    {{ trans('texts.standing') }} + + + + + +
    {{ trans('texts.balance') }}{{ Utils::formatMoney($totalexpense, $vendor->getCurrencyId()) }}
    +

    +
    +
    +
    +
    + + @if ($vendor->hasAddress()) +
    +
    + @endif + + + +
    +
    + {!! Datatable::table() + ->addColumn( + trans('texts.expense_date'), + trans('texts.amount'), + trans('texts.public_notes')) + ->setUrl(url('api/expenseVendor/' . $vendor->public_id)) + ->setCustomValues('entityType', 'expenses') + ->setOptions('sPaginationType', 'bootstrap') + ->setOptions('bFilter', false) + ->setOptions('aaSorting', [['0', 'asc']]) + ->render('datatable') + !!} +
    +
    + + + +@stop