mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-07 18:44:28 -04:00
commit
9dc1c82547
62
README.md
62
README.md
@ -18,18 +18,34 @@ Just make sure to add the `invoice-ninja` tag to your question.
|
||||
|
||||
Version 5 of Invoice Ninja is here! We've taken the best parts of version 4 and bolted on all of the most requested features to produce a invoicing application like no other.
|
||||
|
||||
The new interface has a lot more functionality so it isn't a carbon copy of v4, but once you get used to the new layout and functionality we are sure you will love it!
|
||||
All Pro and Enterprise features from the hosted app are included in the open-code. We offer a $30 per year white-label license to remove the Invoice Ninja branding from client facing parts of the app.
|
||||
|
||||
## Referral Program
|
||||
* Earn 50% of Pro & Enterprise Plans up to 4 years - [Learn more](https://www.invoiceninja.com/referral-program/)
|
||||
* [Videos](https://www.youtube.com/@appinvoiceninja)
|
||||
* [API Documentation](https://app.swaggerhub.com/apis/invoiceninja/invoiceninja)
|
||||
* [APP Documentation](https://invoiceninja.github.io/)
|
||||
* [Support Forum](https://forum.invoiceninja.com)
|
||||
* [StackOverflow](https://stackoverflow.com/tags/invoice-ninja/)
|
||||
|
||||
## Mobile App
|
||||
* [iPhone](https://apps.apple.com/us/app/invoice-ninja-v5/id1503970375#?platform=iphone)
|
||||
* [Android](https://play.google.com/store/apps/details?id=com.invoiceninja.app)
|
||||
* [Linux](https://github.com/invoiceninja/flutter-mobile)
|
||||
|
||||
## Desktop App
|
||||
* [MacOS](https://apps.apple.com/app/id1503970375)
|
||||
* [Windows](https://microsoft.com/en-us/p/invoice-ninja/9n3f2bbcfdr6)
|
||||
* [MacOS Desktop](https://snapcraft.io/invoiceninja)
|
||||
|
||||
|
||||
## Installation Options
|
||||
* [Docker File](https://hub.docker.com/r/invoiceninja/invoiceninja/)
|
||||
* [Cloudron](https://cloudron.io/store/com.invoiceninja.cloudronapp.html)
|
||||
* [Softaculous](https://www.softaculous.com/apps/ecommerce/Invoice_Ninja)
|
||||
|
||||
## Recommended Providers
|
||||
* [Stripe](https://stripe.com/)
|
||||
* [Postmark](https://postmarkapp.com/)
|
||||
|
||||
## Development
|
||||
* [API Documentation](https://app.swaggerhub.com/apis/invoiceninja/invoiceninja)
|
||||
* [APP Documentation](https://invoiceninja.github.io/)
|
||||
|
||||
## Quick Start
|
||||
|
||||
@ -67,43 +83,11 @@ user: user@example.com
|
||||
pass: password
|
||||
```
|
||||
|
||||
## Contribution guide.
|
||||
|
||||
Code Style to follow [PSR-2](https://www.php-fig.org/psr/psr-2/) standards.
|
||||
|
||||
All methods names to be in CamelCase
|
||||
|
||||
All variables names to be in snake_case
|
||||
|
||||
Where practical code should be strongly typed, ie your methods must return a type ie
|
||||
|
||||
`public function doThis() : void`
|
||||
|
||||
PHP >= 7.3 allows the return type Nullable so there should be no circumstance a type cannot be return by using the following:
|
||||
|
||||
`public function doThat() ?:string`
|
||||
|
||||
To improve chances of PRs being merged please include tests to ensure your code works well and integrates with the rest of the project.
|
||||
|
||||
## Documentation
|
||||
|
||||
API documentation is hosted using Swagger and can be found [HERE](https://app.swaggerhub.com/apis/invoiceninja/invoiceninja)
|
||||
|
||||
Installation, Configuration and Troubleshooting documentation can be found [HERE] (https://invoiceninja.github.io)
|
||||
|
||||
## Credits
|
||||
* [Hillel Coren](https://hillelcoren.com/)
|
||||
* [David Bomba](https://github.com/turbo124)
|
||||
* [All contributors](https://github.com/invoiceninja/invoiceninja/graphs/contributors)
|
||||
|
||||
**Special thanks to:**
|
||||
* [Holger Lösken](https://github.com/codedge) - [codedge](http://codedge.de)
|
||||
* [Samuel Laulhau](https://github.com/lalop) - [Lalop](http://lalop.co/)
|
||||
* [Alexander Vanderveen](https://blog.technicallycomputers.ca/) - [Technically Computers](https://www.technicallycomputers.ca/)
|
||||
* [Efthymios Sarmpanis](https://github.com/esarbanis)
|
||||
* [Gianfranco Gasbarri](https://github.com/gincos)
|
||||
* [Clemens Mol](https://github.com/clemensmol)
|
||||
* [Benjamin Beganović](https://github.com/beganovich)
|
||||
* [All contributors](https://github.com/invoiceninja/invoiceninja/graphs/contributors)
|
||||
|
||||
## Security
|
||||
|
||||
|
@ -1 +1 @@
|
||||
5.5.39
|
||||
5.5.40
|
@ -22,6 +22,7 @@ use App\Jobs\Ninja\CompanySizeCheck;
|
||||
use App\Jobs\Ninja\QueueSize;
|
||||
use App\Jobs\Ninja\SystemMaintenance;
|
||||
use App\Jobs\Ninja\TaskScheduler;
|
||||
use App\Jobs\Quote\QuoteCheckExpired;
|
||||
use App\Jobs\Util\DiskCleanup;
|
||||
use App\Jobs\Util\ReminderJob;
|
||||
use App\Jobs\Util\SchedulerCheck;
|
||||
@ -70,6 +71,9 @@ class Kernel extends ConsoleKernel
|
||||
/* Sends recurring invoices*/
|
||||
$schedule->job(new RecurringExpensesCron)->dailyAt('00:10')->withoutOverlapping();
|
||||
|
||||
/* Fires notifications for expired Quotes */
|
||||
$schedule->job(new QuoteCheckExpired)->dailyAt('05:00')->withoutOverlapping();
|
||||
|
||||
/* Performs auto billing */
|
||||
$schedule->job(new AutoBillCron)->dailyAt('06:00')->withoutOverlapping();
|
||||
|
||||
|
@ -28,6 +28,7 @@ class BlackListRule implements Rule
|
||||
'wnpop.com',
|
||||
'dataservices.space',
|
||||
'karenkey.com',
|
||||
'sharklasers.com',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -87,7 +87,7 @@ class Csv extends BaseImport implements ImportInterface
|
||||
|
||||
foreach($data as $key => $value)
|
||||
{
|
||||
$data[$key]['bank.bank_integration_id'] = $this->decodePrimaryKey($this->request['bank_integration_id']);
|
||||
$data[$key]['transaction.bank_integration_id'] = $this->decodePrimaryKey($this->request['bank_integration_id']);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -31,17 +31,17 @@ class BankTransformer extends BaseTransformer
|
||||
$now = now();
|
||||
|
||||
$transformed = [
|
||||
'bank_integration_id' => $transaction['bank.bank_integration_id'],
|
||||
'transaction_id' => $this->getNumber($transaction,'bank.transaction_id'),
|
||||
'amount' => abs($this->getFloat($transaction, 'bank.amount')),
|
||||
'currency_id' => $this->getCurrencyByCode($transaction, 'bank.currency'),
|
||||
'account_type' => strlen($this->getString($transaction, 'bank.account_type')) > 1 ? $this->getString($transaction, 'bank.account_type') : 'bank',
|
||||
'category_id' => $this->getNumber($transaction, 'bank.category_id') > 0 ? $this->getNumber($transaction, 'bank.category_id') : null,
|
||||
'category_type' => $this->getString($transaction, 'bank.category_type'),
|
||||
'date' => array_key_exists('bank.date', $transaction) ? $this->parseDate($transaction['bank.date'])
|
||||
'bank_integration_id' => $transaction['transaction.bank_integration_id'],
|
||||
'transaction_id' => $this->getNumber($transaction,'transaction.transaction_id'),
|
||||
'amount' => abs($this->getFloat($transaction, 'transaction.amount')),
|
||||
'currency_id' => $this->getCurrencyByCode($transaction, 'transaction.currency'),
|
||||
'account_type' => strlen($this->getString($transaction, 'transaction.account_type')) > 1 ? $this->getString($transaction, 'transaction.account_type') : 'bank',
|
||||
'category_id' => $this->getNumber($transaction, 'transaction.category_id') > 0 ? $this->getNumber($transaction, 'transaction.category_id') : null,
|
||||
'category_type' => $this->getString($transaction, 'transaction.category_type'),
|
||||
'date' => array_key_exists('transaction.date', $transaction) ? $this->parseDate($transaction['transaction.date'])
|
||||
: now()->format('Y-m-d'),
|
||||
'bank_account_id' => array_key_exists('bank.bank_account_id', $transaction) ? $transaction['bank.bank_account_id'] : 0,
|
||||
'description' => array_key_exists('bank.description', $transaction) ? $transaction['bank.description'] : '',
|
||||
'bank_account_id' => array_key_exists('transaction.bank_account_id', $transaction) ? $transaction['transaction.bank_account_id'] : 0,
|
||||
'description' => array_key_exists('transaction.description', $transaction) ? $transaction['transaction.description'] : '',
|
||||
'base_type' => $this->calculateType($transaction),
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
@ -56,22 +56,22 @@ class BankTransformer extends BaseTransformer
|
||||
private function calculateType($transaction)
|
||||
{
|
||||
|
||||
if(array_key_exists('bank.base_type', $transaction) && ($transaction['bank.base_type'] == 'CREDIT') || strtolower($transaction['bank.base_type']) == 'deposit')
|
||||
if(array_key_exists('transaction.base_type', $transaction) && (($transaction['transaction.base_type'] == 'CREDIT') || strtolower($transaction['transaction.base_type']) == 'deposit'))
|
||||
return 'CREDIT';
|
||||
|
||||
if(array_key_exists('bank.base_type', $transaction) && ($transaction['bank.base_type'] == 'DEBIT') || strtolower($transaction['bank.bank_type']) == 'withdrawal')
|
||||
if(array_key_exists('transaction.base_type', $transaction) && (($transaction['transaction.base_type'] == 'DEBIT') || strtolower($transaction['transaction.bank_type']) == 'withdrawal'))
|
||||
return 'DEBIT';
|
||||
|
||||
if(array_key_exists('bank.category_id', $transaction))
|
||||
if(array_key_exists('transaction.category_id', $transaction))
|
||||
return 'DEBIT';
|
||||
|
||||
if(array_key_exists('bank.category_type', $transaction) && $transaction['bank.category_type'] == 'Income')
|
||||
if(array_key_exists('transaction.category_type', $transaction) && $transaction['transaction.category_type'] == 'Income')
|
||||
return 'CREDIT';
|
||||
|
||||
if(array_key_exists('bank.category_type', $transaction))
|
||||
if(array_key_exists('transaction.category_type', $transaction))
|
||||
return 'DEBIT';
|
||||
|
||||
if(array_key_exists('bank.amount', $transaction) && is_numeric($transaction['bank.amount']) && $transaction['bank.amount'] > 0)
|
||||
if(array_key_exists('transaction.amount', $transaction) && is_numeric($transaction['transaction.amount']) && $transaction['transaction.amount'] > 0)
|
||||
return 'CREDIT';
|
||||
|
||||
return 'DEBIT';
|
||||
|
112
app/Jobs/Quote/QuoteCheckExpired.php
Normal file
112
app/Jobs/Quote/QuoteCheckExpired.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Jobs\Quote;
|
||||
|
||||
use App\Jobs\Mail\NinjaMailer;
|
||||
use App\Jobs\Mail\NinjaMailerJob;
|
||||
use App\Jobs\Mail\NinjaMailerObject;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Mail\Admin\QuoteExpiredObject;
|
||||
use App\Models\Quote;
|
||||
use App\Repositories\BaseRepository;
|
||||
use App\Utils\Traits\Notifications\UserNotifies;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class QuoteCheckExpired implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, UserNotifies;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct() {}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (! config('ninja.db.multi_db_enabled'))
|
||||
return $this->checkForExpiredQuotes();
|
||||
|
||||
foreach (MultiDB::$dbs as $db) {
|
||||
|
||||
MultiDB::setDB($db);
|
||||
|
||||
$this->checkForExpiredQuotes();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function checkForExpiredQuotes()
|
||||
{
|
||||
Quote::query()
|
||||
->where('status_id', Quote::STATUS_SENT)
|
||||
->where('is_deleted', false)
|
||||
->whereNull('deleted_at')
|
||||
->whereNotNull('due_date')
|
||||
->whereHas('client', function ($query) {
|
||||
$query->where('is_deleted', 0)
|
||||
->where('deleted_at', null);
|
||||
})
|
||||
->whereHas('company', function ($query) {
|
||||
$query->where('is_disabled', 0);
|
||||
})
|
||||
// ->where('due_date', '<='. now()->toDateTimeString())
|
||||
->whereBetween('due_date', [now()->subDay()->startOfDay(), now()->startOfDay()->subSecond()])
|
||||
->cursor()
|
||||
->each(function ($quote){
|
||||
$this->queueExpiredQuoteNotification($quote);
|
||||
});
|
||||
}
|
||||
|
||||
private function queueExpiredQuoteNotification(Quote $quote)
|
||||
{
|
||||
$nmo = new NinjaMailerObject;
|
||||
$nmo->mailable = new NinjaMailer((new QuoteExpiredObject($quote, $quote->company))->build());
|
||||
$nmo->company = $quote->company;
|
||||
$nmo->settings = $quote->company->settings;
|
||||
|
||||
/* We loop through each user and determine whether they need to be notified */
|
||||
foreach ($quote->company->company_users as $company_user) {
|
||||
|
||||
/* The User */
|
||||
$user = $company_user->user;
|
||||
|
||||
if (! $user) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Returns an array of notification methods */
|
||||
$methods = $this->findUserNotificationTypes($quote->invitations()->first(), $company_user, 'quote', ['all_notifications', 'quote_expired', 'quote_expired_all']);
|
||||
|
||||
/* If one of the methods is email then we fire the EntitySentMailer */
|
||||
if (($key = array_search('mail', $methods)) !== false) {
|
||||
unset($methods[$key]);
|
||||
|
||||
$nmo->to_user = $user;
|
||||
|
||||
NinjaMailerJob::dispatch($nmo);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -21,8 +21,6 @@ class InvoicePaidActivity implements ShouldQueue
|
||||
{
|
||||
protected $activity_repo;
|
||||
|
||||
public $delay = 10;
|
||||
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
|
@ -21,8 +21,6 @@ class UpdateInvoiceActivity implements ShouldQueue
|
||||
{
|
||||
protected $activity_repo;
|
||||
|
||||
public $delay = 5;
|
||||
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
|
103
app/Mail/Admin/QuoteExpiredObject.php
Normal file
103
app/Mail/Admin/QuoteExpiredObject.php
Normal file
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Mail\Admin;
|
||||
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\Company;
|
||||
use App\Models\Quote;
|
||||
use App\Utils\Ninja;
|
||||
use App\Utils\Number;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use stdClass;
|
||||
|
||||
class QuoteExpiredObject
|
||||
{
|
||||
public $quote;
|
||||
|
||||
public $company;
|
||||
|
||||
public $settings;
|
||||
|
||||
public function __construct(Quote $quote, Company $company)
|
||||
{
|
||||
$this->quote = $quote;
|
||||
$this->company = $company;
|
||||
}
|
||||
|
||||
public function build()
|
||||
{
|
||||
MultiDB::setDb($this->company->db);
|
||||
|
||||
if (! $this->quote) {
|
||||
return;
|
||||
}
|
||||
|
||||
App::forgetInstance('translator');
|
||||
/* Init a new copy of the translator*/
|
||||
$t = app('translator');
|
||||
/* Set the locale*/
|
||||
App::setLocale($this->company->getLocale());
|
||||
/* Set customized translations _NOW_ */
|
||||
$t->replace(Ninja::transformTranslations($this->company->settings));
|
||||
|
||||
$mail_obj = new stdClass;
|
||||
$mail_obj->amount = $this->getAmount();
|
||||
$mail_obj->subject = $this->getSubject();
|
||||
$mail_obj->data = $this->getData();
|
||||
$mail_obj->markdown = 'email.admin.generic';
|
||||
$mail_obj->tag = $this->company->company_key;
|
||||
|
||||
return $mail_obj;
|
||||
}
|
||||
|
||||
private function getAmount()
|
||||
{
|
||||
return Number::formatMoney($this->quote->amount, $this->quote->client);
|
||||
}
|
||||
|
||||
private function getSubject()
|
||||
{
|
||||
return
|
||||
ctrans(
|
||||
'texts.notification_quote_expired_subject',
|
||||
[
|
||||
'client' => $this->quote->client->present()->name(),
|
||||
'invoice' => $this->quote->number,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function getData()
|
||||
{
|
||||
$settings = $this->quote->client->getMergedSettings();
|
||||
|
||||
$data = [
|
||||
'title' => $this->getSubject(),
|
||||
'message' => ctrans(
|
||||
'texts.notification_quote_expired',
|
||||
[
|
||||
'amount' => $this->getAmount(),
|
||||
'client' => $this->quote->client->present()->name(),
|
||||
'invoice' => $this->quote->number,
|
||||
]
|
||||
),
|
||||
'url' => $this->quote->invitations->first()->getAdminLink(),
|
||||
'button' => ctrans('texts.view_quote'),
|
||||
'signature' => $settings->email_signature,
|
||||
'logo' => $this->company->present()->logo(),
|
||||
'settings' => $settings,
|
||||
'whitelabel' => $this->company->account->isPaid() ? true : false,
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
@ -124,6 +124,7 @@ class RecurringInvoice extends BaseModel
|
||||
'exchange_rate',
|
||||
'vendor_id',
|
||||
'next_send_date_client',
|
||||
'uses_inclusive_taxes',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
@ -45,6 +45,22 @@ class TriggeredActions extends AbstractService
|
||||
$this->credit = $this->credit->service()->markSent()->save();
|
||||
}
|
||||
|
||||
if($this->request->has('save_default_footer') && $this->request->input('save_default_footer') == 'true') {
|
||||
$company = $this->credit->company;
|
||||
$settings = $company->settings;
|
||||
$settings->credit_footer = $this->credit->footer;
|
||||
$company->settings = $settings;
|
||||
$company->save();
|
||||
}
|
||||
|
||||
if($this->request->has('save_default_terms') && $this->request->input('save_default_terms') == 'true') {
|
||||
$company = $this->credit->company;
|
||||
$settings = $company->settings;
|
||||
$settings->credit_terms = $this->credit->terms;
|
||||
$company->settings = $settings;
|
||||
$company->save();
|
||||
}
|
||||
|
||||
return $this->credit;
|
||||
}
|
||||
|
||||
|
@ -52,6 +52,22 @@ class TriggeredActions extends AbstractService
|
||||
// $this->purchase_order = $this->purchase_order->service()->handleCancellation()->save();
|
||||
// }
|
||||
|
||||
if($this->request->has('save_default_footer') && $this->request->input('save_default_footer') == 'true') {
|
||||
$company = $this->purchase_order->company;
|
||||
$settings = $company->settings;
|
||||
$settings->purchase_order_footer = $this->purchase_order->footer;
|
||||
$company->settings = $settings;
|
||||
$company->save();
|
||||
}
|
||||
|
||||
if($this->request->has('save_default_terms') && $this->request->input('save_default_terms') == 'true') {
|
||||
$company = $this->purchase_order->company;
|
||||
$settings = $company->settings;
|
||||
$settings->purchase_order_terms = $this->purchase_order->terms;
|
||||
$company->settings = $settings;
|
||||
$company->save();
|
||||
}
|
||||
|
||||
return $this->purchase_order;
|
||||
}
|
||||
|
||||
|
@ -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.39',
|
||||
'app_tag' => '5.5.39',
|
||||
'app_version' => '5.5.40',
|
||||
'app_tag' => '5.5.40',
|
||||
'minimum_client_version' => '5.0.16',
|
||||
'terms_version' => '1.0.1',
|
||||
'api_secret' => env('API_SECRET', ''),
|
||||
|
@ -4837,7 +4837,8 @@ $LANG = array(
|
||||
'enable_applying_payments_later' => 'Enable Applying Payments Later',
|
||||
'line_item_tax_rates' => 'Line Item Tax Rates',
|
||||
'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.',
|
||||
);
|
||||
|
||||
return $LANG;
|
||||
|
394
resources/views/pdf-designs/calmness.html
Normal file
394
resources/views/pdf-designs/calmness.html
Normal file
@ -0,0 +1,394 @@
|
||||
<style id="style">
|
||||
@import url($font_url);
|
||||
|
||||
:root {
|
||||
--primary-color: $primary_color;
|
||||
--secondary-color: $secondary_color;
|
||||
--line-height: 1.6;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-family: $font_name, Helvetica, sans-serif;
|
||||
font-size: "$font_size";
|
||||
zoom: 80%;
|
||||
}
|
||||
|
||||
html {
|
||||
margin: 0;
|
||||
padding-top: 1rem;
|
||||
padding-left: 4rem;
|
||||
padding-right: 4rem;
|
||||
}
|
||||
|
||||
@page {
|
||||
margin: 0 !important;
|
||||
size: $page_size $page_layout;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.header-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 0.5fr;
|
||||
line-height: var(--line-height);
|
||||
}
|
||||
|
||||
.header-wrapper2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 0.5fr;
|
||||
margin-top: 2rem;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.company-logo {
|
||||
max-width: 65%;
|
||||
}
|
||||
|
||||
.client-and-entity-wrapper {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #d8d8d8;
|
||||
border-bottom: 1px solid #d8d8d8;
|
||||
}
|
||||
|
||||
.header-wrapper #company-address {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: var(--line-height);
|
||||
}
|
||||
|
||||
.header-wrapper #entity-details {
|
||||
margin-top: 0.5rem;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-wrapper #entity-details > tr,
|
||||
.header-wrapper #entity-details th {
|
||||
font-weight: normal;
|
||||
padding-left: 0.9rem;
|
||||
padding-top: 0.3rem;
|
||||
padding-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.header-wrapper
|
||||
#entity-details
|
||||
[data-element='entity-balance-due-label'],
|
||||
.header-wrapper
|
||||
#entity-details
|
||||
[data-element='entity-balance-due'] {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
#client-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: var(--line-height);
|
||||
}
|
||||
|
||||
[data-ref="table"] {
|
||||
margin-top: 2rem;
|
||||
min-width: 100%;
|
||||
table-layout: fixed;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.task-time-details {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
[data-ref="table"] > thead {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
[data-ref="table"] > thead > tr > th {
|
||||
padding: 1rem;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
[data-ref="table"] > thead > tr > th:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
[data-ref="table"] > tbody > tr > td {
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
[data-ref="table"] > tbody > tr > td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
[data-ref="table"] > tbody > tr:nth-child(even) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
#table-totals {
|
||||
margin-top: 0.5rem;
|
||||
display: grid;
|
||||
grid-template-columns: 1.5fr 1fr;
|
||||
padding-top: .5rem;
|
||||
gap: 80px;
|
||||
page-break-inside:auto;
|
||||
overflow: visible !important;
|
||||
font-weight: bold;
|
||||
line-height: var(--line-height);
|
||||
|
||||
}
|
||||
|
||||
#table-totals .totals-table-right-side>* {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
#table-totals>.totals-table-right-side>*> :nth-child(1) {
|
||||
text-align: left;
|
||||
margin-top: .25rem;
|
||||
padding-left: 7px;
|
||||
}
|
||||
|
||||
#table-totals>.totals-table-right-side> * > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(.25rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(.25rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
#table-totals>.totals-table-right-side>*> :nth-child(2) {
|
||||
text-align: right;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
|
||||
#entity-details {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#entity-details th {
|
||||
font-weight:normal;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
#table-totals
|
||||
> *
|
||||
[data-element='total-table-balance-due-label'],
|
||||
#table-totals
|
||||
> *
|
||||
[data-element='total-table-balance-due'] {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#table-totals > * > :last-child {
|
||||
text-align: right;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
[data-ref="total_table-footer"] {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
[data-ref="totals_table-outstanding"] {
|
||||
color: var(--primary-color)
|
||||
}
|
||||
|
||||
/** Markdown-specific styles. **/
|
||||
[data-ref="table"] h3 {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
[data-ref="totals_table-outstanding-label"],
|
||||
[data-ref="totals_table-outstanding"] {
|
||||
background-color: #e6e6e6;
|
||||
color: black;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
padding-right: 7px;
|
||||
}
|
||||
|
||||
[data-ref="statement-totals"] {
|
||||
margin-top: 1rem;
|
||||
text-align: right;
|
||||
margin-right: .75rem;
|
||||
}
|
||||
|
||||
[data-ref*=".line_total-td"] {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.repeating-footer,
|
||||
.repeating-footer-space {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.repeating-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.repeating-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
|
||||
}
|
||||
|
||||
#header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
border-top: 1px solid #000;
|
||||
width: 82%;
|
||||
min-height:100px;
|
||||
padding-top: 0.5rem;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
[data-element='product_table-product.description-td'], td {
|
||||
min-width:100%;
|
||||
max-width: 300px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
[data-ref="total_table-public_notes"] { font-weight: normal; }
|
||||
|
||||
/** Useful snippets, uncomment to enable. **/
|
||||
|
||||
/** Hide company logo **/
|
||||
/* .company-logo { display: none } */
|
||||
|
||||
/* Hide company details */
|
||||
/* # > * { display: none } */
|
||||
|
||||
/* Hide company address */
|
||||
/* #company-address > * { display: none } */
|
||||
|
||||
/* Hide terms label */
|
||||
/* [data-ref="total_table-terms-label"] { display: none } */
|
||||
|
||||
/* Hide totals table */
|
||||
/* #table-totals { display: none } */
|
||||
|
||||
/* Hide totals table left side */
|
||||
/* #table-totals div:first-child > * { display: none !important } */
|
||||
|
||||
/* Hide totals table right side */
|
||||
/* .totals-table-right-side { display: none } */
|
||||
|
||||
/** 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>
|
||||
|
||||
<table style="min-width: 100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="repeating-header-space"> </div>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div id="body">
|
||||
<div class="header-wrapper">
|
||||
<div>
|
||||
<img class="company-logo" src="$company.logo" alt="$company.name logo">
|
||||
</div>
|
||||
<div style="float:right; width:100%;">
|
||||
<div id="company-details"></div>
|
||||
<div id="company-address" style="margin-top:10px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-wrapper2">
|
||||
<div id="client-details"></div>
|
||||
<div id="vendor-details"></div>
|
||||
|
||||
<div>
|
||||
<p class="entity-label" style="font-size:32px; color:$primary_color;">$entity_label</p>
|
||||
<table id="entity-details" cellspacing="0" dir="ltr"></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="product-table" cellspacing="0" data-ref="table"></table>
|
||||
<table id="task-table" cellspacing="0" data-ref="table"></table>
|
||||
<table id="delivery-note-table" cellspacing="0" data-ref="table"></table>
|
||||
<table id="statement-invoice-table" cellspacing="0" data-ref="table"></table>
|
||||
<div id="statement-invoice-table-totals" data-ref="statement-totals"></div>
|
||||
<table id="statement-payment-table" cellspacing="0" data-ref="table"></table>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="repeating-footer-space"> </div>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<div class="repeating-header" id="header"></div>
|
||||
|
||||
<div id="footer" style="">
|
||||
<div style="width: 100%;">
|
||||
<p data-ref="total_table-footer">$entity_footer</p>
|
||||
<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',
|
||||
'vendor-details', 'client-details'
|
||||
];
|
||||
|
||||
tables.forEach((tableIdentifier) => {
|
||||
console.log(document.getElementById(tableIdentifier));
|
||||
|
||||
document.getElementById(tableIdentifier)?.childElementCount === 0
|
||||
? document.getElementById(tableIdentifier).style.setProperty('display', 'none', 'important')
|
||||
: '';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</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'
|
||||
];
|
||||
|
||||
tables.forEach((tableIdentifier) => {
|
||||
console.log(document.getElementById(tableIdentifier));
|
||||
|
||||
document.getElementById(tableIdentifier)?.childElementCount === 0
|
||||
? document.getElementById(tableIdentifier).style.setProperty('display', 'none', 'important')
|
||||
: '';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</div>
|
Loading…
x
Reference in New Issue
Block a user