diff --git a/app/Events/VendorWasArchived.php b/app/Events/VendorWasArchived.php
new file mode 100644
index 000000000000..65622c748f0c
--- /dev/null
+++ b/app/Events/VendorWasArchived.php
@@ -0,0 +1,21 @@
+vendor = $vendor;
+ }
+}
diff --git a/app/Events/VendorWasCreated.php b/app/Events/VendorWasCreated.php
new file mode 100644
index 000000000000..c52445d59974
--- /dev/null
+++ b/app/Events/VendorWasCreated.php
@@ -0,0 +1,21 @@
+vendor = $vendor;
+ }
+}
diff --git a/app/Events/VendorWasDeleted.php b/app/Events/VendorWasDeleted.php
new file mode 100644
index 000000000000..3f889bcad231
--- /dev/null
+++ b/app/Events/VendorWasDeleted.php
@@ -0,0 +1,21 @@
+vendor = $vendor;
+ }
+}
diff --git a/app/Events/VendorWasRestored.php b/app/Events/VendorWasRestored.php
new file mode 100644
index 000000000000..b7d350656bae
--- /dev/null
+++ b/app/Events/VendorWasRestored.php
@@ -0,0 +1,21 @@
+vendor = $vendor;
+ }
+}
diff --git a/app/Events/VendorWasUpdated.php b/app/Events/VendorWasUpdated.php
new file mode 100644
index 000000000000..44d489f9c35d
--- /dev/null
+++ b/app/Events/VendorWasUpdated.php
@@ -0,0 +1,21 @@
+vendor = $vendor;
+ }
+}
diff --git a/app/Http/Controllers/BaseAPIController.php b/app/Http/Controllers/BaseAPIController.php
index e5878e22ab55..4d783556022e 100644
--- a/app/Http/Controllers/BaseAPIController.php
+++ b/app/Http/Controllers/BaseAPIController.php
@@ -121,12 +121,15 @@ class BaseAPIController extends Controller
} elseif ($include == 'clients') {
$data[] = 'clients.contacts';
$data[] = 'clients.user';
- } elseif ($include) {
+ } elseif ($include == 'vendors') {
+ $data[] = 'vendors.vendorcontacts';
+ $data[] = 'vendors.user';
+ }
+ elseif ($include) {
$data[] = $include;
}
}
return $data;
}
-
}
diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php
index 79d5f82fa341..540e38d97296 100644
--- a/app/Http/Controllers/ExportController.php
+++ b/app/Http/Controllers/ExportController.php
@@ -13,6 +13,8 @@ use App\Models\Credit;
use App\Models\Task;
use App\Models\Invoice;
use App\Models\Payment;
+use App\Models\Vendor;
+use App\Models\VendorContact;
class ExportController extends BaseController
{
@@ -155,6 +157,25 @@ class ExportController extends BaseController
->get();
}
+
+ if ($request->input(ENTITY_VENDOR)) {
+ $data['clients'] = Vendor::scope()
+ ->with('user', 'vendorcontacts', 'country')
+ ->withTrashed()
+ ->where('is_deleted', '=', false)
+ ->get();
+
+ $data['vendor_contacts'] = VendorContact::scope()
+ ->with('user', 'vendor.contacts')
+ ->withTrashed()
+ ->get();
+ /*
+ $data['expenses'] = Credit::scope()
+ ->with('user', 'client.contacts')
+ ->get();
+ */
+ }
+
return $data;
}
}
\ No newline at end of file
diff --git a/app/Http/Controllers/VendorActivityController.php b/app/Http/Controllers/VendorActivityController.php
new file mode 100644
index 000000000000..223c817f112b
--- /dev/null
+++ b/app/Http/Controllers/VendorActivityController.php
@@ -0,0 +1,27 @@
+activityService = $activityService;
+ }
+
+ public function getDatatable($vendorPublicId)
+ {
+ return $this->activityService->getDatatable($vendorPublicId);
+ }
+}
diff --git a/app/Http/Controllers/VendorApiController.php b/app/Http/Controllers/VendorApiController.php
new file mode 100644
index 000000000000..b23ec8fc2f18
--- /dev/null
+++ b/app/Http/Controllers/VendorApiController.php
@@ -0,0 +1,94 @@
+vendorRepo = $vendorRepo;
+ }
+
+ public function ping()
+ {
+ $headers = Utils::getApiHeaders();
+
+ return Response::make('', 200, $headers);
+ }
+
+ /**
+ * @SWG\Get(
+ * path="/vendors",
+ * summary="List of vendors",
+ * tags={"vendor"},
+ * @SWG\Response(
+ * response=200,
+ * description="A list with vendors",
+ * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Vendor"))
+ * ),
+ * @SWG\Response(
+ * response="default",
+ * description="an ""unexpected"" error"
+ * )
+ * )
+ */
+ public function index()
+ {
+ $vendors = Vendor::scope()
+ ->with($this->getIncluded())
+ ->orderBy('created_at', 'desc')
+ ->paginate();
+
+ $transformer = new VendorTransformer(Auth::user()->account, Input::get('serializer'));
+ $paginator = Vendor::scope()->paginate();
+ $data = $this->createCollection($vendors, $transformer, ENTITY_VENDOR, $paginator);
+
+ return $this->response($data);
+ }
+
+ /**
+ * @SWG\Post(
+ * path="/vendors",
+ * tags={"vendor"},
+ * summary="Create a vendor",
+ * @SWG\Parameter(
+ * in="body",
+ * name="body",
+ * @SWG\Schema(ref="#/definitions/Vendor")
+ * ),
+ * @SWG\Response(
+ * response=200,
+ * description="New vendor",
+ * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Vendor"))
+ * ),
+ * @SWG\Response(
+ * response="default",
+ * description="an ""unexpected"" error"
+ * )
+ * )
+ */
+ public function store(CreateVendorRequest $request)
+ {
+ $vendor = $this->vendorRepo->save($request->input());
+
+ $vendor = Vendor::scope($vendor->public_id)
+ ->with('country', 'vendorcontacts', 'industry', 'size', 'currency')
+ ->first();
+
+ $transformer = new VendorTransformer(Auth::user()->account, Input::get('serializer'));
+ $data = $this->createItem($vendor, $transformer, ENTITY_VENDOR);
+ return $this->response($data);
+ }
+}
diff --git a/app/Http/Controllers/VendorController.php b/app/Http/Controllers/VendorController.php
new file mode 100644
index 000000000000..bc116e18927e
--- /dev/null
+++ b/app/Http/Controllers/VendorController.php
@@ -0,0 +1,211 @@
+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',
+ //'last_login',
+ 'balance',
+ ''
+ ]),
+ ));
+ }
+
+ public function getDatatable()
+ {
+ return $this->vendorService->getDatatable(Input::get('sSearch'));
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @return Response
+ */
+ public function store(CreateVendorRequest $request)
+ {
+ $vendor = $this->vendorService->save($request->input());
+
+ Session::flash('message', trans('texts.created_vendor'));
+
+ return redirect()->to($vendor->getRoute());
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param int $id
+ * @return Response
+ */
+ public function show($publicId)
+ {
+ $vendor = Vendor::withTrashed()->scope($publicId)->with('vendorcontacts', 'size', 'industry')->firstOrFail();
+ Utils::trackViewed($vendor->getDisplayName(), 'vendor');
+
+ $actionLinks = [
+ ['label' => trans('texts.new_expense'), 'url' => '/expenses/create/'.$vendor->public_id]
+ ];
+
+ $data = array(
+ 'actionLinks' => $actionLinks,
+ 'showBreadcrumbs' => false,
+ 'vendor' => $vendor,
+ 'credit' => $vendor->getTotalCredit(),
+ 'title' => trans('texts.view_vendor'),
+ 'hasRecurringInvoices' => false,
+ 'hasQuotes' => false,
+ 'hasTasks' => false,
+ 'gatewayLink' => $vendor->getGatewayLink(),
+ );
+
+ return View::make('vendors.show', $data);
+ }
+
+ /**
+ * Show the form for creating a new resource.
+ *
+ * @return Response
+ */
+ public function create()
+ {
+ if (Vendor::scope()->count() > Auth::user()->getMaxNumVendors()) {
+ return View::make('error', ['hideHeader' => true, 'error' => "Sorry, you've exceeded the limit of ".Auth::user()->getMaxNumVendors()." vendors"]);
+ }
+
+ $data = [
+ 'vendor' => null,
+ 'method' => 'POST',
+ 'url' => 'vendors',
+ 'title' => trans('texts.new_vendor'),
+ ];
+
+ $data = array_merge($data, self::getViewModel());
+
+ return View::make('vendors.edit', $data);
+ }
+
+ /**
+ * Show the form for editing the specified resource.
+ *
+ * @param int $id
+ * @return Response
+ */
+ public function edit($publicId)
+ {
+ $vendor = Vendor::scope($publicId)->with('vendorcontacts')->firstOrFail();
+ $data = [
+ 'vendor' => $vendor,
+ 'method' => 'PUT',
+ 'url' => 'vendors/'.$publicId,
+ 'title' => trans('texts.edit_vendor'),
+ ];
+
+ $data = array_merge($data, self::getViewModel());
+
+ if (Auth::user()->account->isNinjaAccount()) {
+ if ($account = Account::whereId($vendor->public_id)->first()) {
+ $data['proPlanPaid'] = $account['pro_plan_paid'];
+ }
+ }
+
+ return View::make('vendors.edit', $data);
+ }
+
+ private static function getViewModel()
+ {
+ return [
+ 'data' => Input::old('data'),
+ 'account' => Auth::user()->account,
+ 'sizes' => Cache::get('sizes'),
+ 'paymentTerms' => Cache::get('paymentTerms'),
+ 'industries' => Cache::get('industries'),
+ 'currencies' => Cache::get('currencies'),
+ 'languages' => Cache::get('languages'),
+ 'countries' => Cache::get('countries'),
+ 'customLabel1' => Auth::user()->account->custom_vendor_label1,
+ 'customLabel2' => Auth::user()->account->custom_vendor_label2,
+ ];
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param int $id
+ * @return Response
+ */
+ public function update(UpdateVendorRequest $request)
+ {
+ $vendor = $this->vendorService->save($request->input());
+
+ Session::flash('message', trans('texts.updated_vendor'));
+
+ return redirect()->to($vendor->getRoute());
+ }
+
+ public function bulk()
+ {
+ $action = Input::get('action');
+ $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
+ $count = $this->vendorService->bulk($ids, $action);
+
+ $message = Utils::pluralize($action.'d_vendor', $count);
+ Session::flash('message', $message);
+
+ if ($action == 'restore' && $count == 1) {
+ return Redirect::to('vendors/' . Utils::getFirst($ids));
+ } else {
+ return Redirect::to('vendors');
+ }
+ }
+}
diff --git a/app/Http/Requests/CreateVendorRequest.php b/app/Http/Requests/CreateVendorRequest.php
new file mode 100644
index 000000000000..8a51c01fc819
--- /dev/null
+++ b/app/Http/Requests/CreateVendorRequest.php
@@ -0,0 +1,44 @@
+ 'valid_contacts',
+ ];
+ }
+
+ public function validator($factory)
+ {
+ // support submiting the form with a single contact record
+ $input = $this->input();
+ if (isset($input['vendor_contact'])) {
+ $input['vendor_contacts'] = [$input['vendor_contact']];
+ unset($input['vendor_contact']);
+ $this->replace($input);
+ }
+
+ return $factory->make(
+ $this->input(), $this->container->call([$this, 'rules']), $this->messages()
+ );
+ }
+}
diff --git a/app/Http/routes.php b/app/Http/routes.php
index 46c12238d388..3f7ed3a84969 100644
--- a/app/Http/routes.php
+++ b/app/Http/routes.php
@@ -181,6 +181,14 @@ Route::group(['middleware' => 'auth'], function() {
get('/resend_confirmation', 'AccountController@resendConfirmation');
post('/update_setup', 'AppController@updateSetup');
+
+
+ // vendor
+ Route::resource('vendors', 'VendorController');
+ Route::get('api/vendor', array('as'=>'api.vendors', 'uses'=>'VendorController@getDatatable'));
+ Route::get('api/vendoractivities/{vendor_id?}', array('as'=>'api.vendoractivities', 'uses'=>'VendorActivityController@getDatatable'));
+ Route::post('vendors/bulk', 'VendorController@bulk');
+
});
// Route groups for API
@@ -202,6 +210,8 @@ 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');
});
// Redirects for legacy links
@@ -258,10 +268,13 @@ if (!defined('CONTACT_EMAIL')) {
define('ENTITY_TAX_RATE', 'tax_rate');
define('ENTITY_PRODUCT', 'product');
define('ENTITY_ACTIVITY', 'activity');
-
+ define('ENTITY_VENDOR','vendor');
+ define('ENTITY_EXPENSE', 'expense');
+
define('PERSON_CONTACT', 'contact');
define('PERSON_USER', 'user');
-
+ define('PERSON_VENDOR_CONTACT','vendorcontact');
+
define('BASIC_SETTINGS', 'basic_settings');
define('ADVANCED_SETTINGS', 'advanced_settings');
@@ -327,6 +340,12 @@ 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);
+
define('DEFAULT_INVOICE_NUMBER', '0001');
define('RECENTLY_VIEWED_LIMIT', 8);
define('LOGGED_ERROR_LIMIT', 100);
@@ -356,6 +375,11 @@ 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);
@@ -426,6 +450,7 @@ if (!defined('CONTACT_EMAIL')) {
define('EVENT_CREATE_INVOICE', 2);
define('EVENT_CREATE_QUOTE', 3);
define('EVENT_CREATE_PAYMENT', 4);
+ define('EVENT_CREATE_VENDOR',5);
define('REQUESTED_PRO_PLAN', 'REQUESTED_PRO_PLAN');
define('DEMO_ACCOUNT_ID', 'DEMO_ACCOUNT_ID');
diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php
index 74ac56169980..8961be952225 100644
--- a/app/Libraries/Utils.php
+++ b/app/Libraries/Utils.php
@@ -574,6 +574,11 @@ class Utils
}
}
+ public static function getVendorDisplayName($model)
+ {
+ return $model->getDisplayName();
+ }
+
public static function getPersonDisplayName($firstName, $lastName, $email)
{
if ($firstName || $lastName) {
@@ -605,7 +610,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;
}
}
diff --git a/app/Listeners/SubscriptionListener.php b/app/Listeners/SubscriptionListener.php
index 7ef7a1116e74..60643a25e6dd 100644
--- a/app/Listeners/SubscriptionListener.php
+++ b/app/Listeners/SubscriptionListener.php
@@ -9,6 +9,8 @@ use App\Events\InvoiceWasCreated;
use App\Events\CreditWasCreated;
use App\Events\PaymentWasCreated;
+use App\Events\VendorWasCreated;
+
class SubscriptionListener
{
public function createdClient(ClientWasCreated $event)
@@ -44,4 +46,9 @@ class SubscriptionListener
Utils::notifyZapier($subscription, $entity);
}
}
+
+ public function createdVendor(VendorWasCreated $event)
+ {
+ $this->checkSubscriptions(ACTIVITY_TYPE_CREATE_VENDOR, $event->vendor);
+ }
}
diff --git a/app/Models/User.php b/app/Models/User.php
index 51f507fa4fef..129ddb0abecd 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -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;
diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php
new file mode 100644
index 000000000000..690c34eecb65
--- /dev/null
+++ b/app/Models/Vendor.php
@@ -0,0 +1,302 @@
+ '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->send_invoice = true;
+ }
+
+ $contact->fill($data);
+ $contact->is_primary = $isPrimary;
+
+ return $this->vendorContacts()->save($contact);
+ }
+
+ public function updateBalances($balanceAdjustment, $paidToDateAdjustment)
+ {
+ if ($balanceAdjustment === 0 && $paidToDateAdjustment === 0) {
+ return;
+ }
+
+ $this->balance = $this->balance + $balanceAdjustment;
+ $this->paid_to_date = $this->paid_to_date + $paidToDateAdjustment;
+
+ $this->save();
+ }
+
+ public function getRoute()
+ {
+ return "/vendors/{$this->public_id}";
+ }
+
+
+ public function getTotalCredit()
+ {
+ return 0;
+ }
+
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function getDisplayName()
+ {
+ if ($this->name) {
+ return $this->name;
+ }
+
+ if ( ! count($this->contacts)) {
+ return '';
+ }
+
+ $contact = $this->contacts[0];
+ return $contact->getDisplayName();
+ }
+
+ 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 getGatewayToken()
+ {
+ $this->account->load('account_gateways');
+
+ if (!count($this->account->account_gateways)) {
+ return false;
+ }
+
+ $accountGateway = $this->account->getGatewayConfig(GATEWAY_STRIPE);
+
+ if (!$accountGateway) {
+ return false;
+ }
+
+ $token = AccountGatewayToken::where('vendor_id', '=', $this->id)->where('account_gateway_id', '=', $accountGateway->id)->first();
+
+ return $token ? $token->token : false;
+ }
+
+ public function getGatewayLink()
+ {
+ $token = $this->getGatewayToken();
+ return $token ? "https://dashboard.stripe.com/customers/{$token}" : false;
+ }
+
+ 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 getCounter($isQuote)
+ {
+ return $isQuote ? $this->quote_number_counter : $this->invoice_number_counter;
+ }
+ */
+
+ public function markLoggedIn()
+ {
+ //$this->last_login = Carbon::now()->toDateTimeString();
+ $this->save();
+ }
+}
+
+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));
+});
+
diff --git a/app/Models/VendorActivity.php b/app/Models/VendorActivity.php
new file mode 100644
index 000000000000..94208cfbc6a7
--- /dev/null
+++ b/app/Models/VendorActivity.php
@@ -0,0 +1,56 @@
+whereAccountId(Auth::user()->account_id);
+ }
+
+ public function account()
+ {
+ return $this->belongsTo('App\Models\Account');
+ }
+
+ public function user()
+ {
+ return $this->belongsTo('App\Models\User')->withTrashed();
+ }
+
+ public function vendorContact()
+ {
+ return $this->belongsTo('App\Models\VendorContact')->withTrashed();
+ }
+
+ public function vendor()
+ {
+ return $this->belongsTo('App\Models\Vendor')->withTrashed();
+ }
+
+ public function getMessage()
+ {
+ $activityTypeId = $this->activity_type_id;
+ $account = $this->account;
+ $vendor = $this->vendor;
+ $user = $this->user;
+ $contactId = $this->contact_id;
+ $isSystem = $this->is_system;
+
+ $data = [
+ 'vendor' => link_to($vendor->getRoute(), $vendor->getDisplayName()),
+ 'user' => $isSystem ? '' . trans('texts.system') . '' : $user->getDisplayName(),
+ 'vendorcontact' => $contactId ? $vendor->getDisplayName() : $user->getDisplayName(),
+ ];
+
+ return trans("texts.activity_{$activityTypeId}", $data);
+ }
+}
diff --git a/app/Models/VendorContact.php b/app/Models/VendorContact.php
new file mode 100644
index 000000000000..64d1dd92c39d
--- /dev/null
+++ b/app/Models/VendorContact.php
@@ -0,0 +1,68 @@
+belongsTo('App\Models\Account');
+ }
+
+ public function user()
+ {
+ return $this->belongsTo('App\Models\User');
+ }
+
+ public function vendor()
+ {
+ return $this->belongsTo('App\Models\Vendor')->withTrashed();
+ }
+
+ public function getPersonType()
+ {
+ return PERSON_VENDOR_CONTACT;
+ }
+
+ public function getName()
+ {
+ return $this->getDisplayName();
+ }
+
+ public function getDisplayName()
+ {
+ if ($this->getFullName()) {
+ return $this->getFullName();
+ } else {
+ return $this->email;
+ }
+ }
+
+ public function getFullName()
+ {
+ if ($this->first_name || $this->last_name) {
+ return $this->first_name.' '.$this->last_name;
+ } else {
+ return '';
+ }
+ }
+}
diff --git a/app/Models/VendorInvitation.php b/app/Models/VendorInvitation.php
new file mode 100644
index 000000000000..7457c8388a4d
--- /dev/null
+++ b/app/Models/VendorInvitation.php
@@ -0,0 +1,90 @@
+belongsTo('App\Models\VendorContact')->withTrashed();
+ }
+
+ public function user()
+ {
+ return $this->belongsTo('App\Models\User')->withTrashed();
+ }
+
+ public function account()
+ {
+ return $this->belongsTo('App\Models\Account');
+ }
+
+ public function getLink($type = 'view')
+ {
+ if (!$this->account) {
+ $this->load('account');
+ }
+
+ $url = SITE_URL;
+ $iframe_url = $this->account->iframe_url;
+
+ if ($this->account->isPro()) {
+ if ($iframe_url) {
+ return "{$iframe_url}/?{$this->invitation_key}";
+ } elseif ($this->account->subdomain) {
+ $url = Utils::replaceSubdomain($url, $this->account->subdomain);
+ }
+ }
+
+ return "{$url}/{$type}/{$this->invitation_key}";
+ }
+
+ public function getStatus()
+ {
+ $hasValue = false;
+ $parts = [];
+ $statuses = $this->message_id ? ['sent', 'opened', 'viewed'] : ['sent', 'viewed'];
+
+ foreach ($statuses as $status) {
+ $field = "{$status}_date";
+ $date = '';
+ if ($this->$field && $this->field != '0000-00-00 00:00:00') {
+ $date = Utils::dateToString($this->$field);
+ $hasValue = true;
+ }
+ $parts[] = trans('texts.invitation_status.' . $status) . ': ' . $date;
+ }
+
+ return $hasValue ? implode($parts, '
') : false;
+ }
+
+ public function getName()
+ {
+ return $this->invitation_key;
+ }
+
+ public function markSent($messageId = null)
+ {
+ $this->message_id = $messageId;
+ $this->email_error = null;
+ $this->sent_date = Carbon::now()->toDateTimeString();
+ $this->save();
+ }
+
+ public function markViewed()
+ {
+ //$invoice = $this->invoice;
+ //$client = $invoice->client;
+
+ $this->viewed_date = Carbon::now()->toDateTimeString();
+ $this->save();
+
+ //$invoice->markViewed();
+ //$client->markLoggedIn();
+ }
+}
diff --git a/app/Ninja/Import/BaseTransformer.php b/app/Ninja/Import/BaseTransformer.php
index ea05584c20d1..8e17bfeec36f 100644
--- a/app/Ninja/Import/BaseTransformer.php
+++ b/app/Ninja/Import/BaseTransformer.php
@@ -87,4 +87,11 @@ class BaseTransformer extends TransformerAbstract
return isset($this->maps[ENTITY_INVOICE.'_'.ENTITY_CLIENT][$invoiceNumber])? $this->maps[ENTITY_INVOICE.'_'.ENTITY_CLIENT][$invoiceNumber] : null;
}
+
+ protected function getVendorId($name)
+ {
+ $name = strtolower($name);
+ return isset($this->maps[ENTITY_VENDOR][$name]) ? $this->maps[ENTITY_VENDOR][$name] : null;
+ }
+
}
\ No newline at end of file
diff --git a/app/Ninja/Import/CSV/VendorTransformer.php b/app/Ninja/Import/CSV/VendorTransformer.php
new file mode 100644
index 000000000000..aba42c84cd5b
--- /dev/null
+++ b/app/Ninja/Import/CSV/VendorTransformer.php
@@ -0,0 +1,35 @@
+name) && $this->hasVendor($data->name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $this->getString($data, 'name'),
+ 'work_phone' => $this->getString($data, 'work_phone'),
+ 'address1' => $this->getString($data, 'address1'),
+ 'city' => $this->getString($data, 'city'),
+ 'state' => $this->getString($data, 'state'),
+ 'postal_code' => $this->getString($data, 'postal_code'),
+ 'private_notes' => $this->getString($data, 'notes'),
+ 'contacts' => [
+ [
+ 'first_name' => $this->getString($data, 'first_name'),
+ 'last_name' => $this->getString($data, 'last_name'),
+ 'email' => $this->getString($data, 'email'),
+ 'phone' => $this->getString($data, 'phone'),
+ ],
+ ],
+ 'country_id' => isset($data->country) ? $this->getCountryId($data->country) : null,
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/FreshBooks/VendorTransformer.php b/app/Ninja/Import/FreshBooks/VendorTransformer.php
new file mode 100644
index 000000000000..32880b1454cb
--- /dev/null
+++ b/app/Ninja/Import/FreshBooks/VendorTransformer.php
@@ -0,0 +1,36 @@
+hasVendor($data->organization)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->organization,
+ 'work_phone' => $data->busphone,
+ 'address1' => $data->street,
+ 'address2' => $data->street2,
+ 'city' => $data->city,
+ 'state' => $data->province,
+ 'postal_code' => $data->postalcode,
+ 'private_notes' => $data->notes,
+ 'contacts' => [
+ [
+ 'first_name' => $data->firstname,
+ 'last_name' => $data->lastname,
+ 'email' => $data->email,
+ 'phone' => $data->mobphone ?: $data->homephone,
+ ],
+ ],
+ 'country_id' => $this->getCountryId($data->country),
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Harvest/VendorContactTransformer.php b/app/Ninja/Import/Harvest/VendorContactTransformer.php
new file mode 100644
index 000000000000..5c4e445db4e6
--- /dev/null
+++ b/app/Ninja/Import/Harvest/VendorContactTransformer.php
@@ -0,0 +1,24 @@
+hasVendor($data->vendor)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'vendor_id' => $this->getVendorId($data->vendor),
+ 'first_name' => $data->first_name,
+ 'last_name' => $data->last_name,
+ 'email' => $data->email,
+ 'phone' => $data->office_phone ?: $data->mobile_phone,
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Harvest/VendorTransformer.php b/app/Ninja/Import/Harvest/VendorTransformer.php
new file mode 100644
index 000000000000..896211c7abf6
--- /dev/null
+++ b/app/Ninja/Import/Harvest/VendorTransformer.php
@@ -0,0 +1,20 @@
+hasVendor($data->vendor_name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->vendor_name,
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Hiveage/VendorTransformer.php b/app/Ninja/Import/Hiveage/VendorTransformer.php
new file mode 100644
index 000000000000..26338ff618a4
--- /dev/null
+++ b/app/Ninja/Import/Hiveage/VendorTransformer.php
@@ -0,0 +1,35 @@
+hasVendor($data->name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->name,
+ 'contacts' => [
+ [
+ 'first_name' => $this->getFirstName($data->primary_contact),
+ 'last_name' => $this->getLastName($data->primary_contactk),
+ 'email' => $data->business_email,
+ ],
+ ],
+ 'address1' => $data->address_1,
+ 'address2' => $data->address_2,
+ 'city' => $data->city,
+ 'state' => $data->state_name,
+ 'postal_code' => $data->zip_code,
+ 'work_phone' => $data->phone,
+ 'website' => $data->website,
+ 'country_id' => $this->getCountryId($data->country),
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Invoiceable/VendorTransformer.php b/app/Ninja/Import/Invoiceable/VendorTransformer.php
new file mode 100644
index 000000000000..5e95bb16fa64
--- /dev/null
+++ b/app/Ninja/Import/Invoiceable/VendorTransformer.php
@@ -0,0 +1,34 @@
+hasVendor($data->vendor_name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->vendor_name,
+ 'work_phone' => $data->tel,
+ 'website' => $data->website,
+ 'address1' => $data->address,
+ 'city' => $data->city,
+ 'state' => $data->state,
+ 'postal_code' => $data->postcode,
+ 'country_id' => $this->getCountryIdBy2($data->country),
+ 'private_notes' => $data->notes,
+ 'contacts' => [
+ [
+ 'email' => $data->email,
+ 'phone' => $data->mobile,
+ ],
+ ],
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Nutcache/VendorTransformer.php b/app/Ninja/Import/Nutcache/VendorTransformer.php
new file mode 100644
index 000000000000..f21ba99e35cc
--- /dev/null
+++ b/app/Ninja/Import/Nutcache/VendorTransformer.php
@@ -0,0 +1,35 @@
+hasVendor($data->name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->name,
+ 'city' => isset($data->city) ? $data->city : '',
+ 'state' => isset($data->city) ? $data->stateprovince : '',
+ 'id_number' => isset($data->registration_number) ? $data->registration_number : '',
+ 'postal_code' => isset($data->postalzip_code) ? $data->postalzip_code : '',
+ 'private_notes' => isset($data->notes) ? $data->notes : '',
+ 'work_phone' => isset($data->phone) ? $data->phone : '',
+ 'contacts' => [
+ [
+ 'first_name' => isset($data->contact_name) ? $this->getFirstName($data->contact_name) : '',
+ 'last_name' => isset($data->contact_name) ? $this->getLastName($data->contact_name) : '',
+ 'email' => $data->email,
+ 'phone' => isset($data->mobile) ? $data->mobile : '',
+ ],
+ ],
+ 'country_id' => isset($data->country) ? $this->getCountryId($data->country) : null,
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Ronin/VendorTransformer.php b/app/Ninja/Import/Ronin/VendorTransformer.php
new file mode 100644
index 000000000000..541d2bdaad14
--- /dev/null
+++ b/app/Ninja/Import/Ronin/VendorTransformer.php
@@ -0,0 +1,28 @@
+hasVendor($data->company)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->company,
+ 'work_phone' => $data->phone,
+ 'contacts' => [
+ [
+ 'first_name' => $this->getFirstName($data->name),
+ 'last_name' => $this->getLastName($data->name),
+ 'email' => $data->email,
+ ],
+ ],
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Wave/VendorTransformer.php b/app/Ninja/Import/Wave/VendorTransformer.php
new file mode 100644
index 000000000000..2aab69fca06e
--- /dev/null
+++ b/app/Ninja/Import/Wave/VendorTransformer.php
@@ -0,0 +1,38 @@
+hasVendor($data->customer_name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->customer_name,
+ 'id_number' => $data->account_number,
+ 'work_phone' => $data->phone,
+ 'website' => $data->website,
+ 'address1' => $data->address_line_1,
+ 'address2' => $data->address_line_2,
+ 'city' => $data->city,
+ 'state' => $data->provincestate,
+ 'postal_code' => $data->postal_codezip_code,
+ 'private_notes' => $data->delivery_instructions,
+ 'contacts' => [
+ [
+ 'first_name' => $data->contact_first_name,
+ 'last_name' => $data->contact_last_name,
+ 'email' => $data->email,
+ 'phone' => $data->mobile,
+ ],
+ ],
+ 'country_id' => $this->getCountryId($data->country),
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Import/Zoho/VendorTransformer.php b/app/Ninja/Import/Zoho/VendorTransformer.php
new file mode 100644
index 000000000000..6007a2227f69
--- /dev/null
+++ b/app/Ninja/Import/Zoho/VendorTransformer.php
@@ -0,0 +1,37 @@
+hasVendor($data->customer_name)) {
+ return false;
+ }
+
+ return new Item($data, function ($data) {
+ return [
+ 'name' => $data->customer_name,
+ 'id_number' => $data->customer_id,
+ 'work_phone' => $data->phonek,
+ 'address1' => $data->billing_address,
+ 'city' => $data->billing_city,
+ 'state' => $data->billing_state,
+ 'postal_code' => $data->billing_code,
+ 'private_notes' => $data->notes,
+ 'website' => $data->website,
+ 'contacts' => [
+ [
+ 'first_name' => $data->first_name,
+ 'last_name' => $data->last_name,
+ 'email' => $data->emailid,
+ 'phone' => $data->mobilephone,
+ ],
+ ],
+ 'country_id' => $this->getCountryId($data->billing_country),
+ ];
+ });
+ }
+}
diff --git a/app/Ninja/Repositories/VendorActivityRepository.php b/app/Ninja/Repositories/VendorActivityRepository.php
new file mode 100644
index 000000000000..d776e19c5ae7
--- /dev/null
+++ b/app/Ninja/Repositories/VendorActivityRepository.php
@@ -0,0 +1,101 @@
+invoice->vendor;
+ } else {
+ $vendor = $entity->vendor;
+ }
+
+ $this->vendor = $vendor;
+
+ // init activity and copy over context
+ $activity = self::getBlank($altEntity ?: $vendor);
+ $activity = Utils::copyContext($activity, $entity);
+ $activity = Utils::copyContext($activity, $altEntity);
+
+ $activity->vendor_id = $vendor->id;
+ $activity->activity_type_id = $activityTypeId;
+ $activity->adjustment = $balanceChange;
+ $activity->balance = $vendor->balance + $balanceChange;
+
+ $keyField = $entity->getKeyField();
+ $activity->$keyField = $entity->id;
+
+ $activity->ip = Request::getClientIp();
+ $activity->save();
+
+ $vendor->updateBalances($balanceChange, $paidToDateChange);
+
+ return $activity;
+ }
+
+ private function getBlank($entity)
+ {
+ $activity = new VendorActivity();
+
+ if (Auth::check() && Auth::user()->account_id == $entity->account_id) {
+ $activity->user_id = Auth::user()->id;
+ $activity->account_id = Auth::user()->account_id;
+ } else {
+ $activity->user_id = $entity->user_id;
+ $activity->account_id = $entity->account_id;
+
+ if ( ! $entity instanceof Invitation) {
+ $activity->is_system = true;
+ }
+ }
+
+ $activity->token_id = session('token_id');
+
+ return $activity;
+ }
+
+ public function findByVendorId($vendorId)
+ {
+ return DB::table('vendor_activities')
+ ->join('accounts', 'accounts.id', '=', 'activities.account_id')
+ ->join('users', 'users.id', '=', 'activities.user_id')
+ ->join('vendors', 'vendors.id', '=', 'activities.vendor_id')
+ ->leftJoin('vendor_contacts', 'vendor_contacts.vendor_id', '=', 'vendors.id')
+ ->where('vendors.id', '=', $vendorId)
+ ->where('vendor_contacts.is_primary', '=', 1)
+ ->whereNull('vendor_contacts.deleted_at')
+ ->select(
+ DB::raw('COALESCE(vendors.currency_id, accounts.currency_id) currency_id'),
+ DB::raw('COALESCE(vendors.country_id, accounts.country_id) country_id'),
+ 'vendor_activities.id',
+ 'vendor_activities.created_at',
+ 'vendor_activities.contact_id',
+ 'vendor_activities.activity_type_id',
+ 'vendor_activities.is_system',
+ 'vendor_activities.balance',
+ 'vendor_activities.adjustment',
+ 'users.first_name as user_first_name',
+ 'users.last_name as user_last_name',
+ 'users.email as user_email',
+ 'vendors.name as vendor_name',
+ 'vendors.public_id as vendor_public_id',
+ 'vendor_contacts.id as contact',
+ 'vendor_contacts.first_name as first_name',
+ 'vendor_contacts.last_name as last_name',
+ 'vendor_contacts.email as email'
+
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/app/Ninja/Repositories/VendorContactRepository.php b/app/Ninja/Repositories/VendorContactRepository.php
new file mode 100644
index 000000000000..9bae790aef78
--- /dev/null
+++ b/app/Ninja/Repositories/VendorContactRepository.php
@@ -0,0 +1,26 @@
+send_invoice = true;
+ $contact->vendor_id = $data['vendor_id'];
+ $contact->is_primary = VendorContact::scope()->where('vendor_id', '=', $contact->vendor_id)->count() == 0;
+ } else {
+ $contact = VendorContact::scope($publicId)->firstOrFail();
+ }
+
+ $contact->fill($data);
+ $contact->save();
+
+ return $contact;
+ }
+}
\ No newline at end of file
diff --git a/app/Ninja/Repositories/VendorRepository.php b/app/Ninja/Repositories/VendorRepository.php
new file mode 100644
index 000000000000..e5d059913594
--- /dev/null
+++ b/app/Ninja/Repositories/VendorRepository.php
@@ -0,0 +1,102 @@
+with('user', 'vendorcontacts', 'country')
+ ->withTrashed()
+ ->where('is_deleted', '=', false)
+ ->get();
+ }
+
+ public function find($filter = null)
+ {
+ $query = DB::table('vendors')
+ ->join('accounts', 'accounts.id', '=', 'vendors.account_id')
+ ->join('vendor_contacts', 'vendor_contacts.vendor_id', '=', 'vendors.id')
+ ->where('vendors.account_id', '=', \Auth::user()->account_id)
+ ->where('vendor_contacts.is_primary', '=', true)
+ ->where('vendor_contacts.deleted_at', '=', null)
+ ->select(
+ DB::raw('COALESCE(vendors.currency_id, accounts.currency_id) currency_id'),
+ DB::raw('COALESCE(vendors.country_id, accounts.country_id) country_id'),
+ 'vendors.public_id',
+ 'vendors.name',
+ 'vendor_contacts.first_name',
+ 'vendor_contacts.last_name',
+ 'vendors.balance',
+ //'vendors.last_login',
+ '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['vendor_contact']) && ! isset($data['vendor_contacts'])) {
+ return $vendor;
+ }
+
+
+ $first = true;
+ $vendorcontacts = isset($data['vendor_contact']) ? [$data['vendor_contact']] : $data['vendor_contacts'];
+ $vendorcontactIds = [];
+
+ foreach ($vendorcontacts as $vendorcontact) {
+ $vendorcontact = $vendor->addVendorContact($vendorcontact, $first);
+ $vendorcontactIds[] = $vendorcontact->public_id;
+ $first = false;
+ }
+
+ foreach ($vendor->vendorcontacts as $vendorcontact) {
+ if (!in_array($vendorcontact->public_id, $vendorcontactIds)) {
+ $vendorcontact->delete();
+ }
+ }
+
+ return $vendor;
+ }
+}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 84a93e177b40..d584ef406ae3 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -53,11 +53,8 @@ class AppServiceProvider extends ServiceProvider {
$str .= '