Settings > Invoice says) and its rate are filled * in, ready to change - the rate the contract in force's (its after-hours * one for a ticket opened outside business hours), else the client's. A ticket still open can be billed from * here too - time logged after that isn't billed. Write it (with AI, for * the Billing use case) says what was done in the description, from the * replies and notes, for the client. ?ticket_id= */ require_once '../../../includes/modal_header.php'; enforceUserPermission('module_sales', 2); $ticket = ticketForModal($_GET['ticket_id'] ?? 0, 2); $ticket_id = intval($ticket['ticket_id']); $reference = ticketReference($ticket); if (!$client_id) { exit(json_encode(['error' => 'Move the ticket to a client before invoicing it.'])); } if (intval($ticket['ticket_invoice_id'])) { $on = billingRow('invoice', intval($ticket['ticket_invoice_id'])); exit(json_encode(['error' => "$reference is already billed" . ($on ? ' on ' . billingNumber('invoice', $on) : '') . '.'])); } if (!intval($ticket['ticket_billable'])) { exit(json_encode(['error' => "$reference isn't billable - mark it billable first."])); } $unbilled = billingUnbilledTickets($client_id, [$ticket_id], true); $row = $unbilled[$ticket_id] ?? null; // All covered by a contract's prepaid hours - there is nothing to bill unless it is put back $prepaid_uses = !$row ? contractHoursTicketUses($ticket_id) : []; if ($prepaid_uses) { exit(json_encode(['error' => "$reference is covered by prepaid hours on " . $prepaid_uses[0]['contract_name'] . " - put it back on the contract to bill it instead."])); } if (!$row) { exit(json_encode(['error' => "$reference can't be billed - it is archived."])); } $rules = billingTimeRules(); $open = !in_array(intval($row['ticket_status']), [TICKET_STATUS_RESOLVED, TICKET_STATUS_CLOSED], true); $currency = (string) $session_company_currency; // The time on it, by who logged it $agents = []; $sql = mysqli_query($mysqli, "SELECT user_name, COUNT(*) AS entries, SUM(TIME_TO_SEC(ticket_reply_time_worked)) AS seconds FROM ticket_replies LEFT JOIN users ON user_id = ticket_reply_by WHERE ticket_reply_ticket_id = $ticket_id AND ticket_reply_archived_at IS NULL AND ticket_reply_time_worked > '00:00:00' GROUP BY ticket_reply_by ORDER BY seconds DESC"); while ($agent = mysqli_fetch_assoc($sql)) { $agents[] = $agent; } $drafts = []; $sql = mysqli_query($mysqli, "SELECT invoice_id, invoice_prefix, invoice_number, invoice_scope, invoice_amount FROM invoices WHERE invoice_client_id = $client_id AND invoice_status = 'Draft' AND invoice_archived_at IS NULL ORDER BY invoice_date DESC, invoice_id DESC"); while ($draft = mysqli_fetch_assoc($sql)) { $drafts[intval($draft['invoice_id'])] = $draft; } $taxes = billingTaxes(); $ai_describe = aiReady('Billing'); $scopes = ["Ticket $reference - " . $row['ticket_subject'], 'Support - ' . date('F Y')]; ob_start(); ?>