Merge remote-tracking branch 'upstream/v5-develop' into v5-2701-dynamic-dates-for-recurring

This commit is contained in:
Benjamin Beganović 2021-02-22 13:15:37 +01:00
commit 8e3875da96
327 changed files with 150399 additions and 142852 deletions

View File

@ -39,7 +39,7 @@ jobs:
mariadb: mariadb:
image: mariadb:latest image: mariadb:latest
ports: ports:
- 3306 - 32768:3306
env: env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_USER: ninja MYSQL_USER: ninja
@ -52,7 +52,6 @@ jobs:
- name: Start mysql service - name: Start mysql service
run: | run: |
sudo /etc/init.d/mysql start sudo /etc/init.d/mysql start
- name: Verify MariaDB connection - name: Verify MariaDB connection
env: env:
DB_PORT: ${{ job.services.mariadb.ports[3306] }} DB_PORT: ${{ job.services.mariadb.ports[3306] }}
@ -62,7 +61,6 @@ jobs:
while ! mysqladmin ping -h"127.0.0.1" -P"$DB_PORT" --silent; do while ! mysqladmin ping -h"127.0.0.1" -P"$DB_PORT" --silent; do
sleep 1 sleep 1
done done
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2
with: with:
@ -77,33 +75,27 @@ jobs:
- name: Copy .env - name: Copy .env
run: | run: |
cp .env.ci .env cp .env.ci .env
- name: Install composer dependencies - name: Install composer dependencies
run: | run: |
composer config -g github-oauth.github.com ${{ secrets.GITHUB_TOKEN }} composer config -g github-oauth.github.com ${{ secrets.GITHUB_TOKEN }}
composer install composer install
- name: Prepare Laravel Application - name: Prepare Laravel Application
run: | run: |
php artisan key:generate php artisan key:generate
php artisan optimize php artisan optimize
php artisan cache:clear php artisan cache:clear
php artisan config:cache php artisan config:cache
- name: Create DB and schemas - name: Create DB and schemas
run: | run: |
mkdir -p database mkdir -p database
touch database/database.sqlite touch database/database.sqlite
- name: Migrate Database - name: Migrate Database
run: | run: |
php artisan migrate:fresh --seed --force && php artisan db:seed --force php artisan migrate:fresh --seed --force && php artisan db:seed --force
- name: Prepare JS/CSS assets - name: Prepare JS/CSS assets
run: | run: |
npm i npm i
npm run production npm run production
- name: Run Testsuite - name: Run Testsuite
run: | run: |
cat .env cat .env
@ -114,4 +106,3 @@ jobs:
- name: Run php-cs-fixer - name: Run php-cs-fixer
run: | run: |
vendor/bin/php-cs-fixer fix vendor/bin/php-cs-fixer fix

View File

@ -4,12 +4,18 @@
![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop) ![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop)
![v5-stable phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-stable) ![v5-stable phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-stable)
[![codecov](https://codecov.io/gh/invoiceninja/invoiceninja/branch/v2/graph/badge.svg)](https://codecov.io/gh/invoiceninja/invoiceninja)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/d39acb4bf0f74a0698dc77f382769ba5)](https://www.codacy.com/app/turbo124/invoiceninja?utm_source=github.com&utm_medium=referral&utm_content=invoiceninja/invoiceninja&utm_campaign=Badge_Grade) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/d39acb4bf0f74a0698dc77f382769ba5)](https://www.codacy.com/app/turbo124/invoiceninja?utm_source=github.com&utm_medium=referral&utm_content=invoiceninja/invoiceninja&utm_campaign=Badge_Grade)
# Invoice Ninja version 5 is in Beta! # Invoice Ninja version 5.1 RC2!
We will be using the lessons learnt in Invoice Ninja 4.0 to build a bigger better platform to work from. If you would like to contribute to the project we will gladly accept contributions for code, user guides, bug tracking and feedback! Please consider the following guidelines prior to submitting a pull request: Invoice Ninja version 5.1 has now reached Release Candidate 2!
What does this mean exactly? We consider this version _almost_ stable. There may be some remaining small issues which we would love to get feedback on. We would really appreciate the community booting up this version and attempting the migration from their Invoice Ninja V4 application and inspect the migrated data.
We'd also like feedback on any issues that you can see, and help us nail down the few remaining issues before Version 5 graduates to Stable Gold Release.
Please note we do not consider this version ready for production use, please stick with your V4 installation for your production clients!
## Quick Start ## Quick Start
@ -17,13 +23,10 @@ Currently the client portal and API are of alpha quality, to get started:
```bash ```bash
git clone https://github.com/invoiceninja/invoiceninja.git git clone https://github.com/invoiceninja/invoiceninja.git
git checkout v2 git checkout v5-stable
cp .env.example .env cp .env.example .env
cp .env.dusk.example .env.dusk.local
php artisan key:generate php artisan key:generate
composer update composer update
npm i
npm run production
``` ```
Please Note: Your APP_KEY in the .env file is used to encrypt data, if you lose this you will not be able to run the application. Please Note: Your APP_KEY in the .env file is used to encrypt data, if you lose this you will not be able to run the application.
@ -33,7 +36,7 @@ Run if you want to load sample data, remember to configure .env
php artisan migrate:fresh --seed && php artisan db:seed && php artisan ninja:create-test-data php artisan migrate:fresh --seed && php artisan db:seed && php artisan ninja:create-test-data
``` ```
To Run the web server To run the web server
``` ```
php artisan serve php artisan serve
``` ```

View File

@ -1 +1 @@
5.0.54 5.1.7

View File

@ -73,6 +73,7 @@ class CheckData extends Command
protected $description = 'Check/fix data'; protected $description = 'Check/fix data';
protected $log = ''; protected $log = '';
protected $isValid = true; protected $isValid = true;
public function handle() public function handle()
@ -90,13 +91,10 @@ class CheckData extends Command
$this->checkContacts(); $this->checkContacts();
$this->checkCompanyData(); $this->checkCompanyData();
//$this->checkLogoFiles();
if (! $this->option('client_id')) { if (! $this->option('client_id')) {
$this->checkOAuth(); $this->checkOAuth();
//$this->checkInvitations(); //$this->checkFailedJobs();
$this->checkFailedJobs();
} }
$this->logMessage('Done: '.strtoupper($this->isValid ? Account::RESULT_SUCCESS : Account::RESULT_FAILURE)); $this->logMessage('Done: '.strtoupper($this->isValid ? Account::RESULT_SUCCESS : Account::RESULT_FAILURE));
@ -289,30 +287,6 @@ class CheckData extends Command
} }
} }
private function checkInvoiceBalances()
{
$wrong_balances = 0;
$wrong_paid_to_dates = 0;
foreach (Client::where('is_deleted', 0)->cursor() as $client) {
$invoice_balance = $client->invoices->where('is_deleted', false)->where('status_id', '>', 1)->sum('balance');
$credit_balance = $client->credits->where('is_deleted', false)->sum('balance');
// $invoice_balance -= $credit_balance;//doesn't make sense to remove the credit amount
$ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first();
if ($ledger && number_format($invoice_balance, 4) != number_format($client->balance, 4)) {
$wrong_balances++;
$this->logMessage("# {$client->id} " . $client->present()->name.' - '.$client->number." - Balance Failure - Invoice Balances = {$invoice_balance} Client Balance = {$client->balance} Ledger Balance = {$ledger->balance}");
$this->isValid = false;
}
}
$this->logMessage("{$wrong_balances} clients with incorrect balances");
}
private function checkPaidToDates() private function checkPaidToDates()
{ {
$wrong_paid_to_dates = 0; $wrong_paid_to_dates = 0;
@ -322,8 +296,6 @@ class CheckData extends Command
$total_invoice_payments = 0; $total_invoice_payments = 0;
foreach ($client->invoices->where('is_deleted', false)->where('status_id', '>', 1) as $invoice) { foreach ($client->invoices->where('is_deleted', false)->where('status_id', '>', 1) as $invoice) {
// $total_amount = $invoice->payments->whereNull('deleted_at')->sum('pivot.amount');
// $total_refund = $invoice->payments->whereNull('deleted_at')->sum('pivot.refunded');
$total_amount = $invoice->payments->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])->sum('pivot.amount'); $total_amount = $invoice->payments->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])->sum('pivot.amount');
$total_refund = $invoice->payments->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])->sum('pivot.refunded'); $total_refund = $invoice->payments->where('is_deleted', false)->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment:: STATUS_PENDING, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])->sum('pivot.refunded');
@ -331,15 +303,15 @@ class CheckData extends Command
$total_invoice_payments += ($total_amount - $total_refund); $total_invoice_payments += ($total_amount - $total_refund);
} }
// 10/02/21
foreach ($client->payments as $payment) { foreach ($client->payments as $payment) {
$credit_total_applied += $payment->paymentables->where('paymentable_type', App\Models\Credit::class)->sum(DB::raw('amount')); $credit_total_applied += $payment->paymentables->where('paymentable_type', App\Models\Credit::class)->sum(DB::raw('amount'));
} }
if ($credit_total_applied < 0) { if ($credit_total_applied < 0) {
$total_invoice_payments += $credit_total_applied; $total_invoice_payments += $credit_total_applied;
} //todo this is contentious }
nlog("total invoice payments = {$total_invoice_payments} with client paid to date of of {$client->paid_to_date}");
if (round($total_invoice_payments, 2) != round($client->paid_to_date, 2)) { if (round($total_invoice_payments, 2) != round($client->paid_to_date, 2)) {
$wrong_paid_to_dates++; $wrong_paid_to_dates++;
@ -390,7 +362,9 @@ class CheckData extends Command
$invoice_balance = Invoice::where('client_id', $client->id)->where('is_deleted', false)->where('status_id', '>', 1)->withTrashed()->sum('balance'); $invoice_balance = Invoice::where('client_id', $client->id)->where('is_deleted', false)->where('status_id', '>', 1)->withTrashed()->sum('balance');
$credit_balance = Credit::where('client_id', $client->id)->where('is_deleted', false)->withTrashed()->sum('balance'); $credit_balance = Credit::where('client_id', $client->id)->where('is_deleted', false)->withTrashed()->sum('balance');
// $invoice_balance -= $credit_balance; /*Legacy - V4 will add credits to the balance - we may need to reverse engineer this and remove the credits from the client balance otherwise we need this hack here and in the invoice balance check.*/
if($client->balance != $invoice_balance)
$invoice_balance -= $credit_balance;
$ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first(); $ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first();
@ -405,6 +379,32 @@ class CheckData extends Command
$this->logMessage("{$wrong_paid_to_dates} clients with incorrect client balances"); $this->logMessage("{$wrong_paid_to_dates} clients with incorrect client balances");
} }
private function checkInvoiceBalances()
{
$wrong_balances = 0;
$wrong_paid_to_dates = 0;
foreach (Client::where('is_deleted', 0)->cursor() as $client) {
$invoice_balance = $client->invoices->where('is_deleted', false)->where('status_id', '>', 1)->sum('balance');
$credit_balance = $client->credits->where('is_deleted', false)->sum('balance');
// if($client->balance != $invoice_balance)
// $invoice_balance -= $credit_balance;//doesn't make sense to remove the credit amount
$ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first();
if ($ledger && number_format($invoice_balance, 4) != number_format($client->balance, 4)) {
$wrong_balances++;
$this->logMessage("# {$client->id} " . $client->present()->name.' - '.$client->number." - Balance Failure - Invoice Balances = {$invoice_balance} Client Balance = {$client->balance} Ledger Balance = {$ledger->balance}");
$this->isValid = false;
}
}
$this->logMessage("{$wrong_balances} clients with incorrect balances");
}
private function checkLogoFiles() private function checkLogoFiles()
{ {
// $accounts = DB::table('accounts') // $accounts = DB::table('accounts')

View File

@ -120,7 +120,7 @@ class CreateSingleAccount extends Command
$company_token->company_id = $company->id; $company_token->company_id = $company->id;
$company_token->account_id = $account->id; $company_token->account_id = $account->id;
$company_token->name = 'test token'; $company_token->name = 'test token';
$company_token->token = Str::random(64); $company_token->token = 'company-token-test';
$company_token->is_system = true; $company_token->is_system = true;
$company_token->save(); $company_token->save();

View File

@ -339,7 +339,7 @@ class DemoMode extends Command
]); ]);
$vendor->id_number = $this->getNextVendorNumber($vendor); $vendor->number = $this->getNextVendorNumber($vendor);
$vendor->save(); $vendor->save();
} }

View File

@ -11,6 +11,7 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Libraries\MultiDB;
use App\Models\Design; use App\Models\Design;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use stdClass; use stdClass;
@ -47,6 +48,30 @@ class DesignUpdate extends Command
* @return mixed * @return mixed
*/ */
public function handle() public function handle()
{
//always return state to first DB
$current_db = config('database.default');
if (! config('ninja.db.multi_db_enabled')) {
$this->handleOnDb();
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->handleOnDb($db);
}
MultiDB::setDB($current_db);
}
}
private function handleOnDb()
{ {
foreach (Design::whereIsCustom(false)->get() as $design) { foreach (Design::whereIsCustom(false)->get() as $design) {
$invoice_design = new \App\Services\PdfMaker\Design(strtolower($design->name)); $invoice_design = new \App\Services\PdfMaker\Design(strtolower($design->name));

View File

@ -80,6 +80,8 @@ class ImportMigrations extends Command
$path = $this->option('path') ?? public_path('storage/migrations/import'); $path = $this->option('path') ?? public_path('storage/migrations/import');
nlog(public_path('storage/migrations/import'));
$directory = new DirectoryIterator($path); $directory = new DirectoryIterator($path);
foreach ($directory as $file) { foreach ($directory as $file) {

View File

@ -0,0 +1,66 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Console\Commands;
use App;
use App\Jobs\Ninja\CheckCompanyData;
use App\Models\Account;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\CompanyLedger;
use App\Models\Contact;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\Payment;
use App\Utils\Ninja;
use DB;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Mail;
use Symfony\Component\Console\Input\InputOption;
/**
* Class CheckData.
*/
class ParallelCheckData extends Command
{
/**
* @var string
*/
protected $name = 'ninja:pcheck-data';
/**
* @var string
*/
protected $description = 'Check company data in parallel';
protected $log = '';
protected $isValid = true;
public function handle()
{
$hash = Str::random(32);
Company::cursor()->each(function ($company) use ($hash){
CheckCompanyData::dispatch($company, $hash)->onQueue('checkdata');
});
}
}

View File

@ -53,6 +53,7 @@ class PostUpdate extends Command
nlog("finished migrating"); nlog("finished migrating");
exec('vendor/bin/composer install --no-dev'); exec('vendor/bin/composer install --no-dev');
exec('vendor/bin/composer dump');
nlog("finished running composer install "); nlog("finished running composer install ");

View File

@ -13,6 +13,7 @@ namespace App\Console\Commands;
use App\Jobs\Ninja\SendReminders; use App\Jobs\Ninja\SendReminders;
use App\Jobs\Util\WebHookHandler; use App\Jobs\Util\WebHookHandler;
use App\Libraries\MultiDB;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Quote; use App\Models\Quote;
use App\Models\Webhook; use App\Models\Webhook;
@ -58,6 +59,24 @@ class SendRemindersCron extends Command
} }
private function webHookOverdueInvoices() private function webHookOverdueInvoices()
{
if (! config('ninja.db.multi_db_enabled')) {
$this->executeWebhooks();
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->executeWebhooks();
}
}
}
private function executeWebhooks()
{ {
$invoices = Invoice::where('is_deleted', 0) $invoices = Invoice::where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
@ -68,10 +87,7 @@ class SendRemindersCron extends Command
$invoices->each(function ($invoice) { $invoices->each(function ($invoice) {
WebHookHandler::dispatch(Webhook::EVENT_LATE_INVOICE, $invoice, $invoice->company); WebHookHandler::dispatch(Webhook::EVENT_LATE_INVOICE, $invoice, $invoice->company);
}); });
}
private function webHookExpiredQuotes()
{
$quotes = Quote::where('is_deleted', 0) $quotes = Quote::where('is_deleted', 0)
->where('status_id', Quote::STATUS_SENT) ->where('status_id', Quote::STATUS_SENT)
->whereDate('due_date', '<=', now()->subDays(1)->startOfDay()) ->whereDate('due_date', '<=', now()->subDays(1)->startOfDay())
@ -81,4 +97,6 @@ class SendRemindersCron extends Command
WebHookHandler::dispatch(Webhook::EVENT_EXPIRED_QUOTE, $quote, $quote->company); WebHookHandler::dispatch(Webhook::EVENT_EXPIRED_QUOTE, $quote, $quote->company);
}); });
} }
} }

View File

@ -148,6 +148,7 @@ class CompanySettings extends BaseSettings
public $gmail_sending_user_id = '0'; //@implemented public $gmail_sending_user_id = '0'; //@implemented
public $reply_to_email = ''; //@TODO public $reply_to_email = ''; //@TODO
public $reply_to_name = ''; //@TODO
public $bcc_email = ''; //@TODO public $bcc_email = ''; //@TODO
public $pdf_email_attachment = false; //@implemented public $pdf_email_attachment = false; //@implemented
public $ubl_email_attachment = false; //@implemented public $ubl_email_attachment = false; //@implemented
@ -232,11 +233,11 @@ class CompanySettings extends BaseSettings
public $id_number = ''; //@implemented public $id_number = ''; //@implemented
public $page_size = 'A4'; //Letter, Legal, Tabloid, Ledger, A0, A1, A2, A3, A4, A5, A6 public $page_size = 'A4'; //Letter, Legal, Tabloid, Ledger, A0, A1, A2, A3, A4, A5, A6
public $font_size = 9; //@implemented public $font_size = 7; //@implemented
public $primary_font = 'Roboto'; public $primary_font = 'Roboto';
public $secondary_font = 'Roboto'; public $secondary_font = 'Roboto';
public $primary_color = '#4caf50'; public $primary_color = '#142cb5';
public $secondary_color = '#2196f3'; public $secondary_color = '#7081e0';
public $hide_paid_to_date = false; //@TODO where? public $hide_paid_to_date = false; //@TODO where?
public $embed_documents = false; //@TODO where? public $embed_documents = false; //@TODO where?
@ -261,6 +262,7 @@ class CompanySettings extends BaseSettings
public $hide_empty_columns_on_pdf = false; public $hide_empty_columns_on_pdf = false;
public static $casts = [ public static $casts = [
'reply_to_name' => 'string',
'hide_empty_columns_on_pdf' => 'bool', 'hide_empty_columns_on_pdf' => 'bool',
'enable_reminder_endless' => 'bool', 'enable_reminder_endless' => 'bool',
'use_credits_payment' => 'string', 'use_credits_payment' => 'string',

View File

@ -224,8 +224,7 @@ class EmailTemplateDefaults
private static function transformText($string) private static function transformText($string)
{ {
//preformat the string, removing trailing colons. //preformat the string, removing trailing colons.
$string = rtrim($string, ":");
return str_replace(':', '$', ctrans('texts.'.$string)); return str_replace(':', '$', rtrim( ctrans('texts.'.$string), ":"));
} }
} }

View File

@ -39,10 +39,10 @@ class PaymentWasEmailedAndFailed
* PaymentWasEmailedAndFailed constructor. * PaymentWasEmailedAndFailed constructor.
* @param Payment $payment * @param Payment $payment
* @param $company * @param $company
* @param array $errors * @param string $errors
* @param array $event_vars * @param array $event_vars
*/ */
public function __construct(Payment $payment, Company $company, array $errors, array $event_vars) public function __construct(Payment $payment, Company $company, string $errors, array $event_vars)
{ {
$this->payment = $payment; $this->payment = $payment;

View File

@ -99,17 +99,21 @@ class Handler extends ExceptionHandler
private function validException($exception) private function validException($exception)
{ {
if (strpos($exception->getMessage(), 'file_put_contents') !== false) { if (strpos($exception->getMessage(), 'file_put_contents') !== false)
return false; return false;
}
if (strpos($exception->getMessage(), 'Permission denied') !== false) { if (strpos($exception->getMessage(), 'Permission denied') !== false)
return false; return false;
}
if (strpos($exception->getMessage(), 'flock()') !== false) { if (strpos($exception->getMessage(), 'flock()') !== false)
return false; return false;
}
if (strpos($exception->getMessage(), 'expects parameter 1 to be resource') !== false)
return false;
if (strpos($exception->getMessage(), 'fwrite()') !== false)
return false;
return true; return true;
} }

View File

@ -35,10 +35,10 @@ function nlog($output, $context = []): void
} }
if (!function_exists('ray')) { // if (!function_exists('ray')) {
function ray($payload) // function ray($payload)
{ // {
return true; // return true;
} // }
} // }

View File

@ -27,7 +27,8 @@ trait CustomValuer
public function valuerTax($custom_value, $has_custom_invoice_taxes) public function valuerTax($custom_value, $has_custom_invoice_taxes)
{ {
if (isset($custom_value) && is_numeric($custom_value) && $has_custom_invoice_taxes === true) {
if (isset($custom_value) && is_numeric($custom_value) && $has_custom_invoice_taxes) {
return round($custom_value * ($this->invoice->tax_rate1 / 100), 2) + round($custom_value * ($this->invoice->tax_rate2 / 100), 2) + round($custom_value * ($this->invoice->tax_rate3 / 100), 2); return round($custom_value * ($this->invoice->tax_rate1 / 100), 2) + round($custom_value * ($this->invoice->tax_rate2 / 100), 2) + round($custom_value * ($this->invoice->tax_rate3 / 100), 2);
} }

View File

@ -122,25 +122,23 @@ class InvoiceItemSum
$item_tax += $item_tax_rate1_total; $item_tax += $item_tax_rate1_total;
if ($item_tax_rate1_total > 0) { if($item_tax_rate1_total != 0)
$this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total); $this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total);
}
$item_tax_rate2_total = $this->calcAmountLineTax($this->item->tax_rate2, $amount); $item_tax_rate2_total = $this->calcAmountLineTax($this->item->tax_rate2, $amount);
$item_tax += $item_tax_rate2_total; $item_tax += $item_tax_rate2_total;
if ($item_tax_rate2_total > 0) { if($item_tax_rate2_total != 0)
$this->groupTax($this->item->tax_name2, $this->item->tax_rate2, $item_tax_rate2_total); $this->groupTax($this->item->tax_name2, $this->item->tax_rate2, $item_tax_rate2_total);
}
$item_tax_rate3_total = $this->calcAmountLineTax($this->item->tax_rate3, $amount); $item_tax_rate3_total = $this->calcAmountLineTax($this->item->tax_rate3, $amount);
$item_tax += $item_tax_rate3_total; $item_tax += $item_tax_rate3_total;
if ($item_tax_rate3_total > 0) { if($item_tax_rate3_total != 0)
$this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total); $this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total);
}
$this->setTotalTaxes($this->formatValue($item_tax, $this->currency->precision)); $this->setTotalTaxes($this->formatValue($item_tax, $this->currency->precision));
@ -240,7 +238,7 @@ class InvoiceItemSum
$item_tax += $item_tax_rate1_total; $item_tax += $item_tax_rate1_total;
if ($item_tax_rate1_total > 0) { if ($item_tax_rate1_total != 0) {
$this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total); $this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total);
} }
@ -248,7 +246,7 @@ class InvoiceItemSum
$item_tax += $item_tax_rate2_total; $item_tax += $item_tax_rate2_total;
if ($item_tax_rate2_total > 0) { if ($item_tax_rate2_total != 0) {
$this->groupTax($this->item->tax_name2, $this->item->tax_rate2, $item_tax_rate2_total); $this->groupTax($this->item->tax_name2, $this->item->tax_rate2, $item_tax_rate2_total);
} }
@ -256,7 +254,7 @@ class InvoiceItemSum
$item_tax += $item_tax_rate3_total; $item_tax += $item_tax_rate3_total;
if ($item_tax_rate3_total > 0) { if ($item_tax_rate3_total != 0) {
$this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total); $this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total);
} }
} }

View File

@ -119,25 +119,23 @@ class InvoiceItemSumInclusive
$item_tax += $this->formatValue($item_tax_rate1_total, $this->currency->precision); $item_tax += $this->formatValue($item_tax_rate1_total, $this->currency->precision);
if ($item_tax_rate1_total > 0) { if($item_tax_rate1_total != 0)
$this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total); $this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total);
}
$item_tax_rate2_total = $this->calcInclusiveLineTax($this->item->tax_rate2, $amount); $item_tax_rate2_total = $this->calcInclusiveLineTax($this->item->tax_rate2, $amount);
$item_tax += $this->formatValue($item_tax_rate2_total, $this->currency->precision); $item_tax += $this->formatValue($item_tax_rate2_total, $this->currency->precision);
if ($item_tax_rate2_total > 0) { if($item_tax_rate2_total != 0)
$this->groupTax($this->item->tax_name2, $this->item->tax_rate2, $item_tax_rate2_total); $this->groupTax($this->item->tax_name2, $this->item->tax_rate2, $item_tax_rate2_total);
}
$item_tax_rate3_total = $this->calcInclusiveLineTax($this->item->tax_rate3, $amount); $item_tax_rate3_total = $this->calcInclusiveLineTax($this->item->tax_rate3, $amount);
$item_tax += $this->formatValue($item_tax_rate3_total, $this->currency->precision); $item_tax += $this->formatValue($item_tax_rate3_total, $this->currency->precision);
if ($item_tax_rate3_total > 0) { if($item_tax_rate3_total != 0)
$this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total); $this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total);
}
$this->setTotalTaxes($this->formatValue($item_tax, $this->currency->precision)); $this->setTotalTaxes($this->formatValue($item_tax, $this->currency->precision));
@ -225,12 +223,17 @@ class InvoiceItemSumInclusive
$item_tax = 0; $item_tax = 0;
foreach ($this->line_items as $this->item) { foreach ($this->line_items as $this->item) {
if($this->sub_total == 0)
$amount = $this->item->line_total;
else
$amount = $this->item->line_total - ($this->item->line_total * ($this->invoice->discount / $this->sub_total)); $amount = $this->item->line_total - ($this->item->line_total * ($this->invoice->discount / $this->sub_total));
$item_tax_rate1_total = $this->calcInclusiveLineTax($this->item->tax_rate1, $amount); $item_tax_rate1_total = $this->calcInclusiveLineTax($this->item->tax_rate1, $amount);
$item_tax += $item_tax_rate1_total; $item_tax += $item_tax_rate1_total;
if ($item_tax_rate1_total > 0) { if ($item_tax_rate1_total != 0) {
$this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total); $this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total);
} }
@ -238,7 +241,7 @@ class InvoiceItemSumInclusive
$item_tax += $item_tax_rate2_total; $item_tax += $item_tax_rate2_total;
if ($item_tax_rate2_total > 0) { if ($item_tax_rate2_total != 0) {
$this->groupTax($this->item->tax_name2, $this->item->tax_rate2, $item_tax_rate2_total); $this->groupTax($this->item->tax_name2, $this->item->tax_rate2, $item_tax_rate2_total);
} }
@ -246,7 +249,7 @@ class InvoiceItemSumInclusive
$item_tax += $item_tax_rate3_total; $item_tax += $item_tax_rate3_total;
if ($item_tax_rate3_total > 0) { if ($item_tax_rate3_total != 0) {
$this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total); $this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total);
} }
} }

View File

@ -57,8 +57,8 @@ class InvoiceSum
{ {
$this->calculateLineItems() $this->calculateLineItems()
->calculateDiscount() ->calculateDiscount()
->calculateCustomValues()
->calculateInvoiceTaxes() ->calculateInvoiceTaxes()
->calculateCustomValues()
->setTaxMap() ->setTaxMap()
->calculateTotals() ->calculateTotals()
->calculateBalance() ->calculateBalance()
@ -89,16 +89,17 @@ class InvoiceSum
private function calculateCustomValues() private function calculateCustomValues()
{ {
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge1, $this->invoice->custom_surcharge_taxes1);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge1, $this->invoice->custom_surcharge_tax1);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge1); $this->total_custom_values += $this->valuer($this->invoice->custom_surcharge1);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge2, $this->invoice->custom_surcharge_taxes2); $this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge2, $this->invoice->custom_surcharge_tax2);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge2); $this->total_custom_values += $this->valuer($this->invoice->custom_surcharge2);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge3, $this->invoice->custom_surcharge_taxes3); $this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge3, $this->invoice->custom_surcharge_tax3);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge3); $this->total_custom_values += $this->valuer($this->invoice->custom_surcharge3);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge4, $this->invoice->custom_surcharge_taxes4); $this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge4, $this->invoice->custom_surcharge_tax4);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge4); $this->total_custom_values += $this->valuer($this->invoice->custom_surcharge4);
$this->total += $this->total_custom_values; $this->total += $this->total_custom_values;
@ -108,19 +109,20 @@ class InvoiceSum
private function calculateInvoiceTaxes() private function calculateInvoiceTaxes()
{ {
if ($this->invoice->tax_rate1 > 0) {
if (strlen($this->invoice->tax_name1) > 1) {
$tax = $this->taxer($this->total, $this->invoice->tax_rate1); $tax = $this->taxer($this->total, $this->invoice->tax_rate1);
$this->total_taxes += $tax; $this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.floatval($this->invoice->tax_rate1).'%', 'total' => $tax]; $this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.floatval($this->invoice->tax_rate1).'%', 'total' => $tax];
} }
if ($this->invoice->tax_rate2 > 0) { if (strlen($this->invoice->tax_name2) > 1) {
$tax = $this->taxer($this->total, $this->invoice->tax_rate2); $tax = $this->taxer($this->total, $this->invoice->tax_rate2);
$this->total_taxes += $tax; $this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.floatval($this->invoice->tax_rate2).'%', 'total' => $tax]; $this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.floatval($this->invoice->tax_rate2).'%', 'total' => $tax];
} }
if ($this->invoice->tax_rate3 > 0) { if (strlen($this->invoice->tax_name3) > 1) {
$tax = $this->taxer($this->total, $this->invoice->tax_rate3); $tax = $this->taxer($this->total, $this->invoice->tax_rate3);
$this->total_taxes += $tax; $this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.floatval($this->invoice->tax_rate3).'%', 'total' => $tax]; $this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.floatval($this->invoice->tax_rate3).'%', 'total' => $tax];

View File

@ -89,16 +89,16 @@ class InvoiceSumInclusive
private function calculateCustomValues() private function calculateCustomValues()
{ {
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge1, $this->invoice->custom_surcharge_taxes1); $this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge1, $this->invoice->custom_surcharge_tax1);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge1); $this->total_custom_values += $this->valuer($this->invoice->custom_surcharge1);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge2, $this->invoice->custom_surcharge_taxes2); $this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge2, $this->invoice->custom_surcharge_tax2);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge2); $this->total_custom_values += $this->valuer($this->invoice->custom_surcharge2);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge3, $this->invoice->custom_surcharge_taxes3); $this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge3, $this->invoice->custom_surcharge_tax3);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge3); $this->total_custom_values += $this->valuer($this->invoice->custom_surcharge3);
$this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge4, $this->invoice->custom_surcharge_taxes4); $this->total_taxes += $this->valuerTax($this->invoice->custom_surcharge4, $this->invoice->custom_surcharge_tax4);
$this->total_custom_values += $this->valuer($this->invoice->custom_surcharge4); $this->total_custom_values += $this->valuer($this->invoice->custom_surcharge4);
$this->total += $this->total_custom_values; $this->total += $this->total_custom_values;

View File

@ -11,6 +11,8 @@
namespace App\Helpers\Mail; namespace App\Helpers\Mail;
use App\Utils\TempFile;
use Dacastro4\LaravelGmail\Facade\LaravelGmail;
use Dacastro4\LaravelGmail\Services\Message\Mail; use Dacastro4\LaravelGmail\Services\Message\Mail;
use Illuminate\Mail\Transport\Transport; use Illuminate\Mail\Transport\Transport;
use Swift_Mime_SimpleMessage; use Swift_Mime_SimpleMessage;
@ -27,49 +29,59 @@ class GmailTransport extends Transport
*/ */
protected $gmail; protected $gmail;
/**
* The GMail OAuth Token.
* @var string token
*/
protected $token;
/** /**
* Create a new Gmail transport instance. * Create a new Gmail transport instance.
* *
* @param Mail $gmail * @param Mail $gmail
* @param string $token * @param string $token
*/ */
public function __construct(Mail $gmail, string $token) public function __construct(Mail $gmail)
{ {
$this->gmail = $gmail; $this->gmail = $gmail;
$this->token = $token;
} }
public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null) public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
{ {
/* For some reason the Injected Mail class carries cached tokens, so we need to reinit the Mail class*/
$this->gmail = null;
$this->gmail = new Mail;
/*We should nest the token in the message and then discard it as needed*/ /*We should nest the token in the message and then discard it as needed*/
$token = $message->getHeaders()->get('GmailToken')->getValue();
$message->getHeaders()->remove('GmailToken');
$this->beforeSendPerformed($message); $this->beforeSendPerformed($message);
$this->gmail->using($this->token); $this->gmail->using($token);
$this->gmail->to($message->getTo()); $this->gmail->to($message->getTo());
$this->gmail->from($message->getFrom()); $this->gmail->from($message->getFrom());
$this->gmail->subject($message->getSubject()); $this->gmail->subject($message->getSubject());
$this->gmail->message($message->getBody()); $this->gmail->message($message->getBody());
//$this->gmail->message($message->toString());
$this->gmail->cc($message->getCc()); $this->gmail->cc($message->getCc());
$this->gmail->bcc($message->getBcc()); $this->gmail->bcc($message->getBcc());
nlog(print_r($message->getChildren(), 1)); foreach ($message->getChildren() as $child)
{
foreach ($message->getChildren() as $child) { nlog("trying to attach");
$this->gmail->attach($child);
} //todo this should 'just work' if($child->getContentType() != 'text/plain')
{
$this->gmail->attach(TempFile::filePath($child->getBody(), $child->getHeaders()->get('Content-Type')->getParameter('name') ));
}
}
$this->gmail->send(); $this->gmail->send();
$this->sendPerformed($message); $this->sendPerformed($message);
return $this->numberOfRecipients($message); return $this->numberOfRecipients($message);
} }
} }

View File

@ -1,17 +1,25 @@
<?php <?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Helpers\Mail; namespace App\Helpers\Mail;
use Illuminate\Mail\MailManager;
use App\CustomMailDriver\CustomTransport;
use Dacastro4\LaravelGmail\Services\Message\Mail; use Dacastro4\LaravelGmail\Services\Message\Mail;
use Illuminate\Mail\TransportManager; use Illuminate\Support\Facades\Config;
class GmailTransportManager extends TransportManager
{
protected function createGmailDriver()
{
$token = $this->app['config']->get('services.gmail.token', []);
$mail = new Mail;
return new GmailTransport($mail, $token); class GmailTransportManager extends MailManager
{
protected function createGmailTransport()
{
return new GmailTransport(new Mail);
} }
} }

View File

@ -12,8 +12,10 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Libraries\MultiDB;
use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\Factory;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails; use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Password;
use Illuminate\View\View; use Illuminate\View\View;
@ -65,4 +67,31 @@ class ContactForgotPasswordController extends Controller
{ {
return Password::broker('contacts'); return Password::broker('contacts');
} }
public function sendResetLinkEmail(Request $request)
{
//MultiDB::userFindAndSetDb($request->input('email'));
$user = MultiDB::hasContact(['email' => $request->input('email')]);
$this->validateEmail($request);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$response = $this->broker()->sendResetLink(
$this->credentials($request)
);
if ($request->ajax()) {
return $response == Password::RESET_LINK_SENT
? response()->json(['message' => 'Reset link sent to your email.', 'status' => true], 201)
: response()->json(['message' => 'Email not found', 'status' => false], 401);
}
return $response == Password::RESET_LINK_SENT
? $this->sendResetLinkResponse($request, $response)
: $this->sendResetLinkFailedResponse($request, $response);
}
} }

View File

@ -18,11 +18,13 @@ class ContactRegisterController extends Controller
$this->middleware(['guest', 'contact.register']); $this->middleware(['guest', 'contact.register']);
} }
public function showRegisterForm(string $company_key) public function showRegisterForm(string $company_key = '')
{ {
$company = Company::where('company_key', $company_key)->firstOrFail(); $key = request()->has('key') ? request('key') : $company_key;
return render('auth.register', compact(['company'])); $company = Company::where('company_key', $key)->firstOrFail();
return render('auth.register', ['company' => $company]);
} }
public function register(RegisterRequest $request) public function register(RegisterRequest $request)

View File

@ -12,6 +12,7 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Libraries\MultiDB;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails; use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Password;
@ -103,6 +104,10 @@ class ForgotPasswordController extends Controller
*/ */
public function sendResetLinkEmail(Request $request) public function sendResetLinkEmail(Request $request)
{ {
//MultiDB::userFindAndSetDb($request->input('email'));
$user = MultiDB::hasUser(['email' => $request->input('email')]);
$this->validateEmail($request); $this->validateEmail($request);
// We will send the password reset link to this user. Once we have attempted // We will send the password reset link to this user. Once we have attempted

View File

@ -289,7 +289,7 @@ class LoginController extends BaseController
$client = new Google_Client(); $client = new Google_Client();
$client->setClientId(config('ninja.auth.google.client_id')); $client->setClientId(config('ninja.auth.google.client_id'));
$client->setClientSecret(config('ninja.auth.google.client_secret')); $client->setClientSecret(config('ninja.auth.google.client_secret'));
$client->setRedirectUri(config('ninja.app_url'));
$token = $client->authenticate(request()->input('server_auth_code')); $token = $client->authenticate(request()->input('server_auth_code'));
$refresh_token = ''; $refresh_token = '';

View File

@ -184,7 +184,9 @@ class BaseController extends Controller
protected function refreshResponse($query) protected function refreshResponse($query)
{ {
if (auth()->user()->getCompany()->is_large) $user = auth()->user();
if ($user->getCompany()->is_large)
$this->manager->parseIncludes($this->mini_load); $this->manager->parseIncludes($this->mini_load);
else else
$this->manager->parseIncludes($this->first_load); $this->manager->parseIncludes($this->first_load);
@ -200,74 +202,145 @@ class BaseController extends Controller
$transformer = new $this->entity_transformer($this->serializer); $transformer = new $this->entity_transformer($this->serializer);
$updated_at = request()->has('updated_at') ? request()->input('updated_at') : 0; $updated_at = request()->has('updated_at') ? request()->input('updated_at') : 0;
// if (auth()->user()->getCompany()->is_large && ! request()->has('updated_at')) {
// return response()->json(['message' => ctrans('texts.large_account_update_parameter'), 'errors' =>[]], 401);
// }
$updated_at = date('Y-m-d H:i:s', $updated_at); $updated_at = date('Y-m-d H:i:s', $updated_at);
$query->with( $query->with(
[ [
'company' => function ($query) use ($updated_at) { 'company' => function ($query) use ($updated_at, $user) {
$query->whereNotNull('updated_at')->with('documents'); $query->whereNotNull('updated_at')->with('documents');
}, },
'company.clients' => function ($query) use ($updated_at) { 'company.clients' => function ($query) use ($updated_at, $user) {
$query->where('clients.updated_at', '>=', $updated_at)->with('contacts.company', 'gateway_tokens', 'documents'); $query->where('clients.updated_at', '>=', $updated_at)->with('contacts.company', 'gateway_tokens', 'documents');
if(!$user->hasPermission('view_client'))
$query->where('clients.user_id', $user->id)->orWhere('clients.assigned_user_id', $user->id);
}, },
'company.company_gateways' => function ($query) { 'company.company_gateways' => function ($query) use ($user) {
$query->whereNotNull('updated_at'); $query->whereNotNull('updated_at');
if(!$user->isAdmin())
$query->where('company_gateways.user_id', $user->id);
}, },
'company.credits'=> function ($query) use ($updated_at) { 'company.credits'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents'); $query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents');
if(!$user->hasPermission('view_credit'))
$query->where('credits.user_id', $user->id)->orWhere('credits.assigned_user_id', $user->id);
}, },
'company.designs'=> function ($query) use ($updated_at) { 'company.designs'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('company'); $query->where('updated_at', '>=', $updated_at)->with('company');
if(!$user->isAdmin())
$query->where('designs.user_id', $user->id);
}, },
'company.documents'=> function ($query) use ($updated_at) { 'company.documents'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at); $query->where('updated_at', '>=', $updated_at);
}, },
'company.expenses'=> function ($query) use ($updated_at) { 'company.expenses'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('documents'); $query->where('updated_at', '>=', $updated_at)->with('documents');
if(!$user->hasPermission('view_expense'))
$query->where('expenses.user_id', $user->id)->orWhere('expenses.assigned_user_id', $user->id);
}, },
'company.groups' => function ($query) use ($updated_at) { 'company.groups' => function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at); $query->where('updated_at', '>=', $updated_at);
if(!$user->isAdmin())
$query->where('group_settings.user_id', $user->id);
}, },
'company.invoices'=> function ($query) use ($updated_at) { 'company.invoices'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents'); $query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents');
if(!$user->hasPermission('view_invoice'))
$query->where('invoices.user_id', $user->id)->orWhere('invoices.assigned_user_id', $user->id);
}, },
'company.payments'=> function ($query) use ($updated_at) { 'company.payments'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('paymentables', 'documents'); $query->where('updated_at', '>=', $updated_at)->with('paymentables', 'documents');
if(!$user->hasPermission('view_payment'))
$query->where('payments.user_id', $user->id)->orWhere('payments.assigned_user_id', $user->id);
}, },
'company.payment_terms'=> function ($query) use ($updated_at) { 'company.payment_terms'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at); $query->where('updated_at', '>=', $updated_at);
if(!$user->isAdmin())
$query->where('payment_terms.user_id', $user->id);
}, },
'company.products' => function ($query) use ($updated_at) { 'company.products' => function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('documents'); $query->where('updated_at', '>=', $updated_at)->with('documents');
if(!$user->hasPermission('view_product'))
$query->where('products.user_id', $user->id)->orWhere('products.assigned_user_id', $user->id);
}, },
'company.projects'=> function ($query) use ($updated_at) { 'company.projects'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('documents'); $query->where('updated_at', '>=', $updated_at)->with('documents');
if(!$user->hasPermission('view_project'))
$query->where('projects.user_id', $user->id)->orWhere('projects.assigned_user_id', $user->id);
}, },
'company.quotes'=> function ($query) use ($updated_at) { 'company.quotes'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents'); $query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents');
if(!$user->hasPermission('view_quote'))
$query->where('quotes.user_id', $user->id)->orWhere('quotes.assigned_user_id', $user->id);
}, },
'company.recurring_invoices'=> function ($query) use ($updated_at) { 'company.recurring_invoices'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents'); $query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents');
if(!$user->hasPermission('view_recurring_invoice'))
$query->where('recurring_invoices.user_id', $user->id)->orWhere('recurring_invoices.assigned_user_id', $user->id);
}, },
'company.tasks'=> function ($query) use ($updated_at) { 'company.tasks'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('documents'); $query->where('updated_at', '>=', $updated_at)->with('documents');
if(!$user->hasPermission('view_task'))
$query->where('tasks.user_id', $user->id)->orWhere('tasks.assigned_user_id', $user->id);
}, },
'company.tax_rates' => function ($query) use ($updated_at) { 'company.tax_rates' => function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at); $query->where('updated_at', '>=', $updated_at);
if(!$user->isAdmin())
$query->where('tax_rates.user_id', $user->id);
}, },
'company.vendors'=> function ($query) use ($updated_at) { 'company.vendors'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('contacts', 'documents'); $query->where('updated_at', '>=', $updated_at)->with('contacts', 'documents');
if(!$user->hasPermission('view_vendor'))
$query->where('vendors.user_id', $user->id)->orWhere('vendors.assigned_user_id', $user->id);
}, },
'company.expense_categories'=> function ($query) use ($updated_at) { 'company.expense_categories'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at); $query->where('updated_at', '>=', $updated_at);
if(!$user->isAdmin())
$query->where('expense_categories.user_id', $user->id);
}, },
'company.task_statuses'=> function ($query) use ($updated_at) { 'company.task_statuses'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at); $query->where('updated_at', '>=', $updated_at);
if(!$user->isAdmin())
$query->where('task_statuses.user_id', $user->id);
}, },
'company.activities'=> function ($query) use($user) {
if(!$user->isAdmin())
$query->where('activities.user_id', $user->id);
}
] ]
); );

View File

@ -21,6 +21,7 @@ use App\Http\Requests\Client\EditClientRequest;
use App\Http\Requests\Client\ShowClientRequest; use App\Http\Requests\Client\ShowClientRequest;
use App\Http\Requests\Client\StoreClientRequest; use App\Http\Requests\Client\StoreClientRequest;
use App\Http\Requests\Client\UpdateClientRequest; use App\Http\Requests\Client\UpdateClientRequest;
use App\Http\Requests\Client\UploadClientRequest;
use App\Jobs\Client\StoreClient; use App\Jobs\Client\StoreClient;
use App\Jobs\Client\UpdateClient; use App\Jobs\Client\UpdateClient;
use App\Models\Client; use App\Models\Client;
@ -29,6 +30,7 @@ use App\Transformers\ClientTransformer;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\BulkOptions; use App\Utils\Traits\BulkOptions;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use App\Utils\Traits\Uploadable; use App\Utils\Traits\Uploadable;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
@ -42,6 +44,7 @@ class ClientController extends BaseController
use MakesHash; use MakesHash;
use Uploadable; use Uploadable;
use BulkOptions; use BulkOptions;
use SavesDocuments;
protected $entity_type = Client::class; protected $entity_type = Client::class;
@ -269,6 +272,7 @@ class ClientController extends BaseController
*/ */
public function update(UpdateClientRequest $request, Client $client) public function update(UpdateClientRequest $request, Client $client)
{ {
if ($request->entityIsDeleted($client)) { if ($request->entityIsDeleted($client)) {
return $request->disallowUpdate(); return $request->disallowUpdate();
} }
@ -515,4 +519,66 @@ class ClientController extends BaseController
{ {
//todo //todo
} }
/**
* Update the specified resource in storage.
*
* @param UploadClientRequest $request
* @param Client $client
* @return Response
*
*
*
* @OA\Put(
* path="/api/v1/clients/{id}/upload",
* operationId="uploadClient",
* tags={"clients"},
* summary="Uploads a document to a client",
* description="Handles the uploading of a document to a client",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Client Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the client object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Client"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function upload(UploadClientRequest $request, Client $client)
{
if ($request->has('documents'))
$this->saveDocuments($request->file('documents'), $client);
return $this->itemResponse($client->fresh());
}
} }

View File

@ -76,10 +76,10 @@ class DocumentController extends Controller
$options->setSendHttpHeaders(true); $options->setSendHttpHeaders(true);
$zip = new ZipStream('files.zip', $options); $zip = new ZipStream(now() . '-documents.zip', $options);
foreach ($documents as $document) { foreach ($documents as $document) {
$zip->addFileFromPath(basename($document->filePath()), TempFile::path($document->filePath())); $zip->addFileFromPath(basename($document->diskPath()), TempFile::path($document->diskPath()));
} }
$zip->finish(); $zip->finish();

View File

@ -33,6 +33,8 @@ class InvitationController extends Controller
public function router(string $entity, string $invitation_key) public function router(string $entity, string $invitation_key)
{ {
Auth::logout();
return $this->genericRouter($entity, $invitation_key); return $this->genericRouter($entity, $invitation_key);
} }
@ -43,6 +45,7 @@ class InvitationController extends Controller
private function genericRouter(string $entity, string $invitation_key) private function genericRouter(string $entity, string $invitation_key)
{ {
$key = $entity.'_id'; $key = $entity.'_id';
$entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation'; $entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
@ -51,17 +54,22 @@ class InvitationController extends Controller
->with('contact.client') ->with('contact.client')
->firstOrFail(); ->firstOrFail();
/* Return early if we have the correct client_hash embedded */ /* Return early if we have the correct client_hash embedded */
if (request()->has('client_hash') && request()->input('client_hash') == $invitation->contact->client->client_hash) { if (request()->has('client_hash') && request()->input('client_hash') == $invitation->contact->client->client_hash) {
auth()->guard('contact')->login($invitation->contact, true); auth()->guard('contact')->login($invitation->contact, true);
} elseif ((bool) $invitation->contact->client->getSetting('enable_client_portal_password') !== false) { } elseif ((bool) $invitation->contact->client->getSetting('enable_client_portal_password') !== false) {
//If no contact password is set - this will cause a 401 error - instead redirect to the client.login route
$this->middleware('auth:contact'); $this->middleware('auth:contact');
return redirect()->route('client.login');
} else { } else {
auth()->guard('contact')->login($invitation->contact, true); auth()->guard('contact')->login($invitation->contact, true);
} }
if (auth()->guard('contact') && ! request()->has('silent') && ! $invitation->viewed_date) { if (auth()->guard('contact') && ! request()->has('silent') && ! $invitation->viewed_date) {
$invitation->markViewed(); $invitation->markViewed();

View File

@ -54,11 +54,11 @@ class InvoiceController extends Controller
'invoice' => $invoice, 'invoice' => $invoice,
]; ];
if ($request->query('mode') === 'portal') { if ($request->query('mode') === 'fullscreen') {
return $this->render('invoices.show', $data); return response()->file($invoice->pdf_file_path(null, 'path'));
} }
return $this->render('invoices.show.fullscreen', $data); return $this->render('invoices.show', $data);
} }
/** /**
@ -90,7 +90,7 @@ class InvoiceController extends Controller
//filter invoices which are payable //filter invoices which are payable
$invoices = $invoices->filter(function ($invoice) { $invoices = $invoices->filter(function ($invoice) {
return $invoice->isPayable() && $invoice->balance > 0; return $invoice->isPayable();
}); });
//return early if no invoices. //return early if no invoices.

View File

@ -106,7 +106,7 @@ class PaymentController extends Controller
if ($payable_invoices->count() == 0) { if ($payable_invoices->count() == 0) {
return redirect() return redirect()
->route('client.invoices.index') ->route('client.invoices.index')
->with(['warning' => 'No payable invoices selected.']); ->with(['message' => 'No payable invoices selected.']);
} }
$settings = auth()->user()->client->getMergedSettings(); $settings = auth()->user()->client->getMergedSettings();
@ -137,33 +137,37 @@ class PaymentController extends Controller
$payable_invoice['amount'] = Number::roundValue(($invoice->partial > 0 ? $invoice->partial : $invoice->balance), auth()->user()->client->currency()->precision); $payable_invoice['amount'] = Number::roundValue(($invoice->partial > 0 ? $invoice->partial : $invoice->balance), auth()->user()->client->currency()->precision);
} }
/* If we DO allow under payments check the minimum amount is present else return */ if (!$settings->client_portal_allow_under_payment && $payable_amount < $invoice_balance) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $invoice_balance]));
}
if ($settings->client_portal_allow_under_payment) { if ($settings->client_portal_allow_under_payment) {
if ($payable_invoice['amount'] < $settings->client_portal_under_payment_minimum) { if ($invoice_balance < $settings->client_portal_under_payment_minimum && $payable_amount < $invoice_balance) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $invoice_balance]));
}
if ($invoice_balance < $settings->client_portal_under_payment_minimum) {
// Skip the under payment rule.
}
if ($invoice_balance >= $settings->client_portal_under_payment_minimum && $payable_amount < $settings->client_portal_under_payment_minimum) {
return redirect() return redirect()
->route('client.invoices.index') ->route('client.invoices.index')
->with('message', ctrans('texts.minimum_required_payment', ['amount' => $settings->client_portal_under_payment_minimum])); ->with('message', ctrans('texts.minimum_required_payment', ['amount' => $settings->client_portal_under_payment_minimum]));
} }
} else {
/*Double check!!*/
if ($payable_amount < $invoice_balance) {
return redirect()
->route('client.invoices.index')
->with('message', ctrans('texts.under_payments_disabled'));
}
} }
/* If we don't allow over payments and the amount exceeds the balance */ /* If we don't allow over payments and the amount exceeds the balance */
if (!$settings->client_portal_allow_over_payment) { if (!$settings->client_portal_allow_over_payment && $payable_amount > $invoice_balance) {
if ($payable_amount > $invoice_balance) {
return redirect() return redirect()
->route('client.invoices.index') ->route('client.invoices.index')
->with('message', ctrans('texts.over_payments_disabled')); ->with('message', ctrans('texts.over_payments_disabled'));
} }
}
} }

View File

@ -35,7 +35,7 @@ class QuoteController extends Controller
* *
* @param ShowQuoteRequest $request * @param ShowQuoteRequest $request
* @param Quote $quote * @param Quote $quote
* @return Factory|View * @return Factory|View|\Symfony\Component\HttpFoundation\BinaryFileResponse
*/ */
public function show(ShowQuoteRequest $request, Quote $quote) public function show(ShowQuoteRequest $request, Quote $quote)
{ {
@ -43,11 +43,11 @@ class QuoteController extends Controller
'quote' => $quote, 'quote' => $quote,
]; ];
if ($request->query('mode') === 'portal') { if ($request->query('mode') === 'fullscreen') {
return $this->render('quotes.show', $data); return response()->file($quote->pdf_file_path(null, 'path'));
} }
return $this->render('quotes.show.fullscreen', $data); return $this->render('quotes.show', $data);
} }
public function bulk(ProcessQuotesInBulkRequest $request) public function bulk(ProcessQuotesInBulkRequest $request)

View File

@ -13,10 +13,15 @@ namespace App\Http\Controllers\ClientPortal;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\ShowRecurringInvoiceRequest; use App\Http\Requests\ClientPortal\ShowRecurringInvoiceRequest;
use App\Jobs\Mail\NinjaMailer;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Mail\RecurringInvoice\ClientContactRequestCancellationObject;
use App\Models\RecurringInvoice; use App\Models\RecurringInvoice;
use App\Notifications\ClientContactRequestCancellation; use App\Notifications\ClientContactRequestCancellation;
use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\Notifications\UserNotifies;
use Illuminate\Contracts\View\Factory; use Illuminate\Contracts\View\Factory;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\View\View; use Illuminate\View\View;
@ -28,6 +33,7 @@ class RecurringInvoiceController extends Controller
{ {
use MakesHash; use MakesHash;
use MakesDates; use MakesDates;
use UserNotifies;
/** /**
* Show the list of recurring invoices. * Show the list of recurring invoices.
@ -57,7 +63,22 @@ class RecurringInvoiceController extends Controller
{ {
//todo double check the user is able to request a cancellation //todo double check the user is able to request a cancellation
//can add locale specific by chaining ->locale(); //can add locale specific by chaining ->locale();
$recurring_invoice->user->notify(new ClientContactRequestCancellation($recurring_invoice, auth()->user()));
$nmo = new NinjaMailerObject;
$nmo->mailable = (new NinjaMailer((new ClientContactRequestCancellationObject($recurring_invoice, auth()->user()))->build()));
$nmo->company = $recurring_invoice->company;
$nmo->settings = $recurring_invoice->company->settings;
$notifiable_users = $this->filterUsersByPermissions($recurring_invoice->company->company_users, $recurring_invoice, ['recurring_cancellation']);
$notifiable_users->each(function ($company_user) use($nmo){
$nmo->to_user = $company_user->user;
NinjaMailerJob::dispatch($nmo);
});
//$recurring_invoice->user->notify(new ClientContactRequestCancellation($recurring_invoice, auth()->user()));
return $this->render('recurring_invoices.cancellation.index', [ return $this->render('recurring_invoices.cancellation.index', [
'invoice' => $recurring_invoice, 'invoice' => $recurring_invoice,

View File

@ -20,6 +20,7 @@ use App\Http\Requests\Company\EditCompanyRequest;
use App\Http\Requests\Company\ShowCompanyRequest; use App\Http\Requests\Company\ShowCompanyRequest;
use App\Http\Requests\Company\StoreCompanyRequest; use App\Http\Requests\Company\StoreCompanyRequest;
use App\Http\Requests\Company\UpdateCompanyRequest; use App\Http\Requests\Company\UpdateCompanyRequest;
use App\Http\Requests\Company\UploadCompanyRequest;
use App\Jobs\Company\CreateCompany; use App\Jobs\Company\CreateCompany;
use App\Jobs\Company\CreateCompanyPaymentTerms; use App\Jobs\Company\CreateCompanyPaymentTerms;
use App\Jobs\Company\CreateCompanyTaskStatuses; use App\Jobs\Company\CreateCompanyTaskStatuses;
@ -503,4 +504,65 @@ class CompanyController extends BaseController
return response()->json(['message' => ctrans('texts.success')], 200); return response()->json(['message' => ctrans('texts.success')], 200);
} }
/**
* Update the specified resource in storage.
*
* @param UploadCompanyRequest $request
* @param Company $client
* @return Response
*
*
*
* @OA\Put(
* path="/api/v1/companies/{id}/upload",
* operationId="uploadCompanies",
* tags={"companies"},
* summary="Uploads a document to a company",
* description="Handles the uploading of a document to a company",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Company Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the client object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Company"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function upload(UploadCompanyRequest $request, Company $company)
{
if ($request->has('documents'))
$this->saveDocuments($request->file('documents'), $company);
return $this->itemResponse($company->fresh());
}
} }

View File

@ -0,0 +1,139 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers;
use App\Libraries\MultiDB;
use App\Libraries\OAuth\Providers\Google;
use Illuminate\Http\Request;
class ConnectedAccountController extends BaseController
{
public function __construct()
{
parent::__construct();
}
/**
* Connect an OAuth account to a regular email/password combination account
*
* @param Request $request
* @return User Refresh Feed.
*
*
* @OA\Post(
* path="/api/v1/connected_account",
* operationId="connected_account",
* tags={"connected_account"},
* summary="Connect an oauth user to an existing user",
* description="Refreshes the dataset",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(ref="#/components/parameters/include_static"),
* @OA\Parameter(ref="#/components/parameters/clear_cache"),
* @OA\Response(
* response=200,
* description="The Company User response",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/User"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function index(Request $request)
{
if ($request->input('provider') == 'google') {
return $this->handleGoogleOauth();
}
return response()
->json(['message' => 'Provider not supported'], 400)
->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version'));
}
private function handleGoogleOauth()
{
$user = false;
$google = new Google();
$user = $google->getTokenResponse(request()->input('id_token'));
if (is_array($user)) {
$query = [
'oauth_user_id' => $google->harvestSubField($user),
'oauth_provider_id'=> 'google',
];
/* Cannot allow duplicates! */
if ($existing_user = MultiDB::hasUser($query)) {
return response()
->json(['message' => 'User already exists in system.'], 401)
->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version'));
}
}
if ($user) {
$client = new Google_Client();
$client->setClientId(config('ninja.auth.google.client_id'));
$client->setClientSecret(config('ninja.auth.google.client_secret'));
$client->setRedirectUri(config('ninja.app_url'));
$token = $client->authenticate(request()->input('server_auth_code'));
$refresh_token = '';
if (array_key_exists('refresh_token', $token)) {
$refresh_token = $token['refresh_token'];
}
$connected_account = [
'password' => '',
'email' => $google->harvestEmail($user),
'oauth_user_id' => $google->harvestSubField($user),
'oauth_user_token' => $token,
'oauth_user_refresh_token' => $refresh_token,
'oauth_provider_id' => 'google',
'email_verified_at' =>now()
];
auth()->user()->update($connected_account);
auth()->user()->email_verified_at = now();
auth()->user()->save();
//$ct = CompanyUser::whereUserId(auth()->user()->id);
return $this->listResponse(auth()->user());
}
return response()
->json(['message' => ctrans('texts.invalid_credentials')], 401)
->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version'));
}
}

View File

@ -1,4 +1,13 @@
<?php <?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers; namespace App\Http\Controllers;
@ -14,6 +23,7 @@ use App\Http\Requests\Credit\EditCreditRequest;
use App\Http\Requests\Credit\ShowCreditRequest; use App\Http\Requests\Credit\ShowCreditRequest;
use App\Http\Requests\Credit\StoreCreditRequest; use App\Http\Requests\Credit\StoreCreditRequest;
use App\Http\Requests\Credit\UpdateCreditRequest; use App\Http\Requests\Credit\UpdateCreditRequest;
use App\Http\Requests\Credit\UploadCreditRequest;
use App\Jobs\Entity\EmailEntity; use App\Jobs\Entity\EmailEntity;
use App\Jobs\Invoice\EmailCredit; use App\Jobs\Invoice\EmailCredit;
use App\Models\Client; use App\Models\Client;
@ -24,6 +34,7 @@ use App\Transformers\CreditTransformer;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\TempFile; use App\Utils\TempFile;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Response; use Illuminate\Http\Response;
/** /**
@ -32,6 +43,7 @@ use Illuminate\Http\Response;
class CreditController extends BaseController class CreditController extends BaseController
{ {
use MakesHash; use MakesHash;
use SavesDocuments;
protected $entity_type = Credit::class; protected $entity_type = Credit::class;
@ -56,7 +68,7 @@ class CreditController extends BaseController
* @OA\Get( * @OA\Get(
* path="/api/v1/credits", * path="/api/v1/credits",
* operationId="getCredits", * operationId="getCredits",
* tags={"invoices"}, * tags={"credits"},
* summary="Gets a list of credits", * summary="Gets a list of credits",
* description="Lists credits, search and filters allow fine grained lists to be generated. * description="Lists credits, search and filters allow fine grained lists to be generated.
* *
@ -576,4 +588,66 @@ class CreditController extends BaseController
return response()->download($file_path); return response()->download($file_path);
} }
/**
* Update the specified resource in storage.
*
* @param UploadCreditRequest $request
* @param Credit $client
* @return Response
*
*
*
* @OA\Put(
* path="/api/v1/credits/{id}/upload",
* operationId="uploadCredits",
* tags={"credits"},
* summary="Uploads a document to a credit",
* description="Handles the uploading of a document to a credit",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Credit Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the Credit object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Credit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function upload(UploadCreditRequest $request, Credit $credit)
{
if ($request->has('documents'))
$this->saveDocuments($request->file('documents'), $credit);
return $this->itemResponse($credit->fresh());
}
} }

View File

@ -21,12 +21,14 @@ use App\Http\Requests\Expense\EditExpenseRequest;
use App\Http\Requests\Expense\ShowExpenseRequest; use App\Http\Requests\Expense\ShowExpenseRequest;
use App\Http\Requests\Expense\StoreExpenseRequest; use App\Http\Requests\Expense\StoreExpenseRequest;
use App\Http\Requests\Expense\UpdateExpenseRequest; use App\Http\Requests\Expense\UpdateExpenseRequest;
use App\Http\Requests\Expense\UploadExpenseRequest;
use App\Models\Expense; use App\Models\Expense;
use App\Repositories\ExpenseRepository; use App\Repositories\ExpenseRepository;
use App\Transformers\ExpenseTransformer; use App\Transformers\ExpenseTransformer;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\BulkOptions; use App\Utils\Traits\BulkOptions;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use App\Utils\Traits\Uploadable; use App\Utils\Traits\Uploadable;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
@ -40,6 +42,7 @@ class ExpenseController extends BaseController
use MakesHash; use MakesHash;
use Uploadable; use Uploadable;
use BulkOptions; use BulkOptions;
use SavesDocuments;
protected $entity_type = Expense::class; protected $entity_type = Expense::class;
@ -507,4 +510,65 @@ class ExpenseController extends BaseController
{ {
//todo //todo
} }
/**
* Update the specified resource in storage.
*
* @param UploadExpenseRequest $request
* @param Expense $expense
* @return Response
*
*
*
* @OA\Put(
* path="/api/v1/expenses/{id}/upload",
* operationId="uploadExpense",
* tags={"expense"},
* summary="Uploads a document to a expense",
* description="Handles the uploading of a document to a expense",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Expense Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the Expense object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Expense"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function upload(UploadExpenseRequest $request, Expense $expense)
{
if ($request->has('documents'))
$this->saveDocuments($request->file('documents'), $expense);
return $this->itemResponse($expense->fresh());
}
} }

View File

@ -14,19 +14,20 @@ namespace App\Http\Controllers;
use App\Http\Requests\Import\ImportRequest; use App\Http\Requests\Import\ImportRequest;
use App\Http\Requests\Import\PreImportRequest; use App\Http\Requests\Import\PreImportRequest;
use App\Jobs\Import\CSVImport; use App\Jobs\Import\CSVImport;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use League\Csv\Reader; use League\Csv\Reader;
use League\Csv\Statement; use League\Csv\Statement;
class ImportController extends Controller class ImportController extends Controller {
{
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
* @param StoreImportRequest $request * @param PreImportRequest $request
* @return Response *
* @return \Illuminate\Http\JsonResponse
* *
* @OA\Post( * @OA\Post(
* path="/api/v1/preimport", * path="/api/v1/preimport",
@ -69,42 +70,61 @@ class ImportController extends Controller
* ), * ),
* ) * )
*/ */
public function preimport(PreImportRequest $request) public function preimport( PreImportRequest $request ) {
{ // Create a reference
//create a reference
$hash = Str::random( 32 ); $hash = Str::random( 32 );
//store the csv in cache with an expiry of 10 minutes
Cache::put($hash, base64_encode(file_get_contents($request->file('file')->getPathname())), 3600);
//parse CSV
$csv_array = $this->getCsvData(file_get_contents($request->file('file')->getPathname()));
$class_map = $this->getEntityMap($request->input('entity_type'));
$data = [ $data = [
'hash' => $hash, 'hash' => $hash,
'available' => $class_map::importable(), 'mappings' => [],
'headers' => array_slice($csv_array, 0, 2)
]; ];
/** @var UploadedFile $file */
foreach ( $request->files->get( 'files' ) as $entityType => $file ) {
$contents = file_get_contents( $file->getPathname() );
// Store the csv in cache with an expiry of 10 minutes
Cache::put( $hash . '-' . $entityType, base64_encode( $contents ), 3600 );
// Parse CSV
$csv_array = $this->getCsvData( $contents );
$class_map = $this->getEntityMap( $entityType );
$data['mappings'][ $entityType ] = [
'available' => $class_map::importable(),
'headers' => array_slice( $csv_array, 0, 2 ),
];
}
return response()->json( $data ); return response()->json( $data );
} }
public function import(ImportRequest $request) public function import( ImportRequest $request ) {
{ $data = $request->all();
CSVImport::dispatch($request->all(), auth()->user()->company());
if ( empty( $data['hash'] ) ) {
// Create a reference
$data['hash'] = $hash = Str::random( 32 );
/** @var UploadedFile $file */
foreach ( $request->files->get( 'files' ) as $entityType => $file ) {
$contents = file_get_contents( $file->getPathname() );
// Store the csv in cache with an expiry of 10 minutes
Cache::put( $hash . '-' . $entityType, base64_encode( $contents ), 3600 );
}
}
CSVImport::dispatch( $data, auth()->user()->company() );
return response()->json( [ 'message' => ctrans( 'texts.import_started' ) ], 200 ); return response()->json( [ 'message' => ctrans( 'texts.import_started' ) ], 200 );
} }
private function getEntityMap($entity_type) private function getEntityMap( $entity_type ) {
{
return sprintf( 'App\\Import\\Definitions\%sMap', ucfirst( $entity_type ) ); return sprintf( 'App\\Import\\Definitions\%sMap', ucfirst( $entity_type ) );
} }
private function getCsvData($csvfile) private function getCsvData( $csvfile ) {
{
if ( ! ini_get( 'auto_detect_line_endings' ) ) { if ( ! ini_get( 'auto_detect_line_endings' ) ) {
ini_set( 'auto_detect_line_endings', '1' ); ini_set( 'auto_detect_line_endings', '1' );
} }
@ -123,7 +143,7 @@ class ImportController extends Controller
if (strstr($firstCell, (string)config('ninja.app_name'))) { if (strstr($firstCell, (string)config('ninja.app_name'))) {
array_shift( $data ); // Invoice Ninja... array_shift( $data ); // Invoice Ninja...
array_shift( $data ); // <blank line> array_shift( $data ); // <blank line>
array_shift($data); // Enitty Type Header array_shift( $data ); // Entity Type Header
} }
} }
} }

View File

@ -25,6 +25,7 @@ use App\Http\Requests\Invoice\EditInvoiceRequest;
use App\Http\Requests\Invoice\ShowInvoiceRequest; use App\Http\Requests\Invoice\ShowInvoiceRequest;
use App\Http\Requests\Invoice\StoreInvoiceRequest; use App\Http\Requests\Invoice\StoreInvoiceRequest;
use App\Http\Requests\Invoice\UpdateInvoiceRequest; use App\Http\Requests\Invoice\UpdateInvoiceRequest;
use App\Http\Requests\Invoice\UploadInvoiceRequest;
use App\Jobs\Entity\EmailEntity; use App\Jobs\Entity\EmailEntity;
use App\Jobs\Invoice\StoreInvoice; use App\Jobs\Invoice\StoreInvoice;
use App\Jobs\Invoice\ZipInvoices; use App\Jobs\Invoice\ZipInvoices;
@ -38,6 +39,7 @@ use App\Transformers\QuoteTransformer;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\TempFile; use App\Utils\TempFile;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
@ -49,6 +51,7 @@ use Illuminate\Support\Facades\Storage;
class InvoiceController extends BaseController class InvoiceController extends BaseController
{ {
use MakesHash; use MakesHash;
use SavesDocuments;
protected $entity_type = Invoice::class; protected $entity_type = Invoice::class;
@ -204,6 +207,7 @@ class InvoiceController extends BaseController
*/ */
public function store(StoreInvoiceRequest $request) public function store(StoreInvoiceRequest $request)
{ {
$client = Client::find($request->input('client_id')); $client = Client::find($request->input('client_id'));
$invoice = $this->invoice_repo->save($request->all(), InvoiceFactory::create(auth()->user()->company()->id, auth()->user()->id)); $invoice = $this->invoice_repo->save($request->all(), InvoiceFactory::create(auth()->user()->company()->id, auth()->user()->id));
@ -392,8 +396,6 @@ class InvoiceController extends BaseController
$invoice = $this->invoice_repo->save($request->all(), $invoice); $invoice = $this->invoice_repo->save($request->all(), $invoice);
UnlinkFile::dispatchNow(config('filesystems.default'), $invoice->client->invoice_filepath().$invoice->number.'.pdf');
event(new InvoiceWasUpdated($invoice, $invoice->company, Ninja::eventVars())); event(new InvoiceWasUpdated($invoice, $invoice->company, Ninja::eventVars()));
return $this->itemResponse($invoice); return $this->itemResponse($invoice);
@ -530,7 +532,7 @@ class InvoiceController extends BaseController
} }
}); });
ZipInvoices::dispatch($invoices, $invoices->first()->company, auth()->user()->email); ZipInvoices::dispatch($invoices, $invoices->first()->company, auth()->user());
return response()->json(['message' => ctrans('texts.sent_message')], 200); return response()->json(['message' => ctrans('texts.sent_message')], 200);
} }
@ -850,4 +852,65 @@ class InvoiceController extends BaseController
return response(['message' => 'Oops, something went wrong. Make sure you have symlink to storage/ in public/ directory.'], 500); return response(['message' => 'Oops, something went wrong. Make sure you have symlink to storage/ in public/ directory.'], 500);
} }
} }
/**
* Update the specified resource in storage.
*
* @param UploadInvoiceRequest $request
* @param Invoice $invoice
* @return Response
*
*
*
* @OA\Put(
* path="/api/v1/invoices/{id}/upload",
* operationId="uploadInvoice",
* tags={"invoices"},
* summary="Uploads a document to a invoice",
* description="Handles the uploading of a document to a invoice",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Invoice Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the Invoice object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Invoice"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function upload(UploadInvoiceRequest $request, Invoice $invoice)
{
if ($request->has('documents'))
$this->saveDocuments($request->file('documents'), $invoice);
return $this->itemResponse($invoice->fresh());
}
} }

View File

@ -14,7 +14,8 @@ namespace App\Http\Controllers;
use App\Console\Commands\ImportMigrations; use App\Console\Commands\ImportMigrations;
use App\DataMapper\CompanySettings; use App\DataMapper\CompanySettings;
use App\Jobs\Mail\MailRouter; use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\Util\StartMigration; use App\Jobs\Util\StartMigration;
use App\Mail\ExistingMigration; use App\Mail\ExistingMigration;
use App\Models\Company; use App\Models\Company;
@ -218,6 +219,8 @@ class MigrationController extends BaseController
*/ */
public function startMigration(Request $request) public function startMigration(Request $request)
{ {
nlog("Starting Migration");
$companies = json_decode($request->companies); $companies = json_decode($request->companies);
if (app()->environment() === 'local') { if (app()->environment() === 'local') {
@ -246,7 +249,13 @@ class MigrationController extends BaseController
if ($checks['existing_company'] == true && $checks['force'] == false) { if ($checks['existing_company'] == true && $checks['force'] == false) {
nlog('Migrating: Existing company without force. (CASE_01)'); nlog('Migrating: Existing company without force. (CASE_01)');
MailRouter::dispatch(new ExistingMigration(), $existing_company, $user); $nmo = new NinjaMailerObject;
$nmo->mailable = new ExistingMigration();
$nmo->company = $existing_company;
$nmo->settings = $existing_company->settings;
$nmo->to_user = $user;
NinjaMailerJob::dispatch($nmo);
return response()->json([ return response()->json([
'_id' => Str::uuid(), '_id' => Str::uuid(),
@ -290,6 +299,9 @@ class MigrationController extends BaseController
// If there's no existing company migrate just normally. // If there's no existing company migrate just normally.
if ($checks['existing_company'] == false) { if ($checks['existing_company'] == false) {
nlog("creating fresh company");
$account = auth()->user()->account; $account = auth()->user()->account;
$fresh_company = (new ImportMigrations())->getCompany($account); $fresh_company = (new ImportMigrations())->getCompany($account);
@ -325,11 +337,13 @@ class MigrationController extends BaseController
); );
if (app()->environment() == 'testing') { if (app()->environment() == 'testing') {
nlog("environment is testing = bailing out now");
return; return;
} }
try { try {
// StartMigration::dispatch(base_path("storage/app/public/$migration_file"), $user, $fresh_company)->delay(now()->addSeconds(5)); // StartMigration::dispatch(base_path("storage/app/public/$migration_file"), $user, $fresh_company)->delay(now()->addSeconds(5));
nlog("starting migration job");
nlog($migration_file); nlog($migration_file);
StartMigration::dispatch($migration_file, $user, $fresh_company); StartMigration::dispatch($migration_file, $user, $fresh_company);
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@ -49,6 +49,9 @@
* @OA\Property(property="custom_surcharge2", type="number", format="float", example="10.00", description="Second Custom Surcharge"), * @OA\Property(property="custom_surcharge2", type="number", format="float", example="10.00", description="Second Custom Surcharge"),
* @OA\Property(property="custom_surcharge3", type="number", format="float", example="10.00", description="Third Custom Surcharge"), * @OA\Property(property="custom_surcharge3", type="number", format="float", example="10.00", description="Third Custom Surcharge"),
* @OA\Property(property="custom_surcharge4", type="number", format="float", example="10.00", description="Fourth Custom Surcharge"), * @OA\Property(property="custom_surcharge4", type="number", format="float", example="10.00", description="Fourth Custom Surcharge"),
* @OA\Property(property="custom_surcharge_taxes", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"), * @OA\Property(property="custom_surcharge_tax1", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax2", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax3", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax4", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* ) * )
*/ */

View File

@ -0,0 +1,23 @@
<?php
/**
* @OA\Schema(
* schema="Document",
* type="object",
* @OA\Property(property="id", type="string", example="AS3df3A", description="The design hashed id"),
* @OA\Property(property="user_id", type="string", example="", description="__________"),
* @OA\Property(property="assigned_user_id", type="string", example="", description="__________"),
* @OA\Property(property="project_id", type="string", example="", description="__________"),
* @OA\Property(property="vendor_id", type="string", example="", description="__________"),
* @OA\Property(property="name", type="string", example="Beauty", description="The design name"),
* @OA\Property(property="url", type="string", example="Beauty", description="The design name"),
* @OA\Property(property="preview", type="string", example="Beauty", description="The design name"),
* @OA\Property(property="type", type="string", example="Beauty", description="The design name"),
* @OA\Property(property="disk", type="string", example="Beauty", description="The design name"),
* @OA\Property(property="hash", type="string", example="Beauty", description="The design name"),
* @OA\Property(property="is_deleted", type="boolean", example=true, description="Flag to determine if the design is deleted"),
* @OA\Property(property="is_default", type="boolean", example=true, description="Flag to determine if the document is a default doc"),
* @OA\Property(property="created_at", type="number", format="integer", example="134341234234", description="Timestamp"),
* @OA\Property(property="updated_at", type="number", format="integer", example="134341234234", description="Timestamp"),
* @OA\Property(property="deleted_at", type="number", format="integer", example="134341234234", description="Timestamp"),
* )
*/

View File

@ -48,6 +48,9 @@
* @OA\Property(property="custom_surcharge2", type="number", format="float", example="10.00", description="Second Custom Surcharge"), * @OA\Property(property="custom_surcharge2", type="number", format="float", example="10.00", description="Second Custom Surcharge"),
* @OA\Property(property="custom_surcharge3", type="number", format="float", example="10.00", description="Third Custom Surcharge"), * @OA\Property(property="custom_surcharge3", type="number", format="float", example="10.00", description="Third Custom Surcharge"),
* @OA\Property(property="custom_surcharge4", type="number", format="float", example="10.00", description="Fourth Custom Surcharge"), * @OA\Property(property="custom_surcharge4", type="number", format="float", example="10.00", description="Fourth Custom Surcharge"),
* @OA\Property(property="custom_surcharge_taxes", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"), * @OA\Property(property="custom_surcharge_tax1", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax2", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax3", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax4", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* ) * )
*/ */

View File

@ -48,6 +48,9 @@
* @OA\Property(property="custom_surcharge2", type="number", format="float", example="10.00", description="Second Custom Surcharge"), * @OA\Property(property="custom_surcharge2", type="number", format="float", example="10.00", description="Second Custom Surcharge"),
* @OA\Property(property="custom_surcharge3", type="number", format="float", example="10.00", description="Third Custom Surcharge"), * @OA\Property(property="custom_surcharge3", type="number", format="float", example="10.00", description="Third Custom Surcharge"),
* @OA\Property(property="custom_surcharge4", type="number", format="float", example="10.00", description="Fourth Custom Surcharge"), * @OA\Property(property="custom_surcharge4", type="number", format="float", example="10.00", description="Fourth Custom Surcharge"),
* @OA\Property(property="custom_surcharge_taxes", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"), * @OA\Property(property="custom_surcharge_tax1", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax2", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax3", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* @OA\Property(property="custom_surcharge_tax4", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* ) * )
*/ */

View File

@ -22,12 +22,14 @@ use App\Http\Requests\Payment\RefundPaymentRequest;
use App\Http\Requests\Payment\ShowPaymentRequest; use App\Http\Requests\Payment\ShowPaymentRequest;
use App\Http\Requests\Payment\StorePaymentRequest; use App\Http\Requests\Payment\StorePaymentRequest;
use App\Http\Requests\Payment\UpdatePaymentRequest; use App\Http\Requests\Payment\UpdatePaymentRequest;
use App\Http\Requests\Payment\UploadPaymentRequest;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Repositories\PaymentRepository; use App\Repositories\PaymentRepository;
use App\Transformers\PaymentTransformer; use App\Transformers\PaymentTransformer;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
@ -37,6 +39,7 @@ use Illuminate\Http\Response;
class PaymentController extends BaseController class PaymentController extends BaseController
{ {
use MakesHash; use MakesHash;
use SavesDocuments;
protected $entity_type = Payment::class; protected $entity_type = Payment::class;
@ -671,4 +674,65 @@ class PaymentController extends BaseController
return $this->itemResponse($payment); return $this->itemResponse($payment);
} }
/**
* Update the specified resource in storage.
*
* @param UploadPaymentRequest $request
* @param Payment $payment
* @return Response
*
*
*
* @OA\Put(
* path="/api/v1/payments/{id}/upload",
* operationId="uploadPayment",
* tags={"payments"},
* summary="Uploads a document to a payment",
* description="Handles the uploading of a document to a payment",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Payment Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the Payment object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Payment"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function upload(UploadPaymentRequest $request, Payment $payment)
{
if ($request->has('documents'))
$this->saveDocuments($request->file('documents'), $payment);
return $this->itemResponse($payment->fresh());
}
} }

View File

@ -0,0 +1,156 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers;
use Illuminate\Http\Request;
/**
* Class PostMarkController.
*/
class PostMarkController extends BaseController
{
public function __construct()
{
}
/**
* Process Postmark Webhook.
*
*
* @OA\Post(
* path="/api/v1/postmark_webhook",
* operationId="postmarkWebhook",
* tags={"postmark"},
* summary="Processing webhooks from PostMark",
* description="Adds an credit to the system",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Response(
* response=200,
* description="Returns the saved credit object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Credit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function webhook(Request $request)
{
if($request->header('X-API-SECURITY') && $request->header('X-API-SECURITY') == config('postmark.secret'))
{
}
}
// {
// "RecordType": "Delivery",
// "ServerID": 23,
// "MessageStream": "outbound",
// "MessageID": "00000000-0000-0000-0000-000000000000",
// "Recipient": "john@example.com",
// "Tag": "welcome-email",
// "DeliveredAt": "2021-02-21T16:34:52Z",
// "Details": "Test delivery webhook details",
// "Metadata": {
// "example": "value",
// "example_2": "value"
// }
// }
private function processDelivery($request)
{
}
// {
// "Metadata": {
// "example": "value",
// "example_2": "value"
// },
// "RecordType": "Bounce",
// "ID": 42,
// "Type": "HardBounce",
// "TypeCode": 1,
// "Name": "Hard bounce",
// "Tag": "Test",
// "MessageID": "00000000-0000-0000-0000-000000000000",
// "ServerID": 1234,
// "MessageStream": "outbound",
// "Description": "The server was unable to deliver your message (ex: unknown user, mailbox not found).",
// "Details": "Test bounce details",
// "Email": "john@example.com",
// "From": "sender@example.com",
// "BouncedAt": "2021-02-21T16:34:52Z",
// "DumpAvailable": true,
// "Inactive": true,
// "CanActivate": true,
// "Subject": "Test subject",
// "Content": "Test content"
// }
private function processBounce($request)
{
}
// {
// "Metadata": {
// "example": "value",
// "example_2": "value"
// },
// "RecordType": "SpamComplaint",
// "ID": 42,
// "Type": "SpamComplaint",
// "TypeCode": 100001,
// "Name": "Spam complaint",
// "Tag": "Test",
// "MessageID": "00000000-0000-0000-0000-000000000000",
// "ServerID": 1234,
// "MessageStream": "outbound",
// "Description": "The subscriber explicitly marked this message as spam.",
// "Details": "Test spam complaint details",
// "Email": "john@example.com",
// "From": "sender@example.com",
// "BouncedAt": "2021-02-21T16:34:52Z",
// "DumpAvailable": true,
// "Inactive": true,
// "CanActivate": false,
// "Subject": "Test subject",
// "Content": "Test content"
// }
private function processSpamComplaint($request)
{
}
}

View File

@ -19,16 +19,19 @@ use App\Http\Requests\Product\EditProductRequest;
use App\Http\Requests\Product\ShowProductRequest; use App\Http\Requests\Product\ShowProductRequest;
use App\Http\Requests\Product\StoreProductRequest; use App\Http\Requests\Product\StoreProductRequest;
use App\Http\Requests\Product\UpdateProductRequest; use App\Http\Requests\Product\UpdateProductRequest;
use App\Http\Requests\Product\UploadProductRequest;
use App\Models\Product; use App\Models\Product;
use App\Repositories\ProductRepository; use App\Repositories\ProductRepository;
use App\Transformers\ProductTransformer; use App\Transformers\ProductTransformer;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
class ProductController extends BaseController class ProductController extends BaseController
{ {
use MakesHash; use MakesHash;
use SavesDocuments;
protected $entity_type = Product::class; protected $entity_type = Product::class;
@ -476,4 +479,65 @@ class ProductController extends BaseController
return $this->listResponse(Product::withTrashed()->whereIn('id', $this->transformKeys($ids))); return $this->listResponse(Product::withTrashed()->whereIn('id', $this->transformKeys($ids)));
} }
/**
* Update the specified resource in storage.
*
* @param UploadProductRequest $request
* @param Product $product
* @return Response
*
*
*
* @OA\Put(
* path="/api/v1/products/{id}/upload",
* operationId="uploadProduct",
* tags={"products"},
* summary="Uploads a document to a product",
* description="Handles the uploading of a document to a product",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Product Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the Product object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Product"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function upload(UploadProductRequest $request, Product $product)
{
if ($request->has('documents'))
$this->saveDocuments($request->file('documents'), $product);
return $this->itemResponse($product->fresh());
}
} }

View File

@ -19,6 +19,7 @@ use App\Http\Requests\Project\EditProjectRequest;
use App\Http\Requests\Project\ShowProjectRequest; use App\Http\Requests\Project\ShowProjectRequest;
use App\Http\Requests\Project\StoreProjectRequest; use App\Http\Requests\Project\StoreProjectRequest;
use App\Http\Requests\Project\UpdateProjectRequest; use App\Http\Requests\Project\UpdateProjectRequest;
use App\Http\Requests\Project\UploadProjectRequest;
use App\Models\Project; use App\Models\Project;
use App\Repositories\ProjectRepository; use App\Repositories\ProjectRepository;
use App\Transformers\ProjectTransformer; use App\Transformers\ProjectTransformer;
@ -266,6 +267,10 @@ class ProjectController extends BaseController
$project->number = empty($project->number) ? $this->getNextProjectNumber($project) : $project->number; $project->number = empty($project->number) ? $this->getNextProjectNumber($project) : $project->number;
$project->save(); $project->save();
if ($request->has('documents')) {
$this->saveDocuments($request->input('documents'), $project);
}
return $this->itemResponse($project->fresh()); return $this->itemResponse($project->fresh());
} }
@ -499,4 +504,65 @@ class ProjectController extends BaseController
return $this->listResponse(Project::withTrashed()->whereIn('id', $this->transformKeys($ids))); return $this->listResponse(Project::withTrashed()->whereIn('id', $this->transformKeys($ids)));
} }
/**
* Update the specified resource in storage.
*
* @param UploadProductRequest $request
* @param Product $project
* @return Response
*
*
*
* @OA\Put(
* path="/api/v1/projects/{id}/upload",
* operationId="uploadProject",
* tags={"projects"},
* summary="Uploads a document to a project",
* description="Handles the uploading of a document to a project",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Project Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the Project object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Project"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function upload(UploadProjectRequest $request, Project $project)
{
if ($request->has('documents'))
$this->saveDocuments($request->file('documents'), $project);
return $this->itemResponse($project->fresh());
}
} }

View File

@ -24,6 +24,7 @@ use App\Http\Requests\Quote\EditQuoteRequest;
use App\Http\Requests\Quote\ShowQuoteRequest; use App\Http\Requests\Quote\ShowQuoteRequest;
use App\Http\Requests\Quote\StoreQuoteRequest; use App\Http\Requests\Quote\StoreQuoteRequest;
use App\Http\Requests\Quote\UpdateQuoteRequest; use App\Http\Requests\Quote\UpdateQuoteRequest;
use App\Http\Requests\Quote\UploadQuoteRequest;
use App\Jobs\Invoice\ZipInvoices; use App\Jobs\Invoice\ZipInvoices;
use App\Models\Client; use App\Models\Client;
use App\Models\Invoice; use App\Models\Invoice;
@ -34,6 +35,7 @@ use App\Transformers\QuoteTransformer;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\TempFile; use App\Utils\TempFile;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
@ -43,6 +45,7 @@ use Illuminate\Http\Response;
class QuoteController extends BaseController class QuoteController extends BaseController
{ {
use MakesHash; use MakesHash;
use SavesDocuments;
protected $entity_type = Quote::class; protected $entity_type = Quote::class;
@ -521,7 +524,7 @@ class QuoteController extends BaseController
} }
}); });
ZipInvoices::dispatch($quotes, $quotes->first()->company, auth()->user()->email); ZipInvoices::dispatch($quotes, $quotes->first()->company, auth()->user());
return response()->json(['message' => ctrans('texts.sent_message')], 200); return response()->json(['message' => ctrans('texts.sent_message')], 200);
} }
@ -717,4 +720,65 @@ class QuoteController extends BaseController
return response()->download($file_path); return response()->download($file_path);
} }
/**
* Update the specified resource in storage.
*
* @param UploadQuoteRequest $request
* @param Quote $quote
* @return Response
*
*
*
* @OA\Put(
* path="/api/v1/quotes/{id}/upload",
* operationId="uploadQuote",
* tags={"quotes"},
* summary="Uploads a document to a quote",
* description="Handles the uploading of a document to a quote",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Quote Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the Quote object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Quote"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function upload(UploadQuoteRequest $request, Quote $quote)
{
if ($request->has('documents'))
$this->saveDocuments($request->file('documents'), $quote);
return $this->itemResponse($quote->fresh());
}
} }

View File

@ -20,10 +20,12 @@ use App\Http\Requests\RecurringInvoice\EditRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\ShowRecurringInvoiceRequest; use App\Http\Requests\RecurringInvoice\ShowRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\StoreRecurringInvoiceRequest; use App\Http\Requests\RecurringInvoice\StoreRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\UpdateRecurringInvoiceRequest; use App\Http\Requests\RecurringInvoice\UpdateRecurringInvoiceRequest;
use App\Http\Requests\RecurringInvoice\UploadRecurringInvoiceRequest;
use App\Models\RecurringInvoice; use App\Models\RecurringInvoice;
use App\Repositories\RecurringInvoiceRepository; use App\Repositories\RecurringInvoiceRepository;
use App\Transformers\RecurringInvoiceTransformer; use App\Transformers\RecurringInvoiceTransformer;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
@ -33,6 +35,7 @@ use Illuminate\Http\Response;
class RecurringInvoiceController extends BaseController class RecurringInvoiceController extends BaseController
{ {
use MakesHash; use MakesHash;
use SavesDocuments;
protected $entity_type = RecurringInvoice::class; protected $entity_type = RecurringInvoice::class;
@ -680,4 +683,65 @@ class RecurringInvoiceController extends BaseController
break; break;
} }
} }
/**
* Update the specified resource in storage.
*
* @param UploadRecurringInvoiceRequest $request
* @param RecurringInvoice $recurring_invoice
* @return Response
*
*
*
* @OA\Put(
* path="/api/v1/recurring_invoices/{id}/upload",
* operationId="uploadRecurringInvoice",
* tags={"recurring_invoices"},
* summary="Uploads a document to a recurring_invoice",
* description="Handles the uploading of a document to a recurring_invoice",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The RecurringInvoice Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the RecurringInvoice object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/RecurringInvoice"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function upload(UploadRecurringInvoiceRequest $request, RecurringInvoice $recurring_invoice)
{
if ($request->has('documents'))
$this->saveDocuments($request->file('documents'), $recurring_invoice);
return $this->itemResponse($recurring_invoice->fresh());
}
} }

View File

@ -21,12 +21,14 @@ use App\Http\Requests\Task\EditTaskRequest;
use App\Http\Requests\Task\ShowTaskRequest; use App\Http\Requests\Task\ShowTaskRequest;
use App\Http\Requests\Task\StoreTaskRequest; use App\Http\Requests\Task\StoreTaskRequest;
use App\Http\Requests\Task\UpdateTaskRequest; use App\Http\Requests\Task\UpdateTaskRequest;
use App\Http\Requests\Task\UploadTaskRequest;
use App\Models\Task; use App\Models\Task;
use App\Repositories\TaskRepository; use App\Repositories\TaskRepository;
use App\Transformers\TaskTransformer; use App\Transformers\TaskTransformer;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\BulkOptions; use App\Utils\Traits\BulkOptions;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use App\Utils\Traits\Uploadable; use App\Utils\Traits\Uploadable;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
@ -40,6 +42,7 @@ class TaskController extends BaseController
use MakesHash; use MakesHash;
use Uploadable; use Uploadable;
use BulkOptions; use BulkOptions;
use SavesDocuments;
protected $entity_type = Task::class; protected $entity_type = Task::class;
@ -506,4 +509,65 @@ class TaskController extends BaseController
{ {
//todo //todo
} }
/**
* Update the specified resource in storage.
*
* @param UploadTaskRequest $request
* @param Task $task
* @return Response
*
*
*
* @OA\Put(
* path="/api/v1/tasks/{id}/upload",
* operationId="uploadTask",
* tags={"tasks"},
* summary="Uploads a document to a task",
* description="Handles the uploading of a document to a task",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Task Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the Task object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Task"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function upload(UploadTaskRequest $request, Task $task)
{
if ($request->has('documents'))
$this->saveDocuments($request->file('documents'), $task);
return $this->itemResponse($task->fresh());
}
} }

View File

@ -0,0 +1,63 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers;
use PragmaRX\Google2FA\Google2FA;
use Crypt;
class TwoFactorController extends BaseController
{
public function setupTwoFactor()
{
$user = auth()->user();
if ($user->google_2fa_secret)
return response()->json(['message' => '2FA already enabled'], 400);
elseif(! $user->phone)
return response()->json(['message' => ctrans('texts.set_phone_for_two_factor')], 400);
elseif(! $user->confirmed)
return response()->json(['message' => 'Please confirm your account first'], 400);
$google2fa = new Google2FA();
$secret = $google2fa->generateSecretKey();
$qr_code = $google2fa->getQRCodeGoogleUrl(
config('ninja.app_name')
$user->email,
$secret
);
$data = [
'secret' => $secret,
'qrCode' => $qrCode,
];
return response()->json(['data' => $data], 200);
}
public function enableTwoFactor()
{
$user = auth()->user();
$secret = request()->input('secret');
$oneTimePassword = request()->input('one_time_password');
if (! $secret || ! \Google2FA::verifyKey($secret, $oneTimePassword)) {
return response()->json('message' > ctrans('texts.invalid_one_time_password'));
} elseif (! $user->google_2fa_secret && $user->phone && $user->confirmed) {
$user->google_2fa_secret = encrypt($secret);
$user->save();
}
return response()->json(['message' => ctrans('texts.enabled_two_factor')], 200);
}
}

View File

@ -23,11 +23,16 @@ use App\Http\Requests\User\CreateUserRequest;
use App\Http\Requests\User\DestroyUserRequest; use App\Http\Requests\User\DestroyUserRequest;
use App\Http\Requests\User\DetachCompanyUserRequest; use App\Http\Requests\User\DetachCompanyUserRequest;
use App\Http\Requests\User\EditUserRequest; use App\Http\Requests\User\EditUserRequest;
use App\Http\Requests\User\ReconfirmUserRequest;
use App\Http\Requests\User\ShowUserRequest; use App\Http\Requests\User\ShowUserRequest;
use App\Http\Requests\User\StoreUserRequest; use App\Http\Requests\User\StoreUserRequest;
use App\Http\Requests\User\UpdateUserRequest; use App\Http\Requests\User\UpdateUserRequest;
use App\Jobs\Company\CreateCompanyToken; use App\Jobs\Company\CreateCompanyToken;
use App\Jobs\Mail\NinjaMailer;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\User\UserEmailChanged; use App\Jobs\User\UserEmailChanged;
use App\Mail\Admin\VerifyUserObject;
use App\Models\CompanyUser; use App\Models\CompanyUser;
use App\Models\User; use App\Models\User;
use App\Repositories\UserRepository; use App\Repositories\UserRepository;
@ -369,13 +374,28 @@ class UserController extends BaseController
*/ */
public function update(UpdateUserRequest $request, User $user) public function update(UpdateUserRequest $request, User $user)
{ {
$old_email = $user->email;
$old_company_user = $user->company_user;
$old_user = json_encode($user);
$old_user_email = $user->getOriginal('email');
$new_email = $request->input('email'); $new_email = $request->input('email');
$new_user = $this->user_repo->save($request->all(), $user);
$new_user = $user->fresh();
$user = $this->user_repo->save($request->all(), $user); /* When changing email address we store the former email in case we need to rollback */
if ($old_user_email != $new_email) {
$user->last_confirmed_email_address = $old_user_email;
$user->save();
UserEmailChanged::dispatch($new_user, json_decode($old_user), auth()->user()->company());
}
if ($old_email != $new_email) {
UserEmailChanged::dispatch($new_email, $old_email, auth()->user()->company()); if(
strcasecmp($old_company_user->permissions, $user->company_user->permissions) != 0 ||
$old_company_user->is_admin != $user->company_user->is_admin
){
$user->company_user()->update(["permissions_updated_at" => now()]);
} }
event(new UserWasUpdated($user, auth()->user(), auth()->user()->company, Ninja::eventVars())); event(new UserWasUpdated($user, auth()->user(), auth()->user()->company, Ninja::eventVars()));
@ -670,4 +690,70 @@ class UserController extends BaseController
return response()->json(['message' => ctrans('texts.user_detached')], 200); return response()->json(['message' => ctrans('texts.user_detached')], 200);
} }
/**
* Detach an existing user to a company.
*
* @OA\Post(
* path="/api/v1/users/{user}/reconfirm",
* operationId="reconfirmUser",
* tags={"users"},
* summary="Reconfirm an existing user to a company",
* description="Reconfirm an existing user from a company",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="user",
* in="path",
* description="The user hashed_id",
* example="FD767dfd7",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Success response",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
* @param ReconfirmUserRequest $request
* @param User $user
* @return \Illuminate\Http\JsonResponse
*/
public function reconfirm(ReconfirmUserRequest $request, User $user)
{
$user->confirmation_code = $this->createDbHash($user->company()->db);
$user->save();
$nmo = new NinjaMailerObject;
$nmo->mailable = new NinjaMailer((new VerifyUserObject($user, $user->company()))->build());
$nmo->company = $user->company();
$nmo->to_user = $user;
$nmo->settings = $user->company->settings;
NinjaMailerJob::dispatch($nmo);
return response()->json(['message' => ctrans('texts.confirmation_resent')], 200);
}
} }

View File

@ -21,12 +21,14 @@ use App\Http\Requests\Vendor\EditVendorRequest;
use App\Http\Requests\Vendor\ShowVendorRequest; use App\Http\Requests\Vendor\ShowVendorRequest;
use App\Http\Requests\Vendor\StoreVendorRequest; use App\Http\Requests\Vendor\StoreVendorRequest;
use App\Http\Requests\Vendor\UpdateVendorRequest; use App\Http\Requests\Vendor\UpdateVendorRequest;
use App\Http\Requests\Vendor\UploadVendorRequest;
use App\Models\Vendor; use App\Models\Vendor;
use App\Repositories\VendorRepository; use App\Repositories\VendorRepository;
use App\Transformers\VendorTransformer; use App\Transformers\VendorTransformer;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\BulkOptions; use App\Utils\Traits\BulkOptions;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use App\Utils\Traits\Uploadable; use App\Utils\Traits\Uploadable;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
@ -39,6 +41,7 @@ class VendorController extends BaseController
use MakesHash; use MakesHash;
use Uploadable; use Uploadable;
use BulkOptions; use BulkOptions;
use SavesDocuments;
protected $entity_type = Vendor::class; protected $entity_type = Vendor::class;
@ -511,4 +514,65 @@ class VendorController extends BaseController
{ {
//todo //todo
} }
/**
* Update the specified resource in storage.
*
* @param UploadVendorRequest $request
* @param Vendor $vendor
* @return Response
*
*
*
* @OA\Put(
* path="/api/v1/vendors/{id}/upload",
* operationId="uploadVendor",
* tags={"vendors"},
* summary="Uploads a document to a vendor",
* description="Handles the uploading of a document to a vendor",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Vendor Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the Vendor object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Vendor"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function upload(UploadVendorRequest $request, Vendor $vendor)
{
if ($request->has('documents'))
$this->saveDocuments($request->file('documents'), $vendor);
return $this->itemResponse($vendor->fresh());
}
} }

View File

@ -28,6 +28,7 @@ class CreditsTable extends Component
{ {
$query = Credit::query() $query = Credit::query()
->where('client_id', auth('contact')->user()->client->id) ->where('client_id', auth('contact')->user()->client->id)
->where('status_id', '<>', Credit::STATUS_DRAFT)
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->paginate($this->per_page); ->paginate($this->per_page);

View File

@ -25,38 +25,15 @@ class DocumentsTable extends Component
public $per_page = 10; public $per_page = 10;
public $status = [
'resources',
];
public function mount($client) public function mount($client)
{ {
$this->client = $client; $this->client = $client;
} }
public function statusChange($status)
{
if (in_array($status, $this->status)) {
return $this->status = array_diff($this->status, [$status]);
}
array_push($this->status, $status);
}
public function render() public function render()
{ {
$query = $this->client->documents(); $query = $this->client
->documents()
if (in_array('resources', $this->status) && ! in_array('client', $this->status)) {
$query = $query->where('documentable_type', '!=', Client::class);
}
if (in_array('client', $this->status) && ! in_array('resources', $this->status)) {
$query = $query->where('documentable_type', Client::class);
}
$query = $query
->where('is_public', true)
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->paginate($this->per_page); ->paginate($this->per_page);

View File

@ -0,0 +1,34 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Livewire;
use Livewire\Component;
class PayNowDropdown extends Component
{
public $total;
public $methods;
public function mount(int $total)
{
$this->total = $total;
$this->methods = auth()->user()->client->service()->getPaymentMethods($total);
}
public function render()
{
return render('components.livewire.pay-now-dropdown');
}
}

View File

@ -2,7 +2,9 @@
namespace App\Http\Middleware; namespace App\Http\Middleware;
use App\Models\Account;
use App\Models\Company; use App\Models\Company;
use App\Utils\Ninja;
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -17,29 +19,39 @@ class ContactRegister
*/ */
public function handle($request, Closure $next) public function handle($request, Closure $next)
{ {
/* // Resolving based on subdomain. Used in version 5 hosted platform.
* Notes:
*
* 1. If request supports subdomain (for hosted) check domain and continue request.
* 2. If request doesn't support subdomain and doesn' have company_key, abort
* 3. firstOrFail() will abort with 404 if company with company_key wasn't found.
* 4. Abort if setting isn't enabled.
*/
if ($request->subdomain) { if ($request->subdomain) {
$company = Company::where('subdomain', $request->subdomain)->firstOrFail(); $company = Company::where('subdomain', $request->subdomain)->firstOrFail();
abort_unless($company->getSetting('enable_client_registration'), 404); abort_unless($company->getSetting('enable_client_registration'), 404);
$request->merge(['key' => $company->company_key]);
return $next($request); return $next($request);
} }
abort_unless($request->company_key, 404); // For self-hosted platforms with multiple companies, resolving is done using company key
// if it doesn't resolve using a domain.
if ($request->route()->parameter('company_key') && Ninja::isSelfHost()) {
$company = Company::where('company_key', $request->company_key)->firstOrFail(); $company = Company::where('company_key', $request->company_key)->firstOrFail();
abort_unless($company->client_can_register, 404); abort_unless($company->client_can_register, 404);
return $next($request); return $next($request);
} }
// As a fallback for self-hosted, it will use default company in the system
// if key isn't provided in the url.
if (!$request->route()->parameter('company_key') && Ninja::isSelfHost()) {
$company = Account::first()->default_company;
abort_unless($company->client_can_register, 404);
$request->merge(['key' => $company->company_key]);
return $next($request);
}
return abort(404);
}
} }

View File

@ -0,0 +1,39 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Client;
use App\Http\Requests\Request;
class UploadClientRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->client);
}
public function rules()
{
$rules = [];
if($this->input('documents'))
$rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000';
return $rules;
}
}

View File

@ -27,7 +27,8 @@ class ShowDocumentRequest extends FormRequest
*/ */
public function authorize() public function authorize()
{ {
return auth()->user('contact')->client->id == $this->document->documentable_id; return auth()->user('contact')->client->id == $this->document->documentable_id
|| $this->document->documentable->client_id == auth()->user('contact')->client->id;
} }
/** /**

View File

@ -0,0 +1,39 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Company;
use App\Http\Requests\Request;
class UploadCompanyRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->company);
}
public function rules()
{
$rules = [];
if($this->input('documents'))
$rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000';
return $rules;
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Credit;
use App\Http\Requests\Request;
class UploadCreditRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->credit);
}
public function rules()
{
$rules = [];
if($this->input('documents'))
$rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000';
return $rules;
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Expense;
use App\Http\Requests\Request;
class UploadExpenseRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->expense);
}
public function rules()
{
$rules = [];
if($this->input('documents'))
$rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000';
return $rules;
}
}

View File

@ -28,10 +28,12 @@ class ImportRequest extends Request
public function rules() public function rules()
{ {
return [ return [
'hash' => 'required|string', 'import_type' => 'required',
'entity_type' => 'required|string', 'files' => 'required_without:hash|array|min:1|max:6',
'column_map' => 'required|array', 'hash' => 'nullable|string',
'skip_header' => 'required|boolean' 'column_map' => 'required_with:hash|array',
'skip_header' => 'required_with:hash|boolean',
'files.*' => 'file|mimes:csv,txt',
]; ];
} }
} }

View File

@ -28,8 +28,9 @@ class PreImportRequest extends Request
public function rules() public function rules()
{ {
return [ return [
'file' => 'required|file|mimes:csv,txt', 'files.*' => 'file|mimes:csv,txt',
'entity_type' => 'required', 'files' => 'required|array|min:1|max:6',
'import_type' => 'required',
]; ];
} }
} }

View File

@ -0,0 +1,39 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Invoice;
use App\Http\Requests\Request;
class UploadInvoiceRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->invoice);
}
public function rules()
{
$rules = [];
if($this->input('documents'))
$rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000';
return $rules;
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Payment Ninja (https://paymentninja.com).
*
* @link https://github.com/paymentninja/paymentninja source repository
*
* @copyright Copyright (c) 2021. Payment Ninja LLC (https://paymentninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Payment;
use App\Http\Requests\Request;
class UploadPaymentRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->payment);
}
public function rules()
{
$rules = [];
if($this->input('documents'))
$rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000';
return $rules;
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Product Ninja (https://paymentninja.com).
*
* @link https://github.com/paymentninja/paymentninja source repository
*
* @copyright Copyright (c) 2021. Product Ninja LLC (https://paymentninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Product;
use App\Http\Requests\Request;
class UploadProductRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->product);
}
public function rules()
{
$rules = [];
if($this->input('documents'))
$rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000';
return $rules;
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Project Ninja (https://paymentninja.com).
*
* @link https://github.com/paymentninja/paymentninja source repository
*
* @copyright Copyright (c) 2021. Project Ninja LLC (https://paymentninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Project;
use App\Http\Requests\Request;
class UploadProjectRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->project);
}
public function rules()
{
$rules = [];
if($this->input('documents'))
$rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000';
return $rules;
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Quote Ninja (https://paymentninja.com).
*
* @link https://github.com/paymentninja/paymentninja source repository
*
* @copyright Copyright (c) 2021. Quote Ninja LLC (https://paymentninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Quote;
use App\Http\Requests\Request;
class UploadQuoteRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->quote);
}
public function rules()
{
$rules = [];
if($this->input('documents'))
$rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000';
return $rules;
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Quote Ninja (https://paymentninja.com).
*
* @link https://github.com/paymentninja/paymentninja source repository
*
* @copyright Copyright (c) 2021. Quote Ninja LLC (https://paymentninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\RecurringInvoice;
use App\Http\Requests\Request;
class UploadRecurringInvoiceRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->recurring_invoice);
}
public function rules()
{
$rules = [];
if($this->input('documents'))
$rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000';
return $rules;
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Quote Ninja (https://paymentninja.com).
*
* @link https://github.com/paymentninja/paymentninja source repository
*
* @copyright Copyright (c) 2021. Quote Ninja LLC (https://paymentninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Task;
use App\Http\Requests\Request;
class UploadTaskRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->task);
}
public function rules()
{
$rules = [];
if($this->input('documents'))
$rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000';
return $rules;
}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\User;
use App\Http\Requests\Request;
use App\Models\User;
class ReconfirmUserRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->Admin();
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Quote Ninja (https://paymentninja.com).
*
* @link https://github.com/paymentninja/paymentninja source repository
*
* @copyright Copyright (c) 2021. Quote Ninja LLC (https://paymentninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Vendor;
use App\Http\Requests\Request;
class UploadVendorRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->vendor);
}
public function rules()
{
$rules = [];
if($this->input('documents'))
$rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000';
return $rules;
}
}

View File

@ -57,6 +57,7 @@ class PortalComposer
$data['client'] = auth()->user()->client; $data['client'] = auth()->user()->client;
$data['settings'] = $this->settings; $data['settings'] = $this->settings;
$data['currencies'] = TranslationHelper::getCurrencies(); $data['currencies'] = TranslationHelper::getCurrencies();
$data['contact'] = auth('contact')->user();
$data['multiple_contacts'] = session()->get('multiple_contacts'); $data['multiple_contacts'] = session()->get('multiple_contacts');
@ -69,8 +70,8 @@ class PortalComposer
//@todo wire this back in when we are happy with dashboard. //@todo wire this back in when we are happy with dashboard.
// if($this->settings->enable_client_portal_dashboard == TRUE) // if($this->settings->enable_client_portal_dashboard == TRUE)
// $data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'activity'];
// $data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'activity'];
$data[] = ['title' => ctrans('texts.invoices'), 'url' => 'client.invoices.index', 'icon' => 'file-text']; $data[] = ['title' => ctrans('texts.invoices'), 'url' => 'client.invoices.index', 'icon' => 'file-text'];
$data[] = ['title' => ctrans('texts.recurring_invoices'), 'url' => 'client.recurring_invoices.index', 'icon' => 'file']; $data[] = ['title' => ctrans('texts.recurring_invoices'), 'url' => 'client.recurring_invoices.index', 'icon' => 'file'];
$data[] = ['title' => ctrans('texts.payments'), 'url' => 'client.payments.index', 'icon' => 'credit-card']; $data[] = ['title' => ctrans('texts.payments'), 'url' => 'client.payments.index', 'icon' => 'credit-card'];

View File

@ -1,10 +1,10 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */

View File

@ -0,0 +1,51 @@
<?php
/**
* client Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Definitions;
class ExpenseMap
{
public static function importable()
{
return [
0 => 'expense.vendor',
1 => 'expense.client',
2 => 'expense.project',
3 => 'expense.category',
4 => 'expense.amount',
5 => 'expense.currency',
6 => 'expense.date',
7 => 'expense.payment_type',
8 => 'expense.payment_date',
9 => 'expense.transaction_reference',
10 => 'expense.public_notes',
11 => 'expense.private_notes',
];
}
public static function import_keys()
{
return [
0 => 'texts.vendor',
1 => 'texts.client',
2 => 'texts.project',
3 => 'texts.category',
4 => 'texts.amount',
5 => 'texts.currency',
6 => 'texts.date',
7 => 'texts.payment_type',
8 => 'texts.payment_date',
9 => 'texts.transaction_reference',
10 => 'texts.public_notes',
11 => 'texts.private_notes',
];
}
}

View File

@ -26,50 +26,51 @@ class InvoiceMap
7 => 'invoice.date', 7 => 'invoice.date',
8 => 'invoice.due_date', 8 => 'invoice.due_date',
9 => 'invoice.terms', 9 => 'invoice.terms',
10 => 'invoice.public_notes', 10 => 'invoice.status',
11 => 'invoice.is_sent', 11 => 'invoice.public_notes',
12 => 'invoice.private_notes', 12 => 'invoice.is_sent',
13 => 'invoice.uses_inclusive_taxes', 13 => 'invoice.private_notes',
14 => 'invoice.tax_name1', 14 => 'invoice.uses_inclusive_taxes',
15 => 'invoice.tax_rate1', 15 => 'invoice.tax_name1',
16 => 'invoice.tax_name2', 16 => 'invoice.tax_rate1',
17 => 'invoice.tax_rate2', 17 => 'invoice.tax_name2',
18 => 'invoice.tax_name3', 18 => 'invoice.tax_rate2',
19 => 'invoice.tax_rate3', 19 => 'invoice.tax_name3',
20 => 'invoice.is_amount_discount', 20 => 'invoice.tax_rate3',
21 => 'invoice.footer', 21 => 'invoice.is_amount_discount',
22 => 'invoice.partial', 22 => 'invoice.footer',
23 => 'invoice.partial_due_date', 23 => 'invoice.partial',
24 => 'invoice.custom_value1', 24 => 'invoice.partial_due_date',
25 => 'invoice.custom_value2', 25 => 'invoice.custom_value1',
26 => 'invoice.custom_value3', 26 => 'invoice.custom_value2',
27 => 'invoice.custom_value4', 27 => 'invoice.custom_value3',
28 => 'invoice.custom_surcharge1', 28 => 'invoice.custom_value4',
29 => 'invoice.custom_surcharge2', 29 => 'invoice.custom_surcharge1',
30 => 'invoice.custom_surcharge3', 30 => 'invoice.custom_surcharge2',
31 => 'invoice.custom_surcharge4', 31 => 'invoice.custom_surcharge3',
32 => 'invoice.exchange_rate', 32 => 'invoice.custom_surcharge4',
33 => 'payment.date', 33 => 'invoice.exchange_rate',
34 => 'payment.amount', 34 => 'payment.date',
35 => 'payment.transaction_reference', 35 => 'payment.amount',
36 => 'item.quantity', 36 => 'payment.transaction_reference',
37 => 'item.cost', 37 => 'item.quantity',
38 => 'item.product_key', 38 => 'item.cost',
39 => 'item.notes', 39 => 'item.product_key',
40 => 'item.discount', 40 => 'item.notes',
41 => 'item.is_amount_discount', 41 => 'item.discount',
42 => 'item.tax_name1', 42 => 'item.is_amount_discount',
43 => 'item.tax_rate1', 43 => 'item.tax_name1',
44 => 'item.tax_name2', 44 => 'item.tax_rate1',
45 => 'item.tax_rate2', 45 => 'item.tax_name2',
46 => 'item.tax_name3', 46 => 'item.tax_rate2',
47 => 'item.tax_rate3', 47 => 'item.tax_name3',
48 => 'item.custom_value1', 48 => 'item.tax_rate3',
49 => 'item.custom_value2', 49 => 'item.custom_value1',
50 => 'item.custom_value3', 50 => 'item.custom_value2',
51 => 'item.custom_value4', 51 => 'item.custom_value3',
52 => 'item.type_id', 52 => 'item.custom_value4',
53 => 'client.email', 53 => 'item.type_id',
54 => 'client.email',
]; ];
} }
@ -86,50 +87,51 @@ class InvoiceMap
7 => 'texts.date', 7 => 'texts.date',
8 => 'texts.due_date', 8 => 'texts.due_date',
9 => 'texts.terms', 9 => 'texts.terms',
10 => 'texts.public_notes', 10 => 'texts.status',
11 => 'texts.sent', 11 => 'texts.public_notes',
12 => 'texts.private_notes', 12 => 'texts.sent',
13 => 'texts.uses_inclusive_taxes', 13 => 'texts.private_notes',
14 => 'texts.tax_name', 14 => 'texts.uses_inclusive_taxes',
15 => 'texts.tax_rate', 15 => 'texts.tax_name',
16 => 'texts.tax_name', 16 => 'texts.tax_rate',
17 => 'texts.tax_rate', 17 => 'texts.tax_name',
18 => 'texts.tax_name', 18 => 'texts.tax_rate',
19 => 'texts.tax_rate', 19 => 'texts.tax_name',
20 => 'texts.is_amount_discount', 20 => 'texts.tax_rate',
21 => 'texts.footer', 21 => 'texts.is_amount_discount',
22 => 'texts.partial', 22 => 'texts.footer',
23 => 'texts.partial_due_date', 23 => 'texts.partial',
24 => 'texts.custom_value1', 24 => 'texts.partial_due_date',
25 => 'texts.custom_value2', 25 => 'texts.custom_value1',
26 => 'texts.custom_value3', 26 => 'texts.custom_value2',
27 => 'texts.custom_value4', 27 => 'texts.custom_value3',
28 => 'texts.surcharge', 28 => 'texts.custom_value4',
29 => 'texts.surcharge', 29 => 'texts.surcharge',
30 => 'texts.surcharge', 30 => 'texts.surcharge',
31 => 'texts.surcharge', 31 => 'texts.surcharge',
32 => 'texts.exchange_rate', 32 => 'texts.surcharge',
33 => 'texts.payment_date', 33 => 'texts.exchange_rate',
34 => 'texts.payment_amount', 34 => 'texts.payment_date',
35 => 'texts.transaction_reference', 35 => 'texts.payment_amount',
36 => 'texts.quantity', 36 => 'texts.transaction_reference',
37 => 'texts.cost', 37 => 'texts.quantity',
38 => 'texts.product_key', 38 => 'texts.cost',
39 => 'texts.notes', 39 => 'texts.product_key',
40 => 'texts.discount', 40 => 'texts.notes',
41 => 'texts.is_amount_discount', 41 => 'texts.discount',
42 => 'texts.tax_name', 42 => 'texts.is_amount_discount',
43 => 'texts.tax_rate', 43 => 'texts.tax_name',
44 => 'texts.tax_name', 44 => 'texts.tax_rate',
45 => 'texts.tax_rate', 45 => 'texts.tax_name',
46 => 'texts.tax_name', 46 => 'texts.tax_rate',
47 => 'texts.tax_rate', 47 => 'texts.tax_name',
48 => 'texts.custom_value', 48 => 'texts.tax_rate',
49 => 'texts.custom_value', 49 => 'texts.custom_value',
50 => 'texts.custom_value', 50 => 'texts.custom_value',
51 => 'texts.custom_value', 51 => 'texts.custom_value',
52 => 'texts.type', 52 => 'texts.custom_value',
53 => 'texts.email', 53 => 'texts.type',
54 => 'texts.email',
]; ];
} }
} }

View File

@ -1,10 +1,10 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */

View File

@ -1,10 +1,10 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */

View File

@ -0,0 +1,61 @@
<?php
/**
* client Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Definitions;
class VendorMap
{
public static function importable()
{
return [
0 => 'vendor.name',
1 => 'vendor.phone',
2 => 'vendor.id_number',
3 => 'vendor.vat_number',
4 => 'vendor.website',
5 => 'vendor.first_name',
6 => 'vendor.last_name',
7 => 'vendor.email',
8 => 'vendor.currency_id',
9 => 'vendor.public_notes',
10 => 'vendor.private_notes',
11 => 'vendor.address1',
12 => 'vendor.address2',
13 => 'vendor.city',
14 => 'vendor.state',
15 => 'vendor.postal_code',
16 => 'vendor.country_id',
];
}
public static function import_keys()
{
return [
0 => 'texts.name',
1 => 'texts.phone',
2 => 'texts.id_number',
3 => 'texts.vat_number',
4 => 'texts.website',
5 => 'texts.first_name',
6 => 'texts.last_name',
7 => 'texts.email',
8 => 'texts.currency',
9 => 'texts.public_notes',
10 => 'texts.private_notes',
11 => 'texts.address1',
12 => 'texts.address2',
13 => 'texts.city',
14 => 'texts.state',
15 => 'texts.postal_code',
16 => 'texts.country',
];
}
}

View File

@ -0,0 +1,6 @@
<?php
namespace App\Import;
class ImportException extends \Exception{
}

View File

@ -1,10 +1,10 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */
@ -53,23 +53,13 @@ class BaseTransformer
return (isset($data[$field]) && $data[$field]) ? $data[$field] : '1'; return (isset($data[$field]) && $data[$field]) ? $data[$field] : '1';
} }
public function getCurrencyByCode($data) public function getCurrencyByCode( $data, $key = 'client.currency_id' ) {
{ $code = array_key_exists( $key, $data ) ? $data[ $key ] : false;
$code = array_key_exists('client.currency_id', $data) ? $data['client.currency_id'] : false;
if ($code) { return $this->maps['currencies'][ $code ] ?? $this->maps['company']->settings->currency_id;
$currency = $this->maps['currencies']->where('code', $code)->first();
if ($currency) {
return $currency->id;
}
} }
return $this->maps['company']->settings->currency_id; public function getClient($client_name, $client_email) {
}
public function getClient($client_name, $client_email)
{
$clients = $this->maps['company']->clients; $clients = $this->maps['company']->clients;
$clients = $clients->where( 'name', $client_name ); $clients = $clients->where( 'name', $client_name );
@ -78,13 +68,14 @@ class BaseTransformer
return $clients->first()->id; return $clients->first()->id;
} }
if ( ! empty( $client_email ) ) {
$contacts = ClientContact::where( 'company_id', $this->maps['company']->id ) $contacts = ClientContact::where( 'company_id', $this->maps['company']->id )
->where( 'email', $client_email ); ->where( 'email', $client_email );
if ( $contacts->count() >= 1 ) { if ( $contacts->count() >= 1 ) {
return $contacts->first()->client_id; return $contacts->first()->client_id;
} }
}
return null; return null;
} }
@ -101,7 +92,7 @@ class BaseTransformer
{ {
$name = trim(strtolower($name)); $name = trim(strtolower($name));
return isset($this->maps[ENTITY_CLIENT][$name]); return isset( $this->maps['client'][ $name ] );
} }
/** /**
@ -113,7 +104,7 @@ class BaseTransformer
{ {
$name = trim(strtolower($name)); $name = trim(strtolower($name));
return isset($this->maps[ENTITY_VENDOR][$name]); return isset( $this->maps['vendor'][ $name ] );
} }
@ -126,7 +117,7 @@ class BaseTransformer
{ {
$key = trim(strtolower($key)); $key = trim(strtolower($key));
return isset($this->maps[ENTITY_PRODUCT][$key]); return isset( $this->maps['product'][ $key ] );
} }
@ -167,7 +158,7 @@ class BaseTransformer
{ {
$name = strtolower(trim($name)); $name = strtolower(trim($name));
return isset($this->maps[ENTITY_CLIENT][$name]) ? $this->maps[ENTITY_CLIENT][$name] : null; return isset( $this->maps['client'][ $name ] ) ? $this->maps['client'][ $name ] : null;
} }
/** /**
@ -322,7 +313,7 @@ class BaseTransformer
*/ */
public function getInvoiceNumber($number) public function getInvoiceNumber($number)
{ {
return $number ? str_pad(trim($number), 4, '0', STR_PAD_LEFT) : null; return $number ? ltrim( trim( $number ), '0' ) : null;
} }
/** /**
@ -334,7 +325,8 @@ class BaseTransformer
{ {
$invoiceNumber = $this->getInvoiceNumber($invoiceNumber); $invoiceNumber = $this->getInvoiceNumber($invoiceNumber);
$invoiceNumber = strtolower($invoiceNumber); $invoiceNumber = strtolower($invoiceNumber);
return isset($this->maps[ENTITY_INVOICE][$invoiceNumber]) ? $this->maps[ENTITY_INVOICE][$invoiceNumber] : null;
return isset( $this->maps['invoice'][ $invoiceNumber ] ) ? $this->maps['invoice'][ $invoiceNumber ] : null;
} }
/** /**
@ -346,7 +338,8 @@ class BaseTransformer
{ {
$invoiceNumber = $this->getInvoiceNumber($invoiceNumber); $invoiceNumber = $this->getInvoiceNumber($invoiceNumber);
$invoiceNumber = strtolower($invoiceNumber); $invoiceNumber = strtolower($invoiceNumber);
return isset($this->maps['invoices'][$invoiceNumber]) ? $this->maps['invoices'][$invoiceNumber]->public_id : null;
return isset( $this->maps['invoice'][ $invoiceNumber ] ) ? $this->maps['invoices'][ $invoiceNumber ]->public_id : null;
} }
/** /**
@ -359,7 +352,7 @@ class BaseTransformer
$invoiceNumber = $this->getInvoiceNumber($invoiceNumber); $invoiceNumber = $this->getInvoiceNumber($invoiceNumber);
$invoiceNumber = strtolower($invoiceNumber); $invoiceNumber = strtolower($invoiceNumber);
return isset($this->maps[ENTITY_INVOICE][$invoiceNumber]); return $this->maps['invoice'][ $invoiceNumber ] ?? null;
} }
/** /**
@ -372,7 +365,7 @@ class BaseTransformer
$invoiceNumber = $this->getInvoiceNumber($invoiceNumber); $invoiceNumber = $this->getInvoiceNumber($invoiceNumber);
$invoiceNumber = strtolower($invoiceNumber); $invoiceNumber = strtolower($invoiceNumber);
return isset($this->maps[ENTITY_INVOICE.'_'.ENTITY_CLIENT][$invoiceNumber]) ? $this->maps[ENTITY_INVOICE.'_'.ENTITY_CLIENT][$invoiceNumber] : null; return $this->maps['invoice_client'][ $invoiceNumber ] ?? null;
} }
/** /**
@ -384,7 +377,7 @@ class BaseTransformer
{ {
$name = strtolower(trim($name)); $name = strtolower(trim($name));
return isset($this->maps[ENTITY_VENDOR][$name]) ? $this->maps[ENTITY_VENDOR][$name] : null; return $this->maps['vendor'][ $name ] ?? null;
} }
/** /**
@ -392,10 +385,31 @@ class BaseTransformer
* *
* @return null * @return null
*/ */
public function getExpenseCategoryId($name) public function getExpenseCategoryId( $name ) {
{
$name = strtolower( trim( $name ) ); $name = strtolower( trim( $name ) );
return isset($this->maps[ENTITY_EXPENSE_CATEGORY][$name]) ? $this->maps[ENTITY_EXPENSE_CATEGORY][$name] : null; return $this->maps['expense_category'][ $name ] ?? null;
}
/**
* @param $name
*
* @return null
*/
public function getProjectId( $name ) {
$name = strtolower( trim( $name ) );
return $this->maps['project'][ $name ] ?? null;
}
/**
* @param $name
*
* @return null
*/
public function getPaymentTypeId( $name ) {
$name = strtolower( trim( $name ) );
return $this->maps['payment_type'][ $name ] ?? null;
} }
} }

View File

@ -1,10 +1,10 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */

View File

@ -0,0 +1,79 @@
<?php
/**
* client Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Csv;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use Illuminate\Support\Str;
/**
* Class ClientTransformer.
*/
class ClientTransformer extends BaseTransformer
{
/**
* @param $data
*
* @return array|bool
*/
public function transform($data)
{
if (isset($data->name) && $this->hasClient($data->name)) {
throw new ImportException('Client already exists');
}
$settings = new \stdClass;
$settings->currency_id = (string)$this->getCurrencyByCode($data);
return [
'company_id' => $this->maps['company']->id,
'name' => $this->getString( $data, 'client.name' ),
'work_phone' => $this->getString( $data, 'client.phone' ),
'address1' => $this->getString( $data, 'client.address1' ),
'address2' => $this->getString( $data, 'client.address2' ),
'city' => $this->getString( $data, 'client.city' ),
'state' => $this->getString( $data, 'client.state' ),
'shipping_address1' => $this->getString( $data, 'client.shipping_address1' ),
'shipping_address2' => $this->getString( $data, 'client.shipping_address2' ),
'shipping_city' => $this->getString( $data, 'client.shipping_city' ),
'shipping_state' => $this->getString( $data, 'client.shipping_state' ),
'shipping_postal_code' => $this->getString( $data, 'client.shipping_postal_code' ),
'public_notes' => $this->getString( $data, 'client.public_notes' ),
'private_notes' => $this->getString( $data, 'client.private_notes' ),
'website' => $this->getString( $data, 'client.website' ),
'vat_number' => $this->getString( $data, 'client.vat_number' ),
'id_number' => $this->getString( $data, 'client.id_number' ),
'custom_value1' => $this->getString( $data, 'client.custom1' ),
'custom_value2' => $this->getString( $data, 'client.custom2' ),
'custom_value3' => $this->getString( $data, 'client.custom3' ),
'custom_value4' => $this->getString( $data, 'client.custom4' ),
'balance' => preg_replace( '/[^0-9,.]+/', '', $this->getFloat( $data, 'client.balance' ) ),
'paid_to_date' => preg_replace( '/[^0-9,.]+/', '', $this->getFloat( $data, 'client.paid_to_date' ) ),
'credit_balance' => 0,
'settings' => $settings,
'client_hash' => Str::random( 40 ),
'contacts' => [
[
'first_name' => $this->getString( $data, 'contact.first_name' ),
'last_name' => $this->getString( $data, 'contact.last_name' ),
'email' => $this->getString( $data, 'contact.email' ),
'phone' => $this->getString( $data, 'contact.phone' ),
'custom_value1' => $this->getString( $data, 'contact.custom1' ),
'custom_value2' => $this->getString( $data, 'contact.custom2' ),
'custom_value3' => $this->getString( $data, 'contact.custom3' ),
'custom_value4' => $this->getString( $data, 'contact.custom4' ),
],
],
'country_id' => isset( $data['client.country'] ) ? $this->getCountryId( $data['client.country']) : null,
'shipping_country_id' => isset($data['client.shipping_country'] ) ? $this->getCountryId( $data['client.shipping_country'] ) : null,
];
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Import\Transformers\Csv;
use App\Import\Transformers\BaseTransformer;
/**
* Class InvoiceTransformer.
*/
class ExpenseTransformer extends BaseTransformer {
/**
* @param $data
*
* @return bool|array
*/
public function transform( $data ) {
$clientId = isset( $data['expense.client'] ) ? $this->getClientId( $data['expense.client'] ) : null;
return [
'company_id' => $this->maps['company']->id,
'amount' => $this->getFloat( $data, 'expense.amount' ),
'currency_id' => $this->getCurrencyByCode( $data, 'expense.currency_id' ),
'vendor_id' => isset( $data['expense.vendor'] ) ? $this->getVendorId( $data['expense.vendor'] ) : null,
'client_id' => isset( $data['expense.client'] ) ? $this->getClientId( $data['expense.client'] ) : null,
'expense_date' => isset( $data['expense.date'] ) ? date( 'Y-m-d', strtotime( $data['expense.date'] ) ) : null,
'public_notes' => $this->getString( $data, 'expense.public_notes' ),
'private_notes' => $this->getString( $data, 'expense.private_notes' ),
'expense_category_id' => isset( $data['expense.category'] ) ? $this->getExpenseCategoryId( $data['expense.category'] ) : null,
'project_id' => isset( $data['expense.project'] ) ? $this->getProjectId( $data['expense.project'] ) : null,
'payment_type_id' => isset( $data['expense.payment_type'] ) ? $this->getPaymentTypeId( $data['expense.payment_type'] ) : null,
'payment_date' => isset( $data['expense.payment_date'] ) ? date( 'Y-m-d', strtotime( $data['expense.payment_date'] ) ) : null,
'transaction_reference' => $this->getString( $data, 'expense.transaction_reference' ),
'should_be_invoiced' => $clientId ? true : false,
];
}
}

View File

@ -0,0 +1,131 @@
<?php
/**
* client Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Csv;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use App\Models\Invoice;
/**
* Class InvoiceTransformer.
*/
class InvoiceTransformer extends BaseTransformer {
/**
* @param $data
*
* @return bool|array
*/
public function transform( $line_items_data ) {
$invoice_data = reset( $line_items_data );
if ( $this->hasInvoice( $invoice_data['invoice.number'] ) ) {
throw new ImportException( 'Invoice number already exists' );
}
$invoiceStatusMap = [
'sent' => Invoice::STATUS_SENT,
'draft' => Invoice::STATUS_DRAFT,
];
$transformed = [
'company_id' => $this->maps['company']->id,
'number' => $this->getString( $invoice_data, 'invoice.number' ),
'user_id' => $this->getString( $invoice_data, 'invoice.user_id' ),
'amount' => $amount = $this->getFloat( $invoice_data, 'invoice.amount' ),
'balance' => isset( $invoice_data['invoice.balance'] ) ? $this->getFloat( $invoice_data, 'invoice.balance' ) : $amount,
'client_id' => $this->getClient( $this->getString( $invoice_data, 'client.name' ), $this->getString( $invoice_data, 'client.email' ) ),
'discount' => $this->getFloat( $invoice_data, 'invoice.discount' ),
'po_number' => $this->getString( $invoice_data, 'invoice.po_number' ),
'date' => isset( $invoice_data['invoice.date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['invoice.date'] ) ) : null,
'due_date' => isset( $invoice_data['invoice.due_date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['invoice.due_date'] ) ) : null,
'terms' => $this->getString( $invoice_data, 'invoice.terms' ),
'public_notes' => $this->getString( $invoice_data, 'invoice.public_notes' ),
'is_sent' => $this->getString( $invoice_data, 'invoice.is_sent' ),
'private_notes' => $this->getString( $invoice_data, 'invoice.private_notes' ),
'tax_name1' => $this->getString( $invoice_data, 'invoice.tax_name1' ),
'tax_rate1' => $this->getFloat( $invoice_data, 'invoice.tax_rate1' ),
'tax_name2' => $this->getString( $invoice_data, 'invoice.tax_name2' ),
'tax_rate2' => $this->getFloat( $invoice_data, 'invoice.tax_rate2' ),
'tax_name3' => $this->getString( $invoice_data, 'invoice.tax_name3' ),
'tax_rate3' => $this->getFloat( $invoice_data, 'invoice.tax_rate3' ),
'custom_value1' => $this->getString( $invoice_data, 'invoice.custom_value1' ),
'custom_value2' => $this->getString( $invoice_data, 'invoice.custom_value2' ),
'custom_value3' => $this->getString( $invoice_data, 'invoice.custom_value3' ),
'custom_value4' => $this->getString( $invoice_data, 'invoice.custom_value4' ),
'footer' => $this->getString( $invoice_data, 'invoice.footer' ),
'partial' => $this->getFloat( $invoice_data, 'invoice.partial' ),
'partial_due_date' => $this->getString( $invoice_data, 'invoice.partial_due_date' ),
'custom_surcharge1' => $this->getString( $invoice_data, 'invoice.custom_surcharge1' ),
'custom_surcharge2' => $this->getString( $invoice_data, 'invoice.custom_surcharge2' ),
'custom_surcharge3' => $this->getString( $invoice_data, 'invoice.custom_surcharge3' ),
'custom_surcharge4' => $this->getString( $invoice_data, 'invoice.custom_surcharge4' ),
'exchange_rate' => $this->getString( $invoice_data, 'invoice.exchange_rate' ),
'status_id' => $invoiceStatusMap[ $status =
strtolower( $this->getString( $invoice_data, 'invoice.status' ) ) ] ??
Invoice::STATUS_SENT,
'viewed' => $status === 'viewed',
'archived' => $status === 'archived',
];
if ( isset( $invoice_data['payment.amount'] ) ) {
$transformed['payments'] = [
[
'date' => isset( $invoice_data['payment.date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['payment.date'] ) ) : date( 'y-m-d' ),
'transaction_reference' => $this->getString( $invoice_data, 'payment.transaction_reference' ),
'amount' => $this->getFloat( $invoice_data, 'payment.amount' ),
],
];
} elseif ( $status === 'paid' ) {
$transformed['payments'] = [
[
'date' => isset( $invoice_data['payment.date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['payment.date'] ) ) : date( 'y-m-d' ),
'transaction_reference' => $this->getString( $invoice_data, 'payment.transaction_reference' ),
'amount' => $this->getFloat( $invoice_data, 'invoice.amount' ),
],
];
} elseif ( isset( $transformed['amount'] ) && isset( $transformed['balance'] ) ) {
$transformed['payments'] = [
[
'date' => isset( $invoice_data['payment.date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['payment.date'] ) ) : date( 'y-m-d' ),
'transaction_reference' => $this->getString( $invoice_data, 'payment.transaction_reference' ),
'amount' => $transformed['amount'] - $transformed['balance'],
],
];
}
$line_items = [];
foreach ( $line_items_data as $record ) {
$line_items[] = [
'quantity' => $this->getFloat( $record, 'item.quantity' ),
'cost' => $this->getFloat( $record, 'item.cost' ),
'product_key' => $this->getString( $record, 'item.product_key' ),
'notes' => $this->getString( $record, 'item.notes' ),
'discount' => $this->getFloat( $record, 'item.discount' ),
'is_amount_discount' => filter_var( $this->getString( $record, 'item.is_amount_discount' ), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ),
'tax_name1' => $this->getString( $record, 'item.tax_name1' ),
'tax_rate1' => $this->getFloat( $record, 'item.tax_rate1' ),
'tax_name2' => $this->getString( $record, 'item.tax_name2' ),
'tax_rate2' => $this->getFloat( $record, 'item.tax_rate2' ),
'tax_name3' => $this->getString( $record, 'item.tax_name3' ),
'tax_rate3' => $this->getFloat( $record, 'item.tax_rate3' ),
'custom_value1' => $this->getString( $record, 'item.custom_value1' ),
'custom_value2' => $this->getString( $record, 'item.custom_value2' ),
'custom_value3' => $this->getString( $record, 'item.custom_value3' ),
'custom_value4' => $this->getString( $record, 'item.custom_value4' ),
'type_id' => $this->getInvoiceTypeId( $record, 'item.type_id' ),
];
}
$transformed['line_items'] = $line_items;
return $transformed;
}
}

View File

@ -0,0 +1,64 @@
<?php
/**
* client Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Csv;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
/**
* Class PaymentTransformer.
*/
class PaymentTransformer extends BaseTransformer {
/**
* @param $data
*
* @return array
*/
public function transform( $data ) {
$client_id =
$this->getClient( $this->getString( $data, 'payment.client_id' ), $this->getString( $data, 'payment.client_id' ) );
if ( empty( $client_id ) ) {
throw new ImportException( 'Could not find client.' );
}
$transformed = [
'company_id' => $this->maps['company']->id,
'number' => $this->getString( $data, 'payment.number' ),
'user_id' => $this->getString( $data, 'payment.user_id' ),
'amount' => $this->getFloat( $data, 'payment.amount' ),
'refunded' => $this->getFloat( $data, 'payment.refunded' ),
'applied' => $this->getFloat( $data, 'payment.applied' ),
'transaction_reference' => $this->getString( $data, 'payment.transaction_reference ' ),
'date' => $this->getString( $data, 'payment.date' ),
'private_notes' => $this->getString( $data, 'payment.private_notes' ),
'custom_value1' => $this->getString( $data, 'payment.custom_value1' ),
'custom_value2' => $this->getString( $data, 'payment.custom_value2' ),
'custom_value3' => $this->getString( $data, 'payment.custom_value3' ),
'custom_value4' => $this->getString( $data, 'payment.custom_value4' ),
'client_id' => $client_id,
];
if ( isset( $data['payment.invoice_number'] ) &&
$invoice_id = $this->getInvoiceId( $data['payment.invoice_number'] ) ) {
$transformed['invoices'] = [
[
'invoice_id' => $invoice_id,
'amount' => $transformed['amount'] ?? null,
],
];
}
return $transformed;
}
}

View File

@ -1,16 +1,16 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */
namespace App\Import\Transformers; namespace App\Import\Transformers\Csv;
use App\Import\Transformers\BaseTransformer;
/** /**
* Class ProductTransformer. * Class ProductTransformer.
*/ */
@ -19,7 +19,7 @@ class ProductTransformer extends BaseTransformer
/** /**
* @param $data * @param $data
* *
* @return bool|Item * @return array
*/ */
public function transform($data) public function transform($data)
{ {

View File

@ -0,0 +1,47 @@
<?php
namespace App\Import\Transformers\Csv;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
/**
* Class VendorTransformer.
*/
class VendorTransformer extends BaseTransformer {
/**
* @param $data
*
* @return array|bool
*/
public function transform( $data ) {
if ( isset( $data->name ) && $this->hasVendor( $data->name ) ) {
throw new ImportException('Vendor already exists');
}
return [
'company_id' => $this->maps['company']->id,
'name' => $this->getString( $data, 'vendor.name' ),
'phone' => $this->getString( $data, 'vendor.phone' ),
'id_number' => $this->getString( $data, 'vendor.id_number' ),
'vat_number' => $this->getString( $data, 'vendor.vat_number' ),
'website' => $this->getString( $data, 'vendor.website' ),
'currency_id' => $this->getCurrencyByCode( $data, 'vendor.currency_id' ),
'public_notes' => $this->getString( $data, 'vendor.public_notes' ),
'private_notes' => $this->getString( $data, 'vendor.private_notes' ),
'address1' => $this->getString( $data, 'vendor.address1' ),
'address2' => $this->getString( $data, 'vendor.address2' ),
'city' => $this->getString( $data, 'vendor.city' ),
'state' => $this->getString( $data, 'vendor.state' ),
'postal_code' => $this->getString( $data, 'vendor.postal_code' ),
'vendor_contacts' => [
[
'first_name' => $this->getString( $data, 'vendor.first_name' ),
'last_name' => $this->getString( $data, 'vendor.last_name' ),
'email' => $this->getString( $data, 'vendor.email' ),
'phone' => $this->getString( $data, 'vendor.phone' ),
],
],
'country_id' => isset( $data['vendor.country_id'] ) ? $this->getCountryId( $data['vendor.country_id'] ) : null,
];
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* Invoice Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Freshbooks;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use Illuminate\Support\Str;
/**
* Class ClientTransformer.
*/
class ClientTransformer extends BaseTransformer {
/**
* @param $data
*
* @return array|bool
*/
public function transform( $data ) {
if ( isset( $data['Organization'] ) && $this->hasClient( $data['Organization'] ) ) {
throw new ImportException('Client already exists');
}
return [
'company_id' => $this->maps['company']->id,
'name' => $this->getString( $data, 'Organization' ),
'work_phone' => $this->getString( $data, 'Phone' ),
'address1' => $this->getString( $data, 'Street' ),
'city' => $this->getString( $data, 'City' ),
'state' => $this->getString( $data, 'Province/State' ),
'postal_code' => $this->getString( $data, 'Postal Code' ),
'country_id' => isset( $data['Country'] ) ? $this->getCountryId( $data['Country'] ) : null,
'private_notes' => $this->getString( $data, 'Notes' ),
'credit_balance' => 0,
'settings' => new \stdClass,
'client_hash' => Str::random( 40 ),
'contacts' => [
[
'first_name' => $this->getString( $data, 'First Name' ),
'last_name' => $this->getString( $data, 'Last Name' ),
'email' => $this->getString( $data, 'Email' ),
'phone' => $this->getString( $data, 'Phone' ),
],
],
];
}
}

View File

@ -0,0 +1,78 @@
<?php
/**
* client Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Freshbooks;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use App\Models\Invoice;
/**
* Class InvoiceTransformer.
*/
class InvoiceTransformer extends BaseTransformer {
/**
* @param $line_items_data
*
* @return bool|array
*/
public function transform( $line_items_data ) {
$invoice_data = reset( $line_items_data );
if ( $this->hasInvoice( $invoice_data['Invoice #'] ) ) {
throw new ImportException( 'Invoice number already exists' );
}
$invoiceStatusMap = [
'sent' => Invoice::STATUS_SENT,
'draft' => Invoice::STATUS_DRAFT,
];
$transformed = [
'company_id' => $this->maps['company']->id,
'client_id' => $this->getClient( $this->getString( $invoice_data, 'Client Name' ), null ),
'number' => $this->getString( $invoice_data, 'Invoice #' ),
'date' => isset( $invoice_data['Date Issued'] ) ? date( 'Y-m-d', strtotime( $invoice_data['Date Issued'] ) ) : null,
'currency_id' => $this->getCurrencyByCode( $invoice_data, 'Currency' ),
'amount' => 0,
'status_id' => $invoiceStatusMap[ $status =
strtolower( $this->getString( $invoice_data, 'Invoice Status' ) ) ] ?? Invoice::STATUS_SENT,
'viewed' => $status === 'viewed',
];
$line_items = [];
foreach ( $line_items_data as $record ) {
$line_items[] = [
'product_key' => $this->getString( $record, 'Item Name' ),
'notes' => $this->getString( $record, 'Item Description' ),
'cost' => $this->getFloat( $record, 'Rate' ),
'quantity' => $this->getFloat( $record, 'Quantity' ),
'discount' => $this->getFloat( $record, 'Discount Percentage' ),
'is_amount_discount' => false,
'tax_name1' => $this->getString( $record, 'Tax 1 Type' ),
'tax_rate1' => $this->getFloat( $record, 'Tax 1 Amount' ),
'tax_name2' => $this->getString( $record, 'Tax 2 Type' ),
'tax_rate2' => $this->getFloat( $record, 'Tax 2 Amount' ),
];
$transformed['amount'] += $this->getFloat( $record, 'Line Total' );
}
$transformed['line_items'] = $line_items;
if ( ! empty( $invoice_data['Date Paid'] ) ) {
$transformed['payments'] = [[
'date' => date( 'Y-m-d', strtotime( $invoice_data['Date Paid'] ) ),
'amount' => $transformed['amount'],
]];
}
return $transformed;
}
}

View File

@ -0,0 +1,89 @@
<?php
/**
* client Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Invoice2Go;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use App\Models\Invoice;
use Illuminate\Support\Str;
/**
* Class InvoiceTransformer.
*/
class InvoiceTransformer extends BaseTransformer {
/**
* @param $line_items_data
*
* @return bool|array
*/
public function transform( $invoice_data ) {
if ( $this->hasInvoice( $invoice_data['DocumentNumber'] ) ) {
throw new ImportException( 'Invoice number already exists' );
}
$invoiceStatusMap = [
'unsent' => Invoice::STATUS_DRAFT,
'sent' => Invoice::STATUS_SENT,
];
$transformed = [
'company_id' => $this->maps['company']->id,
'number' => $this->getString( $invoice_data, 'DocumentNumber' ),
'notes' => $this->getString( $invoice_data, 'Comment' ),
'date' => isset( $invoice_data['DocumentDate'] ) ? date( 'Y-m-d', strtotime( $invoice_data['DocumentDate'] ) ) : null,
'currency_id' => $this->getCurrencyByCode( $invoice_data, 'Currency' ),
'amount' => 0,
'status_id' => $invoiceStatusMap[ $status =
strtolower( $this->getString( $invoice_data, 'DocumentStatus' ) ) ] ?? Invoice::STATUS_SENT,
'viewed' => $status === 'viewed',
'line_items' => [
[
'amount' => $amount = $this->getFloat( $invoice_data, 'TotalAmount' ),
'quantity' => 1,
'discount' => $this->getFloat( $invoice_data, 'DiscountValue' ),
'is_amount_discount' => false,
],
],
];
$client_id =
$this->getClient( $this->getString( $invoice_data, 'Name' ), $this->getString( $invoice_data, 'EmailRecipient' ) );
if ( $client_id ) {
$transformed['client_id'] = $client_id;
} else {
$transformed['client'] = [
'name' => $this->getString( $invoice_data, 'Name' ),
'address1' => $this->getString( $invoice_data, 'DocumentRecipientAddress' ),
'shipping_address1' => $this->getString( $invoice_data, 'ShipAddress' ),
'credit_balance' => 0,
'settings' => new \stdClass,
'client_hash' => Str::random( 40 ),
'contacts' => [
[
'email' => $this->getString( $invoice_data, 'Email' ),
],
],
];
}
if ( ! empty( $invoice_data['Date Paid'] ) ) {
$transformed['payments'] = [
[
'date' => date( 'Y-m-d', strtotime( $invoice_data['DatePaid'] ) ),
'amount' => $this->getFloat( $invoice_data, 'Payments' ),
],
];
}
return $transformed;
}
}

View File

@ -1,10 +1,10 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */

View File

@ -1,10 +1,10 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */

View File

@ -0,0 +1,48 @@
<?php
/**
* Invoice Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Invoicely;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use Illuminate\Support\Str;
/**
* Class ClientTransformer.
*/
class ClientTransformer extends BaseTransformer {
/**
* @param $data
*
* @return array|bool
*/
public function transform( $data ) {
if ( isset( $data['Client Name'] ) && $this->hasClient( $data['Client Name'] ) ) {
throw new ImportException('Client already exists');
}
return [
'company_id' => $this->maps['company']->id,
'name' => $this->getString( $data, 'Client Name' ),
'work_phone' => $this->getString( $data, 'Phone' ),
'country_id' => isset( $data['Country'] ) ? $this->getCountryIdBy2( $data['Country'] ) : null,
'credit_balance' => 0,
'settings' => new \stdClass,
'client_hash' => Str::random( 40 ),
'contacts' => [
[
'email' => $this->getString( $data, 'Email' ),
'phone' => $this->getString( $data, 'Phone' ),
],
],
];
}
}

View File

@ -0,0 +1,58 @@
<?php
/**
* client Ninja (https://clientninja.com).
*
* @link https://github.com/clientninja/clientninja source repository
*
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Import\Transformers\Invoicely;
use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer;
use App\Models\Invoice;
/**
* Class InvoiceTransformer.
*/
class InvoiceTransformer extends BaseTransformer {
/**
* @param $data
*
* @return bool|array
*/
public function transform( $data ) {
if ( $this->hasInvoice( $data['Details'] ) ) {
throw new ImportException( 'Invoice number already exists' );
}
$transformed = [
'company_id' => $this->maps['company']->id,
'client_id' => $this->getClient( $this->getString( $data, 'Client' ), null ),
'number' => $this->getString( $data, 'Details' ),
'date' => isset( $data['Date'] ) ? date( 'Y-m-d', strtotime( $data['Date'] ) ) : null,
'due_date' => isset( $data['Due'] ) ? date( 'Y-m-d', strtotime( $data['Due'] ) ) : null,
'status_id' => Invoice::STATUS_SENT,
'line_items' => [
[
'cost' => $amount = $this->getFloat( $data, 'Total' ),
'quantity' => 1,
],
],
];
if ( strtolower( $data['Status'] ) === 'paid' ) {
$transformed['payments'] = [
[
'date' => date( 'Y-m-d' ),
'amount' => $amount,
],
];
}
return $transformed;
}
}

View File

@ -1,10 +1,10 @@
<?php <?php
/** /**
* client Ninja (https://clientninja.com). * Invoice Ninja (https://invoiceninja.com).
* *
* @link https://github.com/clientninja/clientninja source repository * @link https://github.com/invoiceninja/invoiceninja source repository
* *
* @copyright Copyright (c) 2021. client Ninja LLC (https://clientninja.com) * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
* *
* @license https://opensource.org/licenses/AAL * @license https://opensource.org/licenses/AAL
*/ */

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