Files
itflow/admin/getting_started/billing.php
johnnyq 1e5bffff3c Rates, net terms and SLAs come from the client's contract, else the instance defaults: database 3.5.0 adds a default after-hours rate (Settings > Defaults, now Rates and terms), gives each client whose own rate, net terms or SLAs differ from the defaults an active Standard terms contract setting just those (12-month term renewing itself, starting the day the client was added or the day before its earliest contract, archived with an archived client), deletes per-client SLA assignments and drops clients.client_rate and client_net_terms; time bills at the contract's rate, its after-hours rate or the default after-hours rate, else the default rate, new invoices are due on the contract's net terms else the default, and new tickets get the contract's SLA else the default; a client's own terms still apply until the update runs; the client add and edit windows lose the rate, terms and SLA fields for a note saying what the client is billed on and where it comes from, the Clients list loses Set Hourly Rate and Set Net Terms and shows the contract's rate, the overview's Terms shows the after-hours rate and whether terms are a contract's or the defaults, a contract's page and document show what it leaves empty from another contract in force or the default so signed documents stay unchanged, contract windows show the defaults as placeholders, contract names take a proper possessive; the client import and API create make a Standard terms contract for a rate or terms that aren't the defaults (returning contract_id), API update reports them in not_kept; the client export gives the rates, terms and SLAs as billed; demo data puts each demo client's terms on a contract; changelog updated
2026-10-27 22:22:34 -04:00

88 lines
6.5 KiB
PHP

<?php
/*
* Get started - Billing: the hourly rate and net terms a client is billed on
* without a contract, the tax new lines get (a rate is made when there is none), how
* invoices and quotes are numbered, and - if you like - Stripe, for clients
* to pay invoices by card (getting_started_billing). The same settings as
* Admin > Defaults, Taxes, Invoices, Quotes and Payment Providers.
*/
defined('FROM_GETTING_STARTED') || die("Direct file access is not allowed");
$gs_billing = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_default_hourly_rate, config_default_net_terms, config_invoice_prefix, config_invoice_next_number,
config_quote_prefix, config_quote_next_number FROM settings WHERE company_id = 1")) ?: [];
$gs_rate = floatval($gs_billing['config_default_hourly_rate'] ?? 0);
$gs_tax = taxRow(taxSettings()['default_id']);
$gs_stripe = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT payment_provider_id, payment_provider_active, payment_provider_private_key FROM payment_providers WHERE payment_provider_name = 'Stripe' LIMIT 1"));
$gs_accounts = [];
$gs_sql = mysqli_query($mysqli, "SELECT account_id, account_name FROM accounts WHERE account_archived_at IS NULL ORDER BY account_name ASC");
while ($gs_row = mysqli_fetch_assoc($gs_sql)) {
$gs_accounts[intval($gs_row['account_id'])] = $gs_row['account_name'];
}
$gs_stripe_on = settingsChecked('stripe_on', false);
?>
<form action="post.php" method="post" autocomplete="off" data-gs-form>
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<h2 class="h6 fw-semibold">What you charge</h2>
<div class="row g-3 mb-4">
<div class="col-sm-6">
<?= settingsInput('config_default_hourly_rate', $gs_rate > 0 ? number_format($gs_rate, 2, '.', '') : '', ['icon' => 'fa-coins', 'type' => 'number', 'label' => 'Hourly rate', 'required' => true,
'min' => '0.01', 'step' => '0.01', 'inputmode' => 'decimal', 'placeholder' => '125.00', 'suffix' => escapeHtml($session_company_currency) . ' an hour',
'help' => 'For time on a ticket - a client\'s contract can set its own.']) ?>
</div>
<div class="col-sm-6">
<?= settingsSelect('config_default_net_terms', (string) intval($gs_billing['config_default_net_terms'] ?? 30), settingsNetTerms(), ['icon' => 'fa-calendar-check', 'label' => 'Invoices are due',
'help' => 'Unless a client\'s contract sets its own.']) ?>
</div>
</div>
<h2 class="h6 fw-semibold">Tax</h2>
<?php if ($gs_tax) { ?>
<p class="mb-4" data-gs-tax="<?= intval($gs_tax['tax_id']) ?>"><i class="fas fa-check-circle text-success me-2"></i>New lines get <strong><?= escapeHtml(taxLabel($gs_tax)) ?></strong>. Rates with more than one part, and what is taxed, are in Admin &gt; Taxes.</p>
<?php } else { ?>
<p class="small text-body-secondary">The rate new invoice and quote lines get - leave it empty if you don't charge tax. A rate in parts, as GST and PST, is made in Admin &gt; Taxes.</p>
<div class="row g-3 mb-4" data-gs-tax-new>
<div class="col-sm-7">
<?= settingsInput('tax_name', '', ['icon' => 'fa-receipt', 'label' => 'Name', 'maxlength' => 200, 'placeholder' => 'Sales Tax']) ?>
</div>
<div class="col-sm-5">
<?= settingsInput('tax_percent', '', ['icon' => 'fa-calculator', 'type' => 'number', 'label' => 'Rate', 'min' => 0, 'max' => 100, 'step' => '0.0001', 'inputmode' => 'decimal', 'placeholder' => '6', 'suffix' => '%']) ?>
</div>
</div>
<?php } ?>
<h2 class="h6 fw-semibold">Invoice numbers</h2>
<div class="mb-4"><?= settingsNumberingFields('invoice', (string) ($gs_billing['config_invoice_prefix'] ?? ''), intval($gs_billing['config_invoice_next_number'] ?? 1)) ?></div>
<h2 class="h6 fw-semibold">Quote numbers</h2>
<div class="mb-4"><?= settingsNumberingFields('quote', (string) ($gs_billing['config_quote_prefix'] ?? ''), intval($gs_billing['config_quote_next_number'] ?? 1)) ?></div>
<h2 class="h6 fw-semibold">Card payments</h2>
<?php if ($gs_stripe) { ?>
<p class="mb-4" data-gs-stripe="<?= intval($gs_stripe['payment_provider_id']) ?>"><i class="fas <?= intval($gs_stripe['payment_provider_active']) ? 'fa-check-circle text-success' : 'fa-pause-circle text-body-secondary' ?> me-2"></i>Stripe is set up<?= intval($gs_stripe['payment_provider_active']) ? '' : ', and turned off' ?><?= paymentProviderMode($gs_stripe['payment_provider_private_key']) === 'test' ? ' with test keys' : '' ?> - in Admin &gt; Payment Providers.</p>
<?php } else { ?>
<div class="itflow-gs-optional mb-4" data-gs-stripe-new>
<?= settingsSwitch('stripe_on', $gs_stripe_on, ['id' => 'gsStripeOn', 'label' => 'Let clients pay invoices by card, with Stripe', 'data' => ['gs-reveal' => 'gsStripe'],
'help' => 'A Pay button on each invoice, and cards clients save for autopay.']) ?>
<div id="gsStripe" class="row g-3 mt-0" data-gs-reveal-target<?= $gs_stripe_on ? '' : ' hidden' ?>>
<div class="col-12 small text-body-secondary">The keys are in Stripe's <a href="<?= escapeHtml(paymentProviderKinds()['Stripe']['keys_url']) ?>" target="_blank" rel="noopener noreferrer">Developers &gt; API keys</a>. Test keys work too, until you are ready to take real cards.</div>
<div class="col-sm-6">
<?= settingsInput('stripe_public_key', '', ['icon' => 'fa-key', 'label' => 'Publishable key', 'maxlength' => 250, 'placeholder' => 'pk_live_...', 'autocomplete' => 'off', 'class' => 'font-monospace']) ?>
</div>
<div class="col-sm-6">
<?= settingsInput('stripe_private_key', '', ['icon' => 'fa-lock', 'type' => 'password', 'label' => 'Secret key', 'maxlength' => 250, 'placeholder' => 'sk_live_...', 'autocomplete' => 'new-password', 'class' => 'font-monospace']) ?>
</div>
<div class="col-12">
<?= settingsSelect('stripe_account', (string) (array_key_first($gs_accounts) ?? ''), $gs_accounts ?: ['' => 'No accounts yet - add one in Finance > Accounts'], ['icon' => 'fa-piggy-bank', 'label' => 'Payments go into',
'help' => 'Fees, and a card limit, are set in Admin &gt; Payment Providers.']) ?>
</div>
</div>
</div>
<?php } ?>
<button type="submit" name="getting_started_billing" value="1" class="btn btn-primary" data-gs-save>Save and continue<i class="fas fa-arrow-right ms-2"></i></button>
</form>