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 .= '
  • '.trans("texts.credits").'
  • '.trans("texts.new_credit").'
  • '; - } else if($type == 'expense'){ + } else if ($type == ENTITY_EXPENSE) { $str .= '
  • -
  • '.trans("texts.expenses").'
  • -
  • '.trans("texts.new_expense").'
  • -
  • '.trans("texts.vendors").'
  • '.trans("texts.new_vendor").'
  • '; } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 7e0743d501c4..def84e1fdddd 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -137,6 +137,21 @@ class EventServiceProvider extends ServiceProvider { 'App\Listeners\HandleUserSettingsChanged', ], + // vendor events + 'App\Events\VendorWasCreated' => [ + 'App\Listeners\VendorActivityListener@createdVendor', + 'App\Listeners\SubscriptionListener@createdVendor', + ], + 'App\Events\VendorWasArchived' => [ + 'App\Listeners\VendorActivityListener@archivedVendor', + ], + 'App\Events\VendorWasDeleted' => [ + 'App\Listeners\VendorActivityListener@deletedVendor', + ], + 'App\Events\VendorWasRestored' => [ + 'App\Listeners\VendorActivityListener@restoredVendor', + ], + ]; /** diff --git a/app/Services/VendorActivityService.php b/app/Services/VendorActivityService.php new file mode 100644 index 000000000000..641fbdb1c1f2 --- /dev/null +++ b/app/Services/VendorActivityService.php @@ -0,0 +1,67 @@ +activityRepo = $activityRepo; + $this->datatableService = $datatableService; + } + + public function getDatatable($vendorPublicId = null) + { + $vendorId = Vendor::getPrivateId($vendorPublicId); + + $query = $this->activityRepo->findByVendorId($vendorId); + + return $this->createDatatable(ENTITY_ACTIVITY, $query); + } + + protected function getDatatableColumns($entityType, $hideVendor) + { + return [ + [ + 'vendor_activities.id', + function ($model) { + return Utils::timestampToDateTimeString(strtotime($model->created_at)); + } + ], + [ + 'activity_type_id', + function ($model) { + $data = [ + 'vendor' => link_to('/vendors/' . $model->vendor_public_id, Utils::getVendorDisplayName($model)), + 'user' => $model->is_system ? '' . trans('texts.system') . '' : Utils::getPersonDisplayName($model->user_first_name, $model->user_last_name, $model->user_email), + 'invoice' => $model->invoice ? link_to('/invoices/' . $model->invoice_public_id, $model->is_recurring ? trans('texts.recurring_invoice') : $model->invoice) : null, + 'quote' => $model->invoice ? link_to('/quotes/' . $model->invoice_public_id, $model->invoice) : null, + 'contact' => $model->contact_id ? link_to('/vendors/' . $model->vendor_public_id, Utils::getVendorDisplayName($model)) : Utils::getPersonDisplayName($model->user_first_name, $model->user_last_name, $model->user_email), + 'payment' => $model->payment ?: '', + 'credit' => Utils::formatMoney($model->credit, $model->currency_id, $model->country_id) + ]; + + return trans("texts.activity_{$model->activity_type_id}", $data); + } + ], + [ + 'balance', + function ($model) { + return Utils::formatMoney($model->balance, $model->currency_id, $model->country_id); + } + ], + [ + 'adjustment', + function ($model) { + return $model->adjustment != 0 ? Utils::wrapAdjustment($model->adjustment, $model->currency_id, $model->country_id) : ''; + } + ] + ]; + } +} \ No newline at end of file diff --git a/app/Services/VendorService.php b/app/Services/VendorService.php new file mode 100644 index 000000000000..9418dcdd5cd3 --- /dev/null +++ b/app/Services/VendorService.php @@ -0,0 +1,103 @@ +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)); + } + ], + /*[ + 'last_login', + function ($model) { + return Utils::timestampToDateString(strtotime($model->last_login)); + } + ],*/ + [ + 'balance', + function ($model) { + return Utils::formatMoney($model->balance, $model->currency_id, $model->country_id); + } + ] + ]; + } + + protected function getDatatableActions($entityType) + { + return [ + [ + trans('texts.edit_vendor'), + function ($model) { + return URL::to("vendors/{$model->public_id}/edit"); + } + ], + [], + [ + trans('texts.enter_expense'), + function ($model) { + return URL::to("expenses/create/{$model->public_id}"); + } + ] + ]; + } +} diff --git a/composer.lock b/composer.lock index 4e4a12f12ea8..a5fb2a6d22aa 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "f3b07ee34ed5086cd684a7207fd3617e", + "hash": "71b1d7519dd65f8e028c1c417354606d", "content-hash": "7305e2f5d6864894aeb23ac91f3e1fe7", "packages": [ { @@ -1272,33 +1272,33 @@ }, { "name": "doctrine/cache", - "version": "v1.5.4", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "47cdc76ceb95cc591d9c79a36dc3794975b5d136" + "reference": "f8af318d14bdb0eff0336795b428b547bd39ccb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/47cdc76ceb95cc591d9c79a36dc3794975b5d136", - "reference": "47cdc76ceb95cc591d9c79a36dc3794975b5d136", + "url": "https://api.github.com/repos/doctrine/cache/zipball/f8af318d14bdb0eff0336795b428b547bd39ccb6", + "reference": "f8af318d14bdb0eff0336795b428b547bd39ccb6", "shasum": "" }, "require": { - "php": ">=5.3.2" + "php": "~5.5|~7.0" }, "conflict": { "doctrine/common": ">2.2,<2.4" }, "require-dev": { - "phpunit/phpunit": ">=3.7", + "phpunit/phpunit": "~4.8|~5.0", "predis/predis": "~1.0", "satooshi/php-coveralls": "~0.6" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.5.x-dev" + "dev-master": "1.6.x-dev" } }, "autoload": { @@ -1338,7 +1338,7 @@ "cache", "caching" ], - "time": "2015-12-19 05:03:47" + "time": "2015-12-31 16:37:02" }, { "name": "doctrine/collections", @@ -2254,12 +2254,12 @@ "source": { "type": "git", "url": "https://github.com/Intervention/image.git", - "reference": "e6acb1609ce89f2c1ec7864fbbda0a20a3eeca70" + "reference": "9f29360b8ab94585cb9e80cf9045abd5b85feb89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/image/zipball/e6acb1609ce89f2c1ec7864fbbda0a20a3eeca70", - "reference": "e6acb1609ce89f2c1ec7864fbbda0a20a3eeca70", + "url": "https://api.github.com/repos/Intervention/image/zipball/9f29360b8ab94585cb9e80cf9045abd5b85feb89", + "reference": "9f29360b8ab94585cb9e80cf9045abd5b85feb89", "shasum": "" }, "require": { @@ -2308,7 +2308,7 @@ "thumbnail", "watermark" ], - "time": "2015-12-04 17:09:36" + "time": "2016-01-02 19:15:13" }, { "name": "ircmaxell/password-compat", @@ -7593,16 +7593,16 @@ }, { "name": "facebook/webdriver", - "version": "1.1.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/facebook/php-webdriver.git", - "reference": "518a9e0635e69777f07e41619cdf5d82fae284b6" + "reference": "1c98108ba3eb435b681655764de11502a0653705" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/518a9e0635e69777f07e41619cdf5d82fae284b6", - "reference": "518a9e0635e69777f07e41619cdf5d82fae284b6", + "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/1c98108ba3eb435b681655764de11502a0653705", + "reference": "1c98108ba3eb435b681655764de11502a0653705", "shasum": "" }, "require": { @@ -7632,7 +7632,7 @@ "selenium", "webdriver" ], - "time": "2015-12-08 17:04:30" + "time": "2015-12-31 15:58:49" }, { "name": "fzaninotto/faker", @@ -7722,16 +7722,16 @@ }, { "name": "phpspec/phpspec", - "version": "2.4.0", + "version": "2.4.1", "source": { "type": "git", "url": "https://github.com/phpspec/phpspec.git", - "reference": "1d3938e6d9ffb1bd4805ea8ddac62ea48767f358" + "reference": "5528ce1e93a1efa090c9404aba3395c329b4e6ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/phpspec/zipball/1d3938e6d9ffb1bd4805ea8ddac62ea48767f358", - "reference": "1d3938e6d9ffb1bd4805ea8ddac62ea48767f358", + "url": "https://api.github.com/repos/phpspec/phpspec/zipball/5528ce1e93a1efa090c9404aba3395c329b4e6ed", + "reference": "5528ce1e93a1efa090c9404aba3395c329b4e6ed", "shasum": "" }, "require": { @@ -7796,7 +7796,7 @@ "testing", "tests" ], - "time": "2015-11-29 02:03:49" + "time": "2016-01-01 10:17:54" }, { "name": "phpspec/prophecy", diff --git a/database/migrations/2016_01_04_175228_create_vendors_table.php b/database/migrations/2016_01_04_175228_create_vendors_table.php index 67d4f6c088fa..bcc87afe5b2d 100644 --- a/database/migrations/2016_01_04_175228_create_vendors_table.php +++ b/database/migrations/2016_01_04_175228_create_vendors_table.php @@ -27,7 +27,7 @@ class CreateVendorsTable extends Migration { $table->string('city'); $table->string('state'); $table->string('postal_code'); - $table->integer('country_id',false, true); + $table->integer('country_id')->default(0); $table->string('work_phone'); $table->text('private_notes'); $table->decimal('balance',13,2); @@ -36,11 +36,11 @@ class CreateVendorsTable extends Migration { //$table->dateTime('last_login'); $table->string('website'); - $table->integer('industry_id',false, true); - $table->integer('size_id'); + $table->integer('industry_id')->nullable(); + $table->integer('size_id')->nullable(); $table->tinyInteger('is_deleted')->default(0); $table->integer('payment_terms')->nullable(); - $table->integer('public_id',false, true); + $table->integer('public_id')->default(0); $table->string('custom_value1')->nullable(); $table->string('custom_value2')->nullable(); $table->string('vat_number')->nullable(); diff --git a/database/migrations/2016_01_05_134713_create_vendorcontacts_table.php b/database/migrations/2016_01_05_134713_create_vendorcontacts_table.php new file mode 100644 index 000000000000..da39645e208b --- /dev/null +++ b/database/migrations/2016_01_05_134713_create_vendorcontacts_table.php @@ -0,0 +1,51 @@ +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->boolean('send_invoice')->default(0); + $table->string('first_name')->nullable(); + $table->string('last_name')->nullable(); + $table->string('email')->nullable(); + $table->string('phone')->nullable(); + $table->timestamp('last_login')->nullable(); + + $table->foreign('vendor_id')->references('id')->on('vendors')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');; + + $table->unsignedInteger('public_id')->nullable(); + $table->unique( array('account_id','public_id') ); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('vendor_contacts'); + } + +} diff --git a/database/migrations/2016_01_06_101343_create_table_Vendor_Invitations.php b/database/migrations/2016_01_06_101343_create_table_Vendor_Invitations.php new file mode 100644 index 000000000000..5a0f82b97b5e --- /dev/null +++ b/database/migrations/2016_01_06_101343_create_table_Vendor_Invitations.php @@ -0,0 +1,55 @@ +increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('contact_id'); + //$table->unsignedInteger('invoice_id')->index(); + $table->string('invitation_key')->index()->unique(); + $table->timestamps(); + $table->softDeletes(); + + $table->string('transaction_reference')->nullable(); + $table->timestamp('sent_date')->nullable(); + $table->timestamp('viewed_date')->nullable(); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');; + $table->foreign('contact_id')->references('id')->on('vendor_contacts')->onDelete('cascade'); + //$table->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade'); + + $table->unsignedInteger('public_id')->index(); + $table->unique( array('account_id','public_id') ); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('vendor_invitations', function(Blueprint $table) + { + // + }); + + Schema::dropIfExists('vendor_invitations'); + } + +} diff --git a/database/migrations/2016_01_06_102020_create_table_Vendor_Activities.php b/database/migrations/2016_01_06_102020_create_table_Vendor_Activities.php new file mode 100644 index 000000000000..bb40d585ad4e --- /dev/null +++ b/database/migrations/2016_01_06_102020_create_table_Vendor_Activities.php @@ -0,0 +1,58 @@ +increments('id'); + $table->timestamps(); + + $table->unsignedInteger('account_id'); + $table->unsignedInteger('vendor_id'); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('contact_id')->nullable(); + $table->unsignedInteger('payment_id')->nullable(); + //$table->unsignedInteger('invoice_id')->nullable(); + $table->unsignedInteger('credit_id')->nullable(); + $table->unsignedInteger('invitation_id')->nullable(); + + $table->text('message')->nullable(); + $table->text('json_backup')->nullable(); + $table->integer('activity_type_id'); + $table->decimal('adjustment', 13, 2)->nullable(); + $table->decimal('balance', 13, 2)->nullable(); + $table->unsignedInteger('token_id')->nullable(); + $table->string('ip')->nullable(); + $table->boolean('is_system')->default(0); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('vendor_id')->references('id')->on('vendors')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('vendor_activities', function(Blueprint $table) + { + // + }); + + Schema::dropIfExists('vendor_activities'); + } + +} diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index b79bf9490d7e..68823f059e2b 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -256,6 +256,14 @@ return array( 'deleted_credits' => 'Successfully deleted :count credits', 'imported_file' => 'Successfully imported file', + 'updated_vendor' => 'Successfully updated vendor', + 'created_vendor' => 'Successfully created vendor', + 'archived_vendor' => 'Successfully archived vendor', + 'archived_vendors' => 'Successfully archived :count vendors', + 'deleted_vendor' => 'Successfully deleted vendor', + 'deleted_vendors' => 'Successfully deleted :count vendors', + + // Emails 'confirmation_subject' => 'Invoice Ninja Account Confirmation', 'confirmation_header' => 'Account Confirmation', @@ -997,5 +1005,16 @@ 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 + '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', ); diff --git a/resources/views/header.blade.php b/resources/views/header.blade.php index 60cad066a5ec..1cf55dfc9223 100644 --- a/resources/views/header.blade.php +++ b/resources/views/header.blade.php @@ -374,7 +374,7 @@ {!! HTML::menu_link('task') !!} {!! HTML::menu_link('invoice') !!} {!! HTML::menu_link('payment') !!} - {!! HTML::menu_link('expense') !!} + {!! HTML::menu_link('expense') !!}