Merge: Bugfixed version of Vendor / Expense module (#635)

This commit is contained in:
Hillel Coren 2016-01-21 00:09:10 +02:00
commit dff7ad91c8
84 changed files with 3952 additions and 156 deletions

2
.gitignore vendored
View File

@ -34,3 +34,5 @@ tests/_bootstrap.php
# composer stuff
/c3.php
_ide_helper.php

View File

@ -0,0 +1,22 @@
<?php namespace App\Events;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class ExpenseWasArchived extends Event
{
use SerializesModels;
public $expense;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($expense)
{
$this->expense = $expense;
}
}

View File

@ -0,0 +1,21 @@
<?php namespace App\Events;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class ExpenseWasCreated extends Event
{
use SerializesModels;
public $expense;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($expense)
{
$this->expense = $expense;
}
}

View File

@ -0,0 +1,23 @@
<?php namespace App\Events;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class ExpenseWasDeleted extends Event {
// Expenses
use SerializesModels;
public $expense;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($expense)
{
$this->expense = $expense;
}
}

View File

@ -0,0 +1,23 @@
<?php namespace App\Events;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class ExpenseWasRestored extends Event {
// Expenses
use SerializesModels;
public $expense;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($expense)
{
$this->expense = $expense;
}
}

View File

@ -0,0 +1,21 @@
<?php namespace App\Events;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class ExpenseWasUpdated extends Event
{
use SerializesModels;
public $expense;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($expense)
{
$this->expense = $expense;
}
}

View File

@ -0,0 +1,22 @@
<?php namespace App\Events;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class VendorWasArchived extends Event
{
// vendor
use SerializesModels;
public $vendor;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($vendor)
{
$this->vendor = $vendor;
}
}

View File

@ -0,0 +1,22 @@
<?php namespace App\Events;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class VendorWasCreated extends Event
{
// vendor
use SerializesModels;
public $vendor;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($vendor)
{
$this->vendor = $vendor;
}
}

View File

@ -0,0 +1,22 @@
<?php namespace App\Events;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class VendorWasDeleted extends Event
{
// vendor
use SerializesModels;
public $vendor;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($vendor)
{
$this->vendor = $vendor;
}
}

View File

@ -0,0 +1,22 @@
<?php namespace App\Events;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class VendorWasRestored extends Event
{
// vendor
use SerializesModels;
public $vendor;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($vendor)
{
$this->vendor = $vendor;
}
}

View File

@ -0,0 +1,21 @@
<?php namespace App\Events;
// vendor
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class VendorWasUpdated extends Event
{
use SerializesModels;
public $vendor;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($vendor)
{
$this->vendor = $vendor;
}
}

View File

@ -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'));
}

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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,

View File

@ -0,0 +1,226 @@
<?php namespace App\Http\Controllers;
use Debugbar;
use DB;
use Auth;
use Datatable;
use Utils;
use View;
use URL;
use Validator;
use Input;
use Session;
use Redirect;
use Cache;
use App\Models\Vendor;
use App\Models\Expense;
use App\Models\Client;
use App\Services\ExpenseService;
use App\Ninja\Repositories\ExpenseRepository;
use App\Http\Requests\CreateExpenseRequest;
use App\Http\Requests\UpdateExpenseRequest;
class ExpenseController extends BaseController
{
// Expenses
protected $expenseRepo;
protected $expenseService;
public function __construct(ExpenseRepository $expenseRepo, ExpenseService $expenseService)
{
parent::__construct();
$this->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");
}
}

View File

@ -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;
}
}

View File

@ -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,
];
}

View File

@ -0,0 +1,103 @@
<?php namespace App\Http\Controllers;
use Auth;
use Str;
use DB;
use Datatable;
use Utils;
use URL;
use View;
use Input;
use Session;
use Redirect;
use App\Models\PaymentTerm;
use App\Ninja\Repositories\VendorRepository;
use App\Services\PaymentService;
use App\Services\PaymentTermService;
class PaymentTermController extends BaseController
{
protected $paymentTermService;
public function __construct(PaymentTermService $paymentTermService)
{
parent::__construct();
$this->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);
}
}

View File

@ -0,0 +1,94 @@
<?php namespace App\Http\Controllers;
// vendor
use Utils;
use Response;
use Input;
use Auth;
use App\Models\Vendor;
use App\Ninja\Repositories\VendorRepository;
use App\Http\Requests\CreateVendorRequest;
use App\Http\Controllers\BaseAPIController;
use App\Ninja\Transformers\VendorTransformer;
class VendorApiController extends BaseAPIController
{
protected $vendorRepo;
public function __construct(VendorRepository $vendorRepo)
{
parent::__construct();
$this->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);
}
}

View File

@ -0,0 +1,204 @@
<?php namespace App\Http\Controllers;
use Auth;
use Datatable;
use Utils;
use View;
use URL;
use Validator;
use Input;
use Session;
use Redirect;
use Cache;
use App\Models\Activity;
use App\Models\Vendor;
use App\Models\Account;
use App\Models\VendorContact;
use App\Models\Size;
use App\Models\PaymentTerm;
use App\Models\Industry;
use App\Models\Currency;
use App\Models\Country;
use App\Ninja\Repositories\VendorRepository;
use App\Services\VendorService;
use App\Http\Requests\CreateVendorRequest;
use App\Http\Requests\UpdateVendorRequest;
// vendor
class VendorController extends BaseController
{
protected $vendorService;
protected $vendorRepo;
public function __construct(VendorRepository $vendorRepo, VendorService $vendorService)
{
parent::__construct();
$this->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');
}
}
}

View File

@ -0,0 +1,30 @@
<?php namespace app\Http\Requests;
use App\Http\Requests\Request;
use Illuminate\Validation\Factory;
class CreateExpenseRequest extends Request
{
// Expenses
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'amount' => 'positive',
];
}
}

View File

@ -0,0 +1,30 @@
<?php namespace app\Http\Requests;
use App\Http\Requests\Request;
use Illuminate\Validation\Factory;
class CreatePaymentTermRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'num_days' => 'required',
'name' => 'required',
];
}
}

View File

@ -0,0 +1,44 @@
<?php namespace app\Http\Requests;
// vendor
use App\Http\Requests\Request;
use Illuminate\Validation\Factory;
class CreateVendorRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'vendorcontacts' => '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()
);
}
}

View File

@ -0,0 +1,31 @@
<?php namespace app\Http\Requests;
use App\Http\Requests\Request;
use Illuminate\Validation\Factory;
class UpdateExpenseRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'amount' => 'positive',
'expense_date' => 'required',
];
}
}

View File

@ -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 [];
}
}

View File

@ -0,0 +1,30 @@
<?php namespace app\Http\Requests;
use App\Http\Requests\Request;
use Illuminate\Validation\Factory;
class UpdateExpenseRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'amount' => 'required|positive',
];
}
}

View File

@ -0,0 +1,29 @@
<?php namespace app\Http\Requests;
// vendor
use App\Http\Requests\Request;
use Illuminate\Validation\Factory;
class UpdateVendorRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'vendor_contacts' => 'valid_contacts',
];
}
}

View File

@ -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');

View File

@ -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;
}
}

View File

@ -0,0 +1,25 @@
<?php namespace app\Listeners;
use Carbon;
use App\Models\Expense;
use App\Events\PaymentWasDeleted;
use App\Events\InvoiceWasDeleted;
use App\Ninja\Repositories\ExpenseRepository;
class ExpenseListener
{
// Expenses
protected $expenseRepo;
public function __construct(ExpenseRepository $expenseRepo)
{
$this->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]);
}
}

View File

@ -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);
}
}

View File

@ -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()

101
app/Models/Expense.php Normal file
View File

@ -0,0 +1,101 @@
<?php namespace App\Models;
use Laracasts\Presenter\PresentableTrait;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Events\ExpenseWasCreated;
use App\Events\ExpenseWasUpdated;
use App\Events\ExpenseWasDeleted;
class Expense extends EntityModel
{
// Expenses
use SoftDeletes;
use PresentableTrait;
protected $dates = ['deleted_at','expense_date'];
protected $presenter = 'App\Ninja\Presenters\ExpensePresenter';
protected $fillable = [
'amount',
'foreign_amount',
'exchange_rate',
'private_notes',
'public_notes',
];
public function account()
{
return $this->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));
});

View File

@ -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;
}
*/

View File

@ -1,8 +1,17 @@
<?php namespace App\Models;
use Eloquent;
use Illuminate\Database\Eloquent\SoftDeletes;
class PaymentTerm extends Eloquent
class PaymentTerm extends EntityModel
{
public $timestamps = false;
use SoftDeletes;
public $timestamps = true;
protected $dates = ['deleted_at'];
public function getEntityType()
{
return ENTITY_PAYMENT_TERM;
}
}

View File

@ -153,6 +153,20 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
return MAX_NUM_CLIENTS;
}
public function getMaxNumVendors()
{
if ($this->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;

242
app/Models/Vendor.php Normal file
View File

@ -0,0 +1,242 @@
<?php namespace App\Models;
use Utils;
use DB;
use Carbon;
use App\Events\VendorWasCreated;
use App\Events\VendorWasUpdated;
use Laracasts\Presenter\PresentableTrait;
use Illuminate\Database\Eloquent\SoftDeletes;
class Vendor extends EntityModel
{
use PresentableTrait;
use SoftDeletes;
protected $presenter = 'App\Ninja\Presenters\VendorPresenter';
protected $dates = ['deleted_at'];
protected $fillable = [
'name',
'id_number',
'vat_number',
'work_phone',
'address1',
'address2',
'city',
'state',
'postal_code',
'country_id',
'private_notes',
'currency_id',
'website',
];
public static $fieldName = 'name';
public static $fieldPhone = 'work_phone';
public static $fieldAddress1 = 'address1';
public static $fieldAddress2 = 'address2';
public static $fieldCity = 'city';
public static $fieldState = 'state';
public static $fieldPostalCode = 'postal_code';
public static $fieldNotes = 'notes';
public static $fieldCountry = 'country';
public static function getImportColumns()
{
return [
Vendor::$fieldName,
Vendor::$fieldPhone,
Vendor::$fieldAddress1,
Vendor::$fieldAddress2,
Vendor::$fieldCity,
Vendor::$fieldState,
Vendor::$fieldPostalCode,
Vendor::$fieldCountry,
Vendor::$fieldNotes,
VendorContact::$fieldFirstName,
VendorContact::$fieldLastName,
VendorContact::$fieldPhone,
VendorContact::$fieldEmail,
];
}
public static function getImportMap()
{
return [
'first' => '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));
});

View File

@ -0,0 +1,68 @@
<?php namespace App\Models;
// vendor
use HTML;
use Illuminate\Database\Eloquent\SoftDeletes;
class VendorContact extends EntityModel
{
use SoftDeletes;
protected $dates = ['deleted_at'];
protected $table = 'vendor_contacts';
protected $fillable = [
'first_name',
'last_name',
'email',
'phone',
'send_invoice',
];
public static $fieldFirstName = 'first_name';
public static $fieldLastName = 'last_name';
public static $fieldEmail = 'email';
public static $fieldPhone = 'phone';
public function account()
{
return $this->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 '';
}
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,35 @@
<?php namespace App\Ninja\Import\CSV;
use App\Ninja\Import\BaseTransformer;
use League\Fractal\Resource\Item;
// vendor
class VendorTransformer extends BaseTransformer
{
public function transform($data)
{
if (isset($data->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,
];
});
}
}

View File

@ -0,0 +1,36 @@
<?php namespace App\Ninja\Import\FreshBooks;
use App\Ninja\Import\BaseTransformer;
use League\Fractal\Resource\Item;
// vendor
class VendorTransformer extends BaseTransformer
{
public function transform($data)
{
if ($this->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),
];
});
}
}

View File

@ -0,0 +1,24 @@
<?php namespace App\Ninja\Import\Harvest;
use App\Ninja\Import\BaseTransformer;
use League\Fractal\Resource\Item;
// vendor
class VendorContactTransformer extends BaseTransformer
{
public function transform($data)
{
if ( ! $this->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,
];
});
}
}

View File

@ -0,0 +1,20 @@
<?php namespace App\Ninja\Import\Harvest;
use App\Ninja\Import\BaseTransformer;
use League\Fractal\Resource\Item;
// vendor
class VendorTransformer extends BaseTransformer
{
public function transform($data)
{
if ($this->hasVendor($data->vendor_name)) {
return false;
}
return new Item($data, function ($data) {
return [
'name' => $data->vendor_name,
];
});
}
}

View File

@ -0,0 +1,35 @@
<?php namespace App\Ninja\Import\Hiveage;
use App\Ninja\Import\BaseTransformer;
use League\Fractal\Resource\Item;
// vendor
class VendorTransformer extends BaseTransformer
{
public function transform($data)
{
if ($this->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),
];
});
}
}

View File

@ -0,0 +1,34 @@
<?php namespace App\Ninja\Import\Invoiceable;
use App\Ninja\Import\BaseTransformer;
use League\Fractal\Resource\Item;
// vendor
class VendorTransformer extends BaseTransformer
{
public function transform($data)
{
if ($this->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,
],
],
];
});
}
}

View File

@ -0,0 +1,35 @@
<?php namespace App\Ninja\Import\Nutcache;
use App\Ninja\Import\BaseTransformer;
use League\Fractal\Resource\Item;
// vendor
class VendorTransformer extends BaseTransformer
{
public function transform($data)
{
if ($this->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,
];
});
}
}

View File

@ -0,0 +1,28 @@
<?php namespace App\Ninja\Import\Ronin;
use App\Ninja\Import\BaseTransformer;
use League\Fractal\Resource\Item;
// vendor
class VendorTransformer extends BaseTransformer
{
public function transform($data)
{
if ($this->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,
],
],
];
});
}
}

View File

@ -0,0 +1,38 @@
<?php namespace App\Ninja\Import\Wave;
use App\Ninja\Import\BaseTransformer;
use League\Fractal\Resource\Item;
// vendor
class VendorTransformer extends BaseTransformer
{
public function transform($data)
{
if ($this->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),
];
});
}
}

View File

@ -0,0 +1,37 @@
<?php namespace App\Ninja\Import\Zoho;
use App\Ninja\Import\BaseTransformer;
use League\Fractal\Resource\Item;
// vendor
class VendorTransformer extends BaseTransformer
{
public function transform($data)
{
if ($this->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),
];
});
}
}

View File

@ -0,0 +1,18 @@
<?php namespace App\Ninja\Presenters;
use Utils;
use Laracasts\Presenter\Presenter;
class ExpensePresenter extends Presenter {
// Expenses
public function vendor()
{
return $this->entity->vendor ? $this->entity->vendor->getDisplayName() : '';
}
public function expense_date()
{
return Utils::fromSqlDate($this->entity->expense_date);
}
}

View File

@ -0,0 +1,12 @@
<?php namespace App\Ninja\Presenters;
use Utils;
use Laracasts\Presenter\Presenter;
// vendor
class VendorPresenter extends Presenter {
public function country()
{
return $this->entity->country ? $this->entity->country->name : '';
}
}

View File

@ -0,0 +1,160 @@
<?php namespace App\Ninja\Repositories;
use DB;
use Utils;
use App\Models\Expense;
use App\Models\Vendor;
use App\Ninja\Repositories\BaseRepository;
use Session;
class ExpenseRepository extends BaseRepository
{
// Expenses
public function getClassName()
{
return 'App\Models\Expense';
}
public function all()
{
return Expense::scope()
->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);
}
}

View File

@ -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';

View File

@ -0,0 +1,22 @@
<?php namespace App\Ninja\Repositories;
use DB;
use Utils;
use App\Models\PaymentTerm;
use App\Ninja\Repositories\BaseRepository;
class PaymentTermRepository extends BaseRepository
{
public function getClassName()
{
return 'App\Models\PaymentTerm';
}
public function find($accountId = 0)
{
return DB::table('payment_terms')
//->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');
}
}

View File

@ -0,0 +1,26 @@
<?php namespace App\Ninja\Repositories;
use App\Models\Vendor;
use App\Models\VendorContact;
// vendor
class VendorContactRepository extends BaseRepository
{
public function save($data)
{
$publicId = isset($data['public_id']) ? $data['public_id'] : false;
if (!$publicId || $publicId == '-1') {
$contact = VendorContact::createNew();
//$contact->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;
}
}

View File

@ -0,0 +1,90 @@
<?php namespace App\Ninja\Repositories;
use DB;
use App\Ninja\Repositories\BaseRepository;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Models\Activity;
// vendor
class VendorRepository extends BaseRepository
{
public function getClassName()
{
return 'App\Models\Vendor';
}
public function all()
{
return Vendor::scope()
->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;
}
}

View File

@ -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,
];
}
}
}

View File

@ -0,0 +1,24 @@
<?php namespace App\Ninja\Transformers;
use App\Models\Account;
use App\Models\VendorContact;
use League\Fractal;
// vendor
class VendorContactTransformer extends EntityTransformer
{
public function transform(VendorContact $contact)
{
return [
'id' => (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,
];
}
}

View File

@ -0,0 +1,82 @@
<?php namespace App\Ninja\Transformers;
use App\Models\Account;
use App\Models\Vendor;
use App\Models\Contact;
use League\Fractal;
// vendor
/**
* @SWG\Definition(definition="Vendor", @SWG\Xml(name="Vendor"))
*/
class VendorTransformer extends EntityTransformer
{
/**
* @SWG\Property(property="id", type="integer", example=1, readOnly=true)
* @SWG\Property(property="balance", type="float", example=10, readOnly=true)
* @SWG\Property(property="paid_to_date", type="float", example=10, readOnly=true)
* @SWG\Property(property="user_id", type="integer", example=1)
* @SWG\Property(property="account_key", type="string", example="123456")
* @SWG\Property(property="updated_at", type="timestamp", example="")
* @SWG\Property(property="archived_at", type="timestamp", example="1451160233")
* @SWG\Property(property="address1", type="string", example="10 Main St.")
* @SWG\Property(property="address2", type="string", example="1st Floor")
* @SWG\Property(property="city", type="string", example="New York")
* @SWG\Property(property="state", type="string", example="NY")
* @SWG\Property(property="postal_code", type="string", example=10010)
* @SWG\Property(property="country_id", type="integer", example=840)
* @SWG\Property(property="work_phone", type="string", example="(212) 555-1212")
* @SWG\Property(property="private_notes", type="string", example="Notes...")
* @SWG\Property(property="last_login", type="date-time", example="2016-01-01 12:10:00")
* @SWG\Property(property="website", type="string", example="http://www.example.com")
* @SWG\Property(property="is_deleted", type="boolean", example=false)
* @SWG\Property(property="vat_number", type="string", example="123456")
* @SWG\Property(property="id_number", type="string", example="123456")
*/
protected $availableIncludes = [
'contacts',
'invoices',
];
public function includeContacts(Vendor $vendor)
{
$transformer = new ContactTransformer($this->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
];
}
}

View File

@ -53,7 +53,11 @@ class AppServiceProvider extends ServiceProvider {
$str .= '<li class="divider"></li>
<li><a href="'.URL::to('credits').'">'.trans("texts.credits").'</a></li>
<li><a href="'.URL::to('credits/create').'">'.trans("texts.new_credit").'</a></li>';
}
} else if ($type == ENTITY_EXPENSE) {
$str .= '<li class="divider"></li>
<li><a href="'.URL::to('vendors').'">'.trans("texts.vendors").'</a></li>
<li><a href="'.URL::to('vendors/create').'">'.trans("texts.new_vendor").'</a></li>';
}
$str .= '</ul>
</li>';

View File

@ -137,7 +137,7 @@ class EventServiceProvider extends ServiceProvider {
'App\Events\UserSettingsChanged' => [
'App\Listeners\HandleUserSettingsChanged',
],
];
/**

View File

@ -0,0 +1,161 @@
<?php namespace App\Services;
use DB;
use Utils;
use URL;
use App\Services\BaseService;
use App\Ninja\Repositories\ExpenseRepository;
class ExpenseService extends BaseService
{
// Expenses
protected $expenseRepo;
protected $datatableService;
public function __construct(ExpenseRepository $expenseRepo, DatatableService $datatableService)
{
$this->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 "<h4><div class=\"label label-{$class}\">$label</div></h4>";
}
}

View File

@ -0,0 +1,60 @@
<?php namespace App\Services;
use URL;
use Auth;
use App\Services\BaseService;
use App\Ninja\Repositories\PaymentTermRepository;
class PaymentTermService extends BaseService
{
protected $paymentTermRepo;
protected $datatableService;
public function __construct(PaymentTermRepository $paymentTermRepo, DatatableService $datatableService)
{
$this->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");
}
]
];
}
}

View File

@ -0,0 +1,91 @@
<?php namespace App\Services;
use Utils;
use URL;
use Auth;
use App\Services\BaseService;
use App\Ninja\Repositories\VendorRepository;
use App\Ninja\Repositories\NinjaRepository;
class VendorService extends BaseService
{
protected $vendorRepo;
protected $datatableService;
public function __construct(VendorRepository $vendorRepo, DatatableService $datatableService, NinjaRepository $ninjaRepo)
{
$this->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}");
}
]
];
}
}

View File

@ -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",

2
composer.lock generated
View File

@ -8851,4 +8851,4 @@
"prefer-lowest": false,
"platform": [],
"platform-dev": []
}
}

View File

@ -161,6 +161,8 @@ return [
'App\Providers\ConfigServiceProvider',
'App\Providers\EventServiceProvider',
'App\Providers\RouteServiceProvider',
'Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider',
],
/*

View File

@ -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
),
/*

1
database/.gitignore vendored
View File

@ -1 +1,2 @@
*.sqlite
*-komodoproject

View File

@ -0,0 +1,135 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateVendorsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('vendors', function (Blueprint $table) {
$table->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');
}
}

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Komodo Project File - DO NOT EDIT -->
<project id="63a1b237-cbd6-48f9-bac1-8e5d85debff7" kpf_version="5" name="invoiceninja.komodoproject">
<preference-set idref="63a1b237-cbd6-48f9-bac1-8e5d85debff7" id="project" preftype="project">
<preference-set id="Invocations">
<preference-set id="Project">
<string id="cookieparams"></string>
<string relative="path" id="cwd"></string>
<string id="documentRoot"></string>
<string id="executable-params"></string>
<string id="filename"></string>
<string id="getparams"></string>
<string id="language">PHP</string>
<string id="mpostparams"></string>
<string id="params"></string>
<string id="postparams"></string>
<string id="posttype">application/x-www-form-urlencoded</string>
<string id="request-method">GET</string>
<boolean id="show-dialog">1</boolean>
<boolean id="sim-cgi">0</boolean>
<boolean id="use-console">0</boolean>
<string id="userCGIEnvironment"></string>
<string id="userEnvironment"></string>
</preference-set>
</preference-set>
<string id="currentInvocationLanguage">PHP</string>
<string id="defaultDateFormat">%25d/%25m/%25Y %25H:%25M:%25S</string>
<boolean id="genericLinter:PHP">1</boolean>
<string relative="path" id="import_dirname"></string>
<string id="jshint_linter_chooser">default</string>
<string id="jslint_linter_chooser">default</string>
<string id="language">PHP</string>
<string id="lastInvocation">Project</string>
<string id="last_local_directory">None</string>
<string id="last_remote_directory">None</string>
<string id="lessDefaultInterpreter">None</string>
<string id="phpDefaultInterpreter">c:\wamp\bin\php\php5.6.15\php.exe</string>
<string relative="path" id="phpExtraPaths">vendor;C:/webdev/invoiceninja/app</string>
<long id="prefs_version">1</long>
<string id="sassDefaultInterpreter">None</string>
<string id="scssDefaultInterpreter">None</string>
</preference-set>
</project>

View File

@ -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 = {};

View File

@ -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' => '<p>Automatically sets a due date for the invoice.</p>

View File

@ -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') !!}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! $title !!}</h3>
</div>
<div class="panel-body form-padding-right">
@if ($paymentTerm)
{{ Former::populate($paymentTerm) }}
@endif
{!! Former::text('name')->label('texts.name') !!}
{!! Former::text('num_days')->label('texts.num_days') !!}
</div>
</div>
{!! 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() !!}
<script type="text/javascript">
$(function() {
$('#name').focus();
});
</script>
@stop

View File

@ -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') !!}
<script>
window.onDatatableReady = actionListHandler;
</script>
@stop

View File

@ -0,0 +1,113 @@
@extends('header')
@section('head')
@parent
<style type="text/css">
.input-group-addon div.checkbox {
display: inline;
}
div.client-select > div > div > span.input-group-addon {
padding-right: 30px;
}
</style>
@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
<div class="panel panel-default">
<div class="panel-body">
<div class="row">
<div class="col-md-6">
{!! 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('<i class="glyphicon glyphicon-calendar"></i>') !!}
{!! 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') !!}
</div>
<div class="col-md-6">
{!! Former::textarea('public_notes')->rows(8) !!}
{!! Former::textarea('private_notes')->rows(8) !!}
</div>
</div>
</div>
</div>
<center class="buttons">
{!! 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')) !!}
</center>
{!! Former::close() !!}
<script type="text/javascript">
var vendors = {!! $vendors !!};
var clients = {!! $clients !!};
$(function() {
var $vendorSelect = $('select#vendor');
for (var i = 0; i < vendors.length; i++) {
var vendor = vendors[i];
$vendorSelect.append(new Option(getVendorDisplayName(vendor), vendor.public_id));
}
if ({{ $vendorPublicId ? 'true' : 'false' }}) {
$vendorSelect.val({{ $vendorPublicId }});
}
$vendorSelect.combobox();
$('#currency_id').combobox();
$('#expense_date').datepicker('update', new Date());
@if (!$vendorPublicId)
$('.vendor-select input.form-control').focus();
@else
$('#amount').focus();
@endif
$('.expense_date .input-group-addon').click(function() {
toggleDatePicker('expense_date');
});
var $clientSelect = $('select#client');
for (var i=0; i<clients.length; i++) {
var client = clients[i];
$clientSelect.append(new Option(getClientDisplayName(client), client.public_id));
}
if ({{ $clientPublicId ? 'true' : 'false' }}) {
$clientSelect.val({{ $clientPublicId }});
}
$clientSelect.combobox();
});
</script>
@stop

View File

@ -375,6 +375,7 @@
{!! HTML::menu_link('task') !!}
{!! HTML::menu_link('invoice') !!}
{!! HTML::menu_link('payment') !!}
{!! HTML::menu_link('expense') !!}
</ul>
<div class="navbar-form navbar-right">

View File

@ -31,7 +31,7 @@
<li>{!! link_to(($entityType == ENTITY_QUOTE ? 'quotes' : 'invoices'), trans('texts.' . ($entityType == ENTITY_QUOTE ? 'quotes' : 'invoices'))) !!}</li>
<li class="active">{{ $invoice->invoice_number }}</li>
@endif
</ol>
</ol>
@endif
{!! Former::open($url)
@ -42,7 +42,7 @@
'client' => 'required',
'invoice_number' => 'required',
'product_key' => 'max:255'
)) !!}
)) !!}
@include('partials.autocomplete_fix')
@ -63,7 +63,7 @@
<a id="editClientLink" class="pointer" data-bind="click: $root.showClientForm">{{ trans('texts.edit_client') }}</a> |
{!! link_to('/clients/'.$invoice->client->public_id, trans('texts.view_client'), ['target' => '_blank']) !!}
</div>
</div>
</div>
<div style="display:none">
@endif
@ -72,7 +72,7 @@
<div class="form-group" style="margin-bottom: 8px">
<div class="col-lg-8 col-sm-8 col-lg-offset-4 col-sm-offset-4">
<a id="createClientLink" class="pointer" data-bind="click: $root.showClientForm, html: $root.clientLinkText"></a>
<span data-bind="visible: $root.invoice().client().public_id() > 0" style="display:none">|
<span data-bind="visible: $root.invoice().client().public_id() > 0" style="display:none">|
<a data-bind="attr: {href: '{{ url('/clients') }}/' + $root.invoice().client().public_id()}" target="_blank">{{ trans('texts.view_client') }}</a>
</span>
</div>
@ -88,20 +88,20 @@
<label class="checkbox" data-bind="attr: {for: $index() + '_check'}" onclick="refreshPDF(true)">
<input type="hidden" value="0" data-bind="attr: {name: 'client[contacts][' + $index() + '][send_invoice]'}">
<input type="checkbox" value="1" data-bind="checked: send_invoice, attr: {id: $index() + '_check', name: 'client[contacts][' + $index() + '][send_invoice]'}">
<span data-bind="html: email.display"></span>
<span data-bind="html: email.display"></span>
</label>
<span data-bind="html: $data.view_as_recipient, visible: !$root.invoice().is_recurring()"></span>&nbsp;&nbsp;
@if (Utils::isConfirmed())
<span style="vertical-align:text-top;color:red" class="fa fa-exclamation-triangle"
<span style="vertical-align:text-top;color:red" class="fa fa-exclamation-triangle"
data-bind="visible: $data.email_error, tooltip: {title: $data.email_error}"></span>
<span style="vertical-align:text-top" class="glyphicon glyphicon-info-sign"
data-bind="visible: $data.invitation_status, tooltip: {title: $data.invitation_status, html: true},
<span style="vertical-align:text-top" class="glyphicon glyphicon-info-sign"
data-bind="visible: $data.invitation_status, tooltip: {title: $data.invitation_status, html: true},
style: {color: $data.hasOwnProperty('invitation_viewed') &amp;&amp; $data.invitation_viewed() ? '#57D172':'#B1B5BA'}"></span>
@endif
</div>
</div>
</div>
</div>
<div class="col-md-4" id="col_2">
<div data-bind="visible: !is_recurring()">
@ -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')) !!}
</div>
@ -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)
<div class="form-group" style="margin-bottom: 8px">
<div class="col-lg-8 col-sm-8 col-sm-offset-4" style="padding-top: 10px">
@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)
<span class="smaller">
@if (isset($lastSent) && $lastSent)
{!! trans('texts.last_sent_on', ['date' => link_to('/invoices/'.$lastSent->public_id, $invoice->last_sent_date, ['id' => 'lastSent'])]) !!} <br/>
@endif
@if ($invoice->is_recurring && $invoice->getNextSendDate())
{!! trans('texts.next_send_on', ['date' => '<span data-bind="tooltip: {title: \''.$invoice->getPrettySchedule().'\', html: true}">'.$account->formatDate($invoice->getNextSendDate()).
'<span class="glyphicon glyphicon-info-sign" style="padding-left:10px;color:#B1B5BA"></span></span>']) !!}
@endif
</span>
@endif
</div>
</div>
@endif
</div>
@ -135,7 +155,7 @@
{!! Former::text('invoice_number')
->label(trans("texts.{$entityType}_number_short"))
->data_bind("value: invoice_number, valueUpdate: 'afterkeydown'") !!}
</span>
</span>
<span data-bind="visible: is_recurring()" style="display: none">
{!! 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)
<div class="form-group" style="margin-bottom: 8px">
<div class="col-lg-8 col-sm-8 col-sm-offset-4" style="padding-top: 10px">
<div class="col-lg-8 col-sm-8 col-sm-offset-4" style="padding-top: 10px">
@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 @@
!!}
</td>
<td>
<textarea data-bind="value: wrapped_notes, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][notes]'}"
<textarea data-bind="value: wrapped_notes, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][notes]'}"
rows="1" cols="60" style="resize: vertical" class="form-control word-wrap"></textarea>
<input type="text" data-bind="value: task_public_id, attr: {name: 'invoice_items[' + $index() + '][task_public_id]'}" style="display: none"/>
<input type="text" data-bind="value: expense_public_id, attr: {name: 'invoice_items[' + $index() + '][expense_public_id]'}" style="display: none"/>
</td>
<td>
<input data-bind="value: prettyCost, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][cost]'}"
<input data-bind="value: prettyCost, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][cost]'}"
style="text-align: right" class="form-control invoice-item"/>
</td>
<td style="{{ $account->hide_quantity ? 'display:none' : '' }}">
<input data-bind="value: prettyQty, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][qty]'}"
<input data-bind="value: prettyQty, valueUpdate: 'afterkeydown', attr: {name: 'invoice_items[' + $index() + '][qty]'}"
style="text-align: right" class="form-control invoice-item" name="quantity"/>
</td>
<td style="display:none;" data-bind="visible: $root.invoice_item_taxes.show">
@ -228,7 +249,7 @@
<div class="line-total" data-bind="text: totals.total"></div>
</td>
<td style="cursor:pointer" class="hide-border td-icon">
<i style="padding-left:2px" data-bind="click: $parent.removeItem, visible: actionsVisible() &amp;&amp;
<i style="padding-left:2px" data-bind="click: $parent.removeItem, visible: actionsVisible() &amp;&amp;
$index() < ($parent.invoice_items().length - 1) &amp;&amp;
$parent.invoice_items().length > 1" class="fa fa-minus-circle redlink" title="Remove item"/>
</td>
@ -252,7 +273,7 @@
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="notes" style="padding-bottom:44px">
{!! 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) !!}
</div>
<div role="tabpanel" class="tab-pane" id="terms">
{!! Former::textarea('terms')->data_bind("value:wrapped_terms, placeholder: terms_placeholder, valueUpdate: 'afterkeydown'")
@ -323,7 +344,7 @@
<tr style="display:none" data-bind="visible: $root.invoice_taxes.show">
<td class="hide-border" colspan="3"/>
<td style="display:none" class="hide-border" data-bind="visible: $root.invoice_item_taxes.show"/>
<td style="display:none" class="hide-border" data-bind="visible: $root.invoice_item_taxes.show"/>
@if (!$account->hide_quantity)
<td>{{ trans('texts.tax') }}</td>
@endif
@ -376,7 +397,7 @@
</div>
</div>
</div>
<p>&nbsp;</p>
<div class="form-actions">
@ -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') !!}
</div>
@ -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())
<!-- do nothing -->
@elseif ($invoice->trashed())
@ -454,22 +476,24 @@
->label('client_name') !!}
<span data-bind="visible: $root.showMore">
{!! 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'") !!}
</span>
@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 @@
<div class="col-lg-8 col-lg-offset-4">
<span class="redlink bold" data-bind="visible: $parent.contacts().length > 1">
{!! link_to('#', trans('texts.remove_contact').' -', array('data-bind'=>'click: $parent.removeContact')) !!}
</span>
</span>
<span data-bind="visible: $index() === ($parent.contacts().length - 1)" class="pull-right greenlink bold">
{!! link_to('#', trans('texts.add_contact').' +', array('data-bind'=>'click: $parent.addContact')) !!}
</span>
@ -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 @@
<span class="error-block" id="emailError" style="display:none;float:left;font-weight:bold">{{ trans('texts.provide_name_or_email') }}</span><span>&nbsp;</span>
<button type="button" class="btn btn-default" data-dismiss="modal">{{ trans('texts.cancel') }}</button>
<button type="button" class="btn btn-default" data-bind="click: $root.showMoreFields, text: $root.showMore() ? '{{ trans('texts.less_fields') }}' : '{{ trans('texts.more_fields') }}'"></button>
<button id="clientDoneButton" type="button" class="btn btn-primary" data-bind="click: $root.clientFormComplete">{{ trans('texts.done') }}</button>
<button id="clientDoneButton" type="button" class="btn btn-primary" data-bind="click: $root.clientFormComplete">{{ trans('texts.done') }}</button>
</div>
</div>
</div>
</div>
@ -599,11 +623,11 @@
<div class="modal-footer" style="margin-top: 0px">
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ trans('texts.close') }}</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="recurringDueDateModal" tabindex="-1" role="dialog" aria-labelledby="recurringDueDateModalLabel" aria-hidden="true">
<div class="modal-dialog" style="min-width:150px">
<div class="modal-content">
@ -619,7 +643,7 @@
<div class="modal-footer" style="margin-top: 0px">
<button type="button" class="btn btn-primary" data-dismiss="modal">{{ trans('texts.close') }}</button>
</div>
</div>
</div>
</div>
@ -639,16 +663,16 @@
@include('invoices.knockout')
<script type="text/javascript">
var products = {!! $products !!};
var clients = {!! $clients !!};
var clients = {!! $clients !!};
var account = {!! Auth::user()->account !!};
var clientMap = {};
var $clientSelect = $('select#client');
var invoiceDesigns = {!! $invoiceDesigns !!};
var invoiceFonts = {!! $invoiceFonts !!};
$(function() {
// create client dictionary
@ -662,17 +686,17 @@
contact.send_invoice = true;
}
if (clientName != contactName) {
$clientSelect.append(new Option(contactName, client.public_id));
$clientSelect.append(new Option(contactName, client.public_id));
}
}
clientMap[client.public_id] = client;
$clientSelect.append(new Option(clientName, client.public_id));
$clientSelect.append(new Option(clientName, client.public_id));
}
@if ($data)
// this means we failed so we'll reload the previous state
window.model = new ViewModel({!! $data !!});
@else
@else
// otherwise create blank model
window.model = new ViewModel();
@ -708,7 +732,7 @@
// move the blank invoice line item to the end
var blank = model.invoice().invoice_items.pop();
var tasks = {!! $tasks !!};
for (var i=0; i<tasks.length; i++) {
var task = tasks[i];
var item = model.invoice().addItem();
@ -719,6 +743,24 @@
model.invoice().invoice_items.push(blank);
model.invoice().has_tasks(true);
@endif
@if (isset($expenses) && $expenses)
// move the blank invoice line item to the end
var blank = model.invoice().invoice_items.pop();
var expenses = {!! $expenses !!};
for (var i=0; i<expenses.length; i++) {
var expense = expenses[i];
var item = model.invoice().addItem();
item.notes(expense.description);
item.qty(expense.qty);
item.expense_public_id(expense.publicId);
item.cost(expense.cost);
}
model.invoice().invoice_items.push(blank);
model.invoice().has_expenses(true);
@endif
@endif
model.invoice().tax(model.getTaxRate(model.invoice().tax_name(), model.invoice().tax_rate()));
@ -736,7 +778,7 @@
ko.applyBindings(model);
onItemChange();
$('#country_id').combobox().on('change', function(e) {
var countryId = $('input[name=country_id]').val();
@ -757,15 +799,15 @@
@if ($invoice->client && !$invoice->id)
$('input[name=client]').val({{ $invoice->client->public_id }});
@endif
var $input = $('select#client');
$input.combobox().on('change', function(e) {
var oldId = model.invoice().client().public_id();
var clientId = parseInt($('input[name=client]').val(), 10) || 0;
if (clientId > 0) {
if (clientId > 0) {
var selected = clientMap[clientId];
model.loadClient(selected);
// we enable searching by contact but the selection must be the client
// we enable searching by contact but the selection must be the client
$('.client-input').val(getClientDisplayName(selected));
// if there's an invoice number pattern we'll apply it now
setInvoiceNumber(selected);
@ -782,7 +824,7 @@
$('.client_select input.form-control').on('click', function() {
model.showClientForm();
});
}
}
$('#invoice_footer, #terms, #public_notes, #invoice_number, #invoice_date, #due_date, #start_date, #po_number, #discount, #currency_id, #invoice_design_id, #recurring, #is_amount_discount, #partial, #custom_text_value1, #custom_text_value2').change(function() {
setTimeout(function() {
@ -804,7 +846,7 @@
(function (_field) {
$('.' + _field + ' .input-group-addon').click(function() {
toggleDatePicker(_field);
});
});
})(field);
}
@ -813,7 +855,7 @@
@else
$('.client_select input.form-control').focus();
@endif
$('#clientModal').on('shown.bs.modal', function () {
$('#client\\[name\\]').focus();
}).on('hidden.bs.modal', function () {
@ -822,40 +864,44 @@
refreshPDF(true);
}
})
$('#relatedActions > button:first').click(function() {
onPaymentClick();
});
$('label.radio').addClass('radio-inline');
@if ($invoice->client->id)
$input.trigger('change');
@else
@else
refreshPDF(true);
@endif
var client = model.invoice().client();
setComboboxValue($('.client_select'),
client.public_id(),
client.name.display());
setComboboxValue($('.client_select'),
client.public_id(),
client.name.display());
@if (isset($tasks) && $tasks)
NINJA.formIsChanged = true;
@endif
@if (isset($expenses) && $expenses)
NINJA.formIsChanged = true;
@endif
applyComboboxListeners();
});
});
function onFrequencyChange(){
var currentName = $('#frequency_id').find('option:selected').text()
var currentDueDateNumber = $('#recurring_due_date').find('option:selected').attr('data-num');
var optionClass = currentName && currentName.toLowerCase().indexOf('week') > -1 ? 'weekly' : 'monthly';
var replacementOption = $('#recurring_due_date option[data-num=' + currentDueDateNumber + '].' + optionClass);
$('#recurring_due_date option').hide();
$('#recurring_due_date option.' + optionClass).show();
// Switch to an equivalent option
if(replacementOption.length){
replacementOption.attr('selected','selected');
@ -1045,7 +1091,7 @@
var invoice = createInvoiceModel();
var design = getDesignJavascript();
if (!design) return;
doc = generatePDF(invoice, design, true);
doc.getDataUrl( function(pdfString){
$('#pdfupload').val(pdfString);
@ -1081,7 +1127,7 @@
}
return isValid;
}
function isEmailValid() {
var isValid = true;
var sendTo = false;
@ -1111,7 +1157,7 @@
}
function onConvertClick() {
submitAction('convert');
submitAction('convert');
}
@if ($invoice->id)
@ -1123,7 +1169,7 @@
window.location = '{{ URL::to('credits/create/' . $invoice->client->public_id . '/' . $invoice->public_id ) }}';
}
@endif
function onArchiveClick() {
submitBulkAction('archive');
}
@ -1131,7 +1177,7 @@
function onDeleteClick() {
if (confirm('{!! trans("texts.are_you_sure") !!}')) {
submitBulkAction('delete');
}
}
}
function formEnterClick(event) {
@ -1149,9 +1195,9 @@
}
}
function clientModalEnterClick(event) {
function clientModalEnterClick(event) {
if (event.keyCode === 13){
event.preventDefault();
event.preventDefault();
model.clientFormComplete();
return false;
}
@ -1178,7 +1224,7 @@
var oldVal = val;
val = Math.max(Math.min(val, model.invoice().totals.rawTotal()), 0);
model.invoice().partial(val || '');
if (!silent && val != oldVal) {
$('#partial').tooltip('show');
setTimeout(function() {

View File

@ -29,8 +29,8 @@ function ViewModel(data) {
if (paymentTerms == -1) paymentTerms = 0;
var dueDate = $('#invoice_date').datepicker('getDate');
dueDate.setDate(dueDate.getDate() + paymentTerms);
self.invoice().due_date(dueDate);
// We're using the datepicker to handle the date formatting
self.invoice().due_date(dueDate);
// We're using the datepicker to handle the date formatting
self.invoice().due_date($('#due_date').val());
}
@endif
@ -51,7 +51,7 @@ function ViewModel(data) {
return new TaxRateModel(options.data);
}
},
}
}
if (data) {
ko.mapping.fromJS(data, self.mapping, self);
@ -63,7 +63,7 @@ function ViewModel(data) {
}
if (self.invoice().tax_rate() > 0) {
return true;
}
}
return false;
});
@ -82,7 +82,7 @@ function ViewModel(data) {
self.addTaxRate = function(data) {
var itemModel = new TaxRateModel(data);
self.tax_rates.push(itemModel);
self.tax_rates.push(itemModel);
applyComboboxListeners();
}
@ -200,7 +200,7 @@ function InvoiceModel(data) {
self.id = ko.observable('');
self.discount = ko.observable('');
self.is_amount_discount = ko.observable(0);
self.frequency_id = ko.observable(4); // default to monthly
self.frequency_id = ko.observable(4); // default to monthly
self.terms = ko.observable('');
self.default_terms = ko.observable(account.{{ $entityType }}_terms);
self.terms_placeholder = ko.observable({{ !$invoice->id && $account->{"{$entityType}_terms"} ? "account.{$entityType}_terms" : false}});
@ -229,6 +229,7 @@ function InvoiceModel(data) {
self.invoice_design_id = ko.observable(1);
self.partial = ko.observable(0);
self.has_tasks = ko.observable();
self.has_expenses = ko.observable();
self.custom_value1 = ko.observable(0);
self.custom_value2 = ko.observable(0);
@ -260,7 +261,7 @@ function InvoiceModel(data) {
@if ($account->hide_quantity)
itemModel.qty(1);
@endif
self.invoice_items.push(itemModel);
self.invoice_items.push(itemModel);
applyComboboxListeners();
return itemModel;
}
@ -286,11 +287,11 @@ function InvoiceModel(data) {
},
write: function(value) {
if (value) {
self._tax(value);
self._tax(value);
self.tax_name(value.name());
self.tax_rate(value.rate());
} else {
self._tax(false);
self._tax(false);
self.tax_name('');
self.tax_rate(0);
}
@ -310,7 +311,7 @@ function InvoiceModel(data) {
self.wrapped_notes = ko.computed({
read: function() {
read: function() {
return this.public_notes();
},
write: function(value) {
@ -361,7 +362,7 @@ function InvoiceModel(data) {
if (parseInt(self.is_amount_discount())) {
return roundToTwo(self.discount());
} else {
return roundToTwo(self.totals.rawSubtotal() * (self.discount()/100));
return roundToTwo(self.totals.rawSubtotal() * (self.discount()/100));
}
});
@ -416,7 +417,7 @@ function InvoiceModel(data) {
} else {
taxes[key] = {name:item.tax_name(), rate:item.tax_rate(), amount:taxAmount};
}
}
}
}
return taxes;
});
@ -432,24 +433,24 @@ function InvoiceModel(data) {
return count > 0;
});
self.totals.itemTaxRates = ko.computed(function() {
self.totals.itemTaxRates = ko.computed(function() {
var taxes = self.totals.itemTaxes();
var parts = [];
var parts = [];
for (var key in taxes) {
if (taxes.hasOwnProperty(key)) {
parts.push(taxes[key].name + ' ' + (taxes[key].rate*1) + '%');
}
}
}
return parts.join('<br/>');
});
self.totals.itemTaxAmounts = ko.computed(function() {
var taxes = self.totals.itemTaxes();
var parts = [];
var parts = [];
for (var key in taxes) {
if (taxes.hasOwnProperty(key)) {
parts.push(self.formatMoney(taxes[key].amount));
}
}
}
return parts.join('<br/>');
});
@ -464,7 +465,7 @@ function InvoiceModel(data) {
});
self.totals.rawTotal = ko.computed(function() {
var total = accounting.toFixed(self.totals.rawSubtotal(),2);
var total = accounting.toFixed(self.totals.rawSubtotal(),2);
var discount = self.totals.rawDiscounted();
total -= discount;
@ -509,7 +510,7 @@ function InvoiceModel(data) {
self.totals.total = ko.computed(function() {
return self.formatMoney(self.partial() ? self.partial() : self.totals.rawTotal());
});
});
self.onDragged = function(item) {
refreshPDF(true);
@ -569,7 +570,7 @@ function ClientModel(data) {
}
self.removeContact = function() {
self.contacts.remove(this);
self.contacts.remove(this);
}
self.name.display = ko.computed(function() {
@ -577,13 +578,13 @@ function ClientModel(data) {
return self.name();
}
if (self.contacts().length == 0) return;
var contact = self.contacts()[0];
var contact = self.contacts()[0];
if (contact.first_name() || contact.last_name()) {
return contact.first_name() + ' ' + contact.last_name();
return contact.first_name() + ' ' + contact.last_name();
} else {
return contact.email();
}
});
});
self.name.placeholder = ko.computed(function() {
if (self.contacts().length == 0) return '';
@ -593,13 +594,13 @@ function ClientModel(data) {
} else {
return contact.email();
}
});
});
if (data) {
ko.mapping.fromJS(data, {}, this);
} else {
self.addContact();
}
}
}
function ContactModel(data) {
@ -608,7 +609,7 @@ function ContactModel(data) {
self.first_name = ko.observable('');
self.last_name = ko.observable('');
self.email = ko.observable('');
self.phone = ko.observable('');
self.phone = ko.observable('');
self.send_invoice = ko.observable(false);
self.invitation_link = ko.observable('');
self.invitation_status = ko.observable('');
@ -623,10 +624,10 @@ function ContactModel(data) {
var str = '';
if (self.first_name() || self.last_name()) {
str += self.first_name() + ' ' + self.last_name() + '\n';
}
}
if (self.email()) {
str += self.email() + '\n';
}
}
return str;
});
@ -635,9 +636,9 @@ function ContactModel(data) {
var str = '';
if (self.first_name() || self.last_name()) {
str += self.first_name() + ' ' + self.last_name() + '<br/>';
}
}
if (self.email()) {
str += self.email() + '<br/>';
str += self.email() + '<br/>';
}
return str;
});
@ -651,7 +652,7 @@ function ContactModel(data) {
@endif
return str;
});
});
}
function TaxRateModel(data) {
@ -675,7 +676,7 @@ function TaxRateModel(data) {
this.rate(value);
},
owner: this
});
});
self.displayName = ko.computed({
@ -687,8 +688,8 @@ function TaxRateModel(data) {
write: function (value) {
// do nothing
},
owner: this
});
owner: this
});
self.hideActions = function() {
self.actionsVisible(false);
@ -696,15 +697,15 @@ function TaxRateModel(data) {
self.showActions = function() {
self.actionsVisible(true);
}
}
self.isEmpty = function() {
return !self.rate() && !self.name();
}
}
}
function ItemModel(data) {
var self = this;
var self = this;
self.product_key = ko.observable('');
self.notes = ko.observable('');
self.cost = ko.observable(0);
@ -712,6 +713,7 @@ function ItemModel(data) {
self.tax_name = ko.observable('');
self.tax_rate = ko.observable(0);
self.task_public_id = ko.observable('');
self.expense_public_id = ko.observable('');
self.actionsVisible = ko.observable(false);
self._tax = ko.observable();
@ -720,7 +722,7 @@ function ItemModel(data) {
return self._tax();
},
write: function(value) {
self._tax(value);
self._tax(value);
self.tax_name(value.name());
self.tax_rate(value.rate());
}
@ -734,7 +736,7 @@ function ItemModel(data) {
this.qty(value);
},
owner: this
});
});
this.prettyCost = ko.computed({
read: function () {
@ -744,7 +746,7 @@ function ItemModel(data) {
this.cost(value);
},
owner: this
});
});
self.mapping = {
'tax': {
@ -755,7 +757,7 @@ function ItemModel(data) {
}
if (data) {
ko.mapping.fromJS(data, self.mapping, this);
ko.mapping.fromJS(data, self.mapping, this);
}
self.wrapped_notes = ko.computed({
@ -775,7 +777,7 @@ function ItemModel(data) {
this.totals.rawTotal = ko.computed(function() {
var cost = roundToTwo(NINJA.parseFloat(self.cost()));
var qty = roundToTwo(NINJA.parseFloat(self.qty()));
var value = cost * qty;
var value = cost * qty;
return value ? roundToTwo(value) : 0;
});
@ -806,4 +808,4 @@ function ItemModel(data) {
this.onSelect = function() {}
}
</script>
</script>

View File

@ -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")'],

View File

@ -0,0 +1,99 @@
{!!-- // vendor --!!}
<div class="row">
<div class="col-md-6">
{!! 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') !!}
</div>
<div class="col-md-6">
{!! Former::legend('VendorContacts') !!}
<div data-bind='template: { foreach: vendor_contacts,
beforeRemove: hideContact,
afterAdd: showContact }'>
{!! 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'") !!}
<div class="form-group">
<div class="col-lg-8 col-lg-offset-4">
<span data-bind="visible: $parent.vendor_contacts().length > 1">
{!! link_to('#', 'Remove contact', array('data-bind'=>'click: $parent.removeContact')) !!}
</span>
<span data-bind="visible: $index() === ($parent.vendor_contacts().length - 1)" class="pull-right">
{!! link_to('#', 'Add contact', array('onclick'=>'return addContact()')) !!}
</span>
</div>
</div>
</div>
</div>
</div>
{!! Former::hidden('data')->data_bind("value: ko.toJSON(model)") !!}
<script type="text/javascript">
$(function() {
$('#country_id').combobox();
});
function VendorContactModel() {
var self = this;
self.public_id = ko.observable('');
self.first_name = ko.observable('');
self.last_name = ko.observable('');
self.email = ko.observable('');
self.phone = ko.observable('');
}
function VendorContactsModel() {
var self = this;
self.vendor_contacts = ko.observableArray();
}
@if ($vendor)
window.model = ko.mapping.fromJS({!! $vendor !!});
@else
window.model = new VendorContactsModel();
addContact();
@endif
model.showContact = function(elem) { if (elem.nodeType === 1) $(elem).hide().slideDown() }
model.hideContact = function(elem) { if (elem.nodeType === 1) $(elem).slideUp(function() { $(elem).remove(); }) }
ko.applyBindings(model);
function addContact() {
model.vendor_contacts.push(new VendorContactModel());
return false;
}
model.removeContact = function() {
model.vendor_contacts.remove(this);
}
</script>

218
resources/views/vendors/edit.blade.php vendored Normal file
View File

@ -0,0 +1,218 @@
@extends('header')
@section('onReady')
$('input#name').focus();
@stop
@section('content')
@if ($errors->first('vendorcontacts'))
<div class="alert alert-danger">{{ trans($errors->first('vendorcontacts')) }}</div>
@endif
<div class="row">
{!! 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
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.organization') !!}</h3>
</div>
<div class="panel-body">
{!! Former::text('name')->data_bind("attr { placeholder: placeholderName }") !!}
{!! Former::text('id_number') !!}
{!! Former::text('vat_number') !!}
{!! Former::text('website') !!}
{!! Former::text('work_phone') !!}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.address') !!}</h3>
</div>
<div class="panel-body">
{!! 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') !!}
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.contacts') !!}</h3>
</div>
<div class="panel-body">
<div data-bind='template: { foreach: vendorcontacts,
beforeRemove: hideContact,
afterAdd: showContact }'>
{!! 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]'}") !!}
<div class="form-group">
<div class="col-lg-8 col-lg-offset-4 bold">
<span class="redlink bold" data-bind="visible: $parent.vendorcontacts().length > 1">
{!! link_to('#', trans('texts.remove_contact').' -', array('data-bind'=>'click: $parent.removeContact')) !!}
</span>
<span data-bind="visible: $index() === ($parent.vendorcontacts().length - 1)" class="pull-right greenlink bold">
{!! link_to('#', trans('texts.add_contact').' +', array('onclick'=>'return addContact()')) !!}
</span>
</div>
</div>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans('texts.additional_info') !!}</h3>
</div>
<div class="panel-body">
{!! 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('<i class="glyphicon glyphicon-calendar"></i>') !!}
<script type="text/javascript">
$(function() {
$('#pro_plan_paid').datepicker();
});
</script>
@endif
</div>
</div>
</div>
</div>
{!! Former::hidden('data')->data_bind("value: ko.toJSON(model)") !!}
<script type="text/javascript">
$(function() {
$('#country_id').combobox();
});
function VendorContactModel(data) {
var self = this;
self.public_id = ko.observable('');
self.first_name = ko.observable('');
self.last_name = ko.observable('');
self.email = ko.observable('');
self.phone = ko.observable('');
if (data) {
ko.mapping.fromJS(data, {}, this);
}
}
function VendorModel(data) {
var self = this;
self.vendorcontacts = ko.observableArray();
self.mapping = {
'vendorcontacts': {
create: function(options) {
return new VendorContactModel(options.data);
}
}
}
if (data) {
ko.mapping.fromJS(data, self.mapping, this);
} else {
self.vendorcontacts.push(new VendorContactModel());
}
self.placeholderName = ko.computed(function() {
if (self.vendorcontacts().length == 0) return '';
var contact = self.vendorcontacts()[0];
if (contact.first_name() || contact.last_name()) {
return contact.first_name() + ' ' + contact.last_name();
} else {
return contact.email();
}
});
}
@if ($data)
window.model = new VendorModel({!! $data !!});
@else
window.model = new VendorModel({!! $vendor !!});
@endif
model.showContact = function(elem) { if (elem.nodeType === 1) $(elem).hide().slideDown() }
model.hideContact = function(elem) { if (elem.nodeType === 1) $(elem).slideUp(function() { $(elem).remove(); }) }
ko.applyBindings(model);
function addContact() {
model.vendorcontacts.push(new VendorContactModel());
return false;
}
model.removeContact = function() {
model.vendorcontacts.remove(this);
}
</script>
<center class="buttons">
{!! 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')) !!}
</center>
{!! Former::close() !!}
</div>
@stop

258
resources/views/vendors/show.blade.php vendored Normal file
View File

@ -0,0 +1,258 @@
@extends('header')
@section('head')
@parent
@if ($vendor->hasAddress())
<style>
#map {
width: 100%;
height: 200px;
border-width: 1px;
border-style: solid;
border-color: #ddd;
}
</style>
<script src="https://maps.googleapis.com/maps/api/js"></script>
@endif
@stop
@section('content')
<div class="pull-right">
{!! Former::open('vendors/bulk')->addClass('mainForm') !!}
<div style="display:none">
{!! Former::text('action') !!}
{!! Former::text('public_id')->value($vendor->public_id) !!}
</div>
@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() !!}
</div>
<h2>{{ $vendor->getDisplayName() }}</h2>
<div class="panel panel-default">
<div class="panel-body">
<div class="row">
<div class="col-md-3">
<h3>{{ trans('texts.details') }}</h3>
@if ($vendor->id_number)
<p><i class="fa fa-id-number" style="width: 20px"></i>{{ trans('texts.id_number').': '.$vendor->id_number }}</p>
@endif
@if ($vendor->vat_number)
<p><i class="fa fa-vat-number" style="width: 20px"></i>{{ trans('texts.vat_number').': '.$vendor->vat_number }}</p>
@endif
@if ($vendor->address1)
{{ $vendor->address1 }}<br/>
@endif
@if ($vendor->address2)
{{ $vendor->address2 }}<br/>
@endif
@if ($vendor->getCityState())
{{ $vendor->getCityState() }}<br/>
@endif
@if ($vendor->country)
{{ $vendor->country->name }}<br/>
@endif
@if ($vendor->account->custom_vendor_label1 && $vendor->custom_value1)
{{ $vendor->account->custom_vendor_label1 . ': ' . $vendor->custom_value1 }}<br/>
@endif
@if ($vendor->account->custom_vendor_label2 && $vendor->custom_value2)
{{ $vendor->account->custom_vendor_label2 . ': ' . $vendor->custom_value2 }}<br/>
@endif
@if ($vendor->work_phone)
<i class="fa fa-phone" style="width: 20px"></i>{{ $vendor->work_phone }}
@endif
@if ($vendor->private_notes)
<p><i>{{ $vendor->private_notes }}</i></p>
@endif
@if ($vendor->vendor_industry)
{{ $vendor->vendor_industry->name }}<br/>
@endif
@if ($vendor->vendor_size)
{{ $vendor->vendor_size->name }}<br/>
@endif
@if ($vendor->website)
<p>{!! Utils::formatWebsite($vendor->website) !!}</p>
@endif
@if ($vendor->language)
<p><i class="fa fa-language" style="width: 20px"></i>{{ $vendor->language->name }}</p>
@endif
<p>{{ $vendor->payment_terms ? trans('texts.payment_terms') . ": " . trans('texts.payment_terms_net') . " " . $vendor->payment_terms : '' }}</p>
</div>
<div class="col-md-3">
<h3>{{ trans('texts.contacts') }}</h3>
@foreach ($vendor->vendorcontacts as $contact)
@if ($contact->first_name || $contact->last_name)
<b>{{ $contact->first_name.' '.$contact->last_name }}</b><br/>
@endif
@if ($contact->email)
<i class="fa fa-envelope" style="width: 20px"></i>{!! HTML::mailto($contact->email, $contact->email) !!}<br/>
@endif
@if ($contact->phone)
<i class="fa fa-phone" style="width: 20px"></i>{{ $contact->phone }}<br/>
@endif
@endforeach
</div>
<div class="col-md-4">
<h3>{{ trans('texts.standing') }}
<table class="table" style="width:100%">
<tr>
<td><small>{{ trans('texts.balance') }}</small></td>
<td style="text-align: right">{{ Utils::formatMoney($totalexpense, $vendor->getCurrencyId()) }}</td>
</tr>
</table>
</h3>
</div>
</div>
</div>
</div>
@if ($vendor->hasAddress())
<div id="map"></div>
<br/>
@endif
<ul class="nav nav-tabs nav-justified">
{!! HTML::tab_link('#expenses', trans('texts.expenses')) !!}
</ul>
<div class="tab-content">
<div class="tab-pane" id="expenses">
{!! 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')
!!}
</div>
</div>
<script type="text/javascript">
var loadedTabs = {};
$(function() {
$('.normalDropDown:not(.dropdown-toggle)').click(function() {
window.location = '{{ URL::to('vendors/' . $vendor->public_id . '/edit') }}';
});
$('.primaryDropDown:not(.dropdown-toggle)').click(function() {
window.location = '{{ URL::to('expenses/create/' . $vendor->public_id ) }}';
});
// load datatable data when tab is shown and remember last tab selected
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
var target = $(e.target).attr("href") // activated tab
target = target.substring(1);
localStorage.setItem('vendor_tab', target);
if (!loadedTabs.hasOwnProperty(target)) {
loadedTabs[target] = true;
window['load_' + target]();
}
});
var tab = localStorage.getItem('vendor_tab');
if (tab && tab != 'activity') {
$('.nav-tabs a[href="#' + tab.replace('#', '') + '"]').tab('show');
} else {
window['load_activity']();
}
});
function onArchiveClick() {
$('#action').val('archive');
$('.mainForm').submit();
}
function onRestoreClick() {
$('#action').val('restore');
$('.mainForm').submit();
}
function onDeleteClick() {
if (confirm("{!! trans('texts.are_you_sure') !!}")) {
$('#action').val('delete');
$('.mainForm').submit();
}
}
@if ($vendor->hasAddress())
function initialize() {
var mapCanvas = document.getElementById('map');
var mapOptions = {
zoom: {{ DEFAULT_MAP_ZOOM }},
mapTypeId: google.maps.MapTypeId.ROADMAP,
zoomControl: true,
};
var map = new google.maps.Map(mapCanvas, mapOptions)
var address = "{{ "{$vendor->address1} {$vendor->address2} {$vendor->city} {$vendor->state} {$vendor->postal_code} " . ($vendor->country ? $vendor->country->name : '') }}";
geocoder = new google.maps.Geocoder();
geocoder.geocode( { 'address': address}, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
if (status != google.maps.GeocoderStatus.ZERO_RESULTS) {
var result = results[0];
map.setCenter(result.geometry.location);
var infowindow = new google.maps.InfoWindow(
{ content: '<b>'+result.formatted_address+'</b>',
size: new google.maps.Size(150, 50)
});
var marker = new google.maps.Marker({
position: result.geometry.location,
map: map,
title:address,
});
google.maps.event.addListener(marker, 'click', function() {
infowindow.open(map, marker);
});
} else {
$('#map').hide();
}
} else {
$('#map').hide();
}
});
}
google.maps.event.addDomListener(window, 'load', initialize);
@endif
</script>
@stop