$config_quote_prefix$quote_number 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); if (!$row) { flashAlert('That quote no longer exists.', 'error'); redirect(); } $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' => stripslashes($session_name), ], $error); if (!$result) { flashAlert(escapeHtml($error), 'error'); redirect(); } flashAlert('' . escapeHtml(billingNumber('quote', $quote)) . ' 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); if (!$row) { flashAlert('That quote no longer exists.', 'error'); redirect(); } $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); if (!$row) { flashAlert('That quote no longer exists.', 'error'); redirect(); } $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 $quote_prefix$quote_number 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); if (!$row) { flashAlert('That quote no longer exists.', 'error'); redirect(); } $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", "Send", "$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); if (!$row) { flashAlert('That quote no longer exists.', 'error'); redirect(); } $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); if (!$row) { flashAlert('That quote no longer exists.', 'error'); redirect(); } $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 = billingRow('quote', intval($_POST['quote_id'] ?? 0)); if (!$quote) { flashAlert('That quote no longer exists.', 'error'); redirect('quotes.php'); } $quote_id = intval($quote['quote_id']); $client_id = intval($quote['client_id']); $number = billingNumber('quote', $quote); enforceClientAccess(); // Two ways in. Quick Send skips the window and goes to the primary // contact; the window posts the contacts picked. Either way only this // client's contacts can match, so a tampered id finds nobody. if (!empty($_POST['quick_send'])) { $recipients = billingEmailRecipients($client_id, null, 'quote'); if (!$recipients) { flashAlert("No default contacts to quick send to - use Send to choose recipients", 'error'); redirect(); } } else { $recipients = billingEmailRecipients($client_id, (array) ($_POST['contacts'] ?? [])); if (!$recipients) { flashAlert(empty($_POST['contacts']) ? "Select at least one contact to send to" : "None of the selected contacts have a usable email address", 'error'); redirect(); } } // functions/billing_email.php - the quote, with the agent's note and the PDF [$subject, $email] = billingQuoteEmail($quote, ['note' => trim(mb_substr((string) ($_POST['note'] ?? ''), 0, 2000))]); $attach_pdf = !empty($_POST['quick_send']) ? billingReminderRules()['attach_pdf'] : !empty($_POST['attach_pdf']); $attach = []; if ($attach_pdf && ($file = billingEmailPdf(billingPdf('quote', $quote)))) { $attach[] = $file; } $sent = billingEmailQueue($recipients, $subject, $email, ['from' => 'quote', 'attach' => $attach]); billingLog('quote', $quote_id, $quote['quote_status'] === 'Draft' ? 'Sent' : $quote['quote_status'], 'Quote emailed by ' . stripslashes($session_name) . ' to ' . implode(', ', $sent) . ($attach ? ' with the PDF' : '')); foreach ($recipients as $recipient) { logAudit("Quote", "Email", "$session_name emailed quote " . escapeSql($number) . " to " . escapeSql($recipient['email']), $client_id, $quote_id); } // Sending a draft sends it - anything further along keeps its status if ($quote['quote_status'] === 'Draft') { mysqli_query($mysqli, "UPDATE quotes SET quote_status = 'Sent' WHERE quote_id = $quote_id"); } flashAlert("Quote queued to " . count($sent) . " " . (count($sent) === 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); if (!$row) { flashAlert('That quote no longer exists.', 'error'); redirect(); } $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", "Send", "$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']; } // Category Filter $category_query = ''; if (!empty($_POST['category'])) { $filter_category_id = intval($_POST['category']); $category_query = "AND quote_category_id = $filter_category_id"; $filter_summary['Category'] = getFieldById('categories', $filter_category_id, 'category_name'); } $sql = mysqli_query( $mysqli, "SELECT client_name, quote_id, quote_amount, quote_discount_amount, quote_monthly_amount, quote_date, quote_number, quote_prefix, quote_scope, quote_status, category_name 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 $category_query ORDER BY quote_number ASC" ); $num_rows = mysqli_num_rows($sql); guardExportHasRows($num_rows, 'quotes'); 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']; if (isset($export['columns']['quote_tax'])) { // The tax on what it bills up front, beside its amount $quote_groups = quoteLineGroups(billingItems('quote', $row['quote_id']), floatval($row['quote_discount_amount'])); $row['quote_tax'] = round(floatval($quote_groups['']['tax'] ?? 0), 2); } 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)); }