diff --git a/CHANGELOG.md b/CHANGELOG.md index 06a75c8c..f09eb2bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,43 @@ This file documents all notable changes made to ITFlow. +## [25.06] + +### Breaking CHANGES +- Old Document Verions will be deleted due to the major backend rewrite how document versions work. + +### Added / Changed +- Improved function for retrieving remote IP address for logging purposes. +- Ticket categories are now sorted alphabetically. +- Visiting a deleted invoice or recurring invoice now redirects to the listing page; delete option added to invoice details page. +- Added "Mark as Sent" and "Make Payment" actions directly on the invoice listing page. +- Introduced Ticket Category UI for recurring tickets. +- In Project Details, bulk actions and sorting are now available for tickets. +- Updated ticket details UI to use full card stacks with edit icons for stackable items (e.g., asset, watchers, contact). +- Added a new setting to toggle AutoStart Timer in ticket details (disabled by default). +- Applied gray accent theme in the client section to visually distinguish from the global view. +- Introduced Ticket Due Date functionality (currently supports add/edit only; more updates coming next release). +- Added settings option to display Company Tax ID on invoices. +- Client overview now displays badge counts for all entities. +- Overhauled UI for Invoice, Quote, and Recurring Invoice details; switched PDF generation to TCPDF PHP from PDFMake JS. +- Document versioning has been moved to a separate backend table to resolve permanent link issues -- SEE Breaking CHANGES. +- Migrated Document Templates, Vendor Templates, and Software/License Templates to dedicated tables. +- Added functionality to mark all tasks in a ticket as complete or incomplete. +- Asset CSV import now supports a purchase date field. +- Recurring Payments have been restructured to auto-charge on the invoice due date instead of at generation time. +- Added "Base Template" label for vendor templates when available. +- Backup and restore processes now use a temporary directory; files are cleaned up automatically if operations fail. +- Added confirmation prompt when accepting or declining a quote. +- Other minor code UI/UX cleanups and refactoring throughout the app. + +### Fixed +- Resolved issue with enabling MFA. +- Fixed UI regression where ticket listing columns would misalign. +- Non-billable invoices are no longer included in calculations. +- Addressed multiple minor reported security vulnerabilities. +- Tickets with open tasks are no longer resolved in bulk; a warning is shown along with a count of affected tickets. + + ## [25.05.1] ### Added / Changed diff --git a/accounts.php b/accounts.php index 51025e27..d1b2aecc 100644 --- a/accounts.php +++ b/accounts.php @@ -9,9 +9,6 @@ require_once "includes/inc_all.php"; // Perms enforceUserPermission('module_financial'); -//Rebuild URL -$url_query_strings_sort = http_build_query($get_copy); - $sql = mysqli_query( $mysqli, "SELECT SQL_CALC_FOUND_ROWS * FROM accounts diff --git a/admin_api.php b/admin_api.php index b018f48a..7b70d09f 100644 --- a/admin_api.php +++ b/admin_api.php @@ -6,10 +6,6 @@ $order = "ASC"; require_once "includes/inc_all_admin.php"; - -//Rebuild URL -$url_query_strings_sort = http_build_query($get_copy); - $sql = mysqli_query( $mysqli, "SELECT SQL_CALC_FOUND_ROWS * FROM api_keys diff --git a/admin_app_log.php b/admin_app_log.php index ec30f510..d98afbb0 100644 --- a/admin_app_log.php +++ b/admin_app_log.php @@ -26,9 +26,6 @@ if (isset($_GET['category']) & !empty($_GET['catergory'])) { $category_filter = ''; } -//Rebuild URL -$url_query_strings_sort = http_build_query($get_copy); - $sql = mysqli_query( $mysqli, "SELECT SQL_CALC_FOUND_ROWS * FROM app_logs diff --git a/admin_audit_log.php b/admin_audit_log.php index 9f61b0b1..04b1a41b 100644 --- a/admin_audit_log.php +++ b/admin_audit_log.php @@ -46,9 +46,6 @@ if (isset($_GET['action']) & !empty($_GET['action'])) { $action_filter = ''; } -//Rebuild URL -$url_query_strings_sort = http_build_query($get_copy); - $sql = mysqli_query( $mysqli, "SELECT SQL_CALC_FOUND_ROWS * FROM logs diff --git a/admin_category.php b/admin_category.php index 18bfa5c5..4370b5d4 100644 --- a/admin_category.php +++ b/admin_category.php @@ -13,10 +13,6 @@ if (isset($_GET['category'])) { $category = "Expense"; } -//Rebuild URL -$url_query_strings_sort = http_build_query($get_copy); - - $sql = mysqli_query( $mysqli, "SELECT SQL_CALC_FOUND_ROWS * FROM categories diff --git a/admin_custom_link.php b/admin_custom_link.php index b5e1ebf6..b87ff102 100644 --- a/admin_custom_link.php +++ b/admin_custom_link.php @@ -6,10 +6,6 @@ $order = "ASC"; require_once "includes/inc_all_admin.php"; - -//Rebuild URL -$url_query_strings_sort = http_build_query($get_copy); - $sql = mysqli_query( $mysqli, "SELECT SQL_CALC_FOUND_ROWS * FROM custom_links diff --git a/admin_document_template.php b/admin_document_template.php index c2181e93..4b0b6e5d 100644 --- a/admin_document_template.php +++ b/admin_document_template.php @@ -1,27 +1,16 @@ ">
| Revision | +Version | Date | +Name | Description | Author | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| - | - | - | + | + | + | + | + | + |
0) { ?>
diff --git a/client_overview.php b/client_overview.php
index 3ac80369..4a1c02ba 100644
--- a/client_overview.php
+++ b/client_overview.php
@@ -193,7 +193,7 @@ $sql_asset_retired = mysqli_query(
-
+
@@ -226,19 +226,42 @@ $sql_asset_retired = mysqli_query(
$contact_extension = nullable_htmlentities($row['contact_extension']);
$contact_mobile_country_code = nullable_htmlentities($row['contact_mobile_country_code']);
$contact_mobile = nullable_htmlentities(formatPhoneNumber($row['contact_mobile'], $contact_mobile_country_code));
+ $contact_photo = nullable_htmlentities($row['contact_photo']);
+ $contact_initials = initials($contact_name);
?>
-
-
+ |
+
+
+
-
+
+
+
+
+
+
+
+
+
+ - +
@@ -613,7 +636,7 @@ $sql_asset_retired = mysqli_query(
- |
+
@@ -631,7 +654,7 @@ $sql_asset_retired = mysqli_query(
?>
+ |
|
|
@@ -808,7 +808,7 @@ if ($user_config_dashboard_technical_enable == 1) {
CURRENT_DATABASE_VERSION) {
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.1.4'");
}
+ if (CURRENT_DATABASE_VERSION == '2.1.4') {
+ mysqli_query($mysqli, "ALTER TABLE `settings` ADD `config_ticket_timer_autostart` TINYINT(1) NOT NULL DEFAULT '0' AFTER `config_ticket_default_billable`");
+ mysqli_query($mysqli, "ALTER TABLE `tickets` ADD `ticket_due_at` DATETIME DEFAULT NULL AFTER `ticket_updated_at`");
+ mysqli_query($mysqli, "ALTER TABLE `companies` ADD `company_tax_id` VARCHAR(200) DEFAULT NULL AFTER `company_currency`");
+ mysqli_query($mysqli, "ALTER TABLE `settings` ADD `config_invoice_show_tax_id` TINYINT(1) NOT NULL DEFAULT '0' AFTER `config_invoice_paid_notification_email`");
+ mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.1.5'");
+ }
- // if (CURRENT_DATABASE_VERSION == '2.1.4') {
- // // Insert queries here required to update to DB version 2.1.5
+ if (CURRENT_DATABASE_VERSION == '2.1.5') {
+
+ mysqli_query($mysqli, "CREATE TABLE `document_versions` (
+ `document_version_id` INT(11) NOT NULL AUTO_INCREMENT,
+ `document_version_name` VARCHAR(200) NOT NULL,
+ `document_version_description` TEXT DEFAULT NULL,
+ `document_version_content` LONGTEXT NOT NULL,
+ `document_version_created_by` INT(11) DEFAULT 0,
+ `document_version_created_at` DATETIME NOT NULL,
+ `document_version_document_id` INT(11) NOT NULL,
+ PRIMARY KEY (`document_version_id`)
+ )");
+
+ // Delete all Current Document Versions
+ mysqli_query($mysqli, "
+ DELETE FROM `documents`
+ WHERE `document_parent` > 0 AND `document_parent` != `document_id`
+ ");
+
+ mysqli_query($mysqli, "ALTER TABLE `documents` DROP `document_parent`");
+
+ mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.1.6'");
+ }
+
+ if (CURRENT_DATABASE_VERSION == '2.1.6') {
+ mysqli_query($mysqli, "CREATE TABLE `document_templates` (
+ `document_template_id` INT(11) NOT NULL AUTO_INCREMENT,
+ `document_template_name` VARCHAR(200) NOT NULL,
+ `document_template_description` TEXT DEFAULT NULL,
+ `document_template_content` LONGTEXT NOT NULL,
+ `document_template_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `document_template_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
+ `document_template_archived_at` DATETIME NULL DEFAULT NULL,
+ `document_template_created_by` INT(11) NOT NULL DEFAULT 0,
+ `document_template_updated_by` INT(11) NOT NULL DEFAULT 0,
+ PRIMARY KEY (`document_template_id`)
+ )");
+
+ // Copy Document Templates over to new document templates table
+ mysqli_query($mysqli, "
+ INSERT INTO document_templates (
+ document_template_name,
+ document_template_description,
+ document_template_content,
+ document_template_created_at,
+ document_template_updated_at,
+ document_template_archived_at,
+ document_template_created_by,
+ document_template_updated_by
+ )
+ SELECT
+ document_name,
+ document_description,
+ document_content,
+ document_created_at,
+ document_updated_at,
+ document_archived_at,
+ document_created_by,
+ document_updated_by
+ FROM
+ documents
+ WHERE
+ document_template = 1
+ ");
+
+ mysqli_query($mysqli, "DELETE FROM documents WHERE document_template = 1");
+
+ mysqli_query($mysqli, "ALTER TABLE `documents` DROP `document_template`");
+
+ mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.1.7'");
+ }
+
+ if (CURRENT_DATABASE_VERSION == '2.1.7') {
+ mysqli_query($mysqli, "CREATE TABLE `software_templates` (
+ `software_template_id` INT(11) NOT NULL AUTO_INCREMENT,
+ `software_template_name` VARCHAR(200) NOT NULL,
+ `software_template_description` TEXT DEFAULT NULL,
+ `software_template_version` VARCHAR(200) DEFAULT NULL,
+ `software_template_type` VARCHAR(200) NOT NULL,
+ `software_template_license_type` VARCHAR(200) DEFAULT NULL,
+ `software_template_notes` TEXT DEFAULT NULL,
+ `software_template_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `software_template_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
+ `software_template_archived_at` DATETIME NULL DEFAULT NULL,
+ PRIMARY KEY (`software_template_id`)
+ )");
+
+ // Copy software Templates over to new software templates table
+ mysqli_query($mysqli, "
+ INSERT INTO software_templates (
+ software_template_name,
+ software_template_description,
+ software_template_version,
+ software_template_type,
+ software_template_license_type,
+ software_template_notes,
+ software_template_created_at,
+ software_template_updated_at,
+ software_template_archived_at
+ )
+ SELECT
+ software_name,
+ software_description,
+ software_version,
+ software_type,
+ software_license_type,
+ software_notes,
+ software_created_at,
+ software_updated_at,
+ software_archived_at
+ FROM
+ software
+ WHERE
+ software_template = 1
+ ");
+
+ mysqli_query($mysqli, "DELETE FROM software WHERE software_template = 1");
+
+ mysqli_query($mysqli, "ALTER TABLE `software` DROP `software_template`");
+
+ mysqli_query($mysqli, "ALTER TABLE `software` DROP `software_template_id`");
+
+ mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.1.8'");
+ }
+
+ if (CURRENT_DATABASE_VERSION == '2.1.8') {
+ mysqli_query($mysqli, "CREATE TABLE `vendor_templates` (
+ `vendor_template_id` INT(11) NOT NULL AUTO_INCREMENT,
+ `vendor_template_name` VARCHAR(200) NOT NULL,
+ `vendor_template_description` VARCHAR(200) DEFAULT NULL,
+ `vendor_template_contact_name` VARCHAR(200) DEFAULT NULL,
+ `vendor_template_phone_country_code` VARCHAR(10) DEFAULT NULL,
+ `vendor_template_phone` VARCHAR(200) DEFAULT NULL,
+ `vendor_template_extension` VARCHAR(200) DEFAULT NULL,
+ `vendor_template_email` VARCHAR(200) DEFAULT NULL,
+ `vendor_template_website` VARCHAR(200) DEFAULT NULL,
+ `vendor_template_hours` VARCHAR(200) DEFAULT NULL,
+ `vendor_template_sla` VARCHAR(200) DEFAULT NULL,
+ `vendor_template_code` VARCHAR(200) DEFAULT NULL,
+ `vendor_template_account_number` VARCHAR(200) DEFAULT NULL,
+ `vendor_template_notes` TEXT DEFAULT NULL,
+ `vendor_template_created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `vendor_template_updated_at` DATETIME NULL ON UPDATE CURRENT_TIMESTAMP,
+ `vendor_template_archived_at` DATETIME NULL DEFAULT NULL,
+ PRIMARY KEY (`vendor_template_id`)
+ )");
+
+ // Copy Vendor Templates over to new vendor templates table
+ mysqli_query($mysqli, "
+ INSERT INTO vendor_templates (
+ vendor_template_name,
+ vendor_template_description,
+ vendor_template_contact_name,
+ vendor_template_phone_country_code,
+ vendor_template_phone,
+ vendor_template_extension,
+ vendor_template_email,
+ vendor_template_website,
+ vendor_template_hours,
+ vendor_template_sla,
+ vendor_template_code,
+ vendor_template_account_number,
+ vendor_template_notes,
+ vendor_template_created_at,
+ vendor_template_updated_at,
+ vendor_template_archived_at
+ )
+ SELECT
+ vendor_name,
+ vendor_description,
+ vendor_contact_name,
+ vendor_phone_country_code,
+ vendor_phone,
+ vendor_extension,
+ vendor_email,
+ vendor_website,
+ vendor_hours,
+ vendor_sla,
+ vendor_code,
+ vendor_account_number,
+ vendor_notes,
+ vendor_created_at,
+ vendor_updated_at,
+ vendor_archived_at
+ FROM
+ vendors
+ WHERE
+ vendor_template = 1
+ ");
+
+ mysqli_query($mysqli, "DELETE FROM vendors WHERE vendor_template = 1");
+
+ mysqli_query($mysqli, "ALTER TABLE `vendors` DROP `vendor_template`");
+
+ mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.1.9'");
+ }
+
+ // if (CURRENT_DATABASE_VERSION == '2.1.9') {
+ // // Insert queries here required to update to DB version 2.2.0
// // Then, update the database to the next sequential version
- // mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.1.5'");
+ // mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.2.0'");
// }
} else {
diff --git a/db.sql b/db.sql
index 203f5771..cad2d325 100644
--- a/db.sql
+++ b/db.sql
@@ -555,6 +555,7 @@ CREATE TABLE `companies` (
`company_logo` varchar(250) DEFAULT NULL,
`company_locale` varchar(200) DEFAULT NULL,
`company_currency` varchar(200) NOT NULL,
+ `company_tax_id` varchar(200) DEFAULT NULL,
`company_created_at` datetime NOT NULL DEFAULT current_timestamp(),
`company_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
PRIMARY KEY (`company_id`)
@@ -829,6 +830,46 @@ CREATE TABLE `document_files` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
+--
+-- Table structure for table `document_templates`
+--
+
+DROP TABLE IF EXISTS `document_templates`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8mb4 */;
+CREATE TABLE `document_templates` (
+ `document_template_id` int(11) NOT NULL AUTO_INCREMENT,
+ `document_template_name` varchar(200) NOT NULL,
+ `document_template_description` text DEFAULT NULL,
+ `document_template_content` longtext NOT NULL,
+ `document_template_created_at` datetime NOT NULL DEFAULT current_timestamp(),
+ `document_template_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
+ `document_template_archived_at` datetime DEFAULT NULL,
+ `document_template_created_by` int(11) NOT NULL DEFAULT 0,
+ `document_template_updated_by` int(11) NOT NULL DEFAULT 0,
+ PRIMARY KEY (`document_template_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Table structure for table `document_versions`
+--
+
+DROP TABLE IF EXISTS `document_versions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8mb4 */;
+CREATE TABLE `document_versions` (
+ `document_version_id` int(11) NOT NULL AUTO_INCREMENT,
+ `document_version_name` varchar(200) NOT NULL,
+ `document_version_description` text DEFAULT NULL,
+ `document_version_content` longtext NOT NULL,
+ `document_version_created_by` int(11) DEFAULT 0,
+ `document_version_created_at` datetime NOT NULL,
+ `document_version_document_id` int(11) NOT NULL,
+ PRIMARY KEY (`document_version_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
--
-- Table structure for table `documents`
--
@@ -843,13 +884,11 @@ CREATE TABLE `documents` (
`document_content` longtext NOT NULL,
`document_content_raw` longtext NOT NULL,
`document_important` tinyint(1) NOT NULL DEFAULT 0,
- `document_parent` int(11) NOT NULL DEFAULT 0,
`document_client_visible` int(11) NOT NULL DEFAULT 1,
`document_created_at` datetime NOT NULL DEFAULT current_timestamp(),
`document_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
`document_archived_at` datetime DEFAULT NULL,
`document_accessed_at` datetime DEFAULT NULL,
- `document_template` tinyint(1) NOT NULL DEFAULT 0,
`document_folder_id` int(11) NOT NULL DEFAULT 0,
`document_created_by` int(11) NOT NULL DEFAULT 0,
`document_updated_by` int(11) NOT NULL DEFAULT 0,
@@ -1794,6 +1833,7 @@ CREATE TABLE `settings` (
`config_invoice_late_fee_enable` tinyint(1) NOT NULL DEFAULT 0,
`config_invoice_late_fee_percent` decimal(5,2) NOT NULL DEFAULT 0.00,
`config_invoice_paid_notification_email` varchar(200) DEFAULT NULL,
+ `config_invoice_show_tax_id` tinyint(1) NOT NULL DEFAULT 0,
`config_recurring_invoice_prefix` varchar(200) DEFAULT NULL,
`config_recurring_invoice_next_number` int(11) NOT NULL DEFAULT 1,
`config_quote_prefix` varchar(200) DEFAULT NULL,
@@ -1812,6 +1852,7 @@ CREATE TABLE `settings` (
`config_ticket_autoclose_hours` int(5) NOT NULL DEFAULT 72,
`config_ticket_new_ticket_notification_email` varchar(200) DEFAULT NULL,
`config_ticket_default_billable` tinyint(1) NOT NULL DEFAULT 0,
+ `config_ticket_timer_autostart` tinyint(1) NOT NULL DEFAULT 0,
`config_enable_cron` tinyint(1) NOT NULL DEFAULT 0,
`config_recurring_auto_send_invoice` tinyint(1) NOT NULL DEFAULT 1,
`config_enable_alert_domain_expire` tinyint(1) NOT NULL DEFAULT 1,
@@ -1900,14 +1941,12 @@ CREATE TABLE `software` (
`software_purchase` date DEFAULT NULL,
`software_expire` date DEFAULT NULL,
`software_notes` text DEFAULT NULL,
- `software_template` tinyint(1) NOT NULL DEFAULT 0,
`software_created_at` datetime NOT NULL DEFAULT current_timestamp(),
`software_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
`software_archived_at` datetime DEFAULT NULL,
`software_accessed_at` datetime DEFAULT NULL,
`software_vendor_id` int(11) DEFAULT 0,
`software_client_id` int(11) NOT NULL,
- `software_template_id` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`software_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
@@ -1997,6 +2036,28 @@ CREATE TABLE `software_files` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
+--
+-- Table structure for table `software_templates`
+--
+
+DROP TABLE IF EXISTS `software_templates`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8mb4 */;
+CREATE TABLE `software_templates` (
+ `software_template_id` int(11) NOT NULL AUTO_INCREMENT,
+ `software_template_name` varchar(200) NOT NULL,
+ `software_template_description` text DEFAULT NULL,
+ `software_template_version` varchar(200) DEFAULT NULL,
+ `software_template_type` varchar(200) NOT NULL,
+ `software_template_license_type` varchar(200) DEFAULT NULL,
+ `software_template_notes` text DEFAULT NULL,
+ `software_template_created_at` datetime NOT NULL DEFAULT current_timestamp(),
+ `software_template_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
+ `software_template_archived_at` datetime DEFAULT NULL,
+ PRIMARY KEY (`software_template_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
--
-- Table structure for table `tags`
--
@@ -2244,6 +2305,7 @@ CREATE TABLE `tickets` (
`ticket_url_key` varchar(200) DEFAULT NULL,
`ticket_created_at` datetime NOT NULL DEFAULT current_timestamp(),
`ticket_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
+ `ticket_due_at` datetime DEFAULT NULL,
`ticket_resolved_at` datetime DEFAULT NULL,
`ticket_archived_at` datetime DEFAULT NULL,
`ticket_first_response_at` datetime DEFAULT NULL,
@@ -2458,6 +2520,35 @@ CREATE TABLE `vendor_files` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
+--
+-- Table structure for table `vendor_templates`
+--
+
+DROP TABLE IF EXISTS `vendor_templates`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8mb4 */;
+CREATE TABLE `vendor_templates` (
+ `vendor_template_id` int(11) NOT NULL AUTO_INCREMENT,
+ `vendor_template_name` varchar(200) NOT NULL,
+ `vendor_template_description` varchar(200) DEFAULT NULL,
+ `vendor_template_contact_name` varchar(200) DEFAULT NULL,
+ `vendor_template_phone_country_code` varchar(10) DEFAULT NULL,
+ `vendor_template_phone` varchar(200) DEFAULT NULL,
+ `vendor_template_extension` varchar(200) DEFAULT NULL,
+ `vendor_template_email` varchar(200) DEFAULT NULL,
+ `vendor_template_website` varchar(200) DEFAULT NULL,
+ `vendor_template_hours` varchar(200) DEFAULT NULL,
+ `vendor_template_sla` varchar(200) DEFAULT NULL,
+ `vendor_template_code` varchar(200) DEFAULT NULL,
+ `vendor_template_account_number` varchar(200) DEFAULT NULL,
+ `vendor_template_notes` text DEFAULT NULL,
+ `vendor_template_created_at` datetime NOT NULL DEFAULT current_timestamp(),
+ `vendor_template_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
+ `vendor_template_archived_at` datetime DEFAULT NULL,
+ PRIMARY KEY (`vendor_template_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
--
-- Table structure for table `vendors`
--
@@ -2480,7 +2571,6 @@ CREATE TABLE `vendors` (
`vendor_code` varchar(200) DEFAULT NULL,
`vendor_account_number` varchar(200) DEFAULT NULL,
`vendor_notes` text DEFAULT NULL,
- `vendor_template` tinyint(1) NOT NULL DEFAULT 0,
`vendor_created_at` datetime NOT NULL DEFAULT current_timestamp(),
`vendor_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
`vendor_archived_at` datetime DEFAULT NULL,
@@ -2500,4 +2590,4 @@ CREATE TABLE `vendors` (
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
--- Dump completed on 2025-05-24 13:23:21
+-- Dump completed on 2025-06-17 22:44:10
diff --git a/expenses.php b/expenses.php
index 9e1da9ec..d9641f9c 100644
--- a/expenses.php
+++ b/expenses.php
@@ -154,7 +154,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
Copy
- SetMargins(15, 15, 15);
+ $pdf->setPrintHeader(false);
+ $pdf->setPrintFooter(false);
+ $pdf->AddPage();
+ $pdf->SetFont('helvetica', '', 10);
+
+ // Logo + Right Columns
+ $html = '
'; + + // Billing titles + $html .= '
'; + + // Date table + $html .= '
'; + + // Items header + $html .= ' +
'; + + // Totals + $html .= '
'; + + // Footer + $html .= ' ' . nl2br($config_quote_footer) . ' ';
+
+ $pdf->writeHTML($html, true, false, true, false, '');
+
+ $filename = preg_replace('/[^A-Za-z0-9_\-]/', '_', "{$quote_date}_{$company_name}_{$client_name}_Quote_{$quote_prefix}{$quote_number}");
+ $pdf->Output("$filename.pdf", 'I');
+ }
+ exit;
+}
+
+if (isset($_GET['export_invoice_pdf'])) {
+
+ $invoice_id = intval($_GET['export_invoice_pdf']);
+ $url_key = sanitizeInput($_GET['url_key']);
+
+ $sql = mysqli_query(
+ $mysqli,
+ "SELECT * FROM invoices
+ LEFT JOIN clients ON invoice_client_id = client_id
+ LEFT JOIN contacts ON clients.client_id = contacts.contact_client_id AND contact_primary = 1
+ LEFT JOIN locations ON clients.client_id = locations.location_client_id AND location_primary = 1
+ WHERE invoice_id = $invoice_id AND invoice_url_key = '$url_key'
+ LIMIT 1"
+ );
+
+ if (mysqli_num_rows($sql) == 1) {
+
+ $row = mysqli_fetch_array($sql);
+ $invoice_id = intval($row['invoice_id']);
+ $invoice_prefix = nullable_htmlentities($row['invoice_prefix']);
+ $invoice_number = intval($row['invoice_number']);
+ $invoice_scope = nullable_htmlentities($row['invoice_scope']);
+ $invoice_status = nullable_htmlentities($row['invoice_status']);
+ $invoice_date = nullable_htmlentities($row['invoice_date']);
+ $invoice_due = nullable_htmlentities($row['invoice_due']);
+ $invoice_amount = floatval($row['invoice_amount']);
+ $invoice_discount = floatval($row['invoice_discount_amount']);
+ $invoice_currency_code = nullable_htmlentities($row['invoice_currency_code']);
+ $invoice_note = nullable_htmlentities($row['invoice_note']);
+ $invoice_url_key = nullable_htmlentities($row['invoice_url_key']);
+ $invoice_created_at = nullable_htmlentities($row['invoice_created_at']);
+ $category_id = intval($row['invoice_category_id']);
+ $client_id = intval($row['client_id']);
+ $client_name = nullable_htmlentities($row['client_name']);
+ $location_address = nullable_htmlentities($row['location_address']);
+ $location_city = nullable_htmlentities($row['location_city']);
+ $location_state = nullable_htmlentities($row['location_state']);
+ $location_zip = nullable_htmlentities($row['location_zip']);
+ $location_country = nullable_htmlentities($row['location_country']);
+ $contact_email = nullable_htmlentities($row['contact_email']);
+ $contact_phone_country_code = nullable_htmlentities($row['contact_phone_country_code']);
+ $contact_phone = nullable_htmlentities(formatPhoneNumber($row['contact_phone'], $contact_phone_country_code));
+ $contact_extension = nullable_htmlentities($row['contact_extension']);
+ $contact_mobile_country_code = nullable_htmlentities($row['contact_mobile_country_code']);
+ $contact_mobile = nullable_htmlentities(formatPhoneNumber($row['contact_mobile'], $contact_mobile_country_code));
+ $client_website = nullable_htmlentities($row['client_website']);
+ $client_currency_code = nullable_htmlentities($row['client_currency_code']);
+ $client_net_terms = intval($row['client_net_terms']);
+ if ($client_net_terms == 0) {
+ $client_net_terms = $config_default_net_terms;
+ }
+
+ $sql = mysqli_query($mysqli, "SELECT * FROM companies WHERE company_id = 1");
+ $row = mysqli_fetch_array($sql);
+ $company_id = intval($row['company_id']);
+ $company_name = nullable_htmlentities($row['company_name']);
+ $company_country = nullable_htmlentities($row['company_country']);
+ $company_address = nullable_htmlentities($row['company_address']);
+ $company_city = nullable_htmlentities($row['company_city']);
+ $company_state = nullable_htmlentities($row['company_state']);
+ $company_zip = nullable_htmlentities($row['company_zip']);
+ $company_phone_country_code = nullable_htmlentities($row['company_phone_country_code']);
+ $company_phone = nullable_htmlentities(formatPhoneNumber($row['company_phone'], $company_phone_country_code));
+ $company_email = nullable_htmlentities($row['company_email']);
+ $company_website = nullable_htmlentities($row['company_website']);
+ $company_tax_id = nullable_htmlentities($row['company_tax_id']);
+ if ($config_invoice_show_tax_id && !empty($company_tax_id)) {
+ $company_tax_id_display = "Tax ID: $company_tax_id";
+ } else {
+ $company_tax_id_display = "";
+ }
+ $company_logo = nullable_htmlentities($row['company_logo']);
+ $company_locale = nullable_htmlentities($row['company_locale']);
+ //Set Currency Format
+ $currency_format = numfmt_create($company_locale, NumberFormatter::CURRENCY);
+
+ $sql_payments = mysqli_query($mysqli, "SELECT * FROM payments, accounts WHERE payment_account_id = account_id AND payment_invoice_id = $invoice_id ORDER BY payments.payment_id DESC");
+
+ //Add up all the payments for the invoice and get the total amount paid to the invoice
+ $sql_amount_paid = mysqli_query($mysqli, "SELECT SUM(payment_amount) AS amount_paid FROM payments WHERE payment_invoice_id = $invoice_id");
+ $row = mysqli_fetch_array($sql_amount_paid);
+ $amount_paid = floatval($row['amount_paid']);
+
+ $balance = $invoice_amount - $amount_paid;
+
+ //check to see if overdue
+ if ($invoice_status !== "Paid" && $invoice_status !== "Draft" && $invoice_status !== "Cancelled" && $invoice_status !== "Non-Billable") {
+ $unixtime_invoice_due = strtotime($invoice_due) + 86400;
+ if ($unixtime_invoice_due < time()) {
+ $invoice_overdue = "Overdue";
+ }
+ }
+
+ //Set Badge color based off of invoice status
+ $invoice_badge_color = getInvoiceBadgeColor($invoice_status);
+
+ require_once("../plugins/TCPDF/tcpdf.php");
+
+ // Start TCPDF
+ $pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);
+ $pdf->SetMargins(15, 15, 15);
+ $pdf->setPrintHeader(false);
+ $pdf->setPrintFooter(false);
+ $pdf->AddPage();
+ $pdf->SetFont('helvetica', '', 10);
+
+ // Logo + Right Columns
+ $html = '
'; + + // Billing titles + $html .= '
'; + + // Date table + $html .= '
'; + + // Items header + $html .= ' +
'; + + // Totals + $html .= '
'; + + // Footer + $html .= ' ' . nl2br($config_invoice_footer) . ' ';
+
+ $pdf->writeHTML($html, true, false, true, false, '');
+
+ $filename = preg_replace('/[^A-Za-z0-9_\-]/', '_', "{$invoice_date}_{$company_name}_{$client_name}_Invoice_{$invoice_prefix}{$invoice_number}");
+ $pdf->Output("$filename.pdf", 'I');
+ }
+
+ exit;
+
+}
+
if (isset($_POST['guest_quote_upload_file'])) {
$quote_id = intval($_POST['quote_id']);
$url_key = sanitizeInput($_POST['url_key']);
diff --git a/guest/guest_view_invoice.php b/guest/guest_view_invoice.php
index 74cd64d8..175cef38 100644
--- a/guest/guest_view_invoice.php
+++ b/guest/guest_view_invoice.php
@@ -77,6 +77,12 @@ $company_phone_country_code = nullable_htmlentities($row['company_phone_country_
$company_phone = nullable_htmlentities(formatPhoneNumber($row['company_phone'], $company_phone_country_code));
$company_email = nullable_htmlentities($row['company_email']);
$company_website = nullable_htmlentities($row['company_website']);
+$company_tax_id = nullable_htmlentities($row['company_tax_id']);
+if ($config_invoice_show_tax_id && !empty($company_tax_id)) {
+ $company_tax_id_display = "Tax ID: $company_tax_id";
+} else {
+ $company_tax_id_display = "";
+}
$company_logo = nullable_htmlentities($row['company_logo']);
if (!empty($company_logo)) {
$company_logo_base64 = base64_encode(file_get_contents("../uploads/settings/$company_logo"));
@@ -120,7 +126,7 @@ $balance = $invoice_amount - $amount_paid;
//check to see if overdue
$invoice_color = $invoice_badge_color; // Default
-if ($invoice_status !== "Paid" && $invoice_status !== "Draft" && $invoice_status !== "Cancelled") {
+if ($invoice_status !== "Paid" && $invoice_status !== "Draft" && $invoice_status !== "Cancelled" && $invoice_status !== "Non-Billable") {
$unixtime_invoice_due = strtotime($invoice_due) + 86400;
if ($unixtime_invoice_due < time()) {
$invoice_color = "text-danger";
@@ -133,7 +139,7 @@ $sql_invoice_items = mysqli_query($mysqli, "SELECT * FROM invoice_items WHERE it
// Get Total Account Balance
//Add up all the payments for the invoice and get the total amount paid to the invoice
-$sql_invoice_amounts = mysqli_query($mysqli, "SELECT SUM(invoice_amount) AS invoice_amounts FROM invoices WHERE invoice_client_id = $client_id AND invoice_status NOT LIKE 'Draft' AND invoice_status NOT LIKE 'Cancelled' ");
+$sql_invoice_amounts = mysqli_query($mysqli, "SELECT SUM(invoice_amount) AS invoice_amounts FROM invoices WHERE invoice_client_id = $client_id AND invoice_status != 'Draft' AND invoice_status != 'Cancelled' AND invoice_status != 'Non-Billable'");
$row = mysqli_fetch_array($sql_invoice_amounts);
$account_balance = floatval($row['invoice_amounts']);
@@ -153,669 +159,193 @@ if ($balance > 0) {
?>
-
-
-
-
-
- Account Balance:-
-
-
+
+
+
+
-
+
- Account Balance:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Invoice
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
+
-
-
-
+
-
-
-
-
- -
-
-
-
+
+ |
| Subtotal: | ++ |
| Discount: | +- | +
| Tax: | ++ |
| Total: | ++ |
Paid: |
+ + |
| Balance: | ++ |
| Quote #: | ++ |
|---|---|
| Date: | ++ |
| Expires: | ++ |
| Item | +Description | +Qty | +Unit Price | +Tax | +Amount | +
|---|
| Date | -- |
| Expire | -- |
| Product | -Description | -Qty | -Price | -Tax | -Total | -
|---|---|---|---|---|---|
| - | - | - | - | - | - |
| Subtotal | -- |
| Discount | -- |
| Tax | -- |
| Total | -- |
| Subtotal: | ++ |
| Discount: | ++ |
| Tax: | ++ |
| Total: | ++ |