Files
itflow/agent/post/quote.php
johnnyq 2fcdd6006b Invoices, quotes and recurring invoices makeover stage 4: quote lines billed one time or each month, quarter or year with a Billed choice on the add-a-line row and in Edit Line, quotes grouped into One-time, Each month, Each quarter and Each year with a total for each on the page, PDF and client view, lines dragged within their own group, Start Billing on accepted quotes putting one-time lines on a draft invoice due on the client's terms and each recurring part on a new recurring invoice from a chosen date, emailed or left as drafts, or added to one of the client's recurring invoices of the same frequency, with an option to bill the first period on the invoice, an Invoiced as card on the quote linking every invoice and recurring invoice it became including ones it added lines to, From links back on invoices and recurring invoices, monthly amounts in the quotes list and its totals, search, client portal, quote email, Send and Copy windows, quotes export and client export, and Quotes Pipeline valuing quotes at up front plus a year of recurring lines. Fixes Make Invoice invoicing a quote twice or one never accepted, and its audit entry using the current quote prefix instead of the quote's own. Adds DB 2.9.3 with item_frequency, quote_monthly_amount, invoice_quote_id and recurring_invoice_quote_id, linking invoices to their quotes from history, quoteConvert() as the one invoicing path claiming the quote in a transaction, quoteLineGroups(), quoteAmountText() and quoteMadeFrom(), reportCsv() totals written as given, and updates the 26.11 changelog
2026-10-16 14:14:53 -04:00

630 lines
24 KiB
PHP

<?php
/*
* ITFlow - GET/POST request handler for quotes
*/
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
if (isset($_POST['add_quote'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 2);
require_once 'quote_model.php';
$client_id = intval($_POST['client_id']);
enforceClientAccess();
// Atomically increment and get the new quote number
mysqli_query($mysqli, "
UPDATE settings
SET
config_quote_next_number = LAST_INSERT_ID(config_quote_next_number),
config_quote_next_number = config_quote_next_number + 1
WHERE company_id = 1
");
$quote_number = mysqli_insert_id($mysqli);
//Generate a unique URL key for clients to access
$quote_url_key = randomString(32);
mysqli_query($mysqli,"INSERT INTO quotes SET quote_prefix = '$config_quote_prefix', quote_number = $quote_number, quote_scope = '$scope', quote_date = '$date', quote_expire = '$expire', quote_discount_amount = $quote_discount, quote_amount = " . (0 - $quote_discount) . ", quote_currency_code = '$session_company_currency', quote_category_id = $category, quote_status = 'Draft', quote_url_key = '$quote_url_key', quote_client_id = $client_id");
$quote_id = mysqli_insert_id($mysqli);
mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Draft', history_description = 'Quote created!', history_quote_id = $quote_id");
logAudit("Quote", "Create", "$session_name created quote $config_quote_prefix$quote_number", $client_id, $quote_id);
triggerCustomAction('quote_create', $quote_id);
flashAlert("Quote <strong>$config_quote_prefix$quote_number</strong> created");
redirect("quote.php?quote_id=$quote_id");
}
if (isset($_POST['add_quote_copy'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 2);
$quote_id = intval($_POST['quote_id']);
$client_id = intval($_POST['client_id']);
$date = escapeSql($_POST['date']);
$expire = escapeSql($_POST['expire']);
// Source: can you read the quote you're copying FROM?
$source_client_id = intval(getFieldById('quotes', $quote_id, 'quote_client_id'));
enforceClientAccess($source_client_id);
// Destination: can you write to the client you're copying INTO?
enforceClientAccess($client_id);
$config_quote_prefix = escapeSql($config_quote_prefix);
// Atomically increment and get the new quote number
mysqli_query($mysqli, "
UPDATE settings
SET
config_quote_next_number = LAST_INSERT_ID(config_quote_next_number),
config_quote_next_number = config_quote_next_number + 1
WHERE company_id = 1
");
$quote_number = mysqli_insert_id($mysqli);
$sql = mysqli_query($mysqli,"SELECT quote_amount, quote_category_id, quote_currency_code, quote_discount_amount, quote_note,
quote_number, quote_prefix, quote_scope FROM quotes WHERE quote_id = $quote_id");
$row = mysqli_fetch_assoc($sql);
$original_quote_prefix = escapeSql($row['quote_prefix']);
$original_quote_number = escapeSql($row['quote_number']);
$quote_discount_amount = floatval($row['quote_discount_amount']);
$quote_amount = floatval($row['quote_amount']);
$quote_currency_code = escapeSql($row['quote_currency_code']);
$quote_scope = escapeSql($row['quote_scope']);
$quote_note = escapeSql($row['quote_note']);
$category_id = intval($row['quote_category_id']);
//Generate a unique URL key for clients to access
$quote_url_key = randomString(32);
mysqli_query($mysqli,"INSERT INTO quotes SET quote_prefix = '$config_quote_prefix', quote_number = $quote_number, quote_scope = '$quote_scope', quote_date = '$date', quote_expire = '$expire', quote_category_id = $category_id, quote_status = 'Draft', quote_discount_amount = $quote_discount_amount, quote_amount = $quote_amount, quote_currency_code = '$quote_currency_code', quote_note = '$quote_note', quote_url_key = '$quote_url_key', quote_client_id = $client_id");
$new_quote_id = mysqli_insert_id($mysqli);
mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Draft', history_description = 'Quote copied!', history_quote_id = $new_quote_id");
$sql_items = mysqli_query($mysqli,"SELECT item_description, item_id, item_name, item_order, item_price, item_quantity, item_subtotal,
item_tax, item_tax_id, item_total, item_frequency FROM quote_items WHERE item_quote_id = $quote_id");
while($row = mysqli_fetch_assoc($sql_items)) {
$item_id = intval($row['item_id']);
$item_name = escapeSql($row['item_name']);
$item_description = escapeSql($row['item_description']);
$item_quantity = floatval($row['item_quantity']);
$item_price = floatval($row['item_price']);
$item_subtotal = floatval($row['item_subtotal']);
$item_tax = floatval($row['item_tax']);
$item_total = floatval($row['item_total']);
$item_order = intval($row['item_order']);
$tax_id = intval($row['item_tax_id']);
$item_frequency = quoteFrequency($row['item_frequency']);
$item_frequency = $item_frequency === '' ? 'NULL' : "'$item_frequency'";
mysqli_query($mysqli,"INSERT INTO quote_items SET item_name = '$item_name', item_description = '$item_description', item_quantity = $item_quantity, item_price = $item_price, item_subtotal = $item_subtotal, item_tax = $item_tax, item_total = $item_total, item_order = $item_order, item_tax_id = $tax_id, item_frequency = $item_frequency, item_quote_id = $new_quote_id");
}
billingRecalc('quote', $new_quote_id);
logAudit("Quote", "Create", "$session_name created quote $config_quote_prefix$quote_number from quote $original_quote_prefix$original_quote_number", $client_id, $new_quote_id);
triggerCustomAction('quote_create', $new_quote_id);
flashAlert("Quote copied");
redirect("quote.php?quote_id=$new_quote_id");
}
/*
* Make Invoice / Start Billing - an accepted quote's one-time lines on a
* draft invoice, and its monthly, quarterly and yearly lines on recurring
* invoices (new, or added to the client's). quoteConvert() does it.
*/
if (isset($_POST['add_quote_to_invoice'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 2);
$quote = billingRow('quote', intval($_POST['quote_id'] ?? 0));
if (!$quote) {
flashAlert('That quote no longer exists.', 'error');
redirect();
}
$client_id = intval($quote['client_id']);
enforceClientAccess();
$recurring = [];
foreach ((array) ($_POST['recurring'] ?? []) as $frequency => $choice) {
$choice = (array) $choice;
$recurring[(string) $frequency] = [
'to' => intval($choice['to'] ?? 0),
'start' => (string) ($choice['start'] ?? ''),
'auto_send' => intval($choice['auto_send'] ?? 1),
];
}
$result = quoteConvert($quote['quote_id'], [
'date' => (string) ($_POST['date'] ?? ''),
'first_period' => !empty($_POST['first_period']),
'recurring' => $recurring,
'by' => $session_name,
], $error);
if (!$result) {
flashAlert(escapeHtml($error), 'error');
redirect();
}
flashAlert('<strong>' . escapeHtml(billingNumber('quote', $quote)) . '</strong> invoiced as ' . escapeHtml(implode(', ', $result['numbers'])));
if ($result['invoice_id']) {
redirect("invoice.php?invoice_id={$result['invoice_id']}");
}
redirect('recurring_invoice.php?recurring_invoice_id=' . array_key_first($result['recurring']));
}
if (isset($_POST['edit_quote'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 2);
require_once 'quote_model.php';
$quote_id = intval($_POST['quote_id']);
// Get Quote Details for logging
$sql = mysqli_query($mysqli,"SELECT quote_client_id, quote_number, quote_prefix FROM quotes WHERE quote_id = $quote_id");
$row = mysqli_fetch_assoc($sql);
$quote_prefix = escapeSql($row['quote_prefix']);
$quote_number = escapeSql($row['quote_number']);
$client_id = intval($row['quote_client_id']);
enforceClientAccess();
mysqli_query($mysqli,"UPDATE quotes SET quote_scope = '$scope', quote_date = '$date', quote_expire = '$expire', quote_discount_amount = $quote_discount, quote_category_id = $category WHERE quote_id = $quote_id");
billingRecalc('quote', $quote_id);
logAudit("Quote", "Edit", "$session_name edited quote $quote_prefix$quote_number", $client_id, $quote_id);
flashAlert("Quote edited");
redirect();
}
if (isset($_GET['delete_quote'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 3);
$quote_id = intval($_GET['delete_quote']);
// Get Quote Details for logging
$sql = mysqli_query($mysqli,"SELECT quote_client_id, quote_number, quote_prefix FROM quotes WHERE quote_id = $quote_id");
$row = mysqli_fetch_assoc($sql);
$quote_prefix = escapeSql($row['quote_prefix']);
$quote_number = escapeSql($row['quote_number']);
$client_id = intval($row['quote_client_id']);
enforceClientAccess();
mysqli_query($mysqli,"DELETE FROM quotes WHERE quote_id = $quote_id");
//Delete Items Associated with the Quote
$sql = mysqli_query($mysqli,"SELECT item_id FROM quote_items WHERE item_quote_id = $quote_id");
while($row = mysqli_fetch_assoc($sql)) {;
$item_id = intval($row['item_id']);
mysqli_query($mysqli,"DELETE FROM quote_items WHERE item_id = $item_id");
}
//Delete History Associated with the Quote - a recurring invoice's entry for the lines it added stays theirs
mysqli_query($mysqli,"DELETE FROM history WHERE history_quote_id = $quote_id AND history_recurring_invoice_id = 0");
mysqli_query($mysqli,"UPDATE history SET history_quote_id = 0 WHERE history_quote_id = $quote_id");
// What was made from it no longer points at it
mysqli_query($mysqli,"UPDATE invoices SET invoice_quote_id = 0 WHERE invoice_quote_id = $quote_id");
mysqli_query($mysqli,"UPDATE recurring_invoices SET recurring_invoice_quote_id = 0 WHERE recurring_invoice_quote_id = $quote_id");
logAudit("Quote", "Delete", "$session_name deleted quote $quote_prefix$quote_number", $client_id);
flashAlert("Quote <strong>$quote_prefix$quote_number</strong> deleted", 'error');
if (isset($_GET['client_id'])) {
$client_id = intval($_GET['client_id']);
redirect("client_quotes.php?client_id=$client_id");
} else {
redirect("quotes.php");
}
}
if (isset($_POST['mark_quote_sent'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 2);
$quote_id = intval($_POST['quote_id']);
$sql = mysqli_query($mysqli,"SELECT quote_client_id, quote_number, quote_prefix FROM quotes WHERE quote_id = $quote_id");
$row = mysqli_fetch_assoc($sql);
$quote_prefix = escapeSql($row['quote_prefix']);
$quote_number = escapeSql($row['quote_number']);
$client_id = intval($row['quote_client_id']);
enforceClientAccess();
// The modal offers a fixed list, so anything else is a tampered form
$sent_method = $_POST['sent_method'] ?? '';
if (!in_array($sent_method, getSentMethods(), true)) {
flashAlert("Invalid delivery method", 'error');
redirect();
}
$sent_method = escapeSql($sent_method);
$note = escapeSql(substr(trim($_POST['note'] ?? ''), 0, 500));
mysqli_query($mysqli,"UPDATE quotes SET quote_status = 'Sent' WHERE quote_id = $quote_id");
$history_description = "Quote marked sent by $session_name - $sent_method";
if (!empty($note)) {
$history_description .= "\nNote: $note";
}
logHistory('Sent', $history_description, 0, $quote_id);
logAudit("Quote", "Sent", "$session_name marked quote $quote_prefix$quote_number as sent - $sent_method", $client_id, $quote_id);
flashAlert("Quote marked sent");
redirect();
}
if (isset($_GET['accept_quote'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 2);
$quote_id = intval($_GET['accept_quote']);
$sql = mysqli_query($mysqli,"SELECT quote_client_id, quote_number, quote_prefix FROM quotes WHERE quote_id = $quote_id");
$row = mysqli_fetch_assoc($sql);
$quote_prefix = escapeSql($row['quote_prefix']);
$quote_number = escapeSql($row['quote_number']);
$client_id = intval($row['quote_client_id']);
enforceClientAccess();
mysqli_query($mysqli,"UPDATE quotes SET quote_status = 'Accepted' WHERE quote_id = $quote_id");
mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Accepted', history_description = 'Quote accepted by $session_name', history_quote_id = $quote_id");
logAudit("Quote", "Edit", "$session_name marked quote $quote_prefix$quote_number as accepted", $client_id, $quote_id);
triggerCustomAction('quote_accept', $quote_id);
flashAlert("Quote accepted");
redirect();
}
if (isset($_GET['decline_quote'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 2);
$quote_id = intval($_GET['decline_quote']);
$sql = mysqli_query($mysqli,"SELECT quote_client_id, quote_number, quote_prefix FROM quotes WHERE quote_id = $quote_id");
$row = mysqli_fetch_assoc($sql);
$quote_prefix = escapeSql($row['quote_prefix']);
$quote_number = escapeSql($row['quote_number']);
$client_id = intval($row['quote_client_id']);
enforceClientAccess();
mysqli_query($mysqli,"UPDATE quotes SET quote_status = 'Declined' WHERE quote_id = $quote_id");
mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Cancelled', history_description = 'Quote declined by $session_name', history_quote_id = $quote_id");
triggerCustomAction('quote_decline', $quote_id);
logAudit("Quote", "Edit", "$session_name marked quote $quote_prefix$quote_number as declined", $client_id, $quote_id);
flashAlert("Quote declined", 'error');
redirect();
}
if (isset($_POST['email_quote'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 2);
$quote_id = intval($_POST['quote_id']);
$sql = mysqli_query($mysqli,"SELECT client_id, client_name, quote_amount, quote_currency_code,
quote_date, quote_expire, quote_number, quote_prefix, quote_scope, quote_status,
quote_url_key FROM quotes
LEFT JOIN clients ON quote_client_id = client_id
WHERE quote_id = $quote_id"
);
$row = mysqli_fetch_assoc($sql);
$quote_prefix = escapeSql($row['quote_prefix']);
$quote_number = intval($row['quote_number']);
$quote_scope = escapeSql($row['quote_scope']);
$quote_status = escapeSql($row['quote_status']);
$quote_date = escapeSql($row['quote_date']);
$quote_expire = escapeSql($row['quote_expire']);
$quote_amount = floatval($row['quote_amount']);
$quote_url_key = escapeSql($row['quote_url_key']);
$quote_currency_code = escapeSql($row['quote_currency_code']);
$client_id = intval($row['client_id']);
$client_name = escapeSql($row['client_name']);
enforceClientAccess();
// What it comes to - up front, and each month, quarter or year
$quote_groups = quoteGroups(billingRow('quote', $quote_id, false));
$quote_groups_recurring = quoteHasRecurring($quote_groups);
$quote_cost_text = quoteAmountText($quote_groups, $row['quote_currency_code'] ?? $quote_currency_code);
// Two ways in. Quick Send skips the modal and resolves the default
// recipients server-side; the modal posts an explicit contacts[] list.
// Scoping either lookup to this quote's client is what makes a tampered
// contact_id harmless - it simply matches nothing.
if (!empty($_POST['quick_send'])) {
$recipient_filter = documentDefaultContactFilterSql('quote');
} else {
$selected_contacts = $_POST['contacts'] ?? [];
if (!is_array($selected_contacts)) {
$selected_contacts = [];
}
$selected_contact_ids = array_filter(array_unique(array_map('intval', $selected_contacts)));
if (empty($selected_contact_ids)) {
flashAlert("Select at least one contact to send to", 'error');
redirect();
}
$selected_contact_id_list = implode(',', $selected_contact_ids);
$recipient_filter = "AND contact_id IN ($selected_contact_id_list)";
}
$sql_recipients = mysqli_query(
$mysqli,
"SELECT contact_email, contact_name FROM contacts
WHERE contact_client_id = $client_id
AND contact_archived_at IS NULL
AND contact_email IS NOT NULL
AND contact_email != ''
$recipient_filter
ORDER BY contact_primary DESC, contact_billing DESC, contact_name ASC"
);
if (mysqli_num_rows($sql_recipients) == 0) {
if (!empty($_POST['quick_send'])) {
flashAlert("No default contacts to quick send to - use Send Email to choose recipients", 'error');
} else {
flashAlert("None of the selected contacts have a usable email address", 'error');
}
redirect();
}
$sql = mysqli_query($mysqli,"SELECT company_address, company_city, company_country, company_email, company_logo, company_name,
company_phone, company_phone_country_code, company_state, company_website, company_zip FROM companies WHERE company_id = 1");
$row = mysqli_fetch_assoc($sql);
$company_name = escapeSql($row['company_name']);
$company_country = escapeSql($row['company_country']);
$company_address = escapeSql($row['company_address']);
$company_city = escapeSql($row['company_city']);
$company_state = escapeSql($row['company_state']);
$company_zip = escapeSql($row['company_zip']);
$company_phone = escapeSql(formatPhoneNumber($row['company_phone'], $row['company_phone_country_code']));
$company_email = escapeSql($row['company_email']);
$company_website = escapeSql($row['company_website']);
$company_logo = escapeSql($row['company_logo']);
// Sanitize Config vars from get_settings.php
$config_quote_from_name = escapeSql($config_quote_from_name);
$config_quote_from_email = escapeSql($config_quote_from_email);
$config_base_url = escapeSql($config_base_url);
$subject = "Quote [$quote_scope]";
// One queue row per selected contact, each greeting its own recipient
$data = [];
$recipient_labels = [];
while ($recipient = mysqli_fetch_assoc($sql_recipients)) {
$contact_name = escapeSql($recipient['contact_name']);
$contact_email = escapeSql($recipient['contact_email']);
$body = "Hello $contact_name,<br><br>Thank you for your inquiry, we are pleased to provide you with the following estimate.<br><br><br>$quote_scope<br>" . ($quote_groups_recurring ? 'Cost: ' : 'Total Cost: ') . escapeSql(htmlspecialchars($quote_cost_text)) . "<br><br><br>View and accept your estimate online <a href=\'https://$config_base_url/guest/guest_view_quote.php?quote_id=$quote_id&url_key=$quote_url_key\'>here</a><br><br><br>--<br>$company_name - Sales<br>$config_quote_from_email<br>$company_phone";
$data[] = [
'from' => $config_quote_from_email,
'from_name' => $config_quote_from_name,
'recipient' => $contact_email,
'recipient_name' => $contact_name,
'subject' => $subject,
'body' => $body,
];
$recipient_labels[] = "$contact_name <$contact_email>";
logAudit("Quote", "Email", "$session_name emailed quote $quote_prefix$quote_number to $contact_email", $client_id, $quote_id);
}
addToMailQueue($data);
$recipient_list = implode(', ', $recipient_labels);
$recipient_count = count($recipient_labels);
// Update History
logHistory('Sent', "Quote emailed by $session_name to $recipient_list", 0, $quote_id);
//Don't change the status to sent if the status is anything but draft
if ($quote_status == 'Draft') {
mysqli_query($mysqli,"UPDATE quotes SET quote_status = 'Sent' WHERE quote_id = $quote_id");
}
flashAlert("Quote queued to $recipient_count " . ($recipient_count == 1 ? "recipient" : "recipients"));
redirect();
}
if (isset($_GET['mark_quote_invoiced'])) {
validateCSRFToken();
enforceUserPermission('module_sales', 2);
$quote_id = intval($_GET['mark_quote_invoiced']);
$sql = mysqli_query($mysqli,"SELECT quote_client_id, quote_number, quote_prefix FROM quotes WHERE quote_id = $quote_id");
$row = mysqli_fetch_assoc($sql);
$quote_prefix = escapeSql($row['quote_prefix']);
$quote_number = escapeSql($row['quote_number']);
$client_id = intval($row['quote_client_id']);
enforceClientAccess();
mysqli_query($mysqli,"UPDATE quotes SET quote_status = 'Invoiced' WHERE quote_id = $quote_id");
mysqli_query($mysqli,"INSERT INTO history SET history_status = 'Invoiced', history_description = 'Quote marked as invoiced', history_quote_id = $quote_id");
logAudit("Quote", "Sent", "$session_name marked quote $quote_prefix$quote_number as invoiced", $client_id, $quote_id);
flashAlert("Quote marked invoiced");
redirect();
}
if (isExportRequest('export_quotes')) {
validateCSRFToken();
// Exports are reads - see CONTRIBUTING.md
enforceUserPermission('module_sales');
$format = resolveExportFormat($_POST['export_quotes']);
// Filters inherited from the quotes page - mirrors agent/quotes.php
$filter_summary = [];
if (!empty($_POST['client_id'])) {
$client_id = intval($_POST['client_id']);
$client_query = "AND quote_client_id = $client_id";
$client_name = getFieldById('clients', $client_id, 'client_name');
$file_name_prepend = "$client_name-";
$filter_summary['Client'] = $client_name;
enforceClientAccess();
} else {
$client_query = '';
$client_id = 0; // for Logging
$file_name_prepend = "$session_company_name-";
}
// Date Filter
$dtf = escapeSql(!empty($_POST['dtf']) ? $_POST['dtf'] : '1970-01-01');
$dtt = escapeSql(!empty($_POST['dtt']) ? $_POST['dtt'] : '2099-12-31');
$date_range = formatExportDateRange($dtf, $dtt);
if ($date_range) {
$filter_summary['Dated'] = $date_range;
}
// Search Filter
$q = escapeSql($_POST['q'] ?? '');
if (!empty($q)) {
$filter_summary['Search'] = $_POST['q'];
}
$sql = mysqli_query(
$mysqli,
"SELECT client_name, quote_amount, quote_monthly_amount, quote_date, quote_number, quote_prefix, quote_scope,
quote_status FROM quotes
LEFT JOIN clients ON quote_client_id = client_id
LEFT JOIN categories ON quote_category_id = category_id
WHERE (CONCAT(quote_prefix,quote_number) LIKE '%$q%' OR quote_scope LIKE '%$q%' OR category_name LIKE '%$q%' OR quote_status LIKE '%$q%' OR quote_amount LIKE '%$q%' OR client_name LIKE '%$q%')
AND DATE(quote_date) BETWEEN '$dtf' AND '$dtt'
" . clientScopeSql('quote_client_id') . "
$client_query
ORDER BY quote_number ASC"
);
$num_rows = mysqli_num_rows($sql);
if ($num_rows > 0) {
guardExportPdfRowCount($format, $num_rows);
$export = beginExport('quotes', $format, $file_name_prepend . 'Quotes', 'Quotes', summarizeExportFilters($filter_summary));
while ($row = mysqli_fetch_assoc($sql)) {
$row['quote_number_display'] = $row['quote_prefix'] . $row['quote_number'];
addExportRow($export, $row);
}
finishExport($export);
}
logAudit("Quote", "Export", "$session_name exported $num_rows quote(s) to a " . strtoupper($format) . " file", $client_id);
exit;
}
if (isset($_GET['export_quote_pdf'])) {
validateCSRFToken();
enforceUserPermission('module_sales');
$quote = billingRow('quote', intval($_GET['export_quote_pdf']));
if (!$quote) {
flashAlert('That quote no longer exists.', 'error');
redirect('quotes.php');
}
$client_id = intval($quote['client_id']);
enforceClientAccess();
billingPdfSend(billingPdf('quote', $quote));
}