1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-09 20:52:56 +01:00

Merge branch 'release-3.4.0'

This commit is contained in:
Hillel Coren 2017-06-15 12:04:28 +03:00
commit 2dd141abcf
178 changed files with 10435 additions and 1753 deletions

View File

@ -32,9 +32,6 @@ PHANTOMJS_CLOUD_KEY='a-demo-key-with-low-quota-per-ip-address'
LOG=single
REQUIRE_HTTPS=false
API_SECRET=password
IOS_DEVICE=
ANDROID_DEVICE=
FCM_API_TOKEN=
#TRUSTED_PROXIES=

View File

@ -10,7 +10,7 @@
## [Hosted](https://www.invoiceninja.com) | [Self-Hosted](https://www.invoiceninja.org) | [iPhone](https://itunes.apple.com/WebObjects/MZStore.woa/wa/viewSoftware?id=1220337560&mt=8) | [Android](https://play.google.com/store/apps/details?id=com.invoiceninja.invoiceninja)
Watch this [YouTube Video](https://www.youtube.com/watch?v=xHGKvadapbA) for an overview of the app's features.
Watch this [YouTube video](https://www.youtube.com/watch?v=xHGKvadapbA) for an overview of the app's features.
All Pro and Enterprise features from the hosted app are included in the open-source code. We offer a $20 per year white-label license to remove our branding.

View File

@ -0,0 +1,90 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\DbServer;
use App\Models\User;
use App\Models\Company;
class CalculatePayouts extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:calculate-payouts';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Calculate referral payouts';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->info('Running CalculatePayouts...');
$servers = DbServer::orderBy('id')->get(['name']);
$userMap = [];
foreach ($servers as $server) {
$this->info('Processing users: ' . $server->name);
config(['database.default' => $server->name]);
$users = User::where('referral_code', '!=', '')
->get(['email', 'referral_code']);
foreach ($users as $user) {
$userMap[$user->referral_code] = $user->email;
}
}
foreach ($servers as $server) {
$this->info('Processing companies: ' . $server->name);
config(['database.default' => $server->name]);
$companies = Company::where('referral_code', '!=', '')
->with('payment.client.payments')
->whereNotNull('payment_id')
->get();
foreach ($companies as $company) {
$user = $userMap[$company->referral_code];
$payment = $company->payment;
$client = $payment->client;
$this->info("User: $user");
foreach ($client->payments as $payment) {
$this->info("Date: $payment->payment_date, Amount: $payment->amount, Reference: $payment->transaction_reference");
}
}
}
}
protected function getOptions()
{
return [
];
}
}

View File

@ -10,6 +10,7 @@ use Mail;
use Symfony\Component\Console\Input\InputOption;
use Utils;
use App\Models\Contact;
use App\Models\Invoice;
use App\Models\Invitation;
/*
@ -71,6 +72,7 @@ class CheckData extends Command
if (! $this->option('client_id')) {
$this->checkBlankInvoiceHistory();
$this->checkPaidToDate();
$this->checkDraftSentInvoices();
}
$this->checkBalances();
@ -87,7 +89,6 @@ class CheckData extends Command
$this->logMessage('Done: ' . strtoupper($this->isValid ? RESULT_SUCCESS : RESULT_FAILURE));
$errorEmail = env('ERROR_EMAIL');
$this->info($this->log);
if ($errorEmail) {
Mail::raw($this->log, function ($message) use ($errorEmail, $database) {
@ -102,9 +103,34 @@ class CheckData extends Command
private function logMessage($str)
{
$str = date('Y-m-d h:i:s') . ' ' . $str;
$this->info($str);
$this->log .= $str . "\n";
}
private function checkDraftSentInvoices()
{
$invoices = Invoice::whereInvoiceStatusId(INVOICE_STATUS_SENT)
->whereIsPublic(false)
->withTrashed()
->get();
$this->logMessage(count($invoices) . ' draft sent invoices');
if (count($invoices) > 0) {
$this->isValid = false;
}
if ($this->option('fix') == 'true') {
foreach ($invoices as $invoice) {
if ($invoice->is_deleted) {
$invoice->unsetEventDispatcher();
}
$invoice->markSent();
}
}
}
private function checkOAuth()
{
// check for duplicate oauth ids
@ -334,7 +360,10 @@ class CheckData extends Command
private function checkInvitations()
{
$invoices = DB::table('invoices')
->leftJoin('invitations', 'invitations.invoice_id', '=', 'invoices.id')
->leftJoin('invitations', function ($join) {
$join->on('invitations.invoice_id', '=', 'invoices.id')
->whereNull('invitations.deleted_at');
})
->groupBy('invoices.id', 'invoices.user_id', 'invoices.account_id', 'invoices.client_id')
->havingRaw('count(invitations.id) = 0')
->get(['invoices.id', 'invoices.user_id', 'invoices.account_id', 'invoices.client_id']);
@ -398,7 +427,6 @@ class CheckData extends Command
],
'products' => [
ENTITY_USER,
ENTITY_TAX_RATE,
],
'vendors' => [
ENTITY_USER,
@ -413,25 +441,17 @@ class CheckData extends Command
ENTITY_USER,
ENTITY_CLIENT,
],
'accounts' => [
ENTITY_TAX_RATE,
]
];
foreach ($tables as $table => $entityTypes) {
foreach ($entityTypes as $entityType) {
$tableName = Utils::pluralizeEntityType($entityType);
if ($entityType == ENTITY_TAX_RATE) {
$field = 'default_' . $entityType;
} else {
$field = $entityType;
}
$field = $entityType;
if ($table == 'accounts') {
$accountId = 'id';
} else {
$accountId = 'account_id';
}
$records = DB::table($table)
->join($tableName, "{$tableName}.id", '=', "{$table}.{$field}_id")
->where("{$table}.{$accountId}", '!=', DB::raw("{$tableName}.account_id"))

View File

@ -315,7 +315,9 @@ class InitLookup extends Command
private function logMessage($str)
{
$this->log .= date('Y-m-d h:i:s') . ' ' . $str . "\n";
$str = date('Y-m-d h:i:s') . ' ' . $str;
$this->info($str);
$this->log .= $str . "\n";
}
private function logError($str)

View File

@ -84,12 +84,13 @@ class SendRecurringInvoices extends Command
foreach ($invoices as $recurInvoice) {
$shouldSendToday = $recurInvoice->shouldSendToday();
$this->info('Processing Invoice '.$recurInvoice->id.' - Should send '.($shouldSendToday ? 'YES' : 'NO'));
if (! $shouldSendToday) {
continue;
}
$this->info('Processing Invoice: '. $recurInvoice->id);
$account = $recurInvoice->account;
$account->loadLocalizationSettings($recurInvoice->client);
Auth::loginUsingId($recurInvoice->user_id);
@ -117,7 +118,7 @@ class SendRecurringInvoices extends Command
}
if ($invoice->getAutoBillEnabled() && $invoice->client->autoBillLater()) {
$this->info('Processing Autobill-delayed Invoice ' . $invoice->id);
$this->info('Processing Autobill-delayed Invoice: ' . $invoice->id);
Auth::loginUsingId($invoice->user_id);
$this->paymentService->autoBillInvoice($invoice);
Auth::logout();

View File

@ -28,6 +28,7 @@ class Kernel extends ConsoleKernel
'App\Console\Commands\MakeModule',
'App\Console\Commands\MakeClass',
'App\Console\Commands\InitLookup',
'App\Console\Commands\CalculatePayouts',
];
/**

View File

@ -138,7 +138,7 @@ if (! defined('APP_NAME')) {
define('LOGGED_ERROR_LIMIT', 100);
define('RANDOM_KEY_LENGTH', 32);
define('MAX_NUM_USERS', 20);
define('MAX_IMPORT_ROWS', 1000);
define('MAX_IMPORT_ROWS', 5000);
define('MAX_SUBDOMAIN_LENGTH', 30);
define('MAX_IFRAME_URL_LENGTH', 250);
define('MAX_LOGO_FILE_SIZE', 200); // KB
@ -162,6 +162,7 @@ if (! defined('APP_NAME')) {
define('IMPORT_ZOHO', 'Zoho');
define('IMPORT_NUTCACHE', 'Nutcache');
define('IMPORT_INVOICEABLE', 'Invoiceable');
define('IMPORT_INVOICEPLANE', 'InvoicePlane');
define('IMPORT_HARVEST', 'Harvest');
define('MAX_NUM_CLIENTS', 100);
@ -206,7 +207,9 @@ if (! defined('APP_NAME')) {
define('EXPENSE_STATUS_PAID', 5);
define('EXPENSE_STATUS_UNPAID', 6);
define('CUSTOM_DESIGN', 11);
define('CUSTOM_DESIGN1', 11);
define('CUSTOM_DESIGN2', 12);
define('CUSTOM_DESIGN3', 13);
define('FREQUENCY_WEEKLY', 1);
define('FREQUENCY_TWO_WEEKS', 2);
@ -300,7 +303,7 @@ if (! defined('APP_NAME')) {
define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com'));
define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest'));
define('NINJA_DATE', '2000-01-01');
define('NINJA_VERSION', '3.3.3' . env('NINJA_VERSION_SUFFIX'));
define('NINJA_VERSION', '3.4.0' . env('NINJA_VERSION_SUFFIX'));
define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja'));
define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja'));
@ -330,6 +333,7 @@ if (! defined('APP_NAME')) {
define('MSBOT_LUIS_URL', 'https://westus.api.cognitive.microsoft.com/luis/v2.0/apps');
define('SKYPE_API_URL', 'https://apis.skype.com/v3');
define('MSBOT_STATE_URL', 'https://state.botframework.com/v3');
define('INVOICEPLANE_IMPORT', 'https://github.com/turbo124/Plane2Ninja');
define('BOT_PLATFORM_WEB_APP', 'WebApp');
define('BOT_PLATFORM_SKYPE', 'Skype');
@ -341,7 +345,6 @@ if (! defined('APP_NAME')) {
define('DB_NINJA_2', 'db-ninja-2');
define('COUNT_FREE_DESIGNS', 4);
define('COUNT_FREE_DESIGNS_SELF_HOST', 5); // include the custom design
define('PRODUCT_ONE_CLICK_INSTALL', 1);
define('PRODUCT_INVOICE_DESIGNS', 2);
define('PRODUCT_WHITE_LABEL', 3);

View File

@ -59,13 +59,18 @@ class Handler extends ExceptionHandler
if (Utils::isNinja() && strpos(request()->url(), '/logo/') !== false) {
return false;
}
// Log 404s to a separate file
$errorStr = date('Y-m-d h:i:s') . ' ' . request()->url() . "\n" . json_encode(Utils::prepareErrorData('PHP')) . "\n\n";
@file_put_contents(storage_path('logs/not_found.log'), $errorStr, FILE_APPEND);
return false;
} elseif ($e instanceof HttpResponseException) {
return false;
}
if (Utils::isNinja() && ! Utils::isTravis()) {
if (! Utils::isTravis()) {
Utils::logError(Utils::getErrorString($e));
$stacktrace = date('Y-m-d h:i:s') . ' ' . $e->getTraceAsString() . "\n\n";
@file_put_contents(storage_path('logs/stacktrace.log'), $stacktrace, FILE_APPEND);
return false;
} else {
return parent::report($e);

View File

@ -82,7 +82,7 @@ class AccountApiController extends BaseAPIController
$updatedAt = $request->updated_at ? date('Y-m-d H:i:s', $request->updated_at) : false;
$transformer = new AccountTransformer(null, $request->serializer);
$account->load(array_merge($transformer->getDefaultIncludes(), ['projects.client', 'products.default_tax_rate']));
$account->load(array_merge($transformer->getDefaultIncludes(), ['projects.client']));
$account = $this->createItem($account, $transformer, 'account');
return $this->response($account);

View File

@ -373,9 +373,18 @@ class AccountController extends BaseController
private function showAccountManagement()
{
$account = Auth::user()->account;
$planDetails = $account->getPlanDetails(true);
$portalLink = false;
if ($planDetails && $ninjaClient = $this->accountRepo->getNinjaClient($account)) {
$contact = $ninjaClient->getPrimaryContact();
$portalLink = $contact->link;
}
$data = [
'account' => $account,
'planDetails' => $account->getPlanDetails(true),
'portalLink' => $portalLink,
'planDetails' => $planDetails,
'title' => trans('texts.account_management'),
];
@ -486,7 +495,7 @@ class AccountController extends BaseController
$data = [
'account' => Auth::user()->account,
'title' => trans('texts.tax_rates'),
'taxRates' => TaxRate::scope()->whereIsInclusive(false)->get(['id', 'name', 'rate']),
'taxRates' => TaxRate::scope()->whereIsInclusive(false)->get(),
];
return View::make('accounts.tax_rates', $data);
@ -571,7 +580,11 @@ class AccountController extends BaseController
}
if ($section == ACCOUNT_CUSTOMIZE_DESIGN) {
$data['customDesign'] = ($account->custom_design && ! $design) ? $account->custom_design : $design;
if ($custom = $account->getCustomDesign(request()->design_id)) {
$data['customDesign'] = $custom;
} else {
$data['customDesign'] = $design;
}
// sample invoice to help determine variables
$invoice = Invoice::scope()
@ -736,16 +749,21 @@ class AccountController extends BaseController
*/
private function saveCustomizeDesign()
{
$designId = intval(Input::get('design_id')) ?: CUSTOM_DESIGN1;
$field = 'custom_design' . ($designId - 10);
if (Auth::user()->account->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN)) {
$account = Auth::user()->account;
$account->custom_design = Input::get('custom_design');
$account->invoice_design_id = CUSTOM_DESIGN;
if (! $account->custom_design1) {
$account->invoice_design_id = CUSTOM_DESIGN1;
}
$account->$field = Input::get('custom_design');
$account->save();
Session::flash('message', trans('texts.updated_settings'));
}
return Redirect::to('settings/'.ACCOUNT_CUSTOMIZE_DESIGN);
return Redirect::to('settings/' . ACCOUNT_CUSTOMIZE_DESIGN . '?design_id=' . $designId);
}
/**
@ -951,6 +969,7 @@ class AccountController extends BaseController
$account->primary_color = Input::get('primary_color');
$account->secondary_color = Input::get('secondary_color');
$account->invoice_design_id = Input::get('invoice_design_id');
$account->quote_design_id = Input::get('quote_design_id');
$account->font_size = intval(Input::get('font_size'));
$account->page_size = Input::get('page_size');
@ -996,6 +1015,10 @@ class AccountController extends BaseController
$user->notify_approved = Input::get('notify_approved');
$user->save();
$account = $user->account;
$account->fill(request()->all());
$account->save();
Session::flash('message', trans('texts.updated_settings'));
return Redirect::to('settings/'.ACCOUNT_NOTIFICATIONS);
@ -1354,20 +1377,18 @@ class AccountController extends BaseController
$account = Auth::user()->account;
\Log::info("Canceled Account: {$account->name} - {$user->email}");
$company = $account->company;
$refunded = $company->processRefund(Auth::user());
$refunded = false;
if (! $account->hasMultipleAccounts()) {
$company = $account->company;
$refunded = $company->processRefund(Auth::user());
}
Document::scope()->each(function ($item, $key) {
$item->delete();
});
$this->accountRepo->unlinkAccount($account);
if ($account->hasMultipleAccounts()) {
$account->forceDelete();
} else {
$account->company->forceDelete();
}
$account->forceDelete();
Auth::logout();
Session::flush();

View File

@ -74,6 +74,10 @@ class BaseAPIController extends Controller
$entity = $request->entity();
$action = $request->action;
if (! in_array($action, ['archive', 'delete', 'restore'])) {
return $this->errorResponse("Action [$action] is not supported");
}
$repo = Utils::toCamelCase($this->entityType) . 'Repo';
$this->$repo->$action($entity);

View File

@ -37,7 +37,7 @@ class BaseController extends Controller
// when restoring redirect to entity
if ($action == 'restore' && count($ids) == 1) {
return redirect("{$entityTypes}/" . $ids[0] . '/edit');
return redirect("{$entityTypes}/" . $ids[0]);
// when viewing from a datatable list
} elseif (strpos($referer, '/clients/')) {
return redirect($referer);

View File

@ -91,8 +91,8 @@ class ClientPortalController extends BaseController
];
$invoice->invoice_fonts = $account->getFontsData();
if ($invoice->invoice_design_id == CUSTOM_DESIGN) {
$invoice->invoice_design->javascript = $account->custom_design;
if ($design = $account->getCustomDesign($invoice->invoice_design_id)) {
$invoice->invoice_design->javascript = $design;
} else {
$invoice->invoice_design->javascript = $invoice->invoice_design->pdfmake;
}
@ -200,7 +200,8 @@ class ClientPortalController extends BaseController
}
$invoice = $invitation->invoice;
$pdfString = $invoice->getPDFString();
$decode = ! request()->base64;
$pdfString = $invoice->getPDFString($decode);
header('Content-Type: application/pdf');
header('Content-Length: ' . strlen($pdfString));

View File

@ -0,0 +1,185 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\CreditRequest;
use App\Http\Requests\CreateCreditRequest;
use App\Http\Requests\UpdateCreditRequest;
use App\Models\Invoice;
use App\Models\Credit;
use App\Ninja\Repositories\CreditRepository;
use Input;
use Response;
class CreditApiController extends BaseAPIController
{
protected $creditRepo;
protected $entityType = ENTITY_CREDIT;
public function __construct(CreditRepository $creditRepo)
{
parent::__construct();
$this->creditRepo = $creditRepo;
}
/**
* @SWG\Get(
* path="/credits",
* summary="List credits",
* operationId="listCredits",
* tags={"credit"},
* @SWG\Response(
* response=200,
* description="A list of credits",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Credit"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function index()
{
$credits = Credit::scope()
->withTrashed()
->with(['client'])
->orderBy('created_at', 'desc');
return $this->listResponse($credits);
}
/**
* @SWG\Get(
* path="/credits/{credit_id}",
* summary="Retrieve a credit",
* operationId="getCredit",
* tags={"credit"},
* @SWG\Parameter(
* in="path",
* name="credit_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="A single credit",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Credit"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function show(CreditRequest $request)
{
return $this->itemResponse($request->entity());
}
/**
* @SWG\Post(
* path="/credits",
* summary="Create a credit",
* operationId="createCredit",
* tags={"credit"},
* @SWG\Parameter(
* in="body",
* name="credit",
* @SWG\Schema(ref="#/definitions/Credit")
* ),
* @SWG\Response(
* response=200,
* description="New credit",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Credit"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function store(CreateCreditRequest $request)
{
$credit = $this->creditRepo->save($request->input());
return $this->itemResponse($credit);
}
/**
* @SWG\Put(
* path="/credits/{credit_id}",
* summary="Update a credit",
* operationId="updateCredit",
* tags={"credit"},
* @SWG\Parameter(
* in="path",
* name="credit_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* in="body",
* name="credit",
* @SWG\Schema(ref="#/definitions/Credit")
* ),
* @SWG\Response(
* response=200,
* description="Updated credit",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Credit"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*
* @param mixed $publicId
*/
public function update(UpdateCreditRequest $request, $publicId)
{
if ($request->action) {
return $this->handleAction($request);
}
$data = $request->input();
$data['public_id'] = $publicId;
$credit = $this->creditRepo->save($data, $request->entity());
return $this->itemResponse($credit);
}
/**
* @SWG\Delete(
* path="/credits/{credit_id}",
* summary="Delete a credit",
* operationId="deleteCredit",
* tags={"credit"},
* @SWG\Parameter(
* in="path",
* name="credit_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="Deleted credit",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Credit"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function destroy(UpdateCreditRequest $request)
{
$credit = $request->entity();
$this->creditRepo->delete($credit);
return $this->itemResponse($credit);
}
}

View File

@ -85,6 +85,18 @@ class CreditController extends BaseController
return View::make('credits.edit', $data);
}
/**
* @param $publicId
*
* @return \Illuminate\Http\RedirectResponse
*/
public function show($publicId)
{
Session::reflash();
return Redirect::to("credits/{$publicId}/edit");
}
public function update(UpdateCreditRequest $request)
{
$credit = $request->entity();

View File

@ -79,8 +79,8 @@ class DashboardController extends BaseController
'tasks' => $tasks,
'showBlueVinePromo' => $showBlueVinePromo,
'showWhiteLabelExpired' => $showWhiteLabelExpired,
'headerClass' => in_array(\App::getLocale(), ['lt', 'pl', 'cs', 'sl']) ? 'in-large' : 'in-thin',
'footerClass' => in_array(\App::getLocale(), ['lt', 'pl', 'cs', 'sl']) ? '' : 'in-thin',
'headerClass' => in_array(\App::getLocale(), ['lt', 'pl', 'cs', 'sl', 'tr_TR']) ? 'in-large' : 'in-thin',
'footerClass' => in_array(\App::getLocale(), ['lt', 'pl', 'cs', 'sl', 'tr_TR']) ? '' : 'in-thin',
];
if ($showBlueVinePromo) {

View File

@ -169,6 +169,17 @@ class ExpenseController extends BaseController
$data = $request->input();
$data['documents'] = $request->file('documents');
// check for possible duplicate expense
$duplcate = Expense::scope()
->whereAmount($request->amount)
->whereExpenseDate(Utils::toSqlDate($request->expense_date))
->orderBy('created_at')
->first();
if ($duplcate) {
Session::flash('warning', trans('texts.duplicate_expense_warning',
['link' => link_to($duplcate->present()->url, trans('texts.expense_link'), ['target' => '_blank'])]));
}
$expense = $this->expenseService->save($data);
Session::flash('message', trans('texts.created_expense'));

View File

@ -134,4 +134,19 @@ class ImportController extends BaseController
return Redirect::to('/settings/' . ACCOUNT_IMPORT_EXPORT);
}
}
public function cancelImport()
{
try {
$path = env('FILE_IMPORT_PATH') ?: storage_path() . '/import';
foreach ([ENTITY_CLIENT, ENTITY_INVOICE, ENTITY_PAYMENT, ENTITY_QUOTE, ENTITY_PRODUCT] as $entityType) {
$fileName = sprintf('%s/%s_%s_%s.csv', $path, Auth::user()->account_id, request()->timestamp, $entityType);
\File::delete($fileName);
}
} catch (Exception $exception) {
Utils::logError($exception);
}
return Redirect::to('/settings/' . ACCOUNT_IMPORT_EXPORT);
}
}

View File

@ -149,6 +149,7 @@ class InvoiceApiController extends BaseAPIController
'country_id',
'private_notes',
'currency_code',
'country_code',
] as $field) {
if (isset($data[$field])) {
$clientData[$field] = $data[$field];
@ -185,7 +186,7 @@ class InvoiceApiController extends BaseAPIController
$invoice = $this->invoiceService->save($data);
$payment = false;
if ($invoice->isInvoice()) {
if ($invoice->isStandard()) {
if ($isAutoBill) {
$payment = $this->paymentService->autoBillInvoice($invoice);
} elseif ($isPaid) {
@ -251,6 +252,10 @@ class InvoiceApiController extends BaseAPIController
$fields['due_date_sql'] = false;
}
if (isset($data['is_quote']) && filter_var($data['is_quote'], FILTER_VALIDATE_BOOLEAN)) {
$fields['invoice_design_id'] = $account->quote_design_id;
}
foreach ($fields as $key => $val) {
if (! isset($data[$key])) {
$data[$key] = $val;
@ -317,7 +322,7 @@ class InvoiceApiController extends BaseAPIController
}
$headers = Utils::getApiHeaders();
$response = json_encode(RESULT_SUCCESS, JSON_PRETTY_PRINT);
$response = json_encode(['message' => RESULT_SUCCESS], JSON_PRETTY_PRINT);
return Response::make($response, 200, $headers);
}

View File

@ -302,9 +302,8 @@ class InvoiceController extends BaseController
return [
'data' => Input::old('data'),
'account' => Auth::user()->account->load('country'),
'products' => Product::scope()->with('default_tax_rate')->orderBy('product_key')->get(),
'products' => Product::scope()->orderBy('product_key')->get(),
'taxRateOptions' => $taxRateOptions,
'defaultTax' => $account->default_tax_rate,
'currencies' => Cache::get('currencies'),
'sizes' => Cache::get('sizes'),
'invoiceDesigns' => InvoiceDesign::getDesigns(),

View File

@ -346,8 +346,10 @@ class OnlinePaymentController extends BaseController
'frequency_id' => Input::get('frequency_id'),
'auto_bill_id' => Input::get('auto_bill_id'),
'start_date' => Input::get('start_date', date('Y-m-d')),
'tax_rate1' => $account->default_tax_rate ? $account->default_tax_rate->rate : 0,
'tax_name1' => $account->default_tax_rate ? $account->default_tax_rate->name : '',
'tax_rate1' => $account->tax_rate1,
'tax_name1' => $account->tax_name1,
'tax_rate2' => $account->tax_rate2,
'tax_name2' => $account->tax_name2,
'custom_text_value1' => Input::get('custom_invoice1'),
'custom_text_value2' => Input::get('custom_invoice2'),
'invoice_items' => [[
@ -355,8 +357,10 @@ class OnlinePaymentController extends BaseController
'notes' => $product->notes,
'cost' => $product->cost,
'qty' => 1,
'tax_rate1' => $product->default_tax_rate ? $product->default_tax_rate->rate : 0,
'tax_name1' => $product->default_tax_rate ? $product->default_tax_rate->name : '',
'tax_rate1' => $account->tax_rate1,
'tax_name1' => $account->tax_name1,
'tax_rate2' => $account->tax_rate2,
'tax_name2' => $account->tax_name2,
'custom_value1' => Input::get('custom_product1') ?: $product->custom_value1,
'custom_value2' => Input::get('custom_product2') ?: $product->custom_value2,
]],

View File

@ -6,6 +6,7 @@ use App\Http\Requests\CreatePaymentRequest;
use App\Http\Requests\PaymentRequest;
use App\Http\Requests\UpdatePaymentRequest;
use App\Models\Client;
use App\Models\Payment;
use App\Models\Credit;
use App\Models\Invoice;
use App\Ninja\Datatables\PaymentDatatable;
@ -136,7 +137,10 @@ class PaymentController extends BaseController
if ($payment->invoiceJsonBackup()) {
$actions[] = ['url' => url("/invoices/invoice_history/{$payment->invoice->public_id}?payment_id={$payment->public_id}"), 'label' => trans('texts.view_invoice')];
}
$actions[] = ['url' => url("/invoices/{$payment->invoice->public_id}/edit"), 'label' => trans('texts.edit_invoice')];
$actions[] = DropdownButton::DIVIDER;
$actions[] = ['url' => 'javascript:submitAction("email")', 'label' => trans('texts.email_payment')];
if ($payment->canBeRefunded()) {
$actions[] = ['url' => "javascript:showRefundModal({$payment->public_id}, \"{$payment->getCompletedAmount()}\", \"{$payment->present()->completedAmount}\", \"{$payment->present()->currencySymbol}\")", 'label' => trans('texts.refund_payment')];
@ -215,7 +219,7 @@ class PaymentController extends BaseController
*/
public function update(UpdatePaymentRequest $request)
{
if (in_array($request->action, ['archive', 'delete', 'restore', 'refund'])) {
if (in_array($request->action, ['archive', 'delete', 'restore', 'refund', 'email'])) {
return self::bulk();
}
@ -234,11 +238,17 @@ class PaymentController extends BaseController
$action = Input::get('action');
$amount = Input::get('refund_amount');
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
$count = $this->paymentService->bulk($ids, $action, ['refund_amount' => $amount]);
if ($count > 0) {
$message = Utils::pluralize($action == 'refund' ? 'refunded_payment' : $action.'d_payment', $count);
Session::flash('message', $message);
if ($action === 'email') {
$payment = Payment::scope($ids)->first();
$this->contactMailer->sendPaymentConfirmation($payment);
Session::flash('message', trans('texts.emailed_payment'));
} else {
$count = $this->paymentService->bulk($ids, $action, ['refund_amount' => $amount]);
if ($count > 0) {
$message = Utils::pluralize($action == 'refund' ? 'refunded_payment' : $action.'d_payment', $count);
Session::flash('message', $message);
}
}
return $this->returnBulk(ENTITY_PAYMENT, $action, $ids);

View File

@ -83,7 +83,7 @@ class ProductController extends BaseController
$data = [
'account' => $account,
'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->whereIsInclusive(false)->get(['id', 'name', 'rate']) : null,
'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->whereIsInclusive(false)->get() : null,
'product' => $product,
'entity' => $product,
'method' => 'PUT',

View File

@ -98,9 +98,8 @@ class QuoteController extends BaseController
return [
'entityType' => ENTITY_QUOTE,
'account' => $account,
'products' => Product::scope()->with('default_tax_rate')->orderBy('product_key')->get(),
'products' => Product::scope()->orderBy('product_key')->get(),
'taxRateOptions' => $account->present()->taxRateOptions,
'defaultTax' => $account->default_tax_rate,
'countries' => Cache::get('countries'),
'clients' => Client::scope()->with('contacts', 'country')->orderBy('name')->get(),
'taxRates' => TaxRate::scope()->orderBy('name')->get(),

View File

@ -253,11 +253,9 @@ class TaskController extends BaseController
$action = Input::get('action');
$ids = Input::get('public_id') ?: (Input::get('id') ?: Input::get('ids'));
if ($action == 'stop') {
if (in_array($action, ['resume', 'stop'])) {
$this->taskRepo->save($ids, ['action' => $action]);
Session::flash('message', trans('texts.stopped_task'));
return Redirect::to('tasks');
return Redirect::to('tasks')->withMessage(trans($action == 'stop' ? 'texts.stopped_task' : 'texts.resumed_task'));
} elseif ($action == 'invoice' || $action == 'add_to_invoice') {
$tasks = Task::scope($ids)->with('client')->orderBy('project_id', 'id')->get();
$clientPublicId = false;

View File

@ -22,7 +22,7 @@ class CreateCreditRequest extends CreditRequest
public function rules()
{
return [
'client' => 'required',
'client_id' => 'required',
'amount' => 'required|positive',
];
}

View File

@ -40,7 +40,7 @@ class CreatePaymentAPIRequest extends PaymentRequest
]);
$rules = [
'amount' => "required|numeric|between:0.01,{$invoice->balance}",
'amount' => 'required|numeric|not_in:0',
];
if ($this->payment_type_id == PAYMENT_TYPE_CREDIT) {

View File

@ -22,7 +22,7 @@ class UpdateCreditRequest extends CreditRequest
public function rules()
{
return [
'amount' => 'required|positive',
'amount' => 'positive',
];
}
}

View File

@ -281,6 +281,7 @@ Route::group([
Route::post('/export', 'ExportController@doExport');
Route::post('/import', 'ImportController@doImport');
Route::get('/cancel_import', 'ImportController@cancelImport');
Route::post('/import_csv', 'ImportController@doImportCSV');
Route::get('gateways/create/{show_wepay?}', 'AccountGatewayController@create');
@ -329,6 +330,7 @@ Route::group(['middleware' => ['lookup:api', 'api'], 'prefix' => 'api/v1'], func
Route::resource('invoices', 'InvoiceApiController');
Route::resource('payments', 'PaymentApiController');
Route::resource('tasks', 'TaskApiController');
Route::resource('credits', 'CreditApiController');
Route::post('hooks', 'IntegrationController@subscribe');
Route::post('email_invoice', 'InvoiceApiController@emailInvoice');
Route::get('user_accounts', 'AccountApiController@getUserAccounts');

View File

@ -50,6 +50,10 @@ class SendPushNotification extends Job implements ShouldQueue
*/
public function handle(PushService $pushService)
{
if (config('queue.default') !== 'sync') {
$this->invoice->account->loadLocalizationSettings();
}
$pushService->sendNotification($this->invoice, $this->type);
}
}

View File

@ -44,4 +44,16 @@ class HTMLUtils
return $purifier->purify($html);
}
public static function previousUrl($fallback)
{
$previous = url()->previous();
$current = request()->url();
if ($previous == $current) {
return url($fallback);
} else {
return $previous;
}
}
}

View File

@ -183,7 +183,7 @@ class HistoryUtils
}
$icon = '<i class="fa fa-' . EntityModel::getIcon($item->entityType . 's') . '" style="width:24px"></i>';
$str .= sprintf('<li style="text-align:right; padding-right:18px;"><a href="%s">%s %s</a></li>', $item->url, $item->name, $icon);
$str .= sprintf('<li style="text-align:right; padding-right:18px;"><a href="%s">%s %s</a></li>', $item->url, e($item->name), $icon);
}
return $str;

View File

@ -382,7 +382,18 @@ class Utils
return 'logged';
}
$data = [
$data = static::prepareErrorData($context);
if ($info) {
Log::info($error."\n", $data);
} else {
Log::error($error."\n", $data);
}
}
public static function prepareErrorData($context)
{
return [
'context' => $context,
'user_id' => Auth::check() ? Auth::user()->id : 0,
'account_id' => Auth::check() ? Auth::user()->account_id : 0,
@ -394,20 +405,9 @@ class Utils
'ip' => Request::getClientIp(),
'count' => Session::get('error_count', 0),
'is_console' => App::runningInConsole() ? 'yes' : 'no',
'is_api' => session('token_id') ? 'yes' : 'no',
'db_server' => config('database.default'),
];
if ($info) {
Log::info($error."\n", $data);
} else {
Log::error($error."\n", $data);
}
/*
Mail::queue('emails.error', ['message'=>$error.' '.json_encode($data)], function($message)
{
$message->to($email)->subject($subject);
});
*/
}
public static function parseFloat($value)

View File

@ -15,19 +15,26 @@ class AnalyticsListener
*/
public function trackRevenue(PaymentWasCreated $event)
{
if (! Utils::isNinja() || ! env('ANALYTICS_KEY')) {
return;
}
$payment = $event->payment;
$invoice = $payment->invoice;
$account = $payment->account;
if (! $account->isNinjaAccount() && $account->account_key != NINJA_LICENSE_ACCOUNT_KEY) {
$analyticsId = false;
if ($account->isNinjaAccount() || $account->account_key == NINJA_LICENSE_ACCOUNT_KEY) {
$analyticsId = env('ANALYTICS_KEY');
} else {
if (Utils::isNinja()) {
$analyticsId = $account->analytics_key;
} else {
$analyticsId = $account->analytics_key ?: env('ANALYTICS_KEY');
}
}
if (! $analyticsId) {
return;
}
$analyticsId = env('ANALYTICS_KEY');
$client = $payment->client;
$amount = $payment->amount;
$item = $invoice->invoice_items->last()->product_key;

View File

@ -35,8 +35,7 @@ class InvoiceListener
$invoice = $event->invoice;
$account = Auth::user()->account;
if ($invoice->invoice_design_id
&& $account->invoice_design_id != $invoice->invoice_design_id) {
if ($invoice->invoice_design_id && $account->invoice_design_id != $invoice->invoice_design_id) {
$account->invoice_design_id = $invoice->invoice_design_id;
$account->save();
}

View File

@ -1,20 +1,47 @@
<?php namespace App\Listeners;
use App\Ninja\Mailers\UserMailer;
use App\Ninja\Mailers\ContactMailer;
use App\Events\InvoiceWasEmailed;
use App\Events\QuoteWasEmailed;
use App\Events\InvoiceInvitationWasViewed;
use App\Events\QuoteInvitationWasViewed;
use App\Events\QuoteInvitationWasApproved;
use App\Events\PaymentWasCreated;
use App\Jobs\SendPaymentEmail;
use App\Services\PushService;
use App\Jobs\SendNotificationEmail;
use App\Jobs\SendPushNotification;
/**
* Class NotificationListener
*/
class NotificationListener
{
/**
* @var UserMailer
*/
protected $userMailer;
/**
* @var ContactMailer
*/
protected $contactMailer;
/**
* @var PushService
*/
protected $pushService;
/**
* NotificationListener constructor.
* @param UserMailer $userMailer
* @param ContactMailer $contactMailer
* @param PushService $pushService
*/
public function __construct(UserMailer $userMailer, ContactMailer $contactMailer, PushService $pushService)
{
$this->userMailer = $userMailer;
$this->contactMailer = $contactMailer;
$this->pushService = $pushService;
}
/**
* @param $invoice
* @param $type
@ -37,7 +64,7 @@ class NotificationListener
public function emailedInvoice(InvoiceWasEmailed $event)
{
$this->sendEmails($event->invoice, 'sent', null, $event->notes);
dispatch(new SendPushNotification($event->invoice, 'sent'));
$this->pushService->sendNotification($event->invoice, 'sent');
}
/**
@ -46,7 +73,7 @@ class NotificationListener
public function emailedQuote(QuoteWasEmailed $event)
{
$this->sendEmails($event->quote, 'sent', null, $event->notes);
dispatch(new SendPushNotification($event->quote, 'sent'));
$this->pushService->sendNotification($event->quote, 'sent');
}
/**
@ -59,7 +86,7 @@ class NotificationListener
}
$this->sendEmails($event->invoice, 'viewed');
dispatch(new SendPushNotification($event->invoice, 'viewed'));
$this->pushService->sendNotification($event->invoice, 'viewed');
}
/**
@ -72,7 +99,7 @@ class NotificationListener
}
$this->sendEmails($event->quote, 'viewed');
dispatch(new SendPushNotification($event->quote, 'viewed'));
$this->pushService->sendNotification($event->quote, 'viewed');
}
/**
@ -81,7 +108,7 @@ class NotificationListener
public function approvedQuote(QuoteInvitationWasApproved $event)
{
$this->sendEmails($event->quote, 'approved');
dispatch(new SendPushNotification($event->quote, 'approved'));
$this->pushService->sendNotification($event->quote, 'approved');
}
/**
@ -94,9 +121,10 @@ class NotificationListener
return;
}
$this->contactMailer->sendPaymentConfirmation($event->payment);
$this->sendEmails($event->payment->invoice, 'paid', $event->payment);
dispatch(new SendPaymentEmail($event->payment));
dispatch(new SendPushNotification($event->payment->invoice, 'paid'));
$this->pushService->sendNotification($event->payment->invoice, 'paid');
}
}

View File

@ -68,6 +68,7 @@ class Account extends Eloquent
'invoice_taxes',
'invoice_item_taxes',
'invoice_design_id',
'quote_design_id',
'work_phone',
'work_email',
'language_id',
@ -110,7 +111,10 @@ class Account extends Eloquent
'num_days_reminder3',
'custom_invoice_text_label1',
'custom_invoice_text_label2',
'default_tax_rate_id',
'tax_name1',
'tax_rate1',
'tax_name2',
'tax_rate2',
'recurring_hour',
'invoice_number_pattern',
'quote_number_pattern',
@ -166,6 +170,7 @@ class Account extends Eloquent
'custom_contact_label1',
'custom_contact_label2',
'domain_id',
'analytics_key',
];
/**
@ -366,14 +371,6 @@ class Account extends Eloquent
return $this->belongsTo('App\Models\Industry');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function default_tax_rate()
{
return $this->belongsTo('App\Models\TaxRate');
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
@ -890,6 +887,7 @@ class Account extends Eloquent
} else {
if ($entityType == ENTITY_QUOTE) {
$invoice->invoice_type_id = INVOICE_TYPE_QUOTE;
$invoice->invoice_design_id = $this->quote_design_id;
}
if ($this->hasClientNumberPattern($invoice) && ! $clientId) {

View File

@ -51,6 +51,7 @@ class Client extends EntityModel
'website',
'invoice_number_counter',
'quote_number_counter',
'public_notes',
];
/**

View File

@ -69,26 +69,32 @@ class EntityModel extends Eloquent
$entity->setRelation('user', $user);
$entity->setRelation('account', $account);
if (method_exists($className, 'trashed')) {
$lastEntity = $className::whereAccountId($entity->account_id)->withTrashed();
} else {
$lastEntity = $className::whereAccountId($entity->account_id);
}
if (static::$hasPublicId) {
$lastEntity = $lastEntity->orderBy('public_id', 'DESC')
->first();
if ($lastEntity) {
$entity->public_id = $lastEntity->public_id + 1;
} else {
$entity->public_id = 1;
}
$entity->public_id = static::getNextPublicId($entity->account_id);
}
return $entity;
}
private static function getNextPublicId($accountId)
{
$className = get_called_class();
if (method_exists($className, 'trashed')) {
$lastEntity = $className::whereAccountId($accountId)->withTrashed();
} else {
$lastEntity = $className::whereAccountId($accountId);
}
$lastEntity = $lastEntity->orderBy('public_id', 'DESC')->first();
if ($lastEntity) {
return $lastEntity->public_id + 1;
} else {
return 1;
}
}
/**
* @param $publicId
*
@ -379,4 +385,21 @@ class EntityModel extends Eloquent
{
return '';
}
public function save(array $options = [])
{
try {
return parent::save($options);
} catch (\Illuminate\Database\QueryException $exception) {
// check if public_id has been taken
if ($exception->getCode() == 23000 && static::$hasPublicId) {
$nextId = static::getNextPublicId($this->account_id);
if ($nextId != $this->public_id) {
$this->public_id = $nextId;
return $this->save($options);
}
}
throw $exception;
}
}
}

View File

@ -46,7 +46,9 @@ class Invoice extends EntityModel implements BalanceAffecting
'tax_rate1',
'tax_name2',
'tax_rate2',
'private_notes',
'last_sent_date',
'invoice_design_id',
];
/**
@ -430,7 +432,7 @@ class Invoice extends EntityModel implements BalanceAffecting
/**
* @return bool
*/
public function isInvoice()
public function isStandard()
{
return $this->isType(INVOICE_TYPE_STANDARD) && ! $this->is_recurring;
}
@ -612,7 +614,7 @@ class Invoice extends EntityModel implements BalanceAffecting
public function canBePaid()
{
return floatval($this->balance) != 0 && ! $this->is_deleted && $this->isInvoice();
return floatval($this->balance) != 0 && ! $this->is_deleted && $this->isStandard();
}
public static function calcStatusLabel($status, $class, $entityType, $quoteInvoiceId)
@ -1239,7 +1241,7 @@ class Invoice extends EntityModel implements BalanceAffecting
/**
* @return bool|string
*/
public function getPDFString()
public function getPDFString($decode = true)
{
if (! env('PHANTOMJS_CLOUD_KEY') && ! env('PHANTOMJS_BIN_PATH')) {
return false;
@ -1258,14 +1260,12 @@ class Invoice extends EntityModel implements BalanceAffecting
$pdfString = CurlUtils::phantom('GET', $link . '?phantomjs=true&phantomjs_secret=' . env('PHANTOMJS_SECRET'));
}
if (! $pdfString && (Utils::isNinja() || ! env('PHANTOMJS_BIN_PATH'))) {
if ($key = env('PHANTOMJS_CLOUD_KEY')) {
if (Utils::isNinjaDev()) {
$link = env('TEST_LINK');
}
$url = "http://api.phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$link}?phantomjs=true%22,renderType:%22html%22%7D";
$pdfString = CurlUtils::get($url);
if (! $pdfString && ($key = env('PHANTOMJS_CLOUD_KEY'))) {
if (Utils::isNinjaDev()) {
$link = env('TEST_LINK');
}
$url = "http://api.phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$link}?phantomjs=true%22,renderType:%22html%22%7D";
$pdfString = CurlUtils::get($url);
}
$pdfString = strip_tags($pdfString);
@ -1279,11 +1279,15 @@ class Invoice extends EntityModel implements BalanceAffecting
return false;
}
if ($pdf = Utils::decodePDF($pdfString)) {
return $pdf;
if ($decode) {
if ($pdf = Utils::decodePDF($pdfString)) {
return $pdf;
} else {
Utils::logError("PhantomJS - Unable to decode: {$pdfString}");
return false;
}
} else {
Utils::logError("PhantomJS - Unable to decode: {$pdfString}");
return false;
return $pdfString;
}
}

View File

@ -83,11 +83,11 @@ class InvoiceDesign extends Eloquent
$design->javascript = $design->pdfmake;
$design->pdfmake = null;
if ($design->id == CUSTOM_DESIGN) {
if ($account->custom_design) {
$design->javascript = $account->custom_design;
if (in_array($design->id, [CUSTOM_DESIGN1, CUSTOM_DESIGN2, CUSTOM_DESIGN3])) {
if ($javascript = $account->getCustomDesign($design->id)) {
$design->javascript = $javascript;
} else {
$designs->pop();
$designs->forget($design->id - 1);
}
}
}

View File

@ -19,6 +19,13 @@ class Payment extends EntityModel
use PresentableTrait;
use SoftDeletes;
/**
* @var array
*/
protected $fillable = [
'private_notes',
];
public static $statusClasses = [
PAYMENT_STATUS_PENDING => 'info',
PAYMENT_STATUS_COMPLETED => 'success',

View File

@ -30,7 +30,10 @@ class Product extends EntityModel
'notes',
'cost',
'qty',
'default_tax_rate_id',
'tax_name1',
'tax_rate1',
'tax_name2',
'tax_rate2',
'custom_value1',
'custom_value2',
];
@ -84,12 +87,4 @@ class Product extends EntityModel
{
return $this->belongsTo('App\Models\User')->withTrashed();
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function default_tax_rate()
{
return $this->belongsTo('App\Models\TaxRate');
}
}

View File

@ -292,4 +292,16 @@ trait PresentsInvoice
return $data;
}
public function getCustomDesign($designId) {
if ($designId == CUSTOM_DESIGN1) {
return $this->custom_design1;
} elseif ($designId == CUSTOM_DESIGN2) {
return $this->custom_design2;
} elseif ($designId == CUSTOM_DESIGN3) {
return $this->custom_design3;
}
return null;
}
}

View File

@ -147,7 +147,7 @@ class User extends Authenticatable
*/
public function maxInvoiceDesignId()
{
return $this->hasFeature(FEATURE_MORE_INVOICE_DESIGNS) ? 11 : (Utils::isNinja() ? COUNT_FREE_DESIGNS : COUNT_FREE_DESIGNS_SELF_HOST);
return $this->hasFeature(FEATURE_MORE_INVOICE_DESIGNS) ? 13 : COUNT_FREE_DESIGNS;
}
/**

View File

@ -122,6 +122,15 @@ class PaymentDatatable extends EntityDatatable
return Auth::user()->can('editByOwner', [ENTITY_PAYMENT, $model->user_id]);
},
],
[
trans('texts.email_payment'),
function ($model) {
return "javascript:submitForm_payment('email', {$model->public_id})";
},
function ($model) {
return Auth::user()->can('editByOwner', [ENTITY_PAYMENT, $model->user_id]);
},
],
[
trans('texts.refund_payment'),
function ($model) {

View File

@ -20,7 +20,7 @@ class RecurringInvoiceDatatable extends EntityDatatable
$frequency = strtolower($model->frequency);
$frequency = preg_replace('/\s/', '_', $frequency);
return link_to("invoices/{$model->public_id}", trans('texts.freq_'.$frequency))->toHtml();
return link_to("recurring_invoices/{$model->public_id}/edit", trans('texts.freq_'.$frequency))->toHtml();
},
],
[
@ -42,18 +42,26 @@ class RecurringInvoiceDatatable extends EntityDatatable
return Utils::fromSqlDate($model->last_sent_date_sql);
},
],
/*
[
'end_date',
function ($model) {
return Utils::fromSqlDate($model->end_date_sql);
},
],
*/
[
'amount',
function ($model) {
return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id);
},
],
[
'private_notes',
function ($model) {
return $model->private_notes;
},
],
[
'status',
function ($model) {

View File

@ -88,6 +88,15 @@ class TaskDatatable extends EntityDatatable
return $model->invoice_number && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->invoice_user_id]);
},
],
[
trans('texts.resume_task'),
function ($model) {
return "javascript:submitForm_task('resume', {$model->public_id})";
},
function ($model) {
return ! $model->is_running && Auth::user()->can('editByOwner', [ENTITY_TASK, $model->user_id]);
},
],
[
trans('texts.stop_task'),
function ($model) {
@ -103,7 +112,7 @@ class TaskDatatable extends EntityDatatable
return "javascript:submitForm_task('invoice', {$model->public_id})";
},
function ($model) {
return ! $model->invoice_number && (! $model->deleted_at || $model->deleted_at == '0000-00-00') && Auth::user()->can('create', ENTITY_INVOICE);
return ! $model->is_running && ! $model->invoice_number && (! $model->deleted_at || $model->deleted_at == '0000-00-00') && Auth::user()->can('create', ENTITY_INVOICE);
},
],
];

View File

@ -83,11 +83,13 @@ class InvoiceIntent extends BaseIntent
$item['cost'] = $product->cost;
$item['notes'] = $product->notes;
/*
if ($taxRate = $product->default_tax_rate) {
$item['tax_name1'] = $taxRate->name;
$item['tax_rate1'] = $taxRate->rate;
}
*/
$invoiceItems[] = $item;
}
}

View File

@ -38,6 +38,8 @@ class BasePaymentDriver
protected $customerReferenceParam;
protected $transactionReferenceParam;
public $canRefundPayments = false;
public function __construct($accountGateway = false, $invitation = false, $gatewayType = false)
{
$this->accountGateway = $accountGateway;
@ -379,7 +381,7 @@ class BasePaymentDriver
'description' => trans('texts.' . $invoice->getEntityType()) . " {$invoice->invoice_number}",
'transactionId' => $invoice->invoice_number,
'transactionType' => 'Purchase',
'ip' => Request::ip(),
'ip' => Request::getClientIp(),
];
if ($paymentMethod) {
@ -613,6 +615,7 @@ class BasePaymentDriver
public function createPayment($ref = false, $paymentMethod = null)
{
$account = $this->account();
$invitation = $this->invitation;
$invoice = $this->invoice();
$invoice->markSentIfUnsent();
@ -625,7 +628,7 @@ class BasePaymentDriver
$payment->client_id = $invoice->client_id;
$payment->contact_id = $invitation->contact_id;
$payment->transaction_reference = $ref;
$payment->payment_date = date_create()->format('Y-m-d');
$payment->payment_date = $account->getDateTime()->format('Y-m-d');
$payment->ip = Request::ip();
$payment = $this->creatingPayment($payment, $paymentMethod);

View File

@ -11,6 +11,7 @@ class BraintreePaymentDriver extends BasePaymentDriver
{
protected $customerReferenceParam = 'customerId';
protected $sourceReferenceParam = 'paymentMethodToken';
public $canRefundPayments = true;
public function gatewayTypes()
{

View File

@ -10,6 +10,7 @@ use Exception;
class StripePaymentDriver extends BasePaymentDriver
{
protected $customerReferenceParam = 'customerReference';
public $canRefundPayments = true;
public function gatewayTypes()
{

View File

@ -10,6 +10,8 @@ use Utils;
class WePayPaymentDriver extends BasePaymentDriver
{
public $canRefundPayments = true;
public function gatewayTypes()
{
$types = [

View File

@ -175,4 +175,24 @@ class AccountPresenter extends Presenter
return $data;
}
public function customDesigns()
{
$account = $this->entity;
$data = [];
for ($i=1; $i<=3; $i++) {
$label = trans('texts.custom_design' . $i);
if (! $account->{'custom_design' . $i}) {
$label .= ' - ' . trans('texts.empty');
}
$data[] = [
'url' => url('/settings/customize_design?design_id=') . ($i + 10),
'label' => $label
];
}
return $data;
}
}

View File

@ -13,7 +13,7 @@ class EntityPresenter extends Presenter
*/
public function url()
{
return url($this->path());
return SITE_URL . $this->path();
}
public function path()

View File

@ -29,6 +29,14 @@ class InvoicePresenter extends EntityPresenter
return $account->formatMoney($invoice->amount, $invoice->client);
}
public function balance()
{
$invoice = $this->entity;
$account = $invoice->account;
return $account->formatMoney($invoice->balance, $invoice->client);
}
public function requestedAmount()
{
$invoice = $this->entity;

View File

@ -30,7 +30,7 @@ class InvoiceReport extends AbstractReport
->with(['invoices' => function ($query) use ($status) {
if ($status == 'draft') {
$query->whereIsPublic(false);
} elseif ($status == 'unpaid' || $status == 'paid') {
} elseif (in_array($status, ['paid', 'unpaid', 'sent'])) {
$query->whereIsPublic(true);
}
$query->invoices()

View File

@ -30,7 +30,7 @@ class ProductReport extends AbstractReport
->with(['invoices' => function ($query) use ($status) {
if ($status == 'draft') {
$query->whereIsPublic(false);
} elseif ($status == 'unpaid' || $status == 'paid') {
} elseif (in_array($status, ['paid', 'unpaid', 'sent'])) {
$query->whereIsPublic(true);
}
$query->invoices()

View File

@ -87,7 +87,6 @@ class ActivityRepository
'activities.created_at',
'activities.contact_id',
'activities.activity_type_id',
'activities.is_system',
'activities.balance',
'activities.adjustment',
'activities.notes',

View File

@ -106,6 +106,17 @@ class ClientRepository extends BaseRepository
}
}
// convert country code to id
if (isset($data['country_code'])) {
$countryCode = strtolower($data['country_code']);
$country = Cache::get('countries')->filter(function ($item) use ($countryCode) {
return strtolower($item->iso_3166_2) == $countryCode || strtolower($item->iso_3166_3) == $countryCode;
})->first();
if ($country) {
$data['country_id'] = $country->id;
}
}
$client->fill($data);
$client->save();

View File

@ -69,7 +69,6 @@ class CreditRepository extends BaseRepository
->where('credits.client_id', '=', $clientId)
->where('clients.deleted_at', '=', null)
->where('credits.deleted_at', '=', null)
->where('credits.balance', '>', 0)
->select(
DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'),
DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'),
@ -102,21 +101,29 @@ class CreditRepository extends BaseRepository
$publicId = isset($data['public_id']) ? $data['public_id'] : false;
if ($credit) {
$credit->balance = Utils::parseFloat($input['balance']);
// do nothing
} elseif ($publicId) {
$credit = Credit::scope($publicId)->firstOrFail();
$credit->balance = Utils::parseFloat($input['balance']);
\Log::warning('Entity not set in credit repo save');
} else {
$credit = Credit::createNew();
$credit->balance = Utils::parseFloat($input['amount']);
$credit->client_id = Client::getPrivateId($input['client']);
$credit->client_id = Client::getPrivateId($input['client_id']);
$credit->credit_date = date('Y-m-d');
}
$credit->fill($input);
$credit->credit_date = Utils::toSqlDate($input['credit_date']);
$credit->amount = Utils::parseFloat($input['amount']);
$credit->private_notes = trim($input['private_notes']);
if (isset($input['credit_date'])) {
$credit->credit_date = Utils::toSqlDate($input['credit_date']);
}
if (isset($input['amount'])) {
$credit->amount = Utils::parseFloat($input['amount']);
}
if (isset($input['balance'])) {
$credit->balance = Utils::parseFloat($input['balance']);
}
$credit->save();
return $credit;

View File

@ -177,7 +177,8 @@ class InvoiceRepository extends BaseRepository
'invoices.due_date',
'invoices.due_date as due_date_sql',
'invoices.is_recurring',
'invoices.quote_invoice_id'
'invoices.quote_invoice_id',
'invoices.private_notes'
);
if ($clientPublicId) {
@ -411,12 +412,14 @@ class InvoiceRepository extends BaseRepository
$invoice->invoice_date = Utils::toSqlDate($data['invoice_date']);
}
/*
if (isset($data['invoice_status_id'])) {
if ($data['invoice_status_id'] == 0) {
$data['invoice_status_id'] = INVOICE_STATUS_DRAFT;
}
$invoice->invoice_status_id = $data['invoice_status_id'];
}
*/
if ($invoice->is_recurring) {
if (! $isNew && isset($data['start_date']) && $invoice->start_date && $invoice->start_date != Utils::toSqlDate($data['start_date'])) {
@ -469,8 +472,6 @@ class InvoiceRepository extends BaseRepository
$invoice->po_number = trim($data['po_number']);
}
$invoice->invoice_design_id = isset($data['invoice_design_id']) ? $data['invoice_design_id'] : $account->invoice_design_id;
// provide backwards compatibility
if (isset($data['tax_name']) && isset($data['tax_rate'])) {
$data['tax_name1'] = $data['tax_name'];
@ -654,6 +655,10 @@ class InvoiceRepository extends BaseRepository
if ($product && (Auth::user()->can('edit', $product))) {
$product->notes = ($task || $expense) ? '' : $item['notes'];
$product->cost = $expense ? 0 : $item['cost'];
$product->tax_name1 = isset($item['tax_name1']) ? $item['tax_name1'] : null;
$product->tax_rate1 = isset($item['tax_rate1']) ? $item['tax_rate1'] : 0;
$product->tax_name2 = isset($item['tax_name2']) ? $item['tax_name2'] : null;
$product->tax_rate2 = isset($item['tax_rate2']) ? $item['tax_rate2'] : 0;
$product->custom_value1 = isset($item['custom_value1']) ? $item['custom_value1'] : null;
$product->custom_value2 = isset($item['custom_value2']) ? $item['custom_value2'] : null;
$product->save();
@ -720,7 +725,7 @@ class InvoiceRepository extends BaseRepository
}
}
// if no contacts are selected auto-select the first to enusre there's an invitation
// if no contacts are selected auto-select the first to ensure there's an invitation
if (! count($sendInvoiceIds)) {
$sendInvoiceIds[] = $client->contacts[0]->id;
}

View File

@ -189,6 +189,9 @@ class PaymentRepository extends BaseRepository
if (isset($input['transaction_reference'])) {
$payment->transaction_reference = trim($input['transaction_reference']);
}
if (isset($input['private_notes'])) {
$payment->private_notes = trim($input['private_notes']);
}
if (! $publicId) {
$clientId = $input['client_id'];

View File

@ -23,18 +23,14 @@ class ProductRepository extends BaseRepository
public function find($accountId, $filter = null)
{
$query = DB::table('products')
->leftJoin('tax_rates', function ($join) {
$join->on('tax_rates.id', '=', 'products.default_tax_rate_id')
->whereNull('tax_rates.deleted_at');
})
->where('products.account_id', '=', $accountId)
->select(
'products.public_id',
'products.product_key',
'products.notes',
'products.cost',
'tax_rates.name as tax_name',
'tax_rates.rate as tax_rate',
'products.tax_name1 as tax_name',
'products.tax_rate1 as tax_rate',
'products.deleted_at',
'products.is_deleted'
);
@ -82,9 +78,7 @@ class ProductRepository extends BaseRepository
$max = SIMILAR_MIN_THRESHOLD;
$productId = 0;
$products = Product::scope()
->with('default_tax_rate')
->get();
$products = Product::scope()->get();
foreach ($products as $product) {
if (! $product->product_key) {

View File

@ -171,6 +171,7 @@ class AccountTransformer extends EntityTransformer
'invoice_taxes' => (bool) $account->invoice_taxes,
'invoice_item_taxes' => (bool) $account->invoice_item_taxes,
'invoice_design_id' => (int) $account->invoice_design_id,
'quote_design_id' => (int) $account->quote_design_id,
'client_view_css' => (string) $account->client_view_css,
'work_phone' => $account->work_phone,
'work_email' => $account->work_email,
@ -213,7 +214,10 @@ class AccountTransformer extends EntityTransformer
'num_days_reminder3' => $account->num_days_reminder3,
'custom_invoice_text_label1' => $account->custom_invoice_text_label1,
'custom_invoice_text_label2' => $account->custom_invoice_text_label2,
'default_tax_rate_id' => $account->default_tax_rate_id ? $account->default_tax_rate->public_id : 0,
'tax_name1' => $account->tax_name1 ?: '',
'tax_rate1' => (float) $account->tax_rate1,
'tax_name2' => $account->tax_name2 ?: '',
'tax_rate2' => (float) $account->tax_rate2,
'recurring_hour' => $account->recurring_hour,
'invoice_number_pattern' => $account->invoice_number_pattern,
'quote_number_pattern' => $account->quote_number_pattern,

View File

@ -25,6 +25,7 @@ class ClientTransformer extends EntityTransformer
* @SWG\Property(property="country_id", type="integer", example=840)
* @SWG\Property(property="work_phone", type="string", example="(212) 555-1212")
* @SWG\Property(property="private_notes", type="string", example="Notes...")
* @SWG\Property(property="public_notes", type="string", example="Notes...")
* @SWG\Property(property="last_login", type="string", format="date-time", example="2016-01-01 12:10:00")
* @SWG\Property(property="website", type="string", example="http://www.example.com")
* @SWG\Property(property="industry_id", type="integer", example=1)
@ -119,6 +120,7 @@ class ClientTransformer extends EntityTransformer
'country_id' => (int) $client->country_id,
'work_phone' => $client->work_phone,
'private_notes' => $client->private_notes,
'public_notes' => $client->public_notes,
'last_login' => $client->last_login,
'website' => $client->website,
'industry_id' => (int) $client->industry_id,

View File

@ -27,6 +27,7 @@ class CreditTransformer extends EntityTransformer
'credit_number' => $credit->credit_number,
'private_notes' => $credit->private_notes,
'public_notes' => $credit->public_notes,
'client_id' => $credit->client->public_id,
]);
}
}

View File

@ -37,7 +37,13 @@ class EntityTransformer extends TransformerAbstract
protected function getTimestamp($date)
{
return method_exists($date, 'getTimestamp') ? $date->getTimestamp() : null;
if (method_exists($date, 'getTimestamp')) {
return $date->getTimestamp();
} elseif (is_string($date)) {
return strtotime($date);
} else {
return null;
}
}
public function getDefaultIncludes()

View File

@ -17,7 +17,8 @@ class InvoiceTransformer extends EntityTransformer
* @SWG\Property(property="balance", type="number", format="float", example=10, readOnly=true)
* @SWG\Property(property="client_id", type="integer", example=1)
* @SWG\Property(property="invoice_number", type="string", example="0001")
* @SWG\Property(property="invoice_status_id", type="integer", example=1)
* @SWG\Property(property="private_notes", type="string", example="Notes...")
* @SWG\Property(property="public_notes", type="string", example="Notes...")
*/
protected $defaultIncludes = [
'invoice_items',
@ -89,13 +90,14 @@ class InvoiceTransformer extends EntityTransformer
'invoice_status_id' => (int) $invoice->invoice_status_id,
'updated_at' => $this->getTimestamp($invoice->updated_at),
'archived_at' => $this->getTimestamp($invoice->deleted_at),
'invoice_number' => $invoice->invoice_number,
'invoice_number' => $invoice->is_recurring ? '' : $invoice->invoice_number,
'discount' => (float) $invoice->discount,
'po_number' => $invoice->po_number,
'invoice_date' => $invoice->invoice_date,
'due_date' => $invoice->due_date,
'terms' => $invoice->terms,
'public_notes' => $invoice->public_notes,
'private_notes' => $invoice->private_notes,
'is_deleted' => (bool) $invoice->is_deleted,
'invoice_type_id' => (int) $invoice->invoice_type_id,
'is_recurring' => (bool) $invoice->is_recurring,

View File

@ -16,6 +16,7 @@ class PaymentTransformer extends EntityTransformer
* @SWG\Property(property="id", type="integer", example=1, readOnly=true)
* @SWG\Property(property="amount", type="number", format="float", example=10, readOnly=true)
* @SWG\Property(property="invoice_id", type="integer", example=1)
* @SWG\Property(property="private_notes", type="string", example="Notes...")
*/
protected $defaultIncludes = [];
@ -58,6 +59,7 @@ class PaymentTransformer extends EntityTransformer
'payment_type_id' => (int) $payment->payment_type_id,
'invoice_id' => (int) ($this->invoice ? $this->invoice->public_id : $payment->invoice->public_id),
'invoice_number' => $this->invoice ? $this->invoice->invoice_number : $payment->invoice->invoice_number,
'private_notes' => $payment->private_notes,
]);
}
}

View File

@ -15,7 +15,6 @@ class ProductTransformer extends EntityTransformer
* @SWG\Property(property="notes", type="string", example="Notes...")
* @SWG\Property(property="cost", type="number", format="float", example=10.00)
* @SWG\Property(property="qty", type="number", format="float", example=1)
* @SWG\Property(property="default_tax_rate_id", type="integer", example=1)
* @SWG\Property(property="updated_at", type="integer", example=1451160233, readOnly=true)
* @SWG\Property(property="archived_at", type="integer", example=1451160233, readOnly=true)
*/
@ -27,7 +26,10 @@ class ProductTransformer extends EntityTransformer
'notes' => $product->notes,
'cost' => $product->cost,
'qty' => $product->qty,
'default_tax_rate_id' => $product->default_tax_rate_id ? $product->default_tax_rate->public_id : 0,
'tax_name1' => $product->tax_name1 ?: '',
'tax_rate1' => (float) $product->tax_rate1,
'tax_name2' => $product->tax_name2 ?: '',
'tax_rate2' => (float) $product->tax_rate2,
'updated_at' => $this->getTimestamp($product->updated_at),
'archived_at' => $this->getTimestamp($product->deleted_at),
]);

View File

@ -99,6 +99,7 @@ class ImportService
IMPORT_FRESHBOOKS,
IMPORT_HIVEAGE,
IMPORT_INVOICEABLE,
IMPORT_INVOICEPLANE,
IMPORT_NUTCACHE,
IMPORT_RONIN,
IMPORT_WAVE,

View File

@ -178,8 +178,11 @@ class PaymentService extends BaseService
foreach ($payments as $payment) {
if (Auth::user()->can('edit', $payment)) {
$amount = ! empty($params['refund_amount']) ? floatval($params['refund_amount']) : null;
$paymentDriver = false;
if ($accountGateway = $payment->account_gateway) {
$paymentDriver = $accountGateway->paymentDriver();
}
if ($paymentDriver && $paymentDriver->canRefundPayments) {
if ($paymentDriver->refundPayment($payment, $amount)) {
$successful++;
}

View File

@ -55,6 +55,8 @@ class TemplateService
'$contact' => $contact->getDisplayName(),
'$firstName' => $contact->first_name,
'$amount' => $account->formatMoney($data['amount'], $client),
'$total' => $invoice->present()->amount,
'$balance' => $invoice->present()->balance,
'$invoice' => $invoice->invoice_number,
'$quote' => $invoice->invoice_number,
'$link' => $invitation->getLink(),

View File

@ -14,83 +14,83 @@
],
"require": {
"php": ">=5.5.9",
"ext-gmp": "*",
"ext-gd": "*",
"turbo124/laravel-push-notification": "2.*",
"omnipay/mollie": "3.*",
"omnipay/2checkout": "dev-master#e9c079c2dde0d7ba461903b3b7bd5caf6dee1248",
"omnipay/gocardless": "dev-master",
"omnipay/stripe": "dev-master",
"doctrine/dbal": "2.5.x",
"laravelcollective/bus": "5.2.*",
"laravel/framework": "5.2.*",
"laravelcollective/html": "5.2.*",
"symfony/css-selector": "~3.0",
"patricktalmadge/bootstrapper": "5.5.x",
"anahkiasen/former": "4.0.*@dev",
"chumper/datatable": "dev-develop#04ef2bf",
"omnipay/omnipay": "~2.3",
"intervention/image": "dev-master",
"webpatser/laravel-countries": "dev-master",
"lokielse/omnipay-alipay": "~1.4",
"digitickets/omnipay-datacash": "~3.0",
"mfauveau/omnipay-pacnet": "~2.0",
"digitickets/omnipay-realex": "~5.0",
"fruitcakestudio/omnipay-sisow": "~2.0",
"alfaproject/omnipay-skrill": "dev-master",
"omnipay/bitpay": "dev-master",
"guzzlehttp/guzzle": "~6.0",
"wildbit/laravel-postmark-provider": "3.0",
"ext-gmp": "*",
"Dwolla/omnipay-dwolla": "dev-master",
"laravel/socialite": "~2.0",
"simshaun/recurr": "dev-master",
"league/fractal": "0.13.*",
"agmscode/omnipay-agms": "~1.0",
"digitickets/omnipay-barclays-epdq": "~3.0",
"cardgate/omnipay-cardgate": "~2.0",
"fotografde/omnipay-checkoutcom": "~2.0",
"meebio/omnipay-creditcall": "dev-master",
"dioscouri/omnipay-cybersource": "dev-master",
"dercoder/omnipay-ecopayz": "~1.0",
"alfaproject/omnipay-skrill": "dev-master",
"anahkiasen/former": "4.0.*@dev",
"andreas22/omnipay-fasapay": "1.*",
"asgrim/ofxparser": "^1.1",
"barracudanetworks/archivestream-php": "^1.0",
"barryvdh/laravel-cors": "^0.9.1",
"barryvdh/laravel-debugbar": "~2.2",
"barryvdh/laravel-ide-helper": "~2.2",
"cardgate/omnipay-cardgate": "~2.0",
"cerdic/css-tidy": "~v1.5",
"chumper/datatable": "dev-develop#04ef2bf",
"codedge/laravel-selfupdater": "5.x-dev",
"collizo4sky/omnipay-wepay": "^1.3",
"delatbabel/omnipay-fatzebra": "dev-master",
"vink/omnipay-komoju": "~1.0",
"incube8/omnipay-multicards": "dev-master",
"descubraomundo/omnipay-pagarme": "dev-master",
"dercoder/omnipay-ecopayz": "~1.0",
"dercoder/omnipay-paysafecard": "dev-master",
"softcommerce/omnipay-paytrace": "~1.0",
"meebio/omnipay-secure-trading": "dev-master",
"descubraomundo/omnipay-pagarme": "dev-master",
"digitickets/omnipay-barclays-epdq": "~3.0",
"digitickets/omnipay-datacash": "~3.0",
"digitickets/omnipay-realex": "~5.0",
"dioscouri/omnipay-cybersource": "dev-master",
"doctrine/dbal": "2.5.x",
"ezyang/htmlpurifier": "~v4.7",
"fotografde/omnipay-checkoutcom": "~2.0",
"fruitcakestudio/omnipay-sisow": "~2.0",
"fzaninotto/faker": "^1.5",
"gatepay/FedACHdir": "dev-master@dev",
"google/apiclient": "^1.0",
"guzzlehttp/guzzle": "~6.0",
"incube8/omnipay-multicards": "dev-master",
"intervention/image": "dev-master",
"jaybizzle/laravel-crawler-detect": "1.*",
"jlapp/swaggervel": "master-dev",
"jonnyw/php-phantomjs": "4.*",
"justinbusschau/omnipay-secpay": "~2.0",
"laracasts/presenter": "dev-master",
"jlapp/swaggervel": "master-dev",
"maatwebsite/excel": "~2.0",
"ezyang/htmlpurifier": "~v4.7",
"cerdic/css-tidy": "~v1.5",
"asgrim/ofxparser": "^1.1",
"laravel/framework": "5.2.*",
"laravel/socialite": "~2.0",
"laravelcollective/bus": "5.2.*",
"laravelcollective/html": "5.2.*",
"league/flysystem-aws-s3-v3": "~1.0",
"league/flysystem-rackspace": "~1.0",
"barracudanetworks/archivestream-php": "^1.0",
"league/fractal": "0.13.*",
"lokielse/omnipay-alipay": "~1.4",
"maatwebsite/excel": "~2.0",
"meebio/omnipay-creditcall": "dev-master",
"meebio/omnipay-secure-trading": "dev-master",
"mfauveau/omnipay-pacnet": "~2.0",
"nwidart/laravel-modules": "^1.14",
"omnipay/2checkout": "dev-master#e9c079c2dde0d7ba461903b3b7bd5caf6dee1248",
"omnipay/bitpay": "dev-master",
"omnipay/braintree": "~2.0@dev",
"gatepay/FedACHdir": "dev-master@dev",
"omnipay/gocardless": "dev-master",
"omnipay/mollie": "3.*",
"omnipay/omnipay": "~2.3",
"omnipay/stripe": "dev-master",
"patricktalmadge/bootstrapper": "5.5.x",
"predis/predis": "^1.1",
"simshaun/recurr": "dev-master",
"softcommerce/omnipay-paytrace": "~1.0",
"symfony/css-selector": "~3.0",
"turbo124/laravel-push-notification": "2.*",
"vink/omnipay-komoju": "~1.0",
"webpatser/laravel-countries": "dev-master",
"websight/l5-google-cloud-storage": "^1.0",
"wepay/php-sdk": "^0.2",
"barryvdh/laravel-ide-helper": "~2.2",
"barryvdh/laravel-debugbar": "~2.2",
"fzaninotto/faker": "^1.5",
"jaybizzle/laravel-crawler-detect": "1.*",
"codedge/laravel-selfupdater": "5.x-dev",
"predis/predis": "^1.1",
"nwidart/laravel-modules": "^1.14",
"jonnyw/php-phantomjs": "4.*",
"collizo4sky/omnipay-wepay": "^1.3",
"barryvdh/laravel-cors": "^0.9.1",
"google/apiclient":"^1.0"
"wildbit/laravel-postmark-provider": "3.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0",
"phpspec/phpspec": "~2.1",
"codeception/codeception": "*",
"codeception/c3": "~2.0",
"codeception/codeception": "*",
"phpspec/phpspec": "~2.1",
"phpunit/phpunit": "~4.0",
"symfony/dom-crawler": "~3.0"
},
"autoload": {
@ -132,7 +132,9 @@
]
},
"config": {
"preferred-install": "dist"
"preferred-install": "dist",
"sort-packages": true,
"optimize-autoloader": true
},
"repositories": [
{

711
composer.lock generated

File diff suppressed because it is too large Load Diff

44
config/pdf.php Normal file
View File

@ -0,0 +1,44 @@
<?php
return [
'phantomjs' => [
/*
|--------------------------------------------------------------------------
| PhantomJS Secret
|--------------------------------------------------------------------------
|
| This enables the PhantomJS request to bypass client authorization.
|
*/
'secret' => env('PHANTOMJS_SECRET'),
/*
|--------------------------------------------------------------------------
| PhantomJS Bin Path
|--------------------------------------------------------------------------
|
| The path to the local PhantomJS binary.
| For example: /usr/local/bin/phantomjs
| You can run which phantomjs to determine the value
|
*/
'bin_path' => env('PHANTOMJS_BIN_PATH'),
/*
|--------------------------------------------------------------------------
| PhantomJS Cloud Key
|--------------------------------------------------------------------------
|
| Key for the https://phantomjscloud.com service
|
*/
'cloud_key' => env('PHANTOMJS_CLOUD_KEY')
]
];

View File

@ -15,7 +15,7 @@ class AddCustomDesign extends Migration
$table->mediumText('custom_design')->nullable();
});
DB::table('invoice_designs')->insert(['id' => CUSTOM_DESIGN, 'name' => 'Custom']);
DB::table('invoice_designs')->insert(['id' => CUSTOM_DESIGN1, 'name' => 'Custom']);
}
/**

View File

@ -0,0 +1,121 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddDefaultNoteToClient extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('clients', function ($table) {
$table->text('public_notes')->nullable();
});
Schema::table('invoices', function ($table) {
$table->text('private_notes')->nullable();
});
Schema::table('payments', function ($table) {
$table->text('private_notes')->nullable();
});
Schema::table('accounts', function ($table) {
$table->string('tax_name1')->nullable();
$table->decimal('tax_rate1', 13, 3);
$table->string('tax_name2')->nullable();
$table->decimal('tax_rate2', 13, 3);
});
Schema::table('products', function ($table) {
$table->string('tax_name1')->nullable();
$table->decimal('tax_rate1', 13, 3);
$table->string('tax_name2')->nullable();
$table->decimal('tax_rate2', 13, 3);
});
DB::statement('update products
left join tax_rates on tax_rates.id = products.default_tax_rate_id
set products.tax_name1 = tax_rates.name, products.tax_rate1 = tax_rates.rate');
DB::statement('update accounts
left join tax_rates on tax_rates.id = accounts.default_tax_rate_id
set accounts.tax_name1 = tax_rates.name, accounts.tax_rate1 = tax_rates.rate');
if (Schema::hasColumn('accounts', 'default_tax_rate_id')) {
Schema::table('accounts', function ($table) {
$table->dropColumn('default_tax_rate_id');
});
}
if (Schema::hasColumn('products', 'default_tax_rate_id')) {
Schema::table('products', function ($table) {
$table->dropColumn('default_tax_rate_id');
});
}
if (Utils::isNinja()) {
Schema::table('users', function ($table) {
$table->unique(['oauth_user_id', 'oauth_provider_id']);
});
}
Schema::table('accounts', function ($table) {
$table->unsignedInteger('quote_design_id')->default(1);
$table->renameColumn('custom_design', 'custom_design1');
$table->mediumText('custom_design2')->nullable();
$table->mediumText('custom_design3')->nullable();
$table->string('analytics_key')->nullable();
});
DB::statement('update accounts
set quote_design_id = invoice_design_id');
DB::statement('update invoice_designs
set name = "Custom1"
where id = 11
and name = "Custom"');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('clients', function ($table) {
$table->dropColumn('public_notes');
});
Schema::table('invoices', function ($table) {
$table->dropColumn('private_notes');
});
Schema::table('payments', function ($table) {
$table->dropColumn('private_notes');
});
Schema::table('accounts', function ($table) {
$table->renameColumn('custom_design1', 'custom_design');
$table->dropColumn('custom_design2');
$table->dropColumn('custom_design3');
$table->dropColumn('analytics_key');
$table->dropColumn('tax_name1');
$table->dropColumn('tax_rate1');
$table->dropColumn('tax_name2');
$table->dropColumn('tax_rate2');
});
Schema::table('products', function ($table) {
$table->dropColumn('tax_name1');
$table->dropColumn('tax_rate1');
$table->dropColumn('tax_name2');
$table->dropColumn('tax_rate2');
});
}
}

View File

@ -150,6 +150,10 @@ class CountriesSeeder extends Seeder
'SK' => [ // Slovakia
'swap_currency_symbol' => true,
],
'US' => [
'thousand_separator' => ',',
'decimal_separator' => '.',
],
'UY' => [
'swap_postal_code' => true,
],

View File

@ -20,7 +20,7 @@ class InvoiceDesignsSeeder extends Seeder
'Playful',
'Photo',
];
for ($i = 0; $i < count($designs); $i++) {
$design = $designs[$i];
$fileName = storage_path() . '/templates/' . strtolower($design) . '.js';
@ -38,5 +38,15 @@ class InvoiceDesignsSeeder extends Seeder
}
}
}
for ($i = 1; $i <= 3; $i++) {
$name = 'Custom' . $i;
if (! InvoiceDesign::whereName($name)->first()) {
InvoiceDesign::create([
'id' => $i + 10,
'name' => $name,
]);
}
}
}
}

View File

@ -34,6 +34,9 @@ class LanguageSeeder extends Seeder
['name' => 'English - United Kingdom', 'locale' => 'en_UK'],
['name' => 'Portuguese - Portugal', 'locale' => 'pt_PT'],
['name' => 'Slovenian', 'locale' => 'sl'],
['name' => 'Finnish', 'locale' => 'fi'],
['name' => 'Romanian', 'locale' => 'ro'],
['name' => 'Turkish - Turkey', 'locale' => 'tr_TR'],
];
foreach ($languages as $language) {

View File

@ -35,6 +35,7 @@ class PaymentTypesSeeder extends Seeder
['name' => 'iZettle', 'gateway_type_id' => GATEWAY_TYPE_CREDIT_CARD],
['name' => 'Swish', 'gateway_type_id' => GATEWAY_TYPE_BANK_TRANSFER],
['name' => 'Venmo'],
['name' => 'Money Order'],
];
foreach ($paymentTypes as $paymentType) {

View File

@ -36,7 +36,7 @@ class UserTableSeeder extends Seeder
'invoice_terms' => $faker->text($faker->numberBetween(50, 300)),
'work_phone' => $faker->phoneNumber,
'work_email' => $faker->safeEmail,
'invoice_design_id' => InvoiceDesign::where('id', '<', CUSTOM_DESIGN)->get()->random()->id,
'invoice_design_id' => InvoiceDesign::where('id', '<', CUSTOM_DESIGN1)->get()->random()->id,
'header_font_id' => min(Font::all()->random()->id, 17),
'body_font_id' => min(Font::all()->random()->id, 17),
'primary_color' => $faker->hexcolor,

File diff suppressed because one or more lines are too long

View File

@ -74,15 +74,15 @@ Heres an example of creating a client. Note that email address is a property
.. code-block:: shell
curl -X POST ninja.dev/api/v1/clients -H "Content-Type:application/json"
curl -X POST ninja.dev/api/v1/clients -H "Content-Type:application/json" \
-d '{"name":"Client","contact":{"email":"test@example.com"}}' -H "X-Ninja-Token: TOKEN"
You can also update a client by specifying a value for id. Next, heres an example of creating an invoice.
.. code-block:: shell
curl -X POST ninja.dev/api/v1/invoices -H "Content-Type:application/json"
-d '{"client_id":"1", "invoice_items":[{"product_key": "ITEM", "notes":"Test", "cost":10, "qty":1}]}'
curl -X POST ninja.dev/api/v1/invoices -H "Content-Type:application/json" \
-d '{"client_id":"1", "invoice_items":[{"product_key": "ITEM", "notes":"Test", "cost":10, "qty":1}]}' \
-H "X-Ninja-Token: TOKEN"
If the product_key is set and matches an existing record the product fields will be auto-populated. If the email field is set then well search for a matching client. If no matches are found a new client will be created.
@ -103,8 +103,15 @@ Updating Data
.. code-block:: shell
curl -X PUT ninja.dev/api/v1/clients/1 -H "Content-Type:application/json"
-d '{"name":"test", "contacts":[{"id": 1, "first_name": "test"}]}'
curl -X PUT ninja.dev/api/v1/clients/1 -H "Content-Type:application/json" \
-d '{"name":"test", "contacts":[{"id": 1, "first_name": "test"}]}' \
-H "X-Ninja-Token: TOKEN"
You can archive, delete or restore an entity by setting ``action`` in the request
.. code-block:: shell
curl -X PUT ninja.dev/api/v1/invoices/1?action=archive \
-H "X-Ninja-Token: TOKEN"
Emailing Invoices
@ -114,7 +121,7 @@ To email an invoice use the email_invoice command passing the id of the invoice.
.. code-block:: shell
curl -X POST ninja.dev/api/v1/email_invoice -d '{"id":1}'
curl -X POST ninja.dev/api/v1/email_invoice -d '{"id":1}' \
-H "Content-Type:application/json" -H "X-Ninja-Token: TOKEN"
Subscriptions

View File

@ -57,9 +57,9 @@ author = u'Invoice Ninja'
# built documents.
#
# The short X.Y version.
version = u'3.3'
version = u'3.4'
# The full version, including alpha/beta/rc tags.
release = u'3.3.3'
release = u'3.4.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View File

@ -49,19 +49,24 @@ Create an application in either Google, Facebook, GitHub or LinkedIn and then se
PhantomJS
"""""""""
We use phantomjscloud.com to attach PDFs to emails sent by background processes. Check for the following line in the .env file to enable this feature or sign up to increase your daily limit.
There are two methods to attach PDFs to emails sent by background processes: phantomjscloud.com or local PhantomJS install.
To use phantomjscloud.com check for the following line in the .env file.
.. code-block:: shell
PHANTOMJS_CLOUD_KEY='a-demo-key-with-low-quota-per-ip-address'
PHANTOMJS_CLOUD_KEY='a-demo-key-with-low-quota-per-ip-address'
If you require contacts to enter a password to see their invoice you'll need to set a random value for ``PHANTOMJS_SECRET``.
To use a local PhantomJS install add ``PHANTOMJS_BIN_PATH=/usr/local/bin/phantomjs``.
You can install PhantomJS to generate the PDF locally, to enable it add ``PHANTOMJS_BIN_PATH=/usr/local/bin/phantomjs``.
Troubleshooting
---------------
We suggest using version >= 2.1.1, users have reported seeing 'Error: 0' with older versions.
.. TIP:: To determine the path you can run ``which phantomjs`` from the command line.
- Check storage/logs/laravel-error.log for relevant errors.
- To determine the path you can run ``which phantomjs`` from the command line.
- We suggest using PhantomJS version >= 2.1.1, users have reported seeing 'Error: 0' with older versions.
- You can use `this script <https://raw.githubusercontent.com/invoiceninja/invoiceninja/develop/resources/test.pjs>`_ to test from the command line, change ``__YOUR_LINK_HERE__`` to a 'View as recipient' link.
- If you require contacts to enter a password to see their invoice you'll need to set a random value for ``PHANTOMJS_SECRET``.
Custom Fonts
""""""""""""
@ -90,7 +95,7 @@ Follow these steps to add a driver.
Google Map
""""""""""
You need to create a Google Maps API key for the Javascript, Geocoding and Embed APIs and then add ``GOOGLE_MAPS_API_KEY=your_key`` to the .env file.
You need to create a `Google Maps API <https://developers.google.com/maps/documentation/javascript/get-api-key>`_ key for the Javascript, Geocoding and Embed APIs and then add ``GOOGLE_MAPS_API_KEY=your_key`` to the .env file.
You can disable the feature by adding ``GOOGLE_MAPS_ENABLED=false`` to the .env file.

View File

@ -21,6 +21,8 @@ Automated Installers
- Softaculous: `softaculous.com <https://www.softaculous.com/apps/ecommerce/Invoice_Ninja>`_
.. Tip:: You can use `github.com/turbo124/Plane2Ninja <https://github.com/turbo124/Plane2Ninja>`_ to migrate your data from InvoicePlane.
Steps to Install
^^^^^^^^^^^^^^^^
@ -29,7 +31,7 @@ Step 1: Download the code
You can either download the zip file below or checkout the code from our GitHub repository. The zip includes all third party libraries whereas using GitHub requires you to use Composer to install the dependencies.
https://download.invoiceninja.com/ninja-v3.3.1.zip
https://download.invoiceninja.com
.. Note:: All Pro and Enterprise features from our hosted app are included in both the zip file and the GitHub repository. We offer a $20 per year white-label license to remove our branding.
@ -57,7 +59,7 @@ Youll need to create a new database along with a user to access it. Most host
CREATE DATABASE ninja;
CREATE USER 'ninja'@'localhost' IDENTIFIED BY 'ninja';
GRANT ALL PRIVILEGES ON * . * TO 'ninja'@'localhost';
GRANT ALL PRIVILEGES ON ninja.* TO 'ninja'@'localhost';
Step 4: Configure the web server
""""""""""""""""""""""""""""""""

View File

@ -5,6 +5,8 @@ Update
To update the app you just need to copy over the latest code. The app tracks the current version in a file called version.txt, if it notices a change it loads ``/update`` to run the database migrations.
If you're moving servers make sure to copy over the .env file.
If the auto-update fails you can manually run the update with the following commands. Once completed add ``?clear_cache=true`` to the end of the URL to clear the application cache.
.. code-block:: shell
@ -16,6 +18,8 @@ If the auto-update fails you can manually run the update with the following comm
.. NOTE:: If you've downloaded the code from GitHub you also need to run ``composer install``
.. TIP:: You can see the detailed changes for each release on our `GitHub release notes <https://github.com/invoiceninja/invoiceninja/releases>`_.
Version 3.2
"""""""""""

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1093,7 +1093,7 @@ div.panel-body div.panel-body {
}
.invoice-table #document-upload{
width:500px;
width:550px;
}
#document-upload .dropzone{
@ -1289,3 +1289,121 @@ div.panel-body div.panel-body {
.modal-footer {
padding-right: 20px;
}
#upgrade-modal {
display: none;
position: absolute;
z-index: 999999;
#background-color: rgba(76,76,76,.99);
background-color: rgba(0,0,0,.9);
text-align: center;
width: 100%;
height: 100%;
min-height: 1500px;
}
#upgrade-modal h1 {
font-family: 'roboto-thin', 'roboto', Helvetica, arial, sans-serif;
font-size: 28px!important;
padding: 0 0 25px 0;
margin: 0!important;
color: #fff;
padding-top: 0px;
padding-bottom: 20px;
font-weight: 800;
}
#upgrade-modal h2 {
font-family: 'roboto-thin', 'roboto', Helvetica, arial, sans-serif;
color: #36c157;
font-size: 34px;
line-height: 15px;
padding-bottom: 4px;
margin: 0;
font-weight: 100;
}
#upgrade-modal h3 {
font-family: 'roboto-thin', 'roboto', Helvetica, arial, sans-serif;
margin: 20px 0 25px 0;
font-size: 75px;
padding: 0 0 8px 0;
color: #fff;
font-weight: 100;
}
#upgrade-modal h3 span.upgrade_frequency {
font-size: 17px;
text-transform: uppercase;
letter-spacing: 2px;
vertical-align: super;
}
#upgrade-modal h4 {
color: white;
}
#upgrade-modal ul {
list-style: none;
color: #fff;
padding: 20px 0;
}
#upgrade-modal .col-md-4 {
padding:75px 20px;
border-right: 0;
}
#upgrade-modal .col-md-offset-2 {
border-top: 1px solid #343333;
border-right: 1px solid #343333;
}
#upgrade-modal .columns {
border-top: 1px solid #343333;
}
#upgrade-modal ul {
border-top: 1px solid #343333;
}
#upgrade-modal ul li {
font-size: 17px;
line-height: 35px;
font-weight: 400;
}
#upgrade-modal p.subhead {
font-size: 15px;
margin: 5px 0 5px 0;
padding-top: 10px;
padding-bottom: 10px;
font-weight: 400;
color: #fff;
}
#upgrade-modal .btn {
width: 260px;
padding: 16px 0 16px 0;
}
#upgrade-modal i.fa-close {
cursor: pointer;
color: #fff;
font-size: 26px !important;
padding-top: 30px;
}
#upgrade-modal label.radio-inline {
padding: 0px 30px 30px 30px;
font-size: 22px;
color: #fff;
vertical-align: middle;
}
#upgrade-modal select {
vertical-align: top;
width: 140px;
}

View File

@ -320,6 +320,7 @@ NINJA.decodeJavascript = function(invoice, javascript)
var value = getDescendantProp(invoice, field) || ' ';
value = doubleDollarSign(value);
value = value.replace(/\n/g, "\\n").replace(/\r/g, "\\r");
javascript = javascript.replace(match, '"'+value+'"');
}
}
@ -502,14 +503,15 @@ NINJA.invoiceLines = function(invoice) {
var lineTotal = roundToTwo(NINJA.parseFloat(item.cost)) * roundToTwo(NINJA.parseFloat(item.qty));
if (account.include_item_taxes_inline == '1') {
var taxAmount1 = 0;
var taxAmount2 = 0;
if (tax1) {
lineTotal += lineTotal * tax1 / 100;
lineTotal = roundToTwo(lineTotal);
taxAmount1 = roundToTwo(lineTotal * tax1 / 100);
}
if (tax2) {
lineTotal += lineTotal * tax2 / 100;
lineTotal = roundToTwo(lineTotal);
taxAmount2 = roundToTwo(lineTotal * tax2 / 100);
}
lineTotal += taxAmount1 + taxAmount2;
}
lineTotal = formatMoneyInvoice(lineTotal, invoice);

View File

@ -81,7 +81,7 @@ $LANG = array(
'guest' => 'Guest',
'company_details' => 'Company Details',
'online_payments' => 'Online Payments',
'notifications' => 'Email Notifications',
'notifications' => 'Notifications',
'import_export' => 'Import | Export',
'done' => 'Done',
'save' => 'Save',
@ -995,8 +995,8 @@ $LANG = array(
'quote_issued_to' => 'Quote issued to',
'show_currency_code' => 'Currency Code',
'trial_message' => 'Your account will receive a free two week trial of our pro plan.',
'trial_footer' => 'Your free trial lasts :count more days, :link to upgrade now.',
'trial_footer_last_day' => 'This is the last day of your free trial, :link to upgrade now.',
'trial_footer' => 'Your free pro plan trial lasts :count more days, :link to upgrade now.',
'trial_footer_last_day' => 'This is the last day of your free pro plan trial, :link to upgrade now.',
'trial_call_to_action' => 'Start Free Trial',
'trial_success' => 'Successfully enabled two week free pro plan trial',
'overdue' => 'Overdue',
@ -1713,6 +1713,11 @@ $LANG = array(
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
'lang_Finnish' => 'Finnish',
'lang_Romanian' => 'Romanian',
'lang_Turkish - Turkey' => 'Turkish - Turkey',
'lang_Portuguese - Brazilian' => 'Portuguese - Brazilian',
'lang_Portuguese - Portugal' => 'Portuguese - Portugal',
// Frequencies
'freq_weekly' => 'Weekly',
@ -1723,24 +1728,6 @@ $LANG = array(
'freq_six_months' => 'Six months',
'freq_annually' => 'Annually',
// Payment types
'payment_type_Apply Credit' => 'Apply Credit',
'payment_type_Bank Transfer' => 'Bank Transfer',
'payment_type_Cash' => 'Cash',
'payment_type_Debit' => 'Debit',
'payment_type_ACH' => 'ACH',
'payment_type_Visa Card' => 'Visa Card',
'payment_type_MasterCard' => 'MasterCard',
'payment_type_American Express' => 'American Express',
'payment_type_Discover Card' => 'Discover Card',
'payment_type_Diners Card' => 'Diners Card',
'payment_type_EuroCard' => 'EuroCard',
'payment_type_Nova' => 'Nova',
'payment_type_Credit Card Other' => 'Credit Card Other',
'payment_type_PayPal' => 'PayPal',
'payment_type_Google Wallet' => 'Google Wallet',
'payment_type_Check' => 'Check',
// Industries
'industry_Accounting & Legal' => 'Accounting & Legal',
'industry_Advertising' => 'Advertising',
@ -2096,7 +2083,6 @@ $LANG = array(
'view_statement' => 'View Statement',
'statement' => 'Statement',
'statement_date' => 'Statement Date',
'inactivity_logout' => 'Due to inactivity, you have been automatically logged out.',
'mark_active' => 'Mark Active',
'send_automatically' => 'Send Automatically',
'initial_email' => 'Initial Email',
@ -2222,6 +2208,7 @@ $LANG = array(
'sample_commands' => 'Sample commands',
'voice_commands_feedback' => 'We\'re actively working to improve this feature, if there\'s a command you\'d like us to support please email us at :email.',
'payment_type_Venmo' => 'Venmo',
'payment_type_Money Order' => 'Money Order',
'archived_products' => 'Successfully archived :count products',
'recommend_on' => 'We recommend <b>enabling</b> this setting.',
'recommend_off' => 'We recommend <b>disabling</b> this setting.',
@ -2244,6 +2231,42 @@ $LANG = array(
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'plan_price' => 'Plan Price',
'wrong_confirmation' => 'Incorrect confirmation code',
'oauth_taken' => 'The account is already registered',
'emailed_payment' => 'Successfully emailed payment',
'email_payment' => 'Email Payment',
'sent' => 'sent',
'invoiceplane_import' => 'Use :link to migrate your data from InvoicePlane.',
'duplicate_expense_warning' => 'Warning: This :link may be a duplicate',
'expense_link' => 'expense',
'resume_task' => 'Resume Task',
'resumed_task' => 'Successfully resumed task',
'quote_design' => 'Quote Design',
'default_design' => 'Default Design',
'custom_design1' => 'Custom Design 1',
'custom_design2' => 'Custom Design 2',
'custom_design3' => 'Custom Design 3',
'empty' => 'Empty',
'load_design' => 'Load Design',
'accepted_card_logos' => 'Accepted Card Logos',
'phantomjs_local_and_cloud' => 'Using local PhantomJS, falling back to phantomjscloud.com',
'google_analytics' => 'Google Analytics',
'analytics_key' => 'Analytics Key',
'analytics_key_help' => 'Track payments using :link',
'start_date_required' => 'The start date is required',
'application_settings' => 'Application Settings',
'database_connection' => 'Database Connection',
'driver' => 'Driver',
'host' => 'Host',
'database' => 'Database',
'test_connection' => 'Test connection',
'from_name' => 'From Name',
'from_address' => 'From Address',
'port' => 'Port',
'encryption' => 'Encryption',
'mailgun_domain' => 'Mailgun Domain',
'mailgun_private_key' => 'Mailgun Private Key',
'send_test_email' => 'Send test email',
);

View File

@ -81,7 +81,7 @@ $LANG = array(
'guest' => 'Host',
'company_details' => 'Detaily firmy',
'online_payments' => 'Online platby',
'notifications' => 'Emailové notifikace',
'notifications' => 'Notifications',
'import_export' => 'Import | Export | Zrušit',
'done' => 'Hotovo',
'save' => 'Uložit',
@ -997,8 +997,8 @@ $LANG = array(
'quote_issued_to' => 'Náklad je vystaven',
'show_currency_code' => 'Kód měny',
'trial_message' => 'Váš účet získá zdarma 2 týdny zkušební verze Profi plánu.',
'trial_footer' => 'Vaše zkušební verze trvá :count dnů, :link upradujte nyní.',
'trial_footer_last_day' => 'Dnes je poslední den Vašeho zkušebního období , :link upradujte nyní.',
'trial_footer' => 'Your free pro plan trial lasts :count more days, :link to upgrade now.',
'trial_footer_last_day' => 'This is the last day of your free pro plan trial, :link to upgrade now.',
'trial_call_to_action' => 'Vyzkoušet zdarma',
'trial_success' => '14-ti denní zkušební lhůta úspěšně nastavena',
'overdue' => 'Po termínu',
@ -1715,6 +1715,11 @@ $LANG = array(
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
'lang_Finnish' => 'Finnish',
'lang_Romanian' => 'Romanian',
'lang_Turkish - Turkey' => 'Turkish - Turkey',
'lang_Portuguese - Brazilian' => 'Portuguese - Brazilian',
'lang_Portuguese - Portugal' => 'Portuguese - Portugal',
// Frequencies
'freq_weekly' => 'Weekly',
@ -1725,24 +1730,6 @@ $LANG = array(
'freq_six_months' => 'Six months',
'freq_annually' => 'Annually',
// Payment types
'payment_type_Apply Credit' => 'Apply Credit',
'payment_type_Bank Transfer' => 'Bank Transfer',
'payment_type_Cash' => 'Cash',
'payment_type_Debit' => 'Debit',
'payment_type_ACH' => 'ACH',
'payment_type_Visa Card' => 'Visa Card',
'payment_type_MasterCard' => 'MasterCard',
'payment_type_American Express' => 'American Express',
'payment_type_Discover Card' => 'Discover Card',
'payment_type_Diners Card' => 'Diners Card',
'payment_type_EuroCard' => 'EuroCard',
'payment_type_Nova' => 'Nova',
'payment_type_Credit Card Other' => 'Credit Card Other',
'payment_type_PayPal' => 'PayPal',
'payment_type_Google Wallet' => 'Google Wallet',
'payment_type_Check' => 'Check',
// Industries
'industry_Accounting & Legal' => 'Accounting & Legal',
'industry_Advertising' => 'Advertising',
@ -2098,7 +2085,6 @@ $LANG = array(
'view_statement' => 'View Statement',
'statement' => 'Statement',
'statement_date' => 'Statement Date',
'inactivity_logout' => 'Due to inactivity, you have been automatically logged out.',
'mark_active' => 'Mark Active',
'send_automatically' => 'Send Automatically',
'initial_email' => 'Initial Email',
@ -2224,6 +2210,7 @@ $LANG = array(
'sample_commands' => 'Sample commands',
'voice_commands_feedback' => 'We\'re actively working to improve this feature, if there\'s a command you\'d like us to support please email us at :email.',
'payment_type_Venmo' => 'Venmo',
'payment_type_Money Order' => 'Money Order',
'archived_products' => 'Successfully archived :count products',
'recommend_on' => 'We recommend <b>enabling</b> this setting.',
'recommend_off' => 'We recommend <b>disabling</b> this setting.',
@ -2246,6 +2233,42 @@ $LANG = array(
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'plan_price' => 'Plan Price',
'wrong_confirmation' => 'Incorrect confirmation code',
'oauth_taken' => 'The account is already registered',
'emailed_payment' => 'Successfully emailed payment',
'email_payment' => 'Email Payment',
'sent' => 'sent',
'invoiceplane_import' => 'Use :link to migrate your data from InvoicePlane.',
'duplicate_expense_warning' => 'Warning: This :link may be a duplicate',
'expense_link' => 'expense',
'resume_task' => 'Resume Task',
'resumed_task' => 'Successfully resumed task',
'quote_design' => 'Quote Design',
'default_design' => 'Default Design',
'custom_design1' => 'Custom Design 1',
'custom_design2' => 'Custom Design 2',
'custom_design3' => 'Custom Design 3',
'empty' => 'Empty',
'load_design' => 'Load Design',
'accepted_card_logos' => 'Accepted Card Logos',
'phantomjs_local_and_cloud' => 'Using local PhantomJS, falling back to phantomjscloud.com',
'google_analytics' => 'Google Analytics',
'analytics_key' => 'Analytics Key',
'analytics_key_help' => 'Track payments using :link',
'start_date_required' => 'The start date is required',
'application_settings' => 'Application Settings',
'database_connection' => 'Database Connection',
'driver' => 'Driver',
'host' => 'Host',
'database' => 'Database',
'test_connection' => 'Test connection',
'from_name' => 'From Name',
'from_address' => 'From Address',
'port' => 'Port',
'encryption' => 'Encryption',
'mailgun_domain' => 'Mailgun Domain',
'mailgun_private_key' => 'Mailgun Private Key',
'send_test_email' => 'Send test email',
);

View File

@ -81,7 +81,7 @@ $LANG = array(
'guest' => 'Gæst',
'company_details' => 'Firmainformation',
'online_payments' => 'On-line betaling',
'notifications' => 'Notifikationer',
'notifications' => 'Notifications',
'import_export' => 'Import/Eksport',
'done' => 'Færdig',
'save' => 'Gem',
@ -995,8 +995,8 @@ $LANG = array(
'quote_issued_to' => 'Quote issued to',
'show_currency_code' => 'Currency Code',
'trial_message' => 'Your account will receive a free two week trial of our pro plan.',
'trial_footer' => 'Your free trial lasts :count more days, :link to upgrade now.',
'trial_footer_last_day' => 'This is the last day of your free trial, :link to upgrade now.',
'trial_footer' => 'Your free pro plan trial lasts :count more days, :link to upgrade now.',
'trial_footer_last_day' => 'This is the last day of your free pro plan trial, :link to upgrade now.',
'trial_call_to_action' => 'Start Free Trial',
'trial_success' => 'Successfully enabled two week free pro plan trial',
'overdue' => 'Overdue',
@ -1713,6 +1713,11 @@ $LANG = array(
'lang_Albanian' => 'Albanian',
'lang_English - United Kingdom' => 'English - United Kingdom',
'lang_Slovenian' => 'Slovenian',
'lang_Finnish' => 'Finnish',
'lang_Romanian' => 'Romanian',
'lang_Turkish - Turkey' => 'Turkish - Turkey',
'lang_Portuguese - Brazilian' => 'Portuguese - Brazilian',
'lang_Portuguese - Portugal' => 'Portuguese - Portugal',
// Frequencies
'freq_weekly' => 'Weekly',
@ -1723,24 +1728,6 @@ $LANG = array(
'freq_six_months' => 'Six months',
'freq_annually' => 'Annually',
// Payment types
'payment_type_Apply Credit' => 'Apply Credit',
'payment_type_Bank Transfer' => 'Bank Transfer',
'payment_type_Cash' => 'Cash',
'payment_type_Debit' => 'Debit',
'payment_type_ACH' => 'ACH',
'payment_type_Visa Card' => 'Visa Card',
'payment_type_MasterCard' => 'MasterCard',
'payment_type_American Express' => 'American Express',
'payment_type_Discover Card' => 'Discover Card',
'payment_type_Diners Card' => 'Diners Card',
'payment_type_EuroCard' => 'EuroCard',
'payment_type_Nova' => 'Nova',
'payment_type_Credit Card Other' => 'Credit Card Other',
'payment_type_PayPal' => 'PayPal',
'payment_type_Google Wallet' => 'Google Wallet',
'payment_type_Check' => 'Check',
// Industries
'industry_Accounting & Legal' => 'Accounting & Legal',
'industry_Advertising' => 'Advertising',
@ -2096,7 +2083,6 @@ $LANG = array(
'view_statement' => 'View Statement',
'statement' => 'Statement',
'statement_date' => 'Statement Date',
'inactivity_logout' => 'Due to inactivity, you have been automatically logged out.',
'mark_active' => 'Mark Active',
'send_automatically' => 'Send Automatically',
'initial_email' => 'Initial Email',
@ -2222,6 +2208,7 @@ $LANG = array(
'sample_commands' => 'Sample commands',
'voice_commands_feedback' => 'We\'re actively working to improve this feature, if there\'s a command you\'d like us to support please email us at :email.',
'payment_type_Venmo' => 'Venmo',
'payment_type_Money Order' => 'Money Order',
'archived_products' => 'Successfully archived :count products',
'recommend_on' => 'We recommend <b>enabling</b> this setting.',
'recommend_off' => 'We recommend <b>disabling</b> this setting.',
@ -2244,6 +2231,42 @@ $LANG = array(
'white_label_license_error' => 'Failed to validate the license, check storage/logs/laravel-error.log for more details.',
'plan_price' => 'Plan Price',
'wrong_confirmation' => 'Incorrect confirmation code',
'oauth_taken' => 'The account is already registered',
'emailed_payment' => 'Successfully emailed payment',
'email_payment' => 'Email Payment',
'sent' => 'sendt',
'invoiceplane_import' => 'Use :link to migrate your data from InvoicePlane.',
'duplicate_expense_warning' => 'Warning: This :link may be a duplicate',
'expense_link' => 'expense',
'resume_task' => 'Resume Task',
'resumed_task' => 'Successfully resumed task',
'quote_design' => 'Quote Design',
'default_design' => 'Default Design',
'custom_design1' => 'Custom Design 1',
'custom_design2' => 'Custom Design 2',
'custom_design3' => 'Custom Design 3',
'empty' => 'Empty',
'load_design' => 'Load Design',
'accepted_card_logos' => 'Accepted Card Logos',
'phantomjs_local_and_cloud' => 'Using local PhantomJS, falling back to phantomjscloud.com',
'google_analytics' => 'Google Analytics',
'analytics_key' => 'Analytics Key',
'analytics_key_help' => 'Track payments using :link',
'start_date_required' => 'The start date is required',
'application_settings' => 'Application Settings',
'database_connection' => 'Database Connection',
'driver' => 'Driver',
'host' => 'Host',
'database' => 'Database',
'test_connection' => 'Test connection',
'from_name' => 'From Name',
'from_address' => 'From Address',
'port' => 'Port',
'encryption' => 'Encryption',
'mailgun_domain' => 'Mailgun Domain',
'mailgun_private_key' => 'Mailgun Private Key',
'send_test_email' => 'Send test email',
);

Some files were not shown because too many files have changed in this diff Show More