1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-22 01:11:34 +02:00

Merge pull request #7956 from turbo124/v5-stable

v5.5.41
This commit is contained in:
David Bomba 2022-11-20 09:25:49 +11:00 committed by GitHub
commit 0990410c5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 968 additions and 131 deletions

View File

@ -1 +1 @@
5.5.40
5.5.41

View File

@ -175,7 +175,7 @@ class SendRemindersCron extends Command
/**Refresh Invoice values*/
$invoice->calc()->getInvoice()->save();
$invoice->fresh();
$invoice->service()->deletePdf();
$invoice->service()->deletePdf()->save();
/* Refresh the client here to ensure the balance is fresh */
$client = $invoice->client;

View File

@ -55,6 +55,55 @@ class BankTransactionFilters extends QueryFilters
}
/**
* Filter based on client status.
*
* Statuses we need to handle
* - all
* - unmatched
* - matched
* - converted
* - deposits
* - withdrawals
*
* @return Builder
*/
public function client_status(string $value = '') :Builder
{
if (strlen($value) == 0) {
return $this->builder;
}
$status_parameters = explode(',', $value);
if (in_array('all', $status_parameters)) {
return $this->builder;
}
if (in_array('unmatched', $status_parameters)) {
$this->builder->where('status_id', BankTransaction::STATUS_UNMATCHED);
}
if (in_array('matched', $status_parameters)) {
$this->builder->where('status_id', BankTransaction::STATUS_MATCHED);
}
if (in_array('converted', $status_parameters)) {
$this->builder->where('status_id', BankTransaction::STATUS_CONVERTED);
}
if (in_array('deposits', $status_parameters)) {
$this->builder->where('base_type', 'CREDIT');
}
if (in_array('withdrawals', $status_parameters)) {
$this->builder->where('base_type', 'DEBIT');
}
return $this->builder;
}
/**
* Filters the list based on the status
* archived, active, deleted.

View File

@ -44,6 +44,55 @@ class ExpenseFilters extends QueryFilters
});
}
/**
* Filter based on client status.
*
* Statuses we need to handle
* - all
* - logged
* - pending
* - invoiced
* - paid
* - unpaid
*
* @return Builder
*/
public function client_status(string $value = '') :Builder
{
if (strlen($value) == 0) {
return $this->builder;
}
$status_parameters = explode(',', $value);
if (in_array('all', $status_parameters)) {
return $this->builder;
}
if (in_array('logged', $status_parameters)) {
$this->builder->where('amount', '>', 0);
}
if (in_array('pending', $status_parameters)) {
$this->builder->whereNull('invoice_id')->whereNotNull('payment_date');
}
if (in_array('invoiced', $status_parameters)) {
$this->builder->whereNotNull('invoice_id');
}
if (in_array('paid', $status_parameters)) {
$this->builder->whereNotNull('payment_date');
}
if (in_array('unpaid', $status_parameters)) {
$this->builder->whereNull('payment_date');
}
return $this->builder;
}
/**
* Filters the list based on the status
* archived, active, deleted.

View File

@ -17,19 +17,20 @@ use Illuminate\Database\Eloquent\Builder;
class PurchaseOrderFilters extends QueryFilters
{
/**
* Filter based on client status.
*
* Statuses we need to handle
* - all
* - paid
* - unpaid
* - overdue
* - reversed
* - draft
* - sent
* - accepted
* - cancelled
*
* @return Builder
*/
public function credit_status(string $value = '') :Builder
public function client_status(string $value = '') :Builder
{
if (strlen($value) == 0) {
return $this->builder;
@ -45,16 +46,17 @@ class PurchaseOrderFilters extends QueryFilters
$this->builder->where('status_id', PurchaseOrder::STATUS_DRAFT);
}
if (in_array('partial', $status_parameters)) {
$this->builder->where('status_id', PurchaseOrder::STATUS_PARTIAL);
if (in_array('sent', $status_parameters)) {
$this->builder->where('status_id', PurchaseOrder::STATUS_SENT);
}
if (in_array('applied', $status_parameters)) {
$this->builder->where('status_id', PurchaseOrder::STATUS_APPLIED);
if (in_array('accepted', $status_parameters)) {
$this->builder->where('status_id', PurchaseOrder::STATUS_ACCEPTED);
}
//->where('due_date', '>', Carbon::now())
//->orWhere('partial_due_date', '>', Carbon::now());
if (in_array('cancelled', $status_parameters)) {
$this->builder->where('status_id', PurchaseOrder::STATUS_CANCELLED);
}
return $this->builder;
}

View File

@ -11,6 +11,7 @@
namespace App\Filters;
use App\Models\Quote;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
@ -41,6 +42,51 @@ class QuoteFilters extends QueryFilters
});
}
/**
* Filter based on client status.
*
* Statuses we need to handle
* - all
* - active
* - paused
* - completed
*
* @param string client_status The invoice status as seen by the client
* @return Builder
*/
public function client_status(string $value = '') :Builder
{
if (strlen($value) == 0) {
return $this->builder;
}
$status_parameters = explode(',', $value);
if (in_array('all', $status_parameters)) {
return $this->builder;
}
if (in_array('draft', $status_parameters)) {
$this->builder->where('status_id', Quote::STATUS_DRAFT);
}
if (in_array('sent', $status_parameters)) {
$this->builder->where('status_id', Quote::STATUS_SENT);
}
if (in_array('approved', $status_parameters)) {
$this->builder->where('status_id', Quote::STATUS_APPROVED);
}
if (in_array('expired', $status_parameters)) {
$this->builder->where('status_id', Quote::STATUS_SENT)
->where('due_date', '<=', now()->toDateString());
}
return $this->builder;
}
/**
* Filters the list based on the status
* archived, active, deleted.

View File

@ -11,6 +11,7 @@
namespace App\Filters;
use App\Models\RecurringInvoice;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
@ -40,6 +41,46 @@ class RecurringInvoiceFilters extends QueryFilters
});
}
/**
* Filter based on client status.
*
* Statuses we need to handle
* - all
* - active
* - paused
* - completed
*
* @param string client_status The invoice status as seen by the client
* @return Builder
*/
public function client_status(string $value = '') :Builder
{
if (strlen($value) == 0) {
return $this->builder;
}
$status_parameters = explode(',', $value);
if (in_array('all', $status_parameters)) {
return $this->builder;
}
if (in_array('active', $status_parameters)) {
$this->builder->where('status_id', RecurringInvoice::STATUS_ACTIVE);
}
if (in_array('paused', $status_parameters)) {
$this->builder->where('status_id', RecurringInvoice::STATUS_PAUSED);
}
if (in_array('completed', $status_parameters)) {
$this->builder->where('status_id', RecurringInvoice::STATUS_COMPLETED);
}
return $this->builder;
}
/**
* Filters the list based on the status
* archived, active, deleted.

View File

@ -41,6 +41,37 @@ class TaskFilters extends QueryFilters
});
}
/**
* Filter based on client status.
*
* Statuses we need to handle
* - all
* - invoiced
*
* @param string client_status The invoice status as seen by the client
* @return Builder
*/
public function client_status(string $value = '') :Builder
{
if (strlen($value) == 0) {
return $this->builder;
}
$status_parameters = explode(',', $value);
if (in_array('all', $status_parameters)) {
return $this->builder;
}
if (in_array('invoiced', $status_parameters)) {
$this->builder->whereNotNull('invoice_id');
}
return $this->builder;
}
/**
* Filters the list based on the status
* archived, active, deleted.

View File

@ -1000,42 +1000,6 @@ class BaseController extends Controller
return redirect('/setup');
}
public function reactCatch()
{
if ((bool) $this->checkAppSetup() !== false && $account = Account::first()) {
if (config('ninja.require_https') && ! request()->isSecure()) {
return redirect()->secure(request()->getRequestUri());
}
$data = [];
//pass report errors bool to front end
$data['report_errors'] = Ninja::isSelfHost() ? $account->report_errors : true;
//pass referral code to front end
$data['rc'] = request()->has('rc') ? request()->input('rc') : '';
$data['build'] = request()->has('build') ? request()->input('build') : '';
$data['login'] = request()->has('login') ? request()->input('login') : 'false';
$data['signup'] = request()->has('signup') ? request()->input('signup') : 'false';
$data['user_agent'] = request()->server('HTTP_USER_AGENT');
$data['path'] = $this->setBuild();
$this->buildCache();
if (Ninja::isSelfHost() && $account->set_react_as_default_ap) {
return view('react.index', $data);
} else {
abort('page not found', 404);
}
}
return redirect('/setup');
}
private function setBuild()
{
$build = '';

View File

@ -31,12 +31,12 @@ class MatchBankTransactionRequest extends Request
$rules = [
'transactions' => 'bail|array',
'transactions.*.id' => 'bail|required',
'transactions.*.invoice_ids' => 'nullable|string|sometimes',
];
$rules['transactions.*.ninja_category_id'] = 'bail|nullable|sometimes|exists:expense_categories,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
$rules['transactions.*.vendor_id'] = 'bail|sometimes|exists:vendors,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
$rules['transactions.*.id'] = 'bail|required|exists:bank_transactions,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
return $rules;

View File

@ -22,6 +22,14 @@ class UpdateCompanyRequest extends Request
{
use MakesHash;
private array $protected_input = [
'client_portal_privacy_policy',
'client_portal_terms',
'portal_custom_footer',
'portal_custom_css',
'portal_custom_head'
];
/**
* Determine if the user is authorized to make this request.
*
@ -32,6 +40,8 @@ class UpdateCompanyRequest extends Request
return auth()->user()->can('edit', $this->company);
}
public function rules()
{
$input = $this->all();
@ -90,6 +100,14 @@ class UpdateCompanyRequest extends Request
{
$account = $this->company->account;
if(Ninja::isHosted())
{
foreach($this->protected_input as $protected_var)
{
$settings[$protected_var] = str_replace("script", "", $settings[$protected_var]);
}
}
if (! $account->isFreeHostedClient()) {
return $settings;
}

View File

@ -160,6 +160,9 @@ class MatchBankTransactions implements ShouldQueue
{
$this->bt = BankTransaction::find($input['id']);
if(!$this->bt || $this->bt->status_id == BankTransaction::STATUS_CONVERTED)
return $this;
$_invoices = Invoice::withTrashed()->find($this->getInvoices($input['invoice_ids']));
$amount = $this->bt->amount;
@ -180,6 +183,10 @@ class MatchBankTransactions implements ShouldQueue
//if there is a category id, pull it from Yodlee and insert - or just reuse!!
$this->bt = BankTransaction::find($input['id']);
if(!$this->bt || $this->bt->status_id == BankTransaction::STATUS_CONVERTED)
return $this;
$expense = ExpenseFactory::create($this->bt->company_id, $this->bt->user_id);
$expense->category_id = $this->resolveCategory($input);
$expense->amount = $this->bt->amount;

View File

@ -21,8 +21,6 @@ class InvoiceEmailFailedActivity implements ShouldQueue
{
protected $activity_repo;
public $delay = 5;
/**
* Create the event listener.
*

View File

@ -243,6 +243,14 @@ class PaymentEmailEngine extends BaseEmailEngine
$data['$invoices.due_date'] = ['value' => $this->formatInvoiceField('due_date'), 'label' => ctrans('texts.invoices')];
$data['$invoices.po_number'] = ['value' => $this->formatInvoiceField('po_number'), 'label' => ctrans('texts.invoices')];
if($this->payment->status_id == 4) {
$data['$status_logo'] = ['value' => '<div class="stamp is-paid"> ' . ctrans('texts.paid') .'</div>', 'label' => ''];
}
else
$data['$status_logo'] = ['value' => '', 'label' => ''];
$arrKeysLength = array_map('strlen', array_keys($data));
array_multisort($arrKeysLength, SORT_DESC, $data);

View File

@ -136,3 +136,36 @@ class AuthorizeCreateCustomer
// }
// }
}
// $request = new net\authorize\api\contract\v1\GetCustomerProfileIdsRequest();
// $request->setMerchantAuthentication($auth->merchant_authentication);
// $controller = new net\authorize\api\controller\GetCustomerProfileIdsController($request);
// $response = $controller->executeWithApiResponse($auth->mode());
// // $customer_profile_id = end($response->getIds());
// foreach($response->getIds() as $customer_profile_id)
// {
// $request = new net\authorize\api\contract\v1\GetCustomerProfileRequest();
// $request->setMerchantAuthentication($auth->merchant_authentication);
// $request->setCustomerProfileId($customer_profile_id);
// $controller = new net\authorize\api\controller\GetCustomerProfileController($request);
// $response = $controller->executeWithApiResponse($auth->mode());
// $profileSelected = $response->getProfile();
// if($profileSelected->getEmail() == 'katnandan@gmail.com')
// {
// $profileSelected;
// break;
// }
// }

View File

@ -92,12 +92,14 @@ class CreditCard
$payment_hash = PaymentHash::where('hash', $request->input('payment_hash'))->firstOrFail();
$amount_with_fee = $payment_hash->data->total->amount_with_fee;
$invoice_totals = $payment_hash->data->total->invoice_totals;
$fee_total = 0;
$fee_total = null;
$fees_and_limits = $this->forte->company_gateway->getFeesAndLimits(GatewayType::CREDIT_CARD);
if(property_exists($fees_and_limits, 'fee_percent') && $fees_and_limits->fee_percent > 0)
{
$fee_total = 0;
for ($i = ($invoice_totals * 100) ; $i < ($amount_with_fee * 100); $i++) {
$calculated_fee = ( 3 * $i) / 100;
$calculated_amount_with_fee = round(($i + $calculated_fee) / 100,2);

View File

@ -97,6 +97,8 @@ class InstantBankPay implements MethodInterface
$this->go_cardless->setPaymentHash(
$request->getPaymentHash()
);
$this->go_cardless->init();
try {
$billing_request = $this->go_cardless->gateway->billingRequests()->get(

View File

@ -291,13 +291,13 @@ class GoCardlessPaymentDriver extends BaseDriver
return response()->json([], 200);
}
$this->go_cardless->setPaymentHash($hash);
$this->setPaymentHash($hash);
$billing_request = $this->go_cardless->gateway->billingRequests()->get(
$billing_request = $this->gateway->billingRequests()->get(
$event['links']['billing_request']
);
$payment = $this->go_cardless->gateway->payments()->get(
$payment = $this->gateway->payments()->get(
$billing_request->payment_request->links->payment
);
@ -305,12 +305,12 @@ class GoCardlessPaymentDriver extends BaseDriver
$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($hash->invoices(), 'invoice_id')))->withTrashed()->get();
$this->go_cardless->client = $invoices->first()->client;
$this->client = $invoices->first()->client;
$invoices->each(function ($invoice){
//if payments exist already, they just need to be confirmed.
if($invoice->payments()->exists){
if($invoice->payments()->exists()){
$invoice->payments()->where('status_id', 1)->cursor()->each(function ($payment){
$payment->status_id = 4;
@ -347,12 +347,12 @@ class GoCardlessPaymentDriver extends BaseDriver
$data = [
'payment_method' => $payment->links->mandate,
'payment_type' => PaymentType::INSTANT_BANK_PAY,
'amount' => $this->go_cardless->payment_hash->data->amount_with_fee,
'amount' => $this->payment_hash->data->amount_with_fee,
'transaction_reference' => $payment->id,
'gateway_type_id' => GatewayType::INSTANT_BANK_PAY,
];
$payment = $this->go_cardless->createPayment($data, Payment::STATUS_COMPLETED);
$payment = $this->createPayment($data, Payment::STATUS_COMPLETED);
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->save();
@ -361,8 +361,8 @@ class GoCardlessPaymentDriver extends BaseDriver
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_GOCARDLESS,
$this->go_cardless->client,
$this->go_cardless->client->company,
$this->client,
$this->client->company,
);
}

View File

@ -187,7 +187,7 @@ class BaseRepository
if(!$model->id){
$this->new_model = true;
if(is_array($model->line_items))
if(is_array($model->line_items) && !($model instanceof RecurringInvoice))
{
$model->line_items = (collect($model->line_items))->map(function ($item) use($model,$client) {

View File

@ -73,7 +73,8 @@ class Design extends BaseDesign
const PLAIN = 'plain';
const PLAYFUL = 'playful';
const CUSTOM = 'custom';
const CALM = 'calm';
const DELIVERY_NOTE = 'delivery_note';
const STATEMENT = 'statement';
const PURCHASE_ORDER = 'purchase_order';

View File

@ -114,7 +114,7 @@ class Helpers
return '';
}
// 04-10-2022 Return Early if no reserved keywords are present, this is a very expenseive process
// 04-10-2022 Return Early if no reserved keywords are present, this is a very expensive process
$string_hit = false;
foreach ( [':MONTH',':YEAR',':QUARTER',':WEEK'] as $string )
@ -146,19 +146,19 @@ class Helpers
'%s %s %s',
Carbon::now()->subDays(7)->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->translatedFormat($entity->date_format())
Carbon::now()->subDays(1)->translatedFormat($entity->date_format())
),
':WEEK_AHEAD' => \sprintf(
'%s %s %s',
Carbon::now()->addDays(7)->translatedFormat($entity->date_format()),
Carbon::now()->addDays(6)->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->addDays(14)->translatedFormat($entity->date_format())
Carbon::now()->addDays(13)->translatedFormat($entity->date_format())
),
':WEEK' => \sprintf(
'%s %s %s',
Carbon::now()->translatedFormat($entity->date_format()),
Carbon::now()->subDays(7)->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->addDays(7)->translatedFormat($entity->date_format())
Carbon::now()->addDays(13)->translatedFormat($entity->date_format())
),
],
'raw' => [

View File

@ -124,6 +124,7 @@ class HtmlEngine
$data['$line_tax_labels'] = ['value' => $this->lineTaxLabels(), 'label' => ctrans('texts.taxes')];
$data['$line_tax_values'] = ['value' => $this->lineTaxValues(), 'label' => ctrans('texts.taxes')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.date')];
$data['$status_logo'] = ['value' => '', 'label' => ''];
$data['$invoice.date'] = &$data['$date'];
$data['$invoiceDate'] = &$data['$date'];
@ -167,6 +168,10 @@ class HtmlEngine
$data['$invoice.project'] = &$data['$project.name'];
}
if($this->entity->status_id == 4) {
$data['$status_logo'] = ['value' => '<div class="stamp is-paid"> ' . ctrans('texts.paid') .'</div>', 'label' => ''];
}
if($this->entity->vendor) {
$data['$invoice.vendor'] = ['value' => $this->entity->vendor->present()->name(), 'label' => ctrans('texts.vendor_name')];
}

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.5.40',
'app_tag' => '5.5.40',
'app_version' => '5.5.41',
'app_tag' => '5.5.41',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),

View File

@ -0,0 +1,53 @@
<?php
use App\Models\Design;
use App\Services\PdfMaker\Design as PdfMakerDesign;
use App\Utils\Ninja;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Ninja::isHosted()) {
$design = new Design();
$design->name = 'Calm';
$design->is_custom = false;
$design->design = '';
$design->is_active = true;
$design->save();
} elseif (Design::count() !== 0) {
$design = new Design();
$design->name = 'Calm';
$design->is_custom = false;
$design->design = '';
$design->is_active = true;
$design->save();
}
\Illuminate\Support\Facades\Artisan::call('ninja:design-update');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
};

View File

@ -38,6 +38,7 @@ class DesignSeeder extends Seeder
['id' => 8, 'name' => 'Hipster', 'user_id' => null, 'company_id' => null, 'is_custom' => false, 'design' => '', 'is_active' => true],
['id' => 9, 'name' => 'Playful', 'user_id' => null, 'company_id' => null, 'is_custom' => false, 'design' => '', 'is_active' => true],
['id' => 10, 'name' => 'Tech', 'user_id' => null, 'company_id' => null, 'is_custom' => false, 'design' => '', 'is_active' => true],
['id' => 11, 'name' => 'Calm', 'user_id' => null, 'company_id' => null, 'is_custom' => false, 'design' => '', 'is_active' => true],
];
foreach ($designs as $design) {

View File

@ -4839,6 +4839,10 @@ $LANG = array(
'show_tasks_in_client_portal' => 'Show Tasks in Client Portal',
'notification_quote_expired_subject' => 'Quote :invoice has expired for :client',
'notification_quote_expired' => 'The following Quote :invoice for client :client and :amount has now expired.',
'auto_sync' => 'Auto Sync',
'refresh_accounts' => 'Refresh Accounts',
'upgrade_to_connect_bank_account' => 'Upgrade to Enterprise to connect your bank account',
'click_here_to_connect_bank_account' => 'Click here to connect your bank account',
);
return $LANG;

View File

@ -101,6 +101,36 @@
#content .center {
text-align: center;
}
.stamp {
transform: rotate(12deg);
color: #555;
font-size: 3rem;
font-weight: 700;
border: 0.25rem solid #555;
display: inline-block;
padding: 0.25rem 1rem;
text-transform: uppercase;
border-radius: 1rem;
font-family: 'Courier';
mix-blend-mode: multiply;
z-index:200 !important;
position: fixed;
text-align: center;
}
.is-paid {
color: #D23;
border: 1rem double #D23;
transform: rotate(-5deg);
font-size: 6rem;
font-family: "Open sans", Helvetica, Arial, sans-serif;
border-radius: 0;
padding: 0.5rem;
opacity: 0.2;
z-index:200 !important;
position: fixed;
}
</style>
</head>

View File

@ -80,6 +80,33 @@
#content .left {
text-align: left !important;
}
.stamp {
transform: rotate(12deg);
color: #555;
font-size: 3rem;
font-weight: 700;
border: 0.25rem solid #555;
text-transform: uppercase;
border-radius: 1rem;
font-family: 'Courier';
mix-blend-mode: multiply;
z-index:200 !important;
position: relative;
}
.is-paid {
color: #D23;
border: 1rem double #D23;
transform: rotate(-5deg);
font-size: 6rem;
font-family: "Open sans", Helvetica, Arial, sans-serif;
border-radius: 0;
padding: 0.5rem;
opacity: 0.2;
z-index:200 !important;
position: relative;
}
</style>
<!--[if gte mso 9]>

View File

@ -143,6 +143,36 @@
color: {{ $design == 'dark' ? '#ffffff' : '#000000' }} !important;
opacity: {{ $design == 'dark' ? '87%': '100%' }} !important;
}
.stamp {
transform: rotate(12deg);
color: #555;
font-size: 3rem;
font-weight: 700;
border: 0.25rem solid #555;
display: inline-block;
padding: 0.25rem 1rem;
text-transform: uppercase;
border-radius: 1rem;
font-family: 'Courier';
mix-blend-mode: multiply;
z-index:200 !important;
position: fixed;
text-align: center;
}
.is-paid {
color: #D23;
border: 1rem double #D23;
transform: rotate(-5deg);
font-size: 6rem;
font-family: "Open sans", Helvetica, Arial, sans-serif;
border-radius: 0;
padding: 0.5rem;
opacity: 0.2;
z-index:200 !important;
position: fixed;
}
</style>
</head>

View File

@ -47,6 +47,7 @@
line-height: var(--line-height);
position: fixed;
top: 0;
width: 100%;
}
@ -224,7 +225,7 @@
position: fixed;
bottom: 0;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-columns: 1fr;
gap: 15px;
color: white;
}
@ -239,6 +240,11 @@
padding-top: 0.5rem
}
[data-ref="footer_content"]{
padding-right: 2rem;
margin-right: 2rem;
}
table {
width: 100%;
}
@ -285,6 +291,36 @@
overflow-wrap: break-word;
}
.stamp {
transform: rotate(12deg);
color: #555;
font-size: 3rem;
font-weight: 700;
border: 0.25rem solid #555;
display: inline-block;
padding: 0.25rem 1rem;
text-transform: uppercase;
border-radius: 1rem;
font-family: 'Courier';
mix-blend-mode: multiply;
z-index:200 !important;
position: fixed;
text-align: center;
}
.is-paid {
color: #D23;
border: 1rem double #D23;
transform: rotate(-5deg);
font-size: 6rem;
font-family: "Open sans", Helvetica, Arial, sans-serif;
border-radius: 0;
padding: 0.5rem;
opacity: 0.2;
z-index:200 !important;
position: fixed;
}
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/
@ -380,7 +416,7 @@ $entity_images
<div id="footer">
<div style="width: 100%;">
<p data-ref="total_table-footer">$entity_footer</p>
<p data-ref="footer_content">$entity_footer</p>
<script>
// Clear up space a bit, if [product-table, tasks-table, delivery-note-table] isn't present.
@ -402,6 +438,4 @@ $entity_images
});
</script>
</div>
<div> <!-- #2 column --> </div>
<div> <!-- #3 column --> </div>
</div>

View File

@ -276,6 +276,36 @@
overflow-wrap: break-word;
}
.stamp {
transform: rotate(12deg);
color: #555;
font-size: 3rem;
font-weight: 700;
border: 0.25rem solid #555;
display: inline-block;
padding: 0.25rem 1rem;
text-transform: uppercase;
border-radius: 1rem;
font-family: 'Courier';
mix-blend-mode: multiply;
z-index:200 !important;
position: fixed;
text-align: center;
}
.is-paid {
color: #D23;
border: 1rem double #D23;
transform: rotate(-5deg);
font-size: 6rem;
font-family: "Open sans", Helvetica, Arial, sans-serif;
border-radius: 0;
padding: 0.5rem;
opacity: 0.2;
z-index:200 !important;
position: fixed;
}
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/

View File

@ -262,7 +262,37 @@
}
[data-ref="total_table-public_notes"] { font-weight: normal; }
[data-ref="total_table-terms"] { font-weight: normal; }
.stamp {
transform: rotate(12deg);
color: #555;
font-size: 3rem;
font-weight: 700;
border: 0.25rem solid #555;
display: inline-block;
padding: 0.25rem 1rem;
text-transform: uppercase;
border-radius: 1rem;
font-family: 'Courier';
mix-blend-mode: multiply;
z-index:200 !important;
position: fixed;
text-align: center;
}
.is-paid {
color: #D23;
border: 1rem double #D23;
transform: rotate(-5deg);
font-size: 6rem;
font-family: "Open sans", Helvetica, Arial, sans-serif;
border-radius: 0;
padding: 0.5rem;
opacity: 0.2;
z-index:200 !important;
position: fixed;
}
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/
@ -290,6 +320,7 @@
/** To find out selectors on your own: https://invoiceninja.github.io/docs/custom-fields/#snippets **/
</style>
<div id="body">
<table style="min-width: 100%">
<thead>
<tr>
@ -301,7 +332,7 @@
<tbody>
<tr>
<td>
<div id="body">
<div id="">
<div class="header-wrapper">
<div>
<img class="company-logo" src="$company.logo" alt="$company.name logo">
@ -316,7 +347,7 @@
<div id="vendor-details"></div>
<div>
<p class="entity-label" style="font-size:32px; color:$primary_color;">$entity_label</p>
<p class="entity-label" style="font-size:32px; font-weight: bold; color:$primary_color;">$entity_label</p>
<table id="entity-details" cellspacing="0" dir="ltr"></table>
</div>
</div>
@ -343,6 +374,7 @@
</tr>
</tfoot>
</table>
</div>
<div class="repeating-header" id="header"></div>
@ -371,24 +403,23 @@
</div>
</div>
<script>
// Clear up space a bit, if [product-table, tasks-table, delivery-note-table] isn't present.
document.addEventListener('DOMContentLoaded', () => {
let tables = [
'product-table', 'task-table', 'delivery-note-table',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals',
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table',
'client-details','vendor-details', 'swiss-qr'
];
<script>
// Clear up space a bit, if [product-table, tasks-table, delivery-note-table] isn't present.
document.addEventListener('DOMContentLoaded', () => {
let tables = [
'product-table', 'task-table', 'delivery-note-table',
'statement-invoice-table', 'statement-payment-table', 'statement-aging-table-totals',
'statement-invoice-table-totals', 'statement-payment-table-totals', 'statement-aging-table',
'client-details','vendor-details', 'swiss-qr'
];
tables.forEach((tableIdentifier) => {
console.log(document.getElementById(tableIdentifier));
tables.forEach((tableIdentifier) => {
console.log(document.getElementById(tableIdentifier));
document.getElementById(tableIdentifier)?.childElementCount === 0
? document.getElementById(tableIdentifier).style.setProperty('display', 'none', 'important')
: '';
});
document.getElementById(tableIdentifier)?.childElementCount === 0
? document.getElementById(tableIdentifier).style.setProperty('display', 'none', 'important')
: '';
});
</script>
</div>
});
</script>

View File

@ -257,6 +257,36 @@
overflow-wrap: break-word;
}
.stamp {
transform: rotate(12deg);
color: #555;
font-size: 3rem;
font-weight: 700;
border: 0.25rem solid #555;
display: inline-block;
padding: 0.25rem 1rem;
text-transform: uppercase;
border-radius: 1rem;
font-family: 'Courier';
mix-blend-mode: multiply;
z-index:200 !important;
position: fixed;
text-align: center;
}
.is-paid {
color: #D23;
border: 1rem double #D23;
transform: rotate(-5deg);
font-size: 6rem;
font-family: "Open sans", Helvetica, Arial, sans-serif;
border-radius: 0;
padding: 0.5rem;
opacity: 0.2;
z-index:200 !important;
position: fixed;
}
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/

View File

@ -229,6 +229,35 @@
overflow-wrap: break-word;
}
.stamp {
transform: rotate(12deg);
color: #555;
font-size: 3rem;
font-weight: 700;
border: 0.25rem solid #555;
display: inline-block;
padding: 0.25rem 1rem;
text-transform: uppercase;
border-radius: 1rem;
font-family: 'Courier';
mix-blend-mode: multiply;
z-index:200 !important;
position: fixed;
text-align: center;
}
.is-paid {
color: #D23;
border: 1rem double #D23;
transform: rotate(-5deg);
font-size: 6rem;
font-family: "Open sans", Helvetica, Arial, sans-serif;
border-radius: 0;
padding: 0.5rem;
opacity: 0.2;
z-index:200 !important;
position: fixed;
}
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/

View File

@ -233,7 +233,36 @@
max-width: 300px;
overflow-wrap: break-word;
}
.stamp {
transform: rotate(12deg);
color: #555;
font-size: 3rem;
font-weight: 700;
border: 0.25rem solid #555;
display: inline-block;
padding: 0.25rem 1rem;
text-transform: uppercase;
border-radius: 1rem;
font-family: 'Courier';
mix-blend-mode: multiply;
z-index:200 !important;
position: fixed;
text-align: center;
}
.is-paid {
color: #D23;
border: 1rem double #D23;
transform: rotate(-5deg);
font-size: 6rem;
font-family: "Open sans", Helvetica, Arial, sans-serif;
border-radius: 0;
padding: 0.5rem;
opacity: 0.2;
z-index:200 !important;
position: fixed;
}
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/

View File

@ -251,6 +251,35 @@
overflow-wrap: break-word;
}
.stamp {
transform: rotate(12deg);
color: #555;
font-size: 3rem;
font-weight: 700;
border: 0.25rem solid #555;
display: inline-block;
padding: 0.25rem 1rem;
text-transform: uppercase;
border-radius: 1rem;
font-family: 'Courier';
mix-blend-mode: multiply;
z-index:200 !important;
position: fixed;
text-align: center;
}
.is-paid {
color: #D23;
border: 1rem double #D23;
transform: rotate(-5deg);
font-size: 6rem;
font-family: "Open sans", Helvetica, Arial, sans-serif;
border-radius: 0;
padding: 0.5rem;
opacity: 0.2;
z-index:200 !important;
position: fixed;
}
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/

View File

@ -278,6 +278,35 @@
overflow-wrap: break-word;
}
.stamp {
transform: rotate(12deg);
color: #555;
font-size: 3rem;
font-weight: 700;
border: 0.25rem solid #555;
display: inline-block;
padding: 0.25rem 1rem;
text-transform: uppercase;
border-radius: 1rem;
font-family: 'Courier';
mix-blend-mode: multiply;
z-index:200 !important;
position: fixed;
text-align: center;
}
.is-paid {
color: #D23;
border: 1rem double #D23;
transform: rotate(-5deg);
font-size: 6rem;
font-family: "Open sans", Helvetica, Arial, sans-serif;
border-radius: 0;
padding: 0.5rem;
opacity: 0.2;
z-index:200 !important;
position: fixed;
}
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/

View File

@ -221,6 +221,35 @@
overflow-wrap: break-word;
}
.stamp {
transform: rotate(12deg);
color: #555;
font-size: 3rem;
font-weight: 700;
border: 0.25rem solid #555;
display: inline-block;
padding: 0.25rem 1rem;
text-transform: uppercase;
border-radius: 1rem;
font-family: 'Courier';
mix-blend-mode: multiply;
z-index:200 !important;
position: fixed;
text-align: center;
}
.is-paid {
color: #D23;
border: 1rem double #D23;
transform: rotate(-5deg);
font-size: 6rem;
font-family: "Open sans", Helvetica, Arial, sans-serif;
border-radius: 0;
padding: 0.5rem;
opacity: 0.2;
z-index:200 !important;
position: fixed;
}
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/

View File

@ -293,6 +293,36 @@
max-width: 300px;
overflow-wrap: break-word;
}
.stamp {
transform: rotate(12deg);
color: #555;
font-size: 3rem;
font-weight: 700;
border: 0.25rem solid #555;
display: inline-block;
padding: 0.25rem 1rem;
text-transform: uppercase;
border-radius: 1rem;
font-family: 'Courier';
mix-blend-mode: multiply;
z-index:200 !important;
position: fixed;
text-align: center;
}
.is-paid {
color: #D23;
border: 1rem double #D23;
transform: rotate(-5deg);
font-size: 6rem;
font-family: "Open sans", Helvetica, Arial, sans-serif;
border-radius: 0;
padding: 0.5rem;
opacity: 0.2;
z-index:200 !important;
position: fixed;
}
/** Useful snippets, uncomment to enable. **/
@ -323,7 +353,7 @@
/** For more info, please check our docs: https://invoiceninja.github.io **/
/** To find out selectors on your own: https://invoiceninja.github.io/docs/custom-fields/#snippets **/
</style>
<div id="body">
<table style="min-width: 100%">
<thead>
<tr>
@ -335,7 +365,6 @@
<tbody>
<tr>
<td>
<div id="body">
<div class="header-wrapper">
<div>
<img class="company-logo" src="$company.logo" alt="$company.name logo">
@ -367,8 +396,7 @@
<div id="statement-payment-table-totals" data-ref="statement-totals"></div>
<table id="statement-aging-table" cellspacing="0" data-ref="table"></table>
<div id="statement-aging-table-totals" data-ref="statement-totals"></div>
<div id="table-totals" cellspacing="0"></div>
</div>
<div id="table-totals" cellspacing="0"></div>
</td>
</tr>
</tbody>
@ -380,7 +408,7 @@
</tr>
</tfoot>
</table>
</div>
<div class="repeating-header">
<div id="header">
<div style="background-color: #00968B"><!-- 1 --></div>

View File

@ -257,7 +257,36 @@
max-width: 300px;
overflow-wrap: break-word;
}
.stamp {
transform: rotate(12deg);
color: #555;
font-size: 3rem;
font-weight: 700;
border: 0.25rem solid #555;
display: inline-block;
padding: 0.25rem 1rem;
text-transform: uppercase;
border-radius: 1rem;
font-family: 'Courier';
mix-blend-mode: multiply;
z-index:200 !important;
position: fixed;
text-align: center;
}
.is-paid {
color: #D23;
border: 1rem double #D23;
transform: rotate(-5deg);
font-size: 6rem;
font-family: "Open sans", Helvetica, Arial, sans-serif;
border-radius: 0;
padding: 0.5rem;
opacity: 0.2;
z-index:200 !important;
position: fixed;
}
/** Useful snippets, uncomment to enable. **/
/** Hide company logo **/

View File

@ -163,7 +163,7 @@
@yield('footer')
@stack('footer')
@if((bool) \App\Utils\Ninja::isSelfHost() && !empty($client->getSetting('portal_custom_footer')))
@if($company && $company->account->isPaid() && !empty($client->getSetting('portal_custom_footer')))
<div class="py-1 text-sm text-center text-white bg-primary">
{!! $client->getSetting('portal_custom_footer') !!}
</div>

View File

@ -164,7 +164,7 @@
@yield('footer')
@stack('footer')
@if((bool) \App\Utils\Ninja::isSelfHost() && !empty($settings->portal_custom_footer))
@if($company && $company->account->isPaid() && !empty($settings->portal_custom_footer))
<div class="py-1 text-sm text-center text-white bg-primary">
{!! $settings->portal_custom_footer !!}
</div>

View File

@ -58,6 +58,4 @@ Route::get('yodlee/onboard/{token}', [YodleeController::class, 'auth'])->name('y
Route::get('checkout/3ds_redirect/{company_key}/{company_gateway_id}/{hash}', [Checkout3dsController::class, 'index'])->middleware('domain_db')->name('checkout.3ds_redirect');
Route::get('mollie/3ds_redirect/{company_key}/{company_gateway_id}/{hash}', [Mollie3dsController::class, 'index'])->middleware('domain_db')->name('mollie.3ds_redirect');
Route::get('gocardless/ibp_redirect/{company_key}/{company_gateway_id}/{hash}', [GoCardlessController::class, 'ibpRedirect'])->middleware('domain_db')->name('gocardless.ibp_redirect');
Route::get('.well-known/apple-developer-merchantid-domain-association', [ApplePayDomainController::class, 'showAppleMerchantId']);
Route::fallback([BaseController::class, 'reactCatch']);
Route::get('.well-known/apple-developer-merchantid-domain-association', [ApplePayDomainController::class, 'showAppleMerchantId']);

View File

@ -11,6 +11,7 @@
namespace Tests\Feature;
use App\Factory\InvoiceItemFactory;
use App\Factory\InvoiceToRecurringInvoiceFactory;
use App\Factory\RecurringInvoiceToInvoiceFactory;
use App\Models\Client;
@ -51,6 +52,53 @@ class RecurringInvoiceTest extends TestCase
$this->makeTestData();
}
public function testPostRecurringInvoiceWithPlaceholderVariables()
{
$line_items = [];
$item = InvoiceItemFactory::create();
$item->quantity = 1;
$item->cost = 10;
$item->task_id = $this->encodePrimaryKey($this->task->id);
$item->expense_id = $this->encodePrimaryKey($this->expense->id);
$item->notes = "Hello this is the month of :MONTH";
$line_items[] = $item;
$data = [
'frequency_id' => 1,
'status_id' => 1,
'discount' => 0,
'is_amount_discount' => 1,
'po_number' => '3434343',
'public_notes' => 'notes',
'is_deleted' => 0,
'custom_value1' => 0,
'custom_value2' => 0,
'custom_value3' => 0,
'custom_value4' => 0,
'status' => 1,
'client_id' => $this->encodePrimaryKey($this->client->id),
'line_items' => $line_items,
'remaining_cycles' => -1,
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/recurring_invoices/', $data)
->assertStatus(200);
$arr = $response->json();
$this->assertEquals(RecurringInvoice::STATUS_DRAFT, $arr['data']['status_id']);
$notes = end($arr['data']['line_items'])['notes'];
$this->assertTrue(str_contains($notes, ':MONTH'));
}
public function testPostRecurringInvoice()
{
$data = [

View File

@ -48,27 +48,4 @@ class AutoBillInvoiceTest extends TestCase
$this->assertEquals($this->client->fresh()->credit_balance, 0);
}
// public function testAutoBillSetOffFunctionality()
// {
// $settings = $this->company->settings;
// $settings->use_credits_payment = 'off';
// $this->company->settings = $settings;
// $this->company->save();
// $this->assertEquals($this->client->balance, 10);
// $this->assertEquals($this->client->paid_to_date, 0);
// $this->assertEquals($this->client->credit_balance, 10);
// $this->invoice->service()->markSent()->autoBill()->save();
// $this->assertNotNull($this->invoice->payments());
// $this->assertEquals(0, $this->invoice->payments()->sum('payments.amount'));
// $this->assertEquals($this->client->balance, 10);
// $this->assertEquals($this->client->paid_to_date, 0);
// $this->assertEquals($this->client->credit_balance, 10);
// }
}

View File

@ -0,0 +1,86 @@
<?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://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Unit;
use App\DataMapper\InvoiceItem;
use App\Models\Invoice;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
*/
class LateFeeTest extends TestCase
{
use DatabaseTransactions;
use MockAccountData;
protected function setUp() :void
{
parent::setUp();
$this->makeTestData();
}
public function testLateFeeBalances()
{
$this->assertEquals(10, $this->client->balance);
$this->assertEquals(10, $this->invoice->balance);
$this->invoice = $this->setLateFee($this->invoice, 5, 0);
$this->assertEquals(15, $this->client->fresh()->balance);
$this->assertEquals(15, $this->invoice->fresh()->balance);
}
private function setLateFee($invoice, $amount, $percent) :Invoice
{
$temp_invoice_balance = $invoice->balance;
if ($amount <= 0 && $percent <= 0) {
return $invoice;
}
$fee = $amount;
if ($invoice->partial > 0) {
$fee += round($invoice->partial * $percent / 100, 2);
} else {
$fee += round($invoice->balance * $percent / 100, 2);
}
$invoice_item = new InvoiceItem;
$invoice_item->type_id = '5';
$invoice_item->product_key = trans('texts.fee');
$invoice_item->notes = ctrans('texts.late_fee_added', ['date' => now()]);
$invoice_item->quantity = 1;
$invoice_item->cost = $fee;
$invoice_items = $invoice->line_items;
$invoice_items[] = $invoice_item;
$invoice->line_items = $invoice_items;
/**Refresh Invoice values*/
$invoice = $invoice->calc()->getInvoice();
$invoice->client->service()->updateBalance($invoice->balance - $temp_invoice_balance)->save();
$invoice->ledger()->updateInvoiceBalance($invoice->balance - $temp_invoice_balance, "Late Fee Adjustment for invoice {$invoice->number}");
return $invoice;
}
}