mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-25 00:44:03 -04:00
Compare commits
2 Commits
master
...
ubl-invoic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c55e7fcbd | ||
|
|
4d9540633a |
File diff suppressed because it is too large
Load Diff
24
app/Database/Migrations/20260304000000_AddUBLConfig.php
Normal file
24
app/Database/Migrations/20260304000000_AddUBLConfig.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddUBLConfig extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
log_message('info', 'Adding UBL configuration.');
|
||||
|
||||
$config_values = [
|
||||
['key' => 'invoice_format', 'value' => 'pdf_only'],
|
||||
];
|
||||
|
||||
$this->db->table('app_config')->ignore(true)->insertBatch($config_values);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->db->table('app_config')->whereIn('key', ['invoice_format'])->delete();
|
||||
}
|
||||
}
|
||||
229
app/Helpers/country_helper.php
Normal file
229
app/Helpers/country_helper.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
use Config\OSPOS;
|
||||
|
||||
/**
|
||||
* Country code helper for mapping country names to ISO 3166-1 alpha-2 codes
|
||||
*/
|
||||
if (!function_exists('getCountryCode')) {
|
||||
/**
|
||||
* Convert country name to ISO 3166-1 alpha-2 code
|
||||
*
|
||||
* @param string $countryName Country name (full name in English)
|
||||
* @return string ISO 3166-1 alpha-2 code, or 'BE' as default for Belgium
|
||||
*/
|
||||
function getCountryCode(string $countryName): string
|
||||
{
|
||||
if (empty($countryName)) {
|
||||
return 'BE'; // Default to Belgium
|
||||
}
|
||||
|
||||
$countryMap = [
|
||||
// Major countries
|
||||
'Belgium' => 'BE',
|
||||
'Belgique' => 'BE',
|
||||
'België' => 'BE',
|
||||
'United States' => 'US',
|
||||
'USA' => 'US',
|
||||
'United States of America' => 'US',
|
||||
'United Kingdom' => 'GB',
|
||||
'UK' => 'GB',
|
||||
'Great Britain' => 'GB',
|
||||
'France' => 'FR',
|
||||
'Germany' => 'DE',
|
||||
'Deutschland' => 'DE',
|
||||
'Netherlands' => 'NL',
|
||||
'The Netherlands' => 'NL',
|
||||
'Nederland' => 'NL',
|
||||
'Italy' => 'IT',
|
||||
'Italia' => 'IT',
|
||||
'Spain' => 'ES',
|
||||
'España' => 'ES',
|
||||
'Poland' => 'PL',
|
||||
'Polska' => 'PL',
|
||||
'Portugal' => 'PT',
|
||||
'Sweden' => 'SE',
|
||||
'Sverige' => 'SE',
|
||||
'Norway' => 'NO',
|
||||
'Norge' => 'NO',
|
||||
'Denmark' => 'DK',
|
||||
'Danmark' => 'DK',
|
||||
'Finland' => 'FI',
|
||||
'Suomi' => 'FI',
|
||||
'Switzerland' => 'CH',
|
||||
'Suisse' => 'CH',
|
||||
'Schweiz' => 'CH',
|
||||
'Austria' => 'AT',
|
||||
'Österreich' => 'AT',
|
||||
'Ireland' => 'IE',
|
||||
'Luxembourg' => 'LU',
|
||||
'Greece' => 'GR',
|
||||
'Czech Republic' => 'CZ',
|
||||
'Czechia' => 'CZ',
|
||||
'Hungary' => 'HU',
|
||||
'Romania' => 'RO',
|
||||
'Bulgaria' => 'BG',
|
||||
'Slovakia' => 'SK',
|
||||
'Slovenia' => 'SI',
|
||||
'Estonia' => 'EE',
|
||||
'Latvia' => 'LV',
|
||||
'Lithuania' => 'LT',
|
||||
'Croatia' => 'HR',
|
||||
'Serbia' => 'RS',
|
||||
'Montenegro' => 'ME',
|
||||
'Bosnia and Herzegovina' => 'BA',
|
||||
'North Macedonia' => 'MK',
|
||||
'Albania' => 'AL',
|
||||
'Kosovo' => 'XK',
|
||||
'Turkey' => 'TR',
|
||||
'Türkiye' => 'TR',
|
||||
'Russia' => 'RU',
|
||||
'Russian Federation' => 'RU',
|
||||
'Ukraine' => 'UA',
|
||||
'Belarus' => 'BY',
|
||||
'Moldova' => 'MD',
|
||||
'Georgia' => 'GE',
|
||||
'Armenia' => 'AM',
|
||||
'Azerbaijan' => 'AZ',
|
||||
'Kazakhstan' => 'KZ',
|
||||
'Uzbekistan' => 'UZ',
|
||||
|
||||
// Other major economies
|
||||
'China' => 'CN',
|
||||
'Japan' => 'JP',
|
||||
'South Korea' => 'KR',
|
||||
'Korea' => 'KR',
|
||||
'India' => 'IN',
|
||||
'Australia' => 'AU',
|
||||
'New Zealand' => 'NZ',
|
||||
'Canada' => 'CA',
|
||||
'Mexico' => 'MX',
|
||||
'Brazil' => 'BR',
|
||||
'Argentina' => 'AR',
|
||||
'Chile' => 'CL',
|
||||
'Colombia' => 'CO',
|
||||
'Peru' => 'PE',
|
||||
'South Africa' => 'ZA',
|
||||
'Egypt' => 'EG',
|
||||
'Nigeria' => 'NG',
|
||||
'Kenya' => 'KE',
|
||||
'Morocco' => 'MA',
|
||||
|
||||
// If already ISO code, return as-is
|
||||
'BE' => 'BE',
|
||||
'US' => 'US',
|
||||
'GB' => 'GB',
|
||||
'FR' => 'FR',
|
||||
'DE' => 'DE',
|
||||
'NL' => 'NL',
|
||||
'IT' => 'IT',
|
||||
'ES' => 'ES',
|
||||
'PT' => 'PT',
|
||||
'SE' => 'SE',
|
||||
'NO' => 'NO',
|
||||
'DK' => 'DK',
|
||||
'FI' => 'FI',
|
||||
'CH' => 'CH',
|
||||
'AT' => 'AT',
|
||||
'IE' => 'IE',
|
||||
'LU' => 'LU',
|
||||
'GR' => 'GR',
|
||||
'CZ' => 'CZ',
|
||||
'HU' => 'HU',
|
||||
'RO' => 'RO',
|
||||
'BG' => 'BG',
|
||||
'SK' => 'SK',
|
||||
'SI' => 'SI',
|
||||
'EE' => 'EE',
|
||||
'LV' => 'LV',
|
||||
'LT' => 'LT',
|
||||
'HR' => 'HR',
|
||||
'RS' => 'RS',
|
||||
'ME' => 'ME',
|
||||
'BA' => 'BA',
|
||||
'MK' => 'MK',
|
||||
'AL' => 'AL',
|
||||
'TR' => 'TR',
|
||||
'RU' => 'RU',
|
||||
'UA' => 'UA',
|
||||
];
|
||||
|
||||
// Try exact match first
|
||||
$normalized = trim($countryName);
|
||||
if (isset($countryMap[$normalized])) {
|
||||
return $countryMap[$normalized];
|
||||
}
|
||||
|
||||
// Try case-insensitive match
|
||||
$normalizedLower = strtolower($normalized);
|
||||
foreach ($countryMap as $key => $code) {
|
||||
if (strtolower($key) === $normalizedLower) {
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
|
||||
// Try partial match (e.g., "United States" → "US")
|
||||
foreach ($countryMap as $key => $code) {
|
||||
if (stripos($key, $normalized) !== false || stripos($normalized, $key) !== false) {
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
|
||||
// Try matching ISO code directly
|
||||
if (preg_match('/^[A-Z]{2}$/i', $normalized)) {
|
||||
return strtoupper($normalized);
|
||||
}
|
||||
|
||||
// Check if the country_codes config has a default
|
||||
$config = config(OSPOS::class)->settings;
|
||||
if (isset($config['country_codes']) && !empty($config['country_codes'])) {
|
||||
$countries = explode(',', $config['country_codes']);
|
||||
if (!empty($countries)) {
|
||||
return strtoupper(trim($countries[0]));
|
||||
}
|
||||
}
|
||||
|
||||
// Default to Belgium (for Peppol compliance in Belgium)
|
||||
return 'BE';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('getCurrencyCode')) {
|
||||
/**
|
||||
* Get ISO 4217 currency code for a country
|
||||
*
|
||||
* @param string $countryCode ISO 3166-1 alpha-2 country code
|
||||
* @return string ISO 4217 currency code
|
||||
*/
|
||||
function getCurrencyCode(string $countryCode): string
|
||||
{
|
||||
$currencyMap = [
|
||||
'BE' => 'EUR',
|
||||
'FR' => 'EUR',
|
||||
'DE' => 'EUR',
|
||||
'NL' => 'EUR',
|
||||
'IT' => 'EUR',
|
||||
'ES' => 'EUR',
|
||||
'PT' => 'EUR',
|
||||
'IE' => 'EUR',
|
||||
'AT' => 'EUR',
|
||||
'LU' => 'EUR',
|
||||
'FI' => 'EUR',
|
||||
'GR' => 'EUR',
|
||||
'US' => 'USD',
|
||||
'GB' => 'GBP',
|
||||
'CH' => 'CHF',
|
||||
'JP' => 'JPY',
|
||||
'CN' => 'CNY',
|
||||
'CA' => 'CAD',
|
||||
'AU' => 'AUD',
|
||||
'NZ' => 'NZD',
|
||||
'IN' => 'INR',
|
||||
'BR' => 'BRL',
|
||||
'MX' => 'MXN',
|
||||
'ZA' => 'ZAR',
|
||||
];
|
||||
|
||||
return $currencyMap[$countryCode] ?? 'EUR'; // Default to EUR
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,7 @@ function get_sales_manage_table_headers(): string
|
||||
if ($config['invoice_enable']) {
|
||||
$headers[] = ['invoice_number' => lang('Sales.invoice_number')];
|
||||
$headers[] = ['invoice' => '', 'sortable' => false, 'escape' => false];
|
||||
$headers[] = ['ubl' => '', 'sortable' => false, 'escape' => false];
|
||||
}
|
||||
|
||||
$headers[] = ['receipt' => '', 'sortable' => false, 'escape' => false];
|
||||
@@ -121,6 +122,13 @@ function get_sale_data_row(object $sale): array
|
||||
'<span class="glyphicon glyphicon-list-alt"></span>',
|
||||
['title' => lang('Sales.show_invoice')]
|
||||
);
|
||||
$row['ubl'] = empty($sale->invoice_number)
|
||||
? '-'
|
||||
: anchor(
|
||||
"$controller/ublInvoice/$sale->sale_id",
|
||||
'<span class="glyphicon glyphicon-download"></span>',
|
||||
['title' => lang('Sales.download_ubl'), 'target' => '_blank']
|
||||
);
|
||||
}
|
||||
|
||||
$row['receipt'] = anchor(
|
||||
|
||||
@@ -1,234 +1,237 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"customers_available_points" => "Points Available",
|
||||
"rewards_package" => "Rewards",
|
||||
"rewards_remaining_balance" => "Reward Points remaining value is ",
|
||||
"account_number" => "Account #",
|
||||
"add_payment" => "Add Payment",
|
||||
"amount_due" => "Amount Due",
|
||||
"amount_tendered" => "Amount Tendered",
|
||||
"authorized_signature" => "Authorized Signature",
|
||||
"bank_transfer" => "Bank Transfer",
|
||||
"cancel_sale" => "Cancel",
|
||||
"cash" => "Cash",
|
||||
"cash_1" => "",
|
||||
"cash_2" => "",
|
||||
"cash_3" => "",
|
||||
"cash_4" => "",
|
||||
"cash_adjustment" => "Cash Adjustment",
|
||||
"cash_deposit" => "Cash Deposit",
|
||||
"cash_filter" => "Cash",
|
||||
"change_due" => "Change Due",
|
||||
"change_price" => "Change Selling Price",
|
||||
"check" => "Check",
|
||||
"check_balance" => "Check remainder",
|
||||
"check_filter" => "Check",
|
||||
"close" => "",
|
||||
"comment" => "Comment",
|
||||
"comments" => "Comments",
|
||||
"company_name" => "",
|
||||
"complete" => "",
|
||||
"complete_sale" => "Complete",
|
||||
"confirm_cancel_sale" => "Are you sure you want to clear this sale? All items will be cleared.",
|
||||
"confirm_delete" => "Are you sure you want to delete the selected Sale(s)?",
|
||||
"confirm_restore" => "Are you sure you want to restore the selected Sale(s)?",
|
||||
"credit" => "Credit Card",
|
||||
"credit_deposit" => "Credit Deposit",
|
||||
"credit_filter" => "Credit Card",
|
||||
"current_table" => "",
|
||||
"customer" => "Customer",
|
||||
"customer_address" => "Address",
|
||||
"customer_discount" => "Discount",
|
||||
"customer_email" => "Email",
|
||||
"customer_location" => "Location",
|
||||
"customer_mailchimp_status" => "MailChimp Status",
|
||||
"customer_optional" => "(Required for Due Payments)",
|
||||
"customer_required" => "(Required)",
|
||||
"customer_total" => "Total",
|
||||
"customer_total_spent" => "",
|
||||
"daily_sales" => "",
|
||||
"date" => "Sale Date",
|
||||
"date_range" => "Date Range",
|
||||
"date_required" => "A correct date must be entered.",
|
||||
"date_type" => "Date is a required field.",
|
||||
"debit" => "Debit Card",
|
||||
"debit_filter" => "",
|
||||
"delete" => "Allow Delete",
|
||||
"delete_confirmation" => "Are you sure you want to delete this sale? This action cannot be undone.",
|
||||
"delete_entire_sale" => "Delete Entire Sale",
|
||||
"delete_successful" => "Sale delete successful.",
|
||||
"delete_unsuccessful" => "Sale delete failed.",
|
||||
"description_abbrv" => "Desc.",
|
||||
"discard" => "Discard",
|
||||
"discard_quote" => "",
|
||||
"discount" => "Disc",
|
||||
"discount_included" => "% Discount",
|
||||
"discount_short" => "%",
|
||||
"due" => "Due",
|
||||
"due_filter" => "Due",
|
||||
"edit" => "Edit",
|
||||
"edit_item" => "Edit Item",
|
||||
"edit_sale" => "Edit Sale",
|
||||
"email_receipt" => "Email Receipt",
|
||||
"employee" => "Employee",
|
||||
"entry" => "Entry",
|
||||
"error_editing_item" => "Error editing item",
|
||||
"negative_price_invalid" => "Price cannot be negative.",
|
||||
"negative_quantity_invalid" => "Quantity cannot be negative.",
|
||||
"negative_discount_invalid" => "Discount cannot be negative.",
|
||||
"discount_percent_exceeds_100" => "Percentage discount cannot exceed 100%.",
|
||||
"discount_exceeds_item_total" => "Discount cannot exceed the item total.",
|
||||
"negative_total_invalid" => "Sale total cannot be negative. Check item discounts and quantities.",
|
||||
"find_or_scan_item" => "Find or Scan Item",
|
||||
"find_or_scan_item_or_receipt" => "Find or Scan Item or Receipt",
|
||||
"giftcard" => "Gift Card",
|
||||
"giftcard_balance" => "Gift Card Balance",
|
||||
"giftcard_filter" => "",
|
||||
"giftcard_number" => "Gift Card Number",
|
||||
"group_by_category" => "Group by Category",
|
||||
"group_by_type" => "Group by Type",
|
||||
"hsn" => "HSN",
|
||||
"id" => "Sale ID",
|
||||
"include_prices" => "Include Prices?",
|
||||
"invoice" => "Invoice",
|
||||
"invoice_confirm" => "This invoice will be sent to",
|
||||
"invoice_enable" => "Invoice Number",
|
||||
"invoice_filter" => "Invoices",
|
||||
"invoice_no_email" => "This customer does not have a valid email address.",
|
||||
"invoice_number" => "Invoice #",
|
||||
"invoice_number_duplicate" => "Invoice Number {0} must be unique.",
|
||||
"invoice_sent" => "Invoice sent to",
|
||||
"invoice_total" => "Invoice Total",
|
||||
"invoice_type_custom_invoice" => "Custom Invoice (custom_invoice.php)",
|
||||
"invoice_type_custom_tax_invoice" => "Custom Tax Invoice (custom_tax_invoice.php)",
|
||||
"invoice_type_invoice" => "Invoice (invoice.php)",
|
||||
"invoice_type_tax_invoice" => "Tax Invoice (tax_invoice.php)",
|
||||
"invoice_unsent" => "Invoice failed to be sent to",
|
||||
"invoice_update" => "Recount",
|
||||
"item_insufficient_of_stock" => "Item has insufficient stock.",
|
||||
"item_name" => "Item Name",
|
||||
"item_number" => "Item #",
|
||||
"item_out_of_stock" => "Item is out of stock.",
|
||||
"key_browser" => "Helpful Shortcuts",
|
||||
"key_cancel" => "Cancels Current Quote/Invoice/Sale",
|
||||
"key_customer_search" => "Customer Search",
|
||||
"key_finish_quote" => "Finish Quote/Invoice without payment",
|
||||
"key_finish_sale" => "Add Payment and Complete Invoice/Sale",
|
||||
"key_full" => "Open in Full Screen Mode",
|
||||
"key_function" => "Function",
|
||||
"key_help" => "Shortcuts",
|
||||
"key_help_modal" => "Open Shortcuts Window",
|
||||
"key_in" => "Zoom in",
|
||||
"key_item_search" => "Item Search",
|
||||
"key_out" => "Zoom Out",
|
||||
"key_payment" => "Add Payment",
|
||||
"key_print" => "Print Current Page",
|
||||
"key_restore" => "Restore Original Display/Zoom",
|
||||
"key_search" => "Search Reports Tables",
|
||||
"key_suspend" => "Suspend Current Sale",
|
||||
"key_suspended" => "Show Suspended Sales",
|
||||
"key_system" => "System Shortcuts",
|
||||
"key_tendered" => "Edit Amount Tendered",
|
||||
"key_title" => "Sales Keyboard Shortcuts",
|
||||
"mc" => "",
|
||||
"mode" => "Register Mode",
|
||||
"must_enter_numeric" => "Amount Tendered must be a number.",
|
||||
"must_enter_numeric_giftcard" => "Gift Card Number must be a number.",
|
||||
"new_customer" => "New Customer",
|
||||
"new_item" => "New Item",
|
||||
"no_description" => "No description",
|
||||
"no_filter" => "All",
|
||||
"no_items_in_cart" => "There are no Items in the cart.",
|
||||
"no_sales_to_display" => "No Sales to display.",
|
||||
"none_selected" => "You have not selected any Sale(s) to delete.",
|
||||
"nontaxed_ind" => " ",
|
||||
"not_authorized" => "This action is not authorized.",
|
||||
"one_or_multiple" => "Sale(s)",
|
||||
"payment" => "Payment Type",
|
||||
"payment_amount" => "Amount",
|
||||
"payment_not_cover_total" => "Payment Amount must be greater than or equal to Total.",
|
||||
"payment_type" => "Type",
|
||||
"payments" => "",
|
||||
"payments_total" => "Payments Total",
|
||||
"price" => "Price",
|
||||
"print_after_sale" => "Print after Sale",
|
||||
"quantity" => "Quantity",
|
||||
"quantity_less_than_reorder_level" => "Warning: Desired Quantity is below Reorder Level for that Item.",
|
||||
"quantity_less_than_zero" => "Warning: Desired Quantity is insufficient. You can still process the sale, but audit your inventory.",
|
||||
"quantity_of_items" => "Quantity of {0} Items",
|
||||
"quote" => "Quote",
|
||||
"quote_number" => "Quote Number",
|
||||
"quote_number_duplicate" => "Quote Number must be unique.",
|
||||
"quote_sent" => "Quote sent to",
|
||||
"quote_unsent" => "Quote failed to be sent to",
|
||||
"receipt" => "Sales Receipt",
|
||||
"receipt_no_email" => "This customer does not have a valid email address.",
|
||||
"receipt_number" => "Sale #",
|
||||
"receipt_sent" => "Receipt sent to",
|
||||
"receipt_unsent" => "Receipt failed to be sent to",
|
||||
"refund" => "Refund Type",
|
||||
"register" => "Sales Register",
|
||||
"remove_customer" => "Remove Customer",
|
||||
"remove_discount" => "",
|
||||
"return" => "Return",
|
||||
"rewards" => "Reward Points",
|
||||
"rewards_balance" => "Reward Points Balance",
|
||||
"sale" => "Sale",
|
||||
"sale_by_invoice" => "Sale by Invoice",
|
||||
"sale_for_customer" => "Customer:",
|
||||
"sale_time" => "Time",
|
||||
"sales_tax" => "Sales Tax",
|
||||
"sales_total" => "",
|
||||
"select_customer" => "Select Customer",
|
||||
"selected_customer" => "Selected Customer",
|
||||
"send_invoice" => "Send Invoice",
|
||||
"send_quote" => "Send Quote",
|
||||
"send_receipt" => "Send Receipt",
|
||||
"send_work_order" => "Send Work Order",
|
||||
"serial" => "Serial",
|
||||
"service_charge" => "",
|
||||
"show_due" => "",
|
||||
"show_invoice" => "Show Invoice",
|
||||
"show_receipt" => "Show Receipt",
|
||||
"start_typing_customer_name" => "Start typing customer details...",
|
||||
"start_typing_item_name" => "Start typing Item Name or scan Barcode...",
|
||||
"stock" => "Stock",
|
||||
"stock_location" => "Stock Location",
|
||||
"sub_total" => "Subtotal",
|
||||
"successfully_deleted" => "You have successfully deleted",
|
||||
"successfully_restored" => "You have successfully restored",
|
||||
"successfully_suspended_sale" => "Sale suspend successful.",
|
||||
"successfully_updated" => "Sale update successful.",
|
||||
"suspend_sale" => "Suspend",
|
||||
"suspended_doc_id" => "Document",
|
||||
"suspended_sale_id" => "ID",
|
||||
"suspended_sales" => "Suspended",
|
||||
"table" => "Table",
|
||||
"takings" => "Daily Sales",
|
||||
"tax" => "Tax",
|
||||
"tax_id" => "Tax Id",
|
||||
"tax_invoice" => "Tax Invoice",
|
||||
"tax_percent" => "Tax %",
|
||||
"taxed_ind" => "T",
|
||||
"total" => "Total",
|
||||
"total_tax_exclusive" => "Tax excluded",
|
||||
"transaction_failed" => "Sales Transaction failed.",
|
||||
"unable_to_add_item" => "Item add to Sale failed",
|
||||
"unsuccessfully_deleted" => "Sale(s) delete failed.",
|
||||
"unsuccessfully_restored" => "Sale(s) restore failed.",
|
||||
"unsuccessfully_suspended_sale" => "Sale suspend failed.",
|
||||
"unsuccessfully_updated" => "Sale update failed.",
|
||||
"unsuspend" => "Unsuspend",
|
||||
"unsuspend_and_delete" => "Action",
|
||||
"update" => "Update",
|
||||
"upi" => "UPI",
|
||||
"visa" => "",
|
||||
"wallet" => "Wallet",
|
||||
"wholesale" => "",
|
||||
"work_order" => "Work Order",
|
||||
"work_order_number" => "Work Order Number",
|
||||
"work_order_number_duplicate" => "Work Order Number must be unique.",
|
||||
"work_order_sent" => "Work Order sent to",
|
||||
"work_order_unsent" => "Work Order failed to be sent to",
|
||||
'customers_available_points' => 'Points Available',
|
||||
'rewards_package' => 'Rewards',
|
||||
'rewards_remaining_balance' => 'Reward Points remaining value is ',
|
||||
'account_number' => 'Account #',
|
||||
'add_payment' => 'Add Payment',
|
||||
'amount_due' => 'Amount Due',
|
||||
'amount_tendered' => 'Amount Tendered',
|
||||
'authorized_signature' => 'Authorized Signature',
|
||||
'bank_transfer' => 'Bank Transfer',
|
||||
'cancel_sale' => 'Cancel',
|
||||
'cash' => 'Cash',
|
||||
'cash_1' => '',
|
||||
'cash_2' => '',
|
||||
'cash_3' => '',
|
||||
'cash_4' => '',
|
||||
'cash_adjustment' => 'Cash Adjustment',
|
||||
'cash_deposit' => 'Cash Deposit',
|
||||
'cash_filter' => 'Cash',
|
||||
'change_due' => 'Change Due',
|
||||
'change_price' => 'Change Selling Price',
|
||||
'check' => 'Check',
|
||||
'check_balance' => 'Check remainder',
|
||||
'check_filter' => 'Check',
|
||||
'close' => '',
|
||||
'comment' => 'Comment',
|
||||
'comments' => 'Comments',
|
||||
'company_name' => '',
|
||||
'complete' => '',
|
||||
'complete_sale' => 'Complete',
|
||||
'confirm_cancel_sale' => 'Are you sure you want to clear this sale? All items will be cleared.',
|
||||
'confirm_delete' => 'Are you sure you want to delete the selected Sale(s)?',
|
||||
'confirm_restore' => 'Are you sure you want to restore the selected Sale(s)?',
|
||||
'credit' => 'Credit Card',
|
||||
'credit_deposit' => 'Credit Deposit',
|
||||
'credit_filter' => 'Credit Card',
|
||||
'current_table' => '',
|
||||
'customer' => 'Customer',
|
||||
'customer_address' => 'Address',
|
||||
'customer_discount' => 'Discount',
|
||||
'customer_email' => 'Email',
|
||||
'customer_location' => 'Location',
|
||||
'customer_mailchimp_status' => 'MailChimp Status',
|
||||
'customer_optional' => '(Required for Due Payments)',
|
||||
'customer_required' => '(Required)',
|
||||
'customer_total' => 'Total',
|
||||
'customer_total_spent' => '',
|
||||
'daily_sales' => '',
|
||||
'date' => 'Sale Date',
|
||||
'date_range' => 'Date Range',
|
||||
'date_required' => 'A correct date must be entered.',
|
||||
'date_type' => 'Date is a required field.',
|
||||
'debit' => 'Debit Card',
|
||||
'debit_filter' => '',
|
||||
'delete' => 'Allow Delete',
|
||||
'delete_confirmation' => 'Are you sure you want to delete this sale? This action cannot be undone.',
|
||||
'delete_entire_sale' => 'Delete Entire Sale',
|
||||
'delete_successful' => 'Sale delete successful.',
|
||||
'delete_unsuccessful' => 'Sale delete failed.',
|
||||
'description_abbrv' => 'Desc.',
|
||||
'discard' => 'Discard',
|
||||
'discard_quote' => '',
|
||||
'discount' => 'Disc',
|
||||
'discount_included' => '% Discount',
|
||||
'discount_short' => '%',
|
||||
'due' => 'Due',
|
||||
'due_filter' => 'Due',
|
||||
'edit' => 'Edit',
|
||||
'edit_item' => 'Edit Item',
|
||||
'edit_sale' => 'Edit Sale',
|
||||
'email_receipt' => 'Email Receipt',
|
||||
'employee' => 'Employee',
|
||||
'entry' => 'Entry',
|
||||
'error_editing_item' => 'Error editing item',
|
||||
'negative_price_invalid' => 'Price cannot be negative.',
|
||||
'negative_quantity_invalid' => 'Quantity cannot be negative.',
|
||||
'negative_discount_invalid' => 'Discount cannot be negative.',
|
||||
'discount_percent_exceeds_100' => 'Percentage discount cannot exceed 100%.',
|
||||
'discount_exceeds_item_total' => 'Discount cannot exceed the item total.',
|
||||
'negative_total_invalid' => 'Sale total cannot be negative. Check item discounts and quantities.',
|
||||
'find_or_scan_item' => 'Find or Scan Item',
|
||||
'find_or_scan_item_or_receipt' => 'Find or Scan Item or Receipt',
|
||||
'giftcard' => 'Gift Card',
|
||||
'giftcard_balance' => 'Gift Card Balance',
|
||||
'giftcard_filter' => '',
|
||||
'giftcard_number' => 'Gift Card Number',
|
||||
'group_by_category' => 'Group by Category',
|
||||
'group_by_type' => 'Group by Type',
|
||||
'hsn' => 'HSN',
|
||||
'id' => 'Sale ID',
|
||||
'include_prices' => 'Include Prices?',
|
||||
'invoice' => 'Invoice',
|
||||
'invoice_confirm' => 'This invoice will be sent to',
|
||||
'invoice_enable' => 'Invoice Number',
|
||||
'invoice_filter' => 'Invoices',
|
||||
'invoice_no_email' => 'This customer does not have a valid email address.',
|
||||
'invoice_number' => 'Invoice #',
|
||||
'invoice_number_duplicate' => 'Invoice Number {0} must be unique.',
|
||||
'invoice_sent' => 'Invoice sent to',
|
||||
'invoice_total' => 'Invoice Total',
|
||||
'invoice_type_custom_invoice' => 'Custom Invoice (custom_invoice.php)',
|
||||
'invoice_type_custom_tax_invoice' => 'Custom Tax Invoice (custom_tax_invoice.php)',
|
||||
'invoice_type_invoice' => 'Invoice (invoice.php)',
|
||||
'invoice_type_tax_invoice' => 'Tax Invoice (tax_invoice.php)',
|
||||
'invoice_unsent' => 'Invoice failed to be sent to',
|
||||
'invoice_update' => 'Recount',
|
||||
'item_insufficient_of_stock' => 'Item has insufficient stock.',
|
||||
'item_name' => 'Item Name',
|
||||
'item_number' => 'Item #',
|
||||
'item_out_of_stock' => 'Item is out of stock.',
|
||||
'key_browser' => 'Helpful Shortcuts',
|
||||
'key_cancel' => 'Cancels Current Quote/Invoice/Sale',
|
||||
'key_customer_search' => 'Customer Search',
|
||||
'key_finish_quote' => 'Finish Quote/Invoice without payment',
|
||||
'key_finish_sale' => 'Add Payment and Complete Invoice/Sale',
|
||||
'key_full' => 'Open in Full Screen Mode',
|
||||
'key_function' => 'Function',
|
||||
'key_help' => 'Shortcuts',
|
||||
'key_help_modal' => 'Open Shortcuts Window',
|
||||
'key_in' => 'Zoom in',
|
||||
'key_item_search' => 'Item Search',
|
||||
'key_out' => 'Zoom Out',
|
||||
'key_payment' => 'Add Payment',
|
||||
'key_print' => 'Print Current Page',
|
||||
'key_restore' => 'Restore Original Display/Zoom',
|
||||
'key_search' => 'Search Reports Tables',
|
||||
'key_suspend' => 'Suspend Current Sale',
|
||||
'key_suspended' => 'Show Suspended Sales',
|
||||
'key_system' => 'System Shortcuts',
|
||||
'key_tendered' => 'Edit Amount Tendered',
|
||||
'key_title' => 'Sales Keyboard Shortcuts',
|
||||
'mc' => '',
|
||||
'mode' => 'Register Mode',
|
||||
'must_enter_numeric' => 'Amount Tendered must be a number.',
|
||||
'must_enter_numeric_giftcard' => 'Gift Card Number must be a number.',
|
||||
'new_customer' => 'New Customer',
|
||||
'new_item' => 'New Item',
|
||||
'no_description' => 'No description',
|
||||
'no_filter' => 'All',
|
||||
'no_items_in_cart' => 'There are no Items in the cart.',
|
||||
'no_sales_to_display' => 'No Sales to display.',
|
||||
'none_selected' => 'You have not selected any Sale(s) to delete.',
|
||||
'nontaxed_ind' => ' ',
|
||||
'not_authorized' => 'This action is not authorized.',
|
||||
'one_or_multiple' => 'Sale(s)',
|
||||
'payment' => 'Payment Type',
|
||||
'payment_amount' => 'Amount',
|
||||
'payment_not_cover_total' => 'Payment Amount must be greater than or equal to Total.',
|
||||
'payment_type' => 'Type',
|
||||
'payments' => '',
|
||||
'payments_total' => 'Payments Total',
|
||||
'price' => 'Price',
|
||||
'print_after_sale' => 'Print after Sale',
|
||||
'quantity' => 'Quantity',
|
||||
'quantity_less_than_reorder_level' => 'Warning: Desired Quantity is below Reorder Level for that Item.',
|
||||
'quantity_less_than_zero' => 'Warning: Desired Quantity is insufficient. You can still process the sale, but audit your inventory.',
|
||||
'quantity_of_items' => 'Quantity of {0} Items',
|
||||
'quote' => 'Quote',
|
||||
'quote_number' => 'Quote Number',
|
||||
'quote_number_duplicate' => 'Quote Number must be unique.',
|
||||
'quote_sent' => 'Quote sent to',
|
||||
'quote_unsent' => 'Quote failed to be sent to',
|
||||
'receipt' => 'Sales Receipt',
|
||||
'receipt_no_email' => 'This customer does not have a valid email address.',
|
||||
'receipt_number' => 'Sale #',
|
||||
'receipt_sent' => 'Receipt sent to',
|
||||
'receipt_unsent' => 'Receipt failed to be sent to',
|
||||
'refund' => 'Refund Type',
|
||||
'register' => 'Sales Register',
|
||||
'remove_customer' => 'Remove Customer',
|
||||
'remove_discount' => '',
|
||||
'return' => 'Return',
|
||||
'rewards' => 'Reward Points',
|
||||
'rewards_balance' => 'Reward Points Balance',
|
||||
'sale' => 'Sale',
|
||||
'sale_by_invoice' => 'Sale by Invoice',
|
||||
'sale_for_customer' => 'Customer:',
|
||||
'sale_time' => 'Time',
|
||||
'sales_tax' => 'Sales Tax',
|
||||
'sales_total' => '',
|
||||
'select_customer' => 'Select Customer',
|
||||
'selected_customer' => 'Selected Customer',
|
||||
'send_invoice' => 'Send Invoice',
|
||||
'send_quote' => 'Send Quote',
|
||||
'send_receipt' => 'Send Receipt',
|
||||
'send_work_order' => 'Send Work Order',
|
||||
'serial' => 'Serial',
|
||||
'service_charge' => '',
|
||||
'show_due' => '',
|
||||
'show_invoice' => 'Show Invoice',
|
||||
'show_receipt' => 'Show Receipt',
|
||||
'start_typing_customer_name' => 'Start typing customer details...',
|
||||
'start_typing_item_name' => 'Start typing Item Name or scan Barcode...',
|
||||
'stock' => 'Stock',
|
||||
'stock_location' => 'Stock Location',
|
||||
'sub_total' => 'Subtotal',
|
||||
'successfully_deleted' => 'You have successfully deleted',
|
||||
'successfully_restored' => 'You have successfully restored',
|
||||
'successfully_suspended_sale' => 'Sale suspend successful.',
|
||||
'successfully_updated' => 'Sale update successful.',
|
||||
'suspend_sale' => 'Suspend',
|
||||
'suspended_doc_id' => 'Document',
|
||||
'suspended_sale_id' => 'ID',
|
||||
'suspended_sales' => 'Suspended',
|
||||
'table' => 'Table',
|
||||
'takings' => 'Daily Sales',
|
||||
'tax' => 'Tax',
|
||||
'tax_id' => 'Tax Id',
|
||||
'tax_invoice' => 'Tax Invoice',
|
||||
'tax_percent' => 'Tax %',
|
||||
'taxed_ind' => 'T',
|
||||
'total' => 'Total',
|
||||
'total_tax_exclusive' => 'Tax excluded',
|
||||
'transaction_failed' => 'Sales Transaction failed.',
|
||||
'unable_to_add_item' => 'Item add to Sale failed',
|
||||
'unsuccessfully_deleted' => 'Sale(s) delete failed.',
|
||||
'unsuccessfully_restored' => 'Sale(s) restore failed.',
|
||||
'unsuccessfully_suspended_sale' => 'Sale suspend failed.',
|
||||
'unsuccessfully_updated' => 'Sale update failed.',
|
||||
'unsuspend' => 'Unsuspend',
|
||||
'unsuspend_and_delete' => 'Action',
|
||||
'update' => 'Update',
|
||||
'upi' => 'UPI',
|
||||
'visa' => '',
|
||||
'wallet' => 'Wallet',
|
||||
'wholesale' => '',
|
||||
'work_order' => 'Work Order',
|
||||
'work_order_number' => 'Work Order Number',
|
||||
'work_order_number_duplicate' => 'Work Order Number must be unique.',
|
||||
'work_order_sent' => 'Work Order sent to',
|
||||
'work_order_unsent' => 'Work Order failed to be sent to',
|
||||
'download_ubl' => 'Download UBL Invoice',
|
||||
'ubl_generation_failed' => 'UBL invoice generation failed',
|
||||
'sale_not_found' => 'Sale not found',
|
||||
];
|
||||
|
||||
@@ -118,4 +118,38 @@ class Email_lib
|
||||
|
||||
return '<img id="image" src="data:' . $mimeType . ';base64,' . $logo_data . '" alt="company_logo">';
|
||||
}
|
||||
|
||||
/**
|
||||
* Send email with multiple attachments
|
||||
*
|
||||
* @param string $to
|
||||
* @param string $subject
|
||||
* @param string $message
|
||||
* @param array $attachments
|
||||
* @return bool
|
||||
*/
|
||||
public function sendMultipleAttachments(string $to, string $subject, string $message, array $attachments): bool
|
||||
{
|
||||
$email = $this->email;
|
||||
|
||||
$email->setFrom($this->config['email'], $this->config['company']);
|
||||
$email->setTo($to);
|
||||
$email->setSubject($subject);
|
||||
$email->setMessage($message);
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
if (!empty($attachment) && file_exists($attachment)) {
|
||||
$email->attach($attachment);
|
||||
}
|
||||
}
|
||||
|
||||
$result = $email->send();
|
||||
|
||||
if (!$result) {
|
||||
log_message('error', $email->printDebugger());
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
40
app/Libraries/InvoiceAttachment/InvoiceAttachment.php
Normal file
40
app/Libraries/InvoiceAttachment/InvoiceAttachment.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\InvoiceAttachment;
|
||||
|
||||
interface InvoiceAttachment
|
||||
{
|
||||
/**
|
||||
* Generate the attachment content and write to a temp file.
|
||||
*
|
||||
* @param array $saleData The sale data from _load_sale_data()
|
||||
* @param string $type The document type (invoice, tax_invoice, quote, work_order, receipt)
|
||||
* @return string|null Absolute path to generated file, or null on failure
|
||||
*/
|
||||
public function generate(array $saleData, string $type): ?string;
|
||||
|
||||
/**
|
||||
* Check if this attachment type is applicable for the document type.
|
||||
* E.g., UBL only works for invoice/tax_invoice
|
||||
*
|
||||
* @param string $type The document type
|
||||
* @param array $saleData The sale data (to check invoice_number existence)
|
||||
* @return bool
|
||||
*/
|
||||
public function isApplicableForType(string $type, array $saleData): bool;
|
||||
|
||||
/**
|
||||
* Get the file extension for this attachment.
|
||||
*
|
||||
* @return string E.g., 'pdf', 'xml'
|
||||
*/
|
||||
public function getFileExtension(): string;
|
||||
|
||||
/**
|
||||
* Get the config values that enable this attachment.
|
||||
* Returns array of config values that should generate this attachment.
|
||||
*
|
||||
* @return array E.g., ['pdf_only', 'both'] for PDF
|
||||
*/
|
||||
public function getEnabledConfigValues(): array;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\InvoiceAttachment;
|
||||
|
||||
class InvoiceAttachmentGenerator
|
||||
{
|
||||
/** @var InvoiceAttachment[] */
|
||||
private array $attachments = [];
|
||||
|
||||
/**
|
||||
* Register an attachment generator.
|
||||
*/
|
||||
public function register(InvoiceAttachment $attachment): self
|
||||
{
|
||||
$this->attachments[] = $attachment;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create generator with attachments based on config.
|
||||
* Factory method that instantiates the right attachments.
|
||||
*
|
||||
* @param string $invoiceFormat Config value: 'pdf_only', 'ubl_only', or 'both'
|
||||
* @return self
|
||||
*/
|
||||
public static function createFromConfig(string $invoiceFormat): self
|
||||
{
|
||||
$generator = new self();
|
||||
|
||||
if (in_array($invoiceFormat, ['pdf_only', 'both'], true)) {
|
||||
$generator->register(new PdfAttachment());
|
||||
}
|
||||
|
||||
if (in_array($invoiceFormat, ['ubl_only', 'both'], true)) {
|
||||
$generator->register(new UblAttachment());
|
||||
}
|
||||
|
||||
return $generator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate all applicable attachments for a sale.
|
||||
*
|
||||
* @param array $saleData The sale data
|
||||
* @param string $type The document type
|
||||
* @return string[] Array of file paths to generated attachments
|
||||
*/
|
||||
public function generateAttachments(array $saleData, string $type): array
|
||||
{
|
||||
$files = [];
|
||||
|
||||
foreach ($this->attachments as $attachment) {
|
||||
if ($attachment->isApplicableForType($type, $saleData)) {
|
||||
$filepath = $attachment->generate($saleData, $type);
|
||||
if ($filepath !== null) {
|
||||
$files[] = $filepath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up temporary attachment files.
|
||||
*
|
||||
* @param string[] $files
|
||||
*/
|
||||
public static function cleanup(array $files): void
|
||||
{
|
||||
foreach ($files as $file) {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
61
app/Libraries/InvoiceAttachment/PdfAttachment.php
Normal file
61
app/Libraries/InvoiceAttachment/PdfAttachment.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\InvoiceAttachment;
|
||||
|
||||
use CodeIgniter\Config\Services;
|
||||
|
||||
class PdfAttachment implements InvoiceAttachment
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function generate(array $saleData, string $type): ?string
|
||||
{
|
||||
$view = Services::renderer();
|
||||
$html = $view->setData($saleData)->render("sales/{$type}_email");
|
||||
|
||||
helper(['dompdf', 'file']);
|
||||
|
||||
$tempPath = tempnam(sys_get_temp_dir(), 'ospos_pdf_');
|
||||
if ($tempPath === false) {
|
||||
log_message('error', 'PDF attachment: failed to create temp file');
|
||||
return null;
|
||||
}
|
||||
|
||||
$filename = $tempPath . '.pdf';
|
||||
rename($tempPath, $filename);
|
||||
|
||||
$pdfContent = create_pdf($html);
|
||||
if (file_put_contents($filename, $pdfContent) === false) {
|
||||
log_message('error', 'PDF attachment: failed to write content');
|
||||
@unlink($filename);
|
||||
return null;
|
||||
}
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isApplicableForType(string $type, array $saleData): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getFileExtension(): string
|
||||
{
|
||||
return 'pdf';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getEnabledConfigValues(): array
|
||||
{
|
||||
return ['pdf_only', 'both'];
|
||||
}
|
||||
}
|
||||
69
app/Libraries/InvoiceAttachment/UblAttachment.php
Normal file
69
app/Libraries/InvoiceAttachment/UblAttachment.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\InvoiceAttachment;
|
||||
|
||||
use App\Libraries\UBLGenerator;
|
||||
|
||||
class UblAttachment implements InvoiceAttachment
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
require_once ROOTPATH . 'vendor/autoload.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function generate(array $saleData, string $type): ?string
|
||||
{
|
||||
try {
|
||||
$generator = new UBLGenerator();
|
||||
$xml = $generator->generateUblInvoice($saleData);
|
||||
|
||||
$tempPath = tempnam(sys_get_temp_dir(), 'ospos_ubl_');
|
||||
if ($tempPath === false) {
|
||||
log_message('error', 'UBL attachment: failed to create temp file');
|
||||
return null;
|
||||
}
|
||||
|
||||
$filename = $tempPath . '.xml';
|
||||
rename($tempPath, $filename);
|
||||
|
||||
if (file_put_contents($filename, $xml) === false) {
|
||||
log_message('error', 'UBL attachment: failed to write content');
|
||||
@unlink($filename);
|
||||
return null;
|
||||
}
|
||||
|
||||
return $filename;
|
||||
} catch (\Exception $e) {
|
||||
log_message('error', 'UBL attachment generation failed: ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isApplicableForType(string $type, array $saleData): bool
|
||||
{
|
||||
return in_array($type, ['invoice', 'tax_invoice'], true)
|
||||
&& !empty($saleData['invoice_number']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getFileExtension(): string
|
||||
{
|
||||
return 'xml';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getEnabledConfigValues(): array
|
||||
{
|
||||
return ['ubl_only', 'both'];
|
||||
}
|
||||
}
|
||||
369
app/Libraries/UBLGenerator.php
Normal file
369
app/Libraries/UBLGenerator.php
Normal file
@@ -0,0 +1,369 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use DateTime;
|
||||
use NumNum\UBL\AccountingParty;
|
||||
use NumNum\UBL\Address;
|
||||
use NumNum\UBL\AllowanceCharge;
|
||||
use NumNum\UBL\Contact;
|
||||
use NumNum\UBL\Country;
|
||||
use NumNum\UBL\Generator;
|
||||
use NumNum\UBL\Invoice;
|
||||
use NumNum\UBL\InvoiceLine;
|
||||
use NumNum\UBL\Item;
|
||||
use NumNum\UBL\LegalMonetaryTotal;
|
||||
use NumNum\UBL\Party;
|
||||
use NumNum\UBL\PartyTaxScheme;
|
||||
use NumNum\UBL\Price;
|
||||
use NumNum\UBL\TaxCategory;
|
||||
use NumNum\UBL\TaxScheme;
|
||||
use NumNum\UBL\TaxSubTotal;
|
||||
use NumNum\UBL\TaxTotal;
|
||||
use NumNum\UBL\UnitCode;
|
||||
|
||||
helper(['country']);
|
||||
|
||||
class UBLGenerator
|
||||
{
|
||||
/**
|
||||
* Generate UBL invoice XML from sale data
|
||||
*
|
||||
* @param array $saleData Sale data from _load_sale_data()
|
||||
*
|
||||
* @return string UBL XML string
|
||||
*/
|
||||
public function generateUblInvoice(array $saleData): string
|
||||
{
|
||||
$taxScheme = (new TaxScheme())->setId('VAT');
|
||||
$isTaxIncluded = ! empty($saleData['tax_included']);
|
||||
|
||||
$supplierParty = $this->buildSupplierParty($saleData, $taxScheme);
|
||||
$customerParty = $this->buildCustomerParty($saleData['customer_object'] ?? null, $taxScheme);
|
||||
$invoiceLines = $this->buildInvoiceLines($saleData, $taxScheme, $isTaxIncluded);
|
||||
$taxTotal = $this->buildTaxTotal($saleData['taxes'] ?? [], $taxScheme);
|
||||
$monetaryTotal = $this->buildMonetaryTotal($saleData);
|
||||
|
||||
$invoice = (new Invoice())
|
||||
->setUBLVersionId('2.1')
|
||||
->setCustomizationId('urn:cen.eu:en16931:2017')
|
||||
->setProfileId('urn:fdc:peppol.eu:2017:poacc:billing:01:1.0')
|
||||
->setId($saleData['invoice_number'] ?? '')
|
||||
->setIssueDate(new DateTime($saleData['transaction_date'] ?? 'now'))
|
||||
->setInvoiceTypeCode(380)
|
||||
->setAccountingSupplierParty($supplierParty)
|
||||
->setAccountingCustomerParty($customerParty)
|
||||
->setInvoiceLines($invoiceLines)
|
||||
->setTaxTotal($taxTotal)
|
||||
->setLegalMonetaryTotal($monetaryTotal);
|
||||
|
||||
// Set currency if available
|
||||
if (! empty($saleData['currency_code'])) {
|
||||
Generator::$currencyID = $saleData['currency_code'];
|
||||
}
|
||||
|
||||
$generator = new Generator();
|
||||
|
||||
return $generator->invoice($invoice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build supplier (seller) party
|
||||
*/
|
||||
protected function buildSupplierParty(array $saleData, TaxScheme $taxScheme): AccountingParty
|
||||
{
|
||||
$config = $saleData['config'] ?? [];
|
||||
|
||||
$addressParts = $this->parseAddress($config['address'] ?? '');
|
||||
$countryCode = getCountryCode($config['country'] ?? '');
|
||||
|
||||
$country = (new Country())->setIdentificationCode($countryCode);
|
||||
$address = (new Address())
|
||||
->setStreetName($addressParts['street'] ?? '')
|
||||
->setBuildingNumber($addressParts['number'] ?? '')
|
||||
->setCityName($addressParts['city'] ?? '')
|
||||
->setPostalZone($addressParts['zip'] ?? '')
|
||||
->setCountrySubentity($config['state'] ?? '')
|
||||
->setCountry($country);
|
||||
|
||||
$party = (new Party())
|
||||
->setName($config['company'] ?? '')
|
||||
->setPostalAddress($address);
|
||||
|
||||
$partyTaxScheme = null;
|
||||
if (! empty($config['account_number'])) {
|
||||
$partyTaxScheme = (new PartyTaxScheme())
|
||||
->setCompanyId($config['account_number'])
|
||||
->setTaxScheme($taxScheme);
|
||||
$party->setPartyTaxScheme($partyTaxScheme);
|
||||
} elseif (! empty($config['tax_id'])) {
|
||||
// Use tax_id if account_number is not set
|
||||
$partyTaxScheme = (new PartyTaxScheme())
|
||||
->setCompanyId($config['tax_id'])
|
||||
->setTaxScheme($taxScheme);
|
||||
$party->setPartyTaxScheme($partyTaxScheme);
|
||||
}
|
||||
|
||||
return (new AccountingParty())->setParty($party);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build customer (buyer) party
|
||||
*/
|
||||
protected function buildCustomerParty(?object $customerInfo, TaxScheme $taxScheme): AccountingParty
|
||||
{
|
||||
if ($customerInfo === null) {
|
||||
return (new AccountingParty())->setParty(new Party());
|
||||
}
|
||||
|
||||
$countryCode = getCountryCode($customerInfo->country ?? '');
|
||||
|
||||
$country = (new Country())->setIdentificationCode($countryCode);
|
||||
$address = (new Address())
|
||||
->setStreetName($customerInfo->address_1 ?? '')
|
||||
->setAddressLine([$customerInfo->address_2 ?? ''])
|
||||
->setCityName($customerInfo->city ?? '')
|
||||
->setPostalZone($customerInfo->zip ?? '')
|
||||
->setCountrySubentity($customerInfo->state ?? '')
|
||||
->setCountry($country);
|
||||
|
||||
$partyName = ! empty($customerInfo->company_name)
|
||||
? $customerInfo->company_name
|
||||
: trim(($customerInfo->first_name ?? '') . ' ' . ($customerInfo->last_name ?? ''));
|
||||
|
||||
$party = (new Party())
|
||||
->setName($partyName)
|
||||
->setPostalAddress($address);
|
||||
|
||||
if (! empty($customerInfo->email)) {
|
||||
$contact = (new Contact())
|
||||
->setElectronicMail($customerInfo->email)
|
||||
->setTelephone($customerInfo->phone_number ?? '');
|
||||
$party->setContact($contact);
|
||||
}
|
||||
|
||||
$accountingParty = (new AccountingParty())->setParty($party);
|
||||
|
||||
if (! empty($customerInfo->account_number)) {
|
||||
$accountingParty->setSupplierAssignedAccountId($customerInfo->account_number);
|
||||
}
|
||||
|
||||
if (! empty($customerInfo->tax_id)) {
|
||||
$partyTaxScheme = (new PartyTaxScheme())
|
||||
->setCompanyId($customerInfo->tax_id)
|
||||
->setTaxScheme($taxScheme);
|
||||
$party->setPartyTaxScheme($partyTaxScheme);
|
||||
}
|
||||
|
||||
return $accountingParty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build invoice lines
|
||||
*/
|
||||
protected function buildInvoiceLines(array $saleData, TaxScheme $taxScheme, bool $isTaxIncluded): array
|
||||
{
|
||||
$lines = [];
|
||||
$itemTaxes = $saleData['item_taxes'] ?? [];
|
||||
$cart = $saleData['cart'] ?? [];
|
||||
|
||||
foreach ($cart as $item) {
|
||||
$itemId = $item['item_id'] ?? 0;
|
||||
$quantity = (float) ($item['quantity'] ?? 0);
|
||||
$unitPrice = (float) ($item['price'] ?? 0);
|
||||
$discount = (float) ($item['discount'] ?? 0);
|
||||
$discountType = (int) ($item['discount_type'] ?? 0);
|
||||
|
||||
// Calculate discount amount per unit
|
||||
if ($discountType === PERCENT && $discount > 0) {
|
||||
// Percentage discount
|
||||
$discountAmountPerUnit = round($unitPrice * $discount / 100, 4);
|
||||
} else {
|
||||
// Fixed discount (discount is total for the line, divide by quantity)
|
||||
$discountAmountPerUnit = $quantity > 0 ? round($discount / $quantity, 4) : 0;
|
||||
}
|
||||
|
||||
// Net price per unit (after discount)
|
||||
$netPricePerUnit = round($unitPrice - $discountAmountPerUnit, 4);
|
||||
if ($netPricePerUnit < 0) {
|
||||
$netPricePerUnit = 0;
|
||||
}
|
||||
|
||||
// Get tax rate for this item
|
||||
$taxRate = 0.0;
|
||||
$taxCategory = (new TaxCategory())
|
||||
->setId('S')
|
||||
->setPercent(0)
|
||||
->setTaxScheme($taxScheme);
|
||||
|
||||
if (isset($itemTaxes[$itemId]) && ! empty($itemTaxes[$itemId])) {
|
||||
// Use the first (primary) tax for this item
|
||||
$itemTax = $itemTaxes[$itemId][0];
|
||||
$taxRate = (float) ($itemTax['percent'] ?? 0);
|
||||
|
||||
if (abs($taxRate) < 0.001) {
|
||||
$taxCategory->setId('Z'); // Zero rated
|
||||
} elseif ($taxRate < 0) {
|
||||
$taxCategory->setId('E'); // Exempt
|
||||
} else {
|
||||
$taxCategory->setId('S'); // Standard
|
||||
}
|
||||
$taxCategory->setPercent(round($taxRate, 2));
|
||||
}
|
||||
|
||||
// Calculate line extension amount (net line total)
|
||||
$lineExtensionAmount = round($netPricePerUnit * $quantity, 2);
|
||||
|
||||
// Build Price - PriceAmount MUST be the net price excluding VAT per Peppol EN16931 (BR-27)
|
||||
// "The price of an item, exclusive of VAT, after subtracting discount"
|
||||
$price = (new Price())
|
||||
->setBaseQuantity(1.0)
|
||||
->setUnitCode(UnitCode::UNIT);
|
||||
|
||||
if ($isTaxIncluded && $taxRate > 0) {
|
||||
// Tax-inclusive: cart price includes VAT, so extract the net price
|
||||
// net_price = gross_price / (1 + tax_rate/100)
|
||||
$taxExclusivePricePerUnit = round($netPricePerUnit / (1 + $taxRate / 100), 4);
|
||||
$price->setPriceAmount($taxExclusivePricePerUnit);
|
||||
|
||||
// Recalculate line extension amount with tax-exclusive price
|
||||
$lineExtensionAmount = round($taxExclusivePricePerUnit * $quantity, 2);
|
||||
} else {
|
||||
// Tax-exclusive: cart price is already the net price
|
||||
$price->setPriceAmount(round($netPricePerUnit, 4));
|
||||
}
|
||||
|
||||
// Add AllowanceCharge if there's a discount (gross-to-net price reduction)
|
||||
if ($discountAmountPerUnit > 0) {
|
||||
$allowanceCharge = (new AllowanceCharge())
|
||||
->setChargeIndicator(false) // false = allowance/discount
|
||||
->setAllowanceChargeReason('Discount')
|
||||
->setAmount(round($discountAmountPerUnit, 4))
|
||||
->setBaseAmount(round((float) ($item['price'] ?? 0), 4));
|
||||
|
||||
$price->setAllowanceCharge($allowanceCharge);
|
||||
}
|
||||
|
||||
// Build Item
|
||||
$itemObj = (new Item())
|
||||
->setName($item['name'] ?? '')
|
||||
->setDescription($item['description'] ?? '')
|
||||
->setClassifiedTaxCategory($taxCategory);
|
||||
|
||||
// Add SellersItemIdentification if item_number exists (BR-25)
|
||||
if (! empty($item['item_number'])) {
|
||||
$itemObj->setSellersItemIdentification((string) $item['item_number']);
|
||||
}
|
||||
|
||||
// Build InvoiceLine
|
||||
$line = (new InvoiceLine())
|
||||
->setId(isset($item['line']) ? (string) $item['line'] : '1')
|
||||
->setInvoicedQuantity($quantity)
|
||||
->setLineExtensionAmount($lineExtensionAmount)
|
||||
->setItem($itemObj)
|
||||
->setPrice($price);
|
||||
|
||||
$lines[] = $line;
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build tax total from sales_taxes table data
|
||||
*/
|
||||
protected function buildTaxTotal(array $taxes, TaxScheme $taxScheme): TaxTotal
|
||||
{
|
||||
$totalTax = '0';
|
||||
$taxSubTotals = [];
|
||||
|
||||
foreach ($taxes as $tax) {
|
||||
if (isset($tax['tax_rate'])) {
|
||||
$taxRate = (string) $tax['tax_rate'];
|
||||
$taxAmount = (string) ($tax['sale_tax_amount'] ?? '0');
|
||||
|
||||
// Use sale_tax_basis directly from DB instead of reverse-computing
|
||||
$taxableAmount = (string) ($tax['sale_tax_basis'] ?? '0');
|
||||
|
||||
// Determine category ID based on tax rate
|
||||
$categoryId = 'S'; // Standard
|
||||
$floatRate = (float) $taxRate;
|
||||
if (abs($floatRate) < 0.001) {
|
||||
$categoryId = 'Z'; // Zero rated
|
||||
} elseif ($floatRate < 0) {
|
||||
$categoryId = 'E'; // Exempt
|
||||
}
|
||||
|
||||
$taxCategory = (new TaxCategory())
|
||||
->setId($categoryId)
|
||||
->setPercent(round($floatRate, 2))
|
||||
->setTaxScheme($taxScheme);
|
||||
|
||||
$taxSubTotal = (new TaxSubTotal())
|
||||
->setTaxableAmount(round((float) $taxableAmount, 2))
|
||||
->setTaxAmount(round((float) $taxAmount, 2))
|
||||
->setTaxCategory($taxCategory);
|
||||
|
||||
$taxSubTotals[] = $taxSubTotal;
|
||||
$totalTax = bcadd($totalTax, $taxAmount);
|
||||
}
|
||||
}
|
||||
|
||||
$taxTotal = new TaxTotal();
|
||||
$taxTotal->setTaxAmount(round((float) $totalTax, 2));
|
||||
|
||||
foreach ($taxSubTotals as $subTotal) {
|
||||
$taxTotal->addTaxSubTotal($subTotal);
|
||||
}
|
||||
|
||||
return $taxTotal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build monetary total
|
||||
*/
|
||||
protected function buildMonetaryTotal(array $saleData): LegalMonetaryTotal
|
||||
{
|
||||
// In OSPOS, after get_totals(): subtotal is ALWAYS tax-exclusive (net)
|
||||
// total is ALWAYS tax-inclusive (gross)
|
||||
$subtotal = (float) ($saleData['subtotal'] ?? 0);
|
||||
$total = (float) ($saleData['total'] ?? 0);
|
||||
$amountDue = (float) ($saleData['amount_due'] ?? 0);
|
||||
|
||||
return (new LegalMonetaryTotal())
|
||||
->setLineExtensionAmount(round($subtotal, 2))
|
||||
->setTaxExclusiveAmount(round($subtotal, 2))
|
||||
->setTaxInclusiveAmount(round($total, 2))
|
||||
->setPayableAmount(round($amountDue, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse address string into components
|
||||
*/
|
||||
protected function parseAddress(string $address): array
|
||||
{
|
||||
$parts = array_filter(array_map('trim', explode("\n", $address)));
|
||||
|
||||
$result = [
|
||||
'street' => '',
|
||||
'number' => '',
|
||||
'city' => '',
|
||||
'zip' => '',
|
||||
];
|
||||
|
||||
if (! empty($parts)) {
|
||||
$result['street'] = $parts[0];
|
||||
if (isset($parts[1])) {
|
||||
// Match 4-5 digit postal codes (e.g., 1234, 12345) followed by city name
|
||||
if (preg_match('/(\d{4,5})\s*(.+)/', $parts[1], $matches)) {
|
||||
$result['zip'] = $matches[1];
|
||||
$result['city'] = $matches[2];
|
||||
} else {
|
||||
$result['city'] = $parts[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Libraries\Sale_lib;
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
use CodeIgniter\Database\ResultInterface;
|
||||
use CodeIgniter\Model;
|
||||
use App\Libraries\Sale_lib;
|
||||
use Config\OSPOS;
|
||||
use ReflectionException;
|
||||
|
||||
@@ -14,11 +14,11 @@ use ReflectionException;
|
||||
*/
|
||||
class Sale extends Model
|
||||
{
|
||||
protected $table = 'sales';
|
||||
protected $primaryKey = 'sale_id';
|
||||
protected $table = 'sales';
|
||||
protected $primaryKey = 'sale_id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $useSoftDeletes = false;
|
||||
protected $allowedFields = [
|
||||
protected $useSoftDeletes = false;
|
||||
protected $allowedFields = [
|
||||
'sale_time',
|
||||
'customer_id',
|
||||
'employee_id',
|
||||
@@ -28,7 +28,7 @@ class Sale extends Model
|
||||
'invoice_number',
|
||||
'dinner_table_id',
|
||||
'work_order_number',
|
||||
'sale_type'
|
||||
'sale_type',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
@@ -45,16 +45,16 @@ class Sale extends Model
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$this->create_temp_table(['sale_id' => $sale_id]);
|
||||
|
||||
$decimals = totals_decimals();
|
||||
$sales_tax = 'IFNULL(SUM(sales_items_taxes.sales_tax), 0)';
|
||||
$decimals = totals_decimals();
|
||||
$sales_tax = 'IFNULL(SUM(sales_items_taxes.sales_tax), 0)';
|
||||
$cash_adjustment = 'IFNULL(SUM(payments.sale_cash_adjustment), 0)';
|
||||
$sale_price = 'CASE WHEN sales_items.discount_type = ' . PERCENT
|
||||
. " THEN sales_items.quantity_purchased * sales_items.item_unit_price - ROUND(sales_items.quantity_purchased * sales_items.item_unit_price * sales_items.discount / 100, $decimals) "
|
||||
$sale_price = 'CASE WHEN sales_items.discount_type = ' . PERCENT
|
||||
. " THEN sales_items.quantity_purchased * sales_items.item_unit_price - ROUND(sales_items.quantity_purchased * sales_items.item_unit_price * sales_items.discount / 100, {$decimals}) "
|
||||
. 'ELSE sales_items.quantity_purchased * (sales_items.item_unit_price - sales_items.discount) END';
|
||||
|
||||
$sale_total = $config['tax_included']
|
||||
? "ROUND(SUM($sale_price), $decimals) + $cash_adjustment"
|
||||
: "ROUND(SUM($sale_price), $decimals) + $sales_tax + $cash_adjustment";
|
||||
? "ROUND(SUM({$sale_price}), {$decimals}) + {$cash_adjustment}"
|
||||
: "ROUND(SUM({$sale_price}), {$decimals}) + {$sales_tax} + {$cash_adjustment}";
|
||||
|
||||
$sql = 'sales.sale_id AS sale_id,
|
||||
MAX(DATE(sales.sale_time)) AS sale_date,
|
||||
@@ -73,9 +73,9 @@ class Sale extends Model
|
||||
MAX(IFnull(payments.sale_cash_adjustment, 0)) AS cash_adjustment,
|
||||
MAX(IFnull(payments.sale_cash_refund, 0)) AS cash_refund,
|
||||
' . "
|
||||
$sale_total AS amount_due,
|
||||
{$sale_total} AS amount_due,
|
||||
MAX(IFnull(payments.sale_payment_amount, 0)) AS amount_tendered,
|
||||
(MAX(payments.sale_payment_amount)) - ($sale_total) AS change_due,
|
||||
(MAX(payments.sale_payment_amount)) - ({$sale_total}) AS change_due,
|
||||
" . '
|
||||
MAX(payments.payment_type) AS payment_type';
|
||||
|
||||
@@ -89,7 +89,7 @@ class Sale extends Model
|
||||
$builder->join(
|
||||
'sales_items_taxes_temp AS sales_items_taxes',
|
||||
'sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.item_id = sales_items_taxes.item_id AND sales_items.line = sales_items_taxes.line',
|
||||
'LEFT OUTER'
|
||||
'LEFT OUTER',
|
||||
);
|
||||
|
||||
$builder->where('sales.sale_id', $sale_id);
|
||||
@@ -114,15 +114,25 @@ class Sale extends Model
|
||||
public function search(?string $search, array $filters, ?int $rows = 0, ?int $limit_from = 0, ?string $sort = 'sales.sale_time', ?string $order = 'desc', ?bool $count_only = false)
|
||||
{
|
||||
// Set default values
|
||||
if ($rows == null) $rows = 0;
|
||||
if ($limit_from == null) $limit_from = 0;
|
||||
if ($sort == null) $sort = 'sales.sale_time';
|
||||
if ($order == null) $order = 'desc';
|
||||
if ($count_only == null) $count_only = false;
|
||||
if ($rows === null) {
|
||||
$rows = 0;
|
||||
}
|
||||
if ($limit_from === null) {
|
||||
$limit_from = 0;
|
||||
}
|
||||
if ($sort === null) {
|
||||
$sort = 'sales.sale_time';
|
||||
}
|
||||
if ($order === null) {
|
||||
$order = 'desc';
|
||||
}
|
||||
if ($count_only === null) {
|
||||
$count_only = false;
|
||||
}
|
||||
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$db_prefix = $this->db->getPrefix();
|
||||
$decimals = totals_decimals();
|
||||
$decimals = totals_decimals();
|
||||
|
||||
// Only non-suspended records
|
||||
$where = 'sales.sale_status = 0 AND ';
|
||||
@@ -133,18 +143,18 @@ class Sale extends Model
|
||||
$this->create_temp_table_sales_payments_data($where);
|
||||
|
||||
$sale_price = 'CASE WHEN `sales_items`.`discount_type` = ' . PERCENT
|
||||
. " THEN `sales_items`.`quantity_purchased` * `sales_items`.`item_unit_price` - ROUND(`sales_items`.`quantity_purchased` * `sales_items`.`item_unit_price` * `sales_items`.`discount` / 100, $decimals) "
|
||||
. " THEN `sales_items`.`quantity_purchased` * `sales_items`.`item_unit_price` - ROUND(`sales_items`.`quantity_purchased` * `sales_items`.`item_unit_price` * `sales_items`.`discount` / 100, {$decimals}) "
|
||||
. 'ELSE `sales_items`.`quantity_purchased` * (`sales_items`.`item_unit_price` - `sales_items`.`discount`) END';
|
||||
|
||||
$sale_cost = 'SUM(`sales_items`.`item_cost_price` * `sales_items`.`quantity_purchased`)';
|
||||
|
||||
$tax = 'IFNULL(SUM(`sales_items_taxes`.`tax`), 0)';
|
||||
$sales_tax = 'IFNULL(SUM(`sales_items_taxes`.`sales_tax`), 0)';
|
||||
$internal_tax = 'IFNULL(SUM(`sales_items_taxes`.`internal_tax`), 0)';
|
||||
$tax = 'IFNULL(SUM(`sales_items_taxes`.`tax`), 0)';
|
||||
$sales_tax = 'IFNULL(SUM(`sales_items_taxes`.`sales_tax`), 0)';
|
||||
$internal_tax = 'IFNULL(SUM(`sales_items_taxes`.`internal_tax`), 0)';
|
||||
$cash_adjustment = 'IFNULL(SUM(`payments`.`sale_cash_adjustment`), 0)';
|
||||
|
||||
$sale_subtotal = "ROUND(SUM($sale_price), $decimals) - $internal_tax";
|
||||
$sale_total = "ROUND(SUM($sale_price), $decimals) + $sales_tax + $cash_adjustment";
|
||||
$sale_subtotal = "ROUND(SUM({$sale_price}), {$decimals}) - {$internal_tax}";
|
||||
$sale_total = "ROUND(SUM({$sale_price}), {$decimals}) + {$sales_tax} + {$cash_adjustment}";
|
||||
|
||||
$this->create_temp_table_sales_items_taxes_data($where);
|
||||
|
||||
@@ -171,7 +181,7 @@ class Sale extends Model
|
||||
$sale_total . ' AS amount_due',
|
||||
'MAX(`payments`.`sale_payment_amount`) AS amount_tendered',
|
||||
'(MAX(`payments`.`sale_payment_amount`)) - (' . $sale_total . ') AS change_due',
|
||||
'MAX(`payments`.`payment_type`) AS payment_type'
|
||||
'MAX(`payments`.`payment_type`) AS payment_type',
|
||||
], false);
|
||||
}
|
||||
|
||||
@@ -182,7 +192,7 @@ class Sale extends Model
|
||||
$builder->join(
|
||||
'sales_items_taxes_temp AS sales_items_taxes',
|
||||
'sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.item_id = sales_items_taxes.item_id AND sales_items.line = sales_items_taxes.line',
|
||||
'LEFT OUTER'
|
||||
'LEFT OUTER',
|
||||
);
|
||||
|
||||
$builder->where($where);
|
||||
@@ -227,7 +237,7 @@ class Sale extends Model
|
||||
$builder->where('sales.sale_time BETWEEN ' . $this->db->escape(rawurldecode($filters['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($filters['end_date'])));
|
||||
}
|
||||
|
||||
if (!empty($search)) { // TODO: duplicated code. We should think about refactoring out a method.
|
||||
if (! empty($search)) { // TODO: duplicated code. We should think about refactoring out a method.
|
||||
if ($filters['is_valid_receipt']) {
|
||||
$pieces = explode(' ', $search);
|
||||
$builder->where('sales.sale_id', $pieces[1]);
|
||||
@@ -242,13 +252,13 @@ class Sale extends Model
|
||||
}
|
||||
|
||||
// TODO: This needs to be converted to a switch statement
|
||||
if ($filters['sale_type'] == 'sales') { // TODO: we need to think about refactoring this block to a switch statement.
|
||||
if ($filters['sale_type'] === 'sales') { // TODO: we need to think about refactoring this block to a switch statement.
|
||||
$builder->where('sales.sale_status = ' . COMPLETED . ' AND payment_amount > 0');
|
||||
} elseif ($filters['sale_type'] == 'quotes') {
|
||||
} elseif ($filters['sale_type'] === 'quotes') {
|
||||
$builder->where('sales.sale_status = ' . SUSPENDED . ' AND sales.quote_number IS NOT NULL');
|
||||
} elseif ($filters['sale_type'] == 'returns') {
|
||||
} elseif ($filters['sale_type'] === 'returns') {
|
||||
$builder->where('sales.sale_status = ' . COMPLETED . ' AND payment_amount < 0');
|
||||
} elseif ($filters['sale_type'] == 'all') {
|
||||
} elseif ($filters['sale_type'] === 'all') {
|
||||
$builder->where('sales.sale_status = ' . COMPLETED);
|
||||
}
|
||||
|
||||
@@ -290,12 +300,12 @@ class Sale extends Model
|
||||
$payments = $builder->get()->getResultArray();
|
||||
|
||||
// Consider Gift Card as only one type of payment and do not show "Gift Card: 1, Gift Card: 2, etc." in the total
|
||||
$gift_card_count = 0;
|
||||
$gift_card_count = 0;
|
||||
$gift_card_amount = 0;
|
||||
|
||||
foreach ($payments as $key => $payment) {
|
||||
if (strstr($payment['payment_type'], lang('Sales.giftcard'))) {
|
||||
$gift_card_count += $payment['count'];
|
||||
$gift_card_count += $payment['count'];
|
||||
$gift_card_amount += $payment['payment_amount'];
|
||||
|
||||
// Remove the "Gift Card: 1", "Gift Card: 2", etc. payment string
|
||||
@@ -327,7 +337,7 @@ class Sale extends Model
|
||||
{
|
||||
$suggestions = [];
|
||||
|
||||
if (!$this->is_valid_receipt($search)) {
|
||||
if (! $this->is_valid_receipt($search)) {
|
||||
$builder = $this->db->table('sales');
|
||||
$builder->distinct()->select('first_name, last_name');
|
||||
$builder->join('people', 'people.person_id = sales.customer_id');
|
||||
@@ -369,21 +379,11 @@ class Sale extends Model
|
||||
return $builder->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $year
|
||||
* @param int $start_from
|
||||
* @return int
|
||||
*/
|
||||
public function get_invoice_number_for_year(string $year = '', int $start_from = 0): int
|
||||
{
|
||||
return $this->get_number_for_year('invoice_number', $year, $start_from);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $year
|
||||
* @param int $start_from
|
||||
* @return int
|
||||
*/
|
||||
public function get_quote_number_for_year(string $year = '', int $start_from = 0): int
|
||||
{
|
||||
return $this->get_number_for_year('quote_number', $year, $start_from);
|
||||
@@ -394,31 +394,32 @@ class Sale extends Model
|
||||
*/
|
||||
private function get_number_for_year(string $field, string $year = '', int $start_from = 0): int
|
||||
{
|
||||
$year = $year == '' ? date('Y') : $year;
|
||||
$year = $year === '' ? date('Y') : $year;
|
||||
|
||||
$builder = $this->db->table('sales');
|
||||
$builder->select('COUNT( 1 ) AS number_year');
|
||||
$builder->where('DATE_FORMAT(sale_time, "%Y" ) = ', $year);
|
||||
$builder->where("$field IS NOT NULL");
|
||||
$builder->where("{$field} IS NOT NULL");
|
||||
$result = $builder->get()->getRowArray();
|
||||
|
||||
return ($start_from + $result['number_year']);
|
||||
return $start_from + $result['number_year'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if valid receipt
|
||||
*/
|
||||
public function is_valid_receipt(string|null &$receipt_sale_id): bool // TODO: like the others, maybe this should be an array rather than a delimited string... either that or the parameter name needs to be changed. $receipt_sale_id implies that it's an int.
|
||||
public function is_valid_receipt(?string &$receipt_sale_id): bool // TODO: like the others, maybe this should be an array rather than a delimited string... either that or the parameter name needs to be changed. $receipt_sale_id implies that it's an int.
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
|
||||
if (!empty($receipt_sale_id)) {
|
||||
if (! empty($receipt_sale_id)) {
|
||||
// POS #
|
||||
$pieces = explode(' ', $receipt_sale_id);
|
||||
|
||||
if (count($pieces) == 2 && preg_match('/(POS)/i', $pieces[0])) {
|
||||
if (count($pieces) === 2 && preg_match('/(POS)/i', $pieces[0])) {
|
||||
return $this->exists($pieces[1]);
|
||||
} elseif ($config['invoice_enable']) {
|
||||
}
|
||||
if ($config['invoice_enable']) {
|
||||
$sale_info = $this->get_sale_by_invoice_number($receipt_sale_id);
|
||||
|
||||
if ($sale_info->getNumRows() > 0) {
|
||||
@@ -440,11 +441,14 @@ class Sale extends Model
|
||||
$builder = $this->db->table('sales');
|
||||
$builder->where('sale_id', $sale_id);
|
||||
|
||||
return ($builder->get()->getNumRows() == 1); // TODO: ===
|
||||
return $builder->get()->getNumRows() === 1; // TODO: ===
|
||||
}
|
||||
|
||||
/**
|
||||
* Update sale
|
||||
*
|
||||
* @param mixed|null $sale_id
|
||||
* @param mixed|null $sale_data
|
||||
*/
|
||||
public function update($sale_id = null, $sale_data = null): bool
|
||||
{
|
||||
@@ -455,7 +459,7 @@ class Sale extends Model
|
||||
$success = $builder->update($update_data);
|
||||
|
||||
// Touch payment only if update sale is successful and there is a payments object otherwise the result would be to delete all the payments associated to the sale
|
||||
if ($success && !empty($sale_data['payments'])) {
|
||||
if ($success && ! empty($sale_data['payments'])) {
|
||||
// Run these queries as a transaction, we want to make sure we do all or nothing
|
||||
$this->db->transStart();
|
||||
|
||||
@@ -463,14 +467,14 @@ class Sale extends Model
|
||||
|
||||
// Add new payments
|
||||
foreach ($sale_data['payments'] as $payment) {
|
||||
$payment_id = $payment['payment_id'];
|
||||
$payment_type = $payment['payment_type'];
|
||||
$payment_amount = $payment['payment_amount'];
|
||||
$cash_refund = $payment['cash_refund'];
|
||||
$payment_id = $payment['payment_id'];
|
||||
$payment_type = $payment['payment_type'];
|
||||
$payment_amount = $payment['payment_amount'];
|
||||
$cash_refund = $payment['cash_refund'];
|
||||
$cash_adjustment = $payment['cash_adjustment'];
|
||||
$employee_id = $payment['employee_id'];
|
||||
$employee_id = $payment['employee_id'];
|
||||
|
||||
if ($payment_id == NEW_ENTRY && $payment_amount != 0) {
|
||||
if ($payment_id === NEW_ENTRY && $payment_amount !== 0) {
|
||||
// Add a new payment transaction
|
||||
$sales_payments_data = [
|
||||
'sale_id' => $sale_id,
|
||||
@@ -478,17 +482,17 @@ class Sale extends Model
|
||||
'payment_amount' => $payment_amount,
|
||||
'cash_refund' => $cash_refund,
|
||||
'cash_adjustment' => $cash_adjustment,
|
||||
'employee_id' => $employee_id
|
||||
'employee_id' => $employee_id,
|
||||
];
|
||||
$success = $builder->insert($sales_payments_data);
|
||||
} elseif ($payment_id != NEW_ENTRY) {
|
||||
if ($payment_amount != 0) {
|
||||
} elseif ($payment_id !== NEW_ENTRY) {
|
||||
if ($payment_amount !== 0) {
|
||||
// Update existing payment transactions (payment_type only)
|
||||
$sales_payments_data = [
|
||||
'payment_type' => $payment_type,
|
||||
'payment_amount' => $payment_amount,
|
||||
'cash_refund' => $cash_refund,
|
||||
'cash_adjustment' => $cash_adjustment
|
||||
'cash_adjustment' => $cash_adjustment,
|
||||
];
|
||||
|
||||
$builder->where('payment_id', $payment_id);
|
||||
@@ -510,6 +514,7 @@ class Sale extends Model
|
||||
/**
|
||||
* Save the sale information after the sales is complete but before the final document is printed
|
||||
* The sales_taxes variable needs to be initialized to an empty array before calling
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function save_value(
|
||||
@@ -525,22 +530,22 @@ class Sale extends Model
|
||||
int $sale_type,
|
||||
?array $payments,
|
||||
?int $dinner_table_id,
|
||||
?array &$sales_taxes
|
||||
?array &$sales_taxes,
|
||||
): int { // TODO: this method returns the sale_id but the override is expecting it to return a bool. The signature needs to be reworked. Generally when there are more than 3 maybe 4 parameters, there's a good chance that an object needs to be passed rather than so many params.
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$attribute = model(Attribute::class);
|
||||
$customer = model(Customer::class);
|
||||
$giftcard = model(Giftcard::class);
|
||||
$customer = model(Customer::class);
|
||||
$giftcard = model(Giftcard::class);
|
||||
$inventory = model('Inventory');
|
||||
$item = model(Item::class);
|
||||
$item = model(Item::class);
|
||||
|
||||
$item_quantity = model(Item_quantity::class);
|
||||
|
||||
if ($sale_id != NEW_ENTRY) {
|
||||
if ($sale_id !== NEW_ENTRY) {
|
||||
$this->clear_suspended_sale_detail($sale_id);
|
||||
}
|
||||
|
||||
if (count($items) == 0) { // TODO: ===
|
||||
if (count($items) === 0) { // TODO: ===
|
||||
return -1; // TODO: Replace -1 with a constant
|
||||
}
|
||||
|
||||
@@ -554,13 +559,13 @@ class Sale extends Model
|
||||
'quote_number' => $quote_number,
|
||||
'work_order_number' => $work_order_number,
|
||||
'dinner_table_id' => $dinner_table_id,
|
||||
'sale_type' => $sale_type
|
||||
'sale_type' => $sale_type,
|
||||
];
|
||||
|
||||
// Run these queries as a transaction, we want to make sure we do all or nothing
|
||||
$this->db->transStart();
|
||||
|
||||
if ($sale_id == NEW_ENTRY) {
|
||||
if ($sale_id === NEW_ENTRY) {
|
||||
$builder = $this->db->table('sales');
|
||||
$builder->insert($sales_data);
|
||||
$sale_id = $this->db->insertID();
|
||||
@@ -570,19 +575,19 @@ class Sale extends Model
|
||||
$builder->update($sales_data);
|
||||
}
|
||||
|
||||
$total_amount = 0;
|
||||
$total_amount = 0;
|
||||
$total_amount_used = 0;
|
||||
|
||||
foreach ($payments as $payment_id => $payment) {
|
||||
if (!empty(strstr($payment['payment_type'], lang('Sales.giftcard')))) {
|
||||
if (! empty(strstr($payment['payment_type'], lang('Sales.giftcard')))) {
|
||||
// We have a gift card, and we have to deduct the used value from the total value of the card.
|
||||
$splitpayment = explode(':', $payment['payment_type']); // TODO: this variable doesn't follow our naming conventions. Probably should be refactored to split_payment.
|
||||
$splitpayment = explode(':', $payment['payment_type']); // TODO: this variable doesn't follow our naming conventions. Probably should be refactored to split_payment.
|
||||
$cur_giftcard_value = $giftcard->get_giftcard_value($splitpayment[1]); // TODO: this should be refactored to $current_giftcard_value
|
||||
$giftcard->update_giftcard_value($splitpayment[1], $cur_giftcard_value - $payment['payment_amount']);
|
||||
} elseif (!empty(strstr($payment['payment_type'], lang('Sales.rewards')))) {
|
||||
} elseif (! empty(strstr($payment['payment_type'], lang('Sales.rewards')))) {
|
||||
$cur_rewards_value = $customer->get_info($customer_id)->points;
|
||||
$customer->update_reward_points_value($customer_id, $cur_rewards_value - $payment['payment_amount']);
|
||||
$total_amount_used = floatval($total_amount_used) + floatval($payment['payment_amount']);
|
||||
$total_amount_used = (float) $total_amount_used + (float) ($payment['payment_amount']);
|
||||
}
|
||||
|
||||
$sales_payments_data = [
|
||||
@@ -591,13 +596,13 @@ class Sale extends Model
|
||||
'payment_amount' => $payment['payment_amount'],
|
||||
'cash_refund' => $payment['cash_refund'],
|
||||
'cash_adjustment' => $payment['cash_adjustment'],
|
||||
'employee_id' => $employee_id
|
||||
'employee_id' => $employee_id,
|
||||
];
|
||||
|
||||
$builder = $this->db->table('sales_payments');
|
||||
$builder->insert($sales_payments_data);
|
||||
|
||||
$total_amount = floatval($total_amount) + floatval($payment['payment_amount']) - floatval($payment['cash_refund']);
|
||||
$total_amount = (float) $total_amount + (float) ($payment['payment_amount']) - (float) ($payment['cash_refund']);
|
||||
}
|
||||
|
||||
$this->save_customer_rewards($customer_id, $sale_id, $total_amount, $total_amount_used);
|
||||
@@ -607,7 +612,7 @@ class Sale extends Model
|
||||
foreach ($items as $line => $item_data) {
|
||||
$cur_item_info = $item->get_info($item_data['item_id']);
|
||||
|
||||
if ($item_data['price'] == 0.00) {
|
||||
if ($item_data['price'] === 0.00) {
|
||||
$item_data['discount'] = 0.00;
|
||||
}
|
||||
|
||||
@@ -623,13 +628,13 @@ class Sale extends Model
|
||||
'item_cost_price' => $item_data['cost_price'],
|
||||
'item_unit_price' => $item_data['price'],
|
||||
'item_location' => $item_data['item_location'],
|
||||
'print_option' => $item_data['print_option']
|
||||
'print_option' => $item_data['print_option'],
|
||||
];
|
||||
|
||||
$builder = $this->db->table('sales_items');
|
||||
$builder->insert($sales_items_data);
|
||||
|
||||
if ($cur_item_info->stock_type == HAS_STOCK && $sale_status == COMPLETED) { // TODO: === ?
|
||||
if ($cur_item_info->stock_type === HAS_STOCK && $sale_status === COMPLETED) { // TODO: === ?
|
||||
// Update stock quantity if item type is a standard stock item and the sale is a standard sale
|
||||
$item_quantity_data = $item_quantity->get_item_quantity($item_data['item_id'], $item_data['item_location']);
|
||||
|
||||
@@ -637,10 +642,10 @@ class Sale extends Model
|
||||
[
|
||||
'quantity' => $item_quantity_data->quantity - $item_data['quantity'],
|
||||
'item_id' => $item_data['item_id'],
|
||||
'location_id' => $item_data['item_location']
|
||||
'location_id' => $item_data['item_location'],
|
||||
],
|
||||
$item_data['item_id'],
|
||||
$item_data['item_location']
|
||||
$item_data['item_location'],
|
||||
);
|
||||
|
||||
// If an items was deleted but later returned it's restored with this rule
|
||||
@@ -650,13 +655,13 @@ class Sale extends Model
|
||||
|
||||
// Inventory Count Details
|
||||
$sale_remarks = 'POS ' . $sale_id; // TODO: Use string interpolation here.
|
||||
$inv_data = [
|
||||
$inv_data = [
|
||||
'trans_date' => date('Y-m-d H:i:s'),
|
||||
'trans_items' => $item_data['item_id'],
|
||||
'trans_user' => $employee_id,
|
||||
'trans_location' => $item_data['item_location'],
|
||||
'trans_comment' => $sale_remarks,
|
||||
'trans_inventory' => -$item_data['quantity']
|
||||
'trans_inventory' => -$item_data['quantity'],
|
||||
];
|
||||
|
||||
$inventory->insert($inv_data, false);
|
||||
@@ -665,14 +670,14 @@ class Sale extends Model
|
||||
$attribute->copy_attribute_links($item_data['item_id'], 'sale_id', $sale_id);
|
||||
}
|
||||
|
||||
if ($customer_id == NEW_ENTRY || $customer->taxable) {
|
||||
if ($customer_id === NEW_ENTRY || $customer->taxable) {
|
||||
$this->save_sales_tax($sale_id, $sales_taxes[0]);
|
||||
$this->save_sales_items_taxes($sale_id, $sales_taxes[1]);
|
||||
}
|
||||
|
||||
if ($config['dinner_table_enable']) {
|
||||
$dinner_table = model(Dinner_table::class);
|
||||
if ($sale_status == COMPLETED) { // TODO: === ?
|
||||
if ($sale_status === COMPLETED) { // TODO: === ?
|
||||
$dinner_table->release($dinner_table_id);
|
||||
} else {
|
||||
$dinner_table->occupy($dinner_table_id);
|
||||
@@ -721,7 +726,7 @@ class Sale extends Model
|
||||
'item_tax_amount' => $tax_item['item_tax_amount'],
|
||||
'sales_tax_code_id' => $tax_item['sales_tax_code_id'],
|
||||
'tax_category_id' => $tax_item['tax_category_id'],
|
||||
'jurisdiction_id' => $tax_item['jurisdiction_id']
|
||||
'jurisdiction_id' => $tax_item['jurisdiction_id'],
|
||||
];
|
||||
|
||||
$builder->insert($sales_items_taxes);
|
||||
@@ -756,8 +761,36 @@ class Sale extends Model
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all item taxes for a sale (for UBL invoice generation)
|
||||
* Returns array keyed by item_id, each containing array of tax info
|
||||
*/
|
||||
public function get_sale_item_taxes_by_sale(int $sale_id): array
|
||||
{
|
||||
$builder = $this->db->table('sales_items_taxes');
|
||||
$builder->select('item_id, line, name, percent, tax_type, item_tax_amount');
|
||||
$builder->where('sale_id', $sale_id);
|
||||
$builder->orderBy('line', 'asc');
|
||||
|
||||
$results = $builder->get()->getResultArray();
|
||||
|
||||
// Group by item_id
|
||||
$itemTaxes = [];
|
||||
|
||||
foreach ($results as $row) {
|
||||
$itemId = $row['item_id'];
|
||||
if (! isset($itemTaxes[$itemId])) {
|
||||
$itemTaxes[$itemId] = [];
|
||||
}
|
||||
$itemTaxes[$itemId][] = $row;
|
||||
}
|
||||
|
||||
return $itemTaxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes list of sales
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function delete_list(array $sale_ids, int $employee_id, bool $update_inventory = true): bool
|
||||
@@ -787,6 +820,10 @@ class Sale extends Model
|
||||
* Delete sale. Hard deletes are not supported for sales transactions.
|
||||
* When a sale is "deleted" it is simply changed to a status of canceled.
|
||||
* However, if applicable the inventory still needs to be updated
|
||||
*
|
||||
* @param mixed|null $sale_id
|
||||
* @param mixed|null $employee_id
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function delete($sale_id = null, bool $purge = false, bool $update_inventory = true, $employee_id = null): bool
|
||||
@@ -796,11 +833,11 @@ class Sale extends Model
|
||||
|
||||
$sale_status = $this->get_sale_status($sale_id);
|
||||
|
||||
if ($update_inventory && $sale_status == COMPLETED) {
|
||||
if ($update_inventory && $sale_status === COMPLETED) {
|
||||
// Defect, not all item deletions will be undone?
|
||||
// Get array with all the items involved in the sale to update the inventory tracking
|
||||
$inventory = model('Inventory');
|
||||
$item = model(Item::class);
|
||||
$inventory = model('Inventory');
|
||||
$item = model(Item::class);
|
||||
$item_quantity = model(Item_quantity::class);
|
||||
|
||||
$items = $this->get_sale_items($sale_id)->getResultArray();
|
||||
@@ -808,7 +845,7 @@ class Sale extends Model
|
||||
foreach ($items as $item_data) {
|
||||
$cur_item_info = $item->get_info($item_data['item_id']);
|
||||
|
||||
if ($cur_item_info->stock_type == HAS_STOCK) {
|
||||
if ($cur_item_info->stock_type === HAS_STOCK) {
|
||||
// Create query to update inventory tracking
|
||||
$inv_data = [
|
||||
'trans_date' => date('Y-m-d H:i:s'),
|
||||
@@ -816,7 +853,7 @@ class Sale extends Model
|
||||
'trans_user' => $employee_id,
|
||||
'trans_comment' => 'Deleting sale ' . $sale_id,
|
||||
'trans_location' => $item_data['item_location'],
|
||||
'trans_inventory' => $item_data['quantity_purchased']
|
||||
'trans_inventory' => $item_data['quantity_purchased'],
|
||||
];
|
||||
// Update inventory
|
||||
$inventory->insert($inv_data, false);
|
||||
@@ -852,7 +889,7 @@ class Sale extends Model
|
||||
public function get_sale_items_ordered(int $sale_id): ResultInterface
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$item = model(Item::class);
|
||||
$item = model(Item::class);
|
||||
|
||||
$builder = $this->db->table('sales_items AS sales_items');
|
||||
$builder->select('
|
||||
@@ -876,18 +913,18 @@ class Sale extends Model
|
||||
$builder->where('sales_items.sale_id', $sale_id);
|
||||
|
||||
// Entry sequence (this will render kits in the expected sequence)
|
||||
if ($config['line_sequence'] == '0') { // TODO: Replace these with constants and this should be converted to a switch.
|
||||
if ($config['line_sequence'] === '0') { // TODO: Replace these with constants and this should be converted to a switch.
|
||||
$builder->orderBy('line', 'asc');
|
||||
}
|
||||
// Group by Stock Type (nonstock first - type 1, stock next - type 0)
|
||||
elseif ($config['line_sequence'] == '1') {
|
||||
elseif ($config['line_sequence'] === '1') {
|
||||
$builder->orderBy('stock_type', 'desc');
|
||||
$builder->orderBy('sales_items.description', 'asc');
|
||||
$builder->orderBy('items.name', 'asc');
|
||||
$builder->orderBy('items.qty_per_pack', 'asc');
|
||||
}
|
||||
// Group by Item Category
|
||||
elseif ($config['line_sequence'] == '2') {
|
||||
elseif ($config['line_sequence'] === '2') {
|
||||
$builder->orderBy('category', 'asc');
|
||||
$builder->orderBy('sales_items.description', 'asc');
|
||||
$builder->orderBy('items.name', 'asc');
|
||||
@@ -927,8 +964,8 @@ class Sale extends Model
|
||||
$payments[lang('Sales.rewards')] = lang('Sales.rewards');
|
||||
}
|
||||
$sale_lib = new Sale_lib();
|
||||
if ($sale_lib->get_mode() == 'sale_work_order') {
|
||||
$payments[lang('Sales.cash_deposit')] = lang('Sales.cash_deposit');
|
||||
if ($sale_lib->get_mode() === 'sale_work_order') {
|
||||
$payments[lang('Sales.cash_deposit')] = lang('Sales.cash_deposit');
|
||||
$payments[lang('Sales.credit_deposit')] = lang('Sales.credit_deposit');
|
||||
}
|
||||
|
||||
@@ -969,11 +1006,11 @@ class Sale extends Model
|
||||
$builder = $this->db->table('sales');
|
||||
$builder->where('quote_number', $quote_number);
|
||||
|
||||
if (!empty($sale_id)) {
|
||||
if (! empty($sale_id)) {
|
||||
$builder->where('sale_id !=', $sale_id);
|
||||
}
|
||||
|
||||
return ($builder->get()->getNumRows() == 1); // TODO: ===
|
||||
return $builder->get()->getNumRows() === 1; // TODO: ===
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -984,11 +1021,11 @@ class Sale extends Model
|
||||
$builder = $this->db->table('sales');
|
||||
$builder->where('invoice_number', $invoice_number);
|
||||
|
||||
if (!empty($sale_id)) {
|
||||
if (! empty($sale_id)) {
|
||||
$builder->where('sale_id !=', $sale_id);
|
||||
}
|
||||
|
||||
return ($builder->get()->getNumRows() == 1); // TODO: ===
|
||||
return $builder->get()->getNumRows() === 1; // TODO: ===
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -998,11 +1035,11 @@ class Sale extends Model
|
||||
{
|
||||
$builder = $this->db->table('sales');
|
||||
$builder->where('invoice_number', $work_order_number);
|
||||
if (!empty($sale_id)) {
|
||||
if (! empty($sale_id)) {
|
||||
$builder->where('sale_id !=', $sale_id);
|
||||
}
|
||||
|
||||
return ($builder->get()->getNumRows() == 1); // TODO: ===
|
||||
return $builder->get()->getNumRows() === 1; // TODO: ===
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1012,7 +1049,7 @@ class Sale extends Model
|
||||
{
|
||||
$giftcard = model(Giftcard::class);
|
||||
|
||||
if (!$giftcard->exists($giftcard->get_giftcard_id($giftcardNumber))) { // TODO: camelCase is used here for the variable name but we are using _ everywhere else. CI4 moved to camelCase... we should pick one and do that.
|
||||
if (! $giftcard->exists($giftcard->get_giftcard_id($giftcardNumber))) { // TODO: camelCase is used here for the variable name but we are using _ everywhere else. CI4 moved to camelCase... we should pick one and do that.
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1043,22 +1080,22 @@ class Sale extends Model
|
||||
$decimals = totals_decimals();
|
||||
|
||||
$sale_price = 'CASE WHEN sales_items.discount_type = ' . PERCENT
|
||||
. " THEN sales_items.quantity_purchased * sales_items.item_unit_price - ROUND(sales_items.quantity_purchased * sales_items.item_unit_price * sales_items.discount / 100, $decimals) "
|
||||
. " THEN sales_items.quantity_purchased * sales_items.item_unit_price - ROUND(sales_items.quantity_purchased * sales_items.item_unit_price * sales_items.discount / 100, {$decimals}) "
|
||||
. 'ELSE sales_items.quantity_purchased * (sales_items.item_unit_price - sales_items.discount) END';
|
||||
|
||||
$sale_cost = 'SUM(sales_items.item_cost_price * sales_items.quantity_purchased)';
|
||||
|
||||
$tax = 'IFNULL(SUM(sales_items_taxes.tax), 0)';
|
||||
$sales_tax = 'IFNULL(SUM(sales_items_taxes.sales_tax), 0)';
|
||||
$internal_tax = 'IFNULL(SUM(sales_items_taxes.internal_tax), 0)';
|
||||
$tax = 'IFNULL(SUM(sales_items_taxes.tax), 0)';
|
||||
$sales_tax = 'IFNULL(SUM(sales_items_taxes.sales_tax), 0)';
|
||||
$internal_tax = 'IFNULL(SUM(sales_items_taxes.internal_tax), 0)';
|
||||
$cash_adjustment = 'IFNULL(SUM(payments.sale_cash_adjustment), 0)';
|
||||
|
||||
if ($config['tax_included']) {
|
||||
$sale_total = "ROUND(SUM($sale_price), $decimals) + $cash_adjustment";
|
||||
$sale_subtotal = "$sale_total - $internal_tax";
|
||||
$sale_total = "ROUND(SUM({$sale_price}), {$decimals}) + {$cash_adjustment}";
|
||||
$sale_subtotal = "{$sale_total} - {$internal_tax}";
|
||||
} else {
|
||||
$sale_subtotal = "ROUND(SUM($sale_price), $decimals) - $internal_tax + $cash_adjustment";
|
||||
$sale_total = "ROUND(SUM($sale_price), $decimals) + $sales_tax + $cash_adjustment";
|
||||
$sale_subtotal = "ROUND(SUM({$sale_price}), {$decimals}) - {$internal_tax} + {$cash_adjustment}";
|
||||
$sale_total = "ROUND(SUM({$sale_price}), {$decimals}) + {$sales_tax} + {$cash_adjustment}";
|
||||
}
|
||||
|
||||
// Create a temporary table to contain all the sum of taxes per sale item
|
||||
@@ -1100,7 +1137,7 @@ class Sale extends Model
|
||||
|
||||
$this->db->query($sql);
|
||||
$item = model(Item::class);
|
||||
$sql = 'CREATE TEMPORARY TABLE IF NOT EXISTS ' . $this->db->prefixTable('sales_items_temp') .
|
||||
$sql = 'CREATE TEMPORARY TABLE IF NOT EXISTS ' . $this->db->prefixTable('sales_items_temp') .
|
||||
' (INDEX(sale_date), INDEX(sale_time), INDEX(sale_id))
|
||||
(
|
||||
SELECT
|
||||
@@ -1122,7 +1159,7 @@ class Sale extends Model
|
||||
MAX(sales.employee_id) AS employee_id,
|
||||
MAX(CONCAT(employee.first_name, " ", employee.last_name)) AS employee_name,
|
||||
items.item_id AS item_id,
|
||||
MAX(' . $item->get_item_name() . ') AS name,
|
||||
MAX(' . $item->get_item_name() . ") AS name,
|
||||
MAX(items.item_number) AS item_number,
|
||||
MAX(items.category) AS category,
|
||||
MAX(items.supplier_id) AS supplier_id,
|
||||
@@ -1137,12 +1174,12 @@ class Sale extends Model
|
||||
MAX(sales_items.description) AS description,
|
||||
MAX(payments.payment_type) AS payment_type,
|
||||
MAX(payments.sale_payment_amount) AS sale_payment_amount,
|
||||
' . "
|
||||
$sale_subtotal AS subtotal,
|
||||
$tax AS tax,
|
||||
$sale_total AS total,
|
||||
$sale_cost AS cost,
|
||||
($sale_subtotal - $sale_cost) AS profit
|
||||
|
||||
{$sale_subtotal} AS subtotal,
|
||||
{$tax} AS tax,
|
||||
{$sale_total} AS total,
|
||||
{$sale_cost} AS cost,
|
||||
({$sale_subtotal} - {$sale_cost}) AS profit
|
||||
" . '
|
||||
FROM ' . $this->db->prefixTable('sales_items') . ' AS sales_items
|
||||
INNER JOIN ' . $this->db->prefixTable('sales') . ' AS sales
|
||||
@@ -1173,7 +1210,7 @@ class Sale extends Model
|
||||
*/
|
||||
public function get_all_suspended(?int $customer_id = null): array
|
||||
{
|
||||
if ($customer_id == NEW_ENTRY) {
|
||||
if ($customer_id === NEW_ENTRY) {
|
||||
$query = $this->db->query("SELECT sale_id, case when sale_type = '" . SALE_TYPE_QUOTE . "' THEN quote_number WHEN sale_type = '" . SALE_TYPE_WORK_ORDER . "' THEN work_order_number else sale_id end as doc_id, sale_id as suspended_sale_id, sale_status, sale_time, dinner_table_id, customer_id, employee_id, comment FROM "
|
||||
. $this->db->prefixTable('sales') . ' where sale_status = ' . SUSPENDED);
|
||||
} else {
|
||||
@@ -1189,7 +1226,7 @@ class Sale extends Model
|
||||
*/
|
||||
public function get_dinner_table(int $sale_id) // TODO: this is returning null or the table_id. We can keep it this way but multiple return types can't be declared until PHP 8.x
|
||||
{
|
||||
if ($sale_id == NEW_ENTRY) {
|
||||
if ($sale_id === NEW_ENTRY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1221,11 +1258,6 @@ class Sale extends Model
|
||||
return $builder->get()->getRow()->sale_status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $sale_id
|
||||
* @param int $sale_status
|
||||
* @return void
|
||||
*/
|
||||
public function update_sale_status(int $sale_id, int $sale_status): void
|
||||
{
|
||||
$builder = $this->db->table('sales');
|
||||
@@ -1244,7 +1276,7 @@ class Sale extends Model
|
||||
|
||||
$row = $builder->get()->getRow();
|
||||
|
||||
if ($row != null) {
|
||||
if ($row !== null) {
|
||||
return $row->quote_number;
|
||||
}
|
||||
|
||||
@@ -1261,7 +1293,7 @@ class Sale extends Model
|
||||
|
||||
$row = $builder->get()->getRow();
|
||||
|
||||
if ($row != null) { // TODO: === ?
|
||||
if ($row !== null) { // TODO: === ?
|
||||
return $row->work_order_number;
|
||||
}
|
||||
|
||||
@@ -1278,7 +1310,7 @@ class Sale extends Model
|
||||
|
||||
$row = $builder->get()->getRow();
|
||||
|
||||
if ($row != null) { // TODO: === ?
|
||||
if ($row !== null) { // TODO: === ?
|
||||
return $row->comment;
|
||||
}
|
||||
|
||||
@@ -1308,7 +1340,7 @@ class Sale extends Model
|
||||
$config = config(OSPOS::class)->settings;
|
||||
|
||||
if ($config['dinner_table_enable']) {
|
||||
$dinner_table = model(Dinner_table::class);
|
||||
$dinner_table = model(Dinner_table::class);
|
||||
$dinner_table_id = $this->get_dinner_table($sale_id);
|
||||
$dinner_table->release($dinner_table_id);
|
||||
}
|
||||
@@ -1330,7 +1362,7 @@ class Sale extends Model
|
||||
$config = config(OSPOS::class)->settings;
|
||||
|
||||
if ($config['dinner_table_enable']) {
|
||||
$dinner_table = model(Dinner_table::class);
|
||||
$dinner_table = model(Dinner_table::class);
|
||||
$dinner_table_id = $this->get_dinner_table($sale_id);
|
||||
$dinner_table->release($dinner_table_id);
|
||||
}
|
||||
@@ -1365,30 +1397,24 @@ class Sale extends Model
|
||||
return $builder->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $customer_id
|
||||
* @param int $sale_id
|
||||
* @param float $total_amount
|
||||
* @param float $total_amount_used
|
||||
*/
|
||||
private function save_customer_rewards(int $customer_id, int $sale_id, float $total_amount, float $total_amount_used): void
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
|
||||
if (!empty($customer_id) && $config['customer_reward_enable']) {
|
||||
$customer = model(Customer::class);
|
||||
if (! empty($customer_id) && $config['customer_reward_enable']) {
|
||||
$customer = model(Customer::class);
|
||||
$customer_rewards = model(Customer_rewards::class);
|
||||
$rewards = model(Rewards::class);
|
||||
$rewards = model(Rewards::class);
|
||||
|
||||
$package_id = $customer->get_info($customer_id)->package_id;
|
||||
|
||||
if (!empty($package_id)) {
|
||||
$points_percent = $customer_rewards->get_points_percent($package_id);
|
||||
$points = $customer->get_info($customer_id)->points;
|
||||
$points = ($points == null ? 0 : $points);
|
||||
$points_percent = ($points_percent == null ? 0 : $points_percent);
|
||||
if (! empty($package_id)) {
|
||||
$points_percent = $customer_rewards->get_points_percent($package_id);
|
||||
$points = $customer->get_info($customer_id)->points;
|
||||
$points = ($points === null ? 0 : $points);
|
||||
$points_percent = ($points_percent === null ? 0 : $points_percent);
|
||||
$total_amount_earned = ($total_amount * $points_percent / 100);
|
||||
$points = $points + $total_amount_earned;
|
||||
$points += $total_amount_earned;
|
||||
|
||||
$customer->update_reward_points_value($customer_id, $points);
|
||||
|
||||
@@ -1402,7 +1428,6 @@ class Sale extends Model
|
||||
/**
|
||||
* Creates a temporary table to store the sales_payments data
|
||||
*
|
||||
* @param string $where
|
||||
* @return array
|
||||
*/
|
||||
private function create_temp_table_sales_payments_data(string $where): void
|
||||
@@ -1412,7 +1437,7 @@ class Sale extends Model
|
||||
'payments.sale_id',
|
||||
'SUM(CASE WHEN `payments`.`cash_adjustment` = 0 THEN `payments`.`payment_amount` ELSE 0 END) AS sale_payment_amount',
|
||||
'SUM(CASE WHEN `payments`.`cash_adjustment` = 1 THEN `payments`.`payment_amount` ELSE 0 END) AS sale_cash_adjustment',
|
||||
'GROUP_CONCAT(CONCAT(`payments`.`payment_type`, " ", (`payments`.`payment_amount` - `payments`.`cash_refund`)) SEPARATOR ", ") AS payment_type'
|
||||
'GROUP_CONCAT(CONCAT(`payments`.`payment_type`, " ", (`payments`.`payment_amount` - `payments`.`cash_refund`)) SEPARATOR ", ") AS payment_type',
|
||||
]);
|
||||
$builder->join('sales', 'sales.sale_id = payments.sale_id', 'inner');
|
||||
$builder->where($where);
|
||||
@@ -1429,12 +1454,10 @@ class Sale extends Model
|
||||
/**
|
||||
* Temporary table to store the sales_items_taxes data
|
||||
*
|
||||
* @param string $where
|
||||
* @return \CodeIgniter\Database\BaseBuilder
|
||||
* @return BaseBuilder
|
||||
*/
|
||||
private function create_temp_table_sales_items_taxes_data(string $where): void
|
||||
{
|
||||
|
||||
$builder = $this->db->table('sales_items_taxes AS sales_items_taxes');
|
||||
$builder->select([
|
||||
'sales_items_taxes.sale_id AS sale_id',
|
||||
@@ -1442,7 +1465,7 @@ class Sale extends Model
|
||||
'sales_items_taxes.line AS line',
|
||||
'SUM(sales_items_taxes.item_tax_amount) AS tax',
|
||||
'SUM(CASE WHEN sales_items_taxes.tax_type = 0 THEN sales_items_taxes.item_tax_amount ELSE 0 END) AS internal_tax',
|
||||
'SUM(CASE WHEN sales_items_taxes.tax_type = 1 THEN sales_items_taxes.item_tax_amount ELSE 0 END) AS sales_tax'
|
||||
'SUM(CASE WHEN sales_items_taxes.tax_type = 1 THEN sales_items_taxes.item_tax_amount ELSE 0 END) AS sales_tax',
|
||||
]);
|
||||
$builder->join('sales', 'sales.sale_id = sales_items_taxes.sale_id', 'inner');
|
||||
$builder->join('sales_items', 'sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.line = sales_items_taxes.line', 'inner');
|
||||
@@ -1455,15 +1478,9 @@ class Sale extends Model
|
||||
. ' (INDEX(sale_id), INDEX(item_id)) ENGINE=MEMORY AS (' . $sub_query . ')');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $search
|
||||
* @param array $filters
|
||||
* @param BaseBuilder $builder
|
||||
* @return void
|
||||
*/
|
||||
private function add_filters_to_query(?string $search, array $filters, BaseBuilder $builder): void
|
||||
{
|
||||
if (!empty($search)) { // TODO: this is duplicated code. We should think about refactoring out a method
|
||||
if (! empty($search)) { // TODO: this is duplicated code. We should think about refactoring out a method
|
||||
if ($filters['is_valid_receipt']) {
|
||||
$pieces = explode(' ', $search);
|
||||
$builder->where('sales.sale_id', $pieces[1]);
|
||||
@@ -1481,16 +1498,15 @@ class Sale extends Model
|
||||
}
|
||||
}
|
||||
|
||||
if ($filters['location_id'] != 'all') {
|
||||
if ($filters['location_id'] !== 'all') {
|
||||
$builder->where('sales_items.item_location', $filters['location_id']);
|
||||
}
|
||||
|
||||
if ($filters['selected_customer'] != false) {
|
||||
if ($filters['selected_customer'] !== false) {
|
||||
$sale_lib = new Sale_lib();
|
||||
$builder->where('sales.customer_id', $sale_lib->get_customer());
|
||||
}
|
||||
|
||||
|
||||
if ($filters['only_invoices']) {
|
||||
$builder->where('sales.invoice_number IS NOT NULL');
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ if (isset($error_message)) {
|
||||
<div class="btn btn-info btn-sm" id="show_email_button"><?= '<span class="glyphicon glyphicon-envelope"> </span>' . lang('Sales.send_invoice') ?></div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?= anchor("sales/ublInvoice/$sale_id_num", '<span class="glyphicon glyphicon-download"> </span>' . lang('Sales.download_ubl'), ['class' => 'btn btn-info btn-sm']) ?>
|
||||
<?= anchor("sales", '<span class="glyphicon glyphicon-shopping-cart"> </span>' . lang('Sales.register'), ['class' => 'btn btn-info btn-sm', 'id' => 'show_sales_button']) ?>
|
||||
<?= anchor("sales/manage", '<span class="glyphicon glyphicon-list-alt"> </span>' . lang('Sales.takings'), ['class' => 'btn btn-info btn-sm', 'id' => 'show_takings_button']) ?>
|
||||
</div>
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
"dompdf/dompdf": "^2.0.3",
|
||||
"ezyang/htmlpurifier": "^4.17",
|
||||
"laminas/laminas-escaper": "2.18.0",
|
||||
"nesbot/carbon": "^2.72",
|
||||
"num-num/ubl-invoice": "^2.4",
|
||||
"paragonie/random_compat": "^2.0.21",
|
||||
"picqer/php-barcode-generator": "^2.4.0",
|
||||
"tamtamchik/namecase": "^3.0.0"
|
||||
|
||||
1755
composer.lock
generated
1755
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Libraries\InvoiceAttachment;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Libraries\InvoiceAttachment\InvoiceAttachmentGenerator;
|
||||
use App\Libraries\InvoiceAttachment\InvoiceAttachment;
|
||||
use App\Libraries\InvoiceAttachment\PdfAttachment;
|
||||
use App\Libraries\InvoiceAttachment\UblAttachment;
|
||||
|
||||
class InvoiceAttachmentGeneratorTest extends CIUnitTestCase
|
||||
{
|
||||
public function testCreateFromConfigPdfOnly(): void
|
||||
{
|
||||
$generator = InvoiceAttachmentGenerator::createFromConfig('pdf_only');
|
||||
$this->assertInstanceOf(InvoiceAttachmentGenerator::class, $generator);
|
||||
}
|
||||
|
||||
public function testCreateFromConfigUblOnly(): void
|
||||
{
|
||||
$generator = InvoiceAttachmentGenerator::createFromConfig('ubl_only');
|
||||
$this->assertInstanceOf(InvoiceAttachmentGenerator::class, $generator);
|
||||
}
|
||||
|
||||
public function testCreateFromConfigBoth(): void
|
||||
{
|
||||
$generator = InvoiceAttachmentGenerator::createFromConfig('both');
|
||||
$this->assertInstanceOf(InvoiceAttachmentGenerator::class, $generator);
|
||||
}
|
||||
|
||||
public function testCreateFromConfigPdfOnlyRegistersPdfAttachment(): void
|
||||
{
|
||||
$generator = InvoiceAttachmentGenerator::createFromConfig('pdf_only');
|
||||
$attachments = $this->getPrivateProperty($generator, 'attachments');
|
||||
|
||||
$this->assertCount(1, $attachments);
|
||||
$this->assertInstanceOf(PdfAttachment::class, $attachments[0]);
|
||||
}
|
||||
|
||||
public function testCreateFromConfigUblOnlyRegistersUblAttachment(): void
|
||||
{
|
||||
$generator = InvoiceAttachmentGenerator::createFromConfig('ubl_only');
|
||||
$attachments = $this->getPrivateProperty($generator, 'attachments');
|
||||
|
||||
$this->assertCount(1, $attachments);
|
||||
$this->assertInstanceOf(UblAttachment::class, $attachments[0]);
|
||||
}
|
||||
|
||||
public function testCreateFromConfigBothRegistersBothAttachments(): void
|
||||
{
|
||||
$generator = InvoiceAttachmentGenerator::createFromConfig('both');
|
||||
$attachments = $this->getPrivateProperty($generator, 'attachments');
|
||||
|
||||
$this->assertCount(2, $attachments);
|
||||
$this->assertInstanceOf(PdfAttachment::class, $attachments[0]);
|
||||
$this->assertInstanceOf(UblAttachment::class, $attachments[1]);
|
||||
}
|
||||
|
||||
public function testRegisterAddsAttachment(): void
|
||||
{
|
||||
$generator = new InvoiceAttachmentGenerator();
|
||||
$mockAttachment = new class implements InvoiceAttachment {
|
||||
public function generate(array $saleData, string $type): ?string { return null; }
|
||||
public function isApplicableForType(string $type, array $saleData): bool { return true; }
|
||||
public function getFileExtension(): string { return 'test'; }
|
||||
public function getEnabledConfigValues(): array { return ['test']; }
|
||||
};
|
||||
|
||||
$result = $generator->register($mockAttachment);
|
||||
|
||||
$this->assertSame($generator, $result);
|
||||
$attachments = $this->getPrivateProperty($generator, 'attachments');
|
||||
$this->assertCount(1, $attachments);
|
||||
}
|
||||
|
||||
public function testRegisterIsChainable(): void
|
||||
{
|
||||
$generator = new InvoiceAttachmentGenerator();
|
||||
$mockAttachment = new class implements InvoiceAttachment {
|
||||
public function generate(array $saleData, string $type): ?string { return null; }
|
||||
public function isApplicableForType(string $type, array $saleData): bool { return true; }
|
||||
public function getFileExtension(): string { return 'test'; }
|
||||
public function getEnabledConfigValues(): array { return ['test']; }
|
||||
};
|
||||
|
||||
$result = $generator->register($mockAttachment)->register($mockAttachment);
|
||||
|
||||
$attachments = $this->getPrivateProperty($result, 'attachments');
|
||||
$this->assertCount(2, $attachments);
|
||||
}
|
||||
|
||||
public function testGenerateAttachmentsReturnsEmptyArrayWhenNoAttachmentsRegistered(): void
|
||||
{
|
||||
$generator = new InvoiceAttachmentGenerator();
|
||||
$result = $generator->generateAttachments([], 'invoice');
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertEmpty($result);
|
||||
}
|
||||
|
||||
public function testCleanupRemovesFiles(): void
|
||||
{
|
||||
$tempFile1 = tempnam(sys_get_temp_dir(), 'test_');
|
||||
$tempFile2 = tempnam(sys_get_temp_dir(), 'test_');
|
||||
|
||||
$this->assertFileExists($tempFile1);
|
||||
$this->assertFileExists($tempFile2);
|
||||
|
||||
InvoiceAttachmentGenerator::cleanup([$tempFile1, $tempFile2]);
|
||||
|
||||
$this->assertFileDoesNotExist($tempFile1);
|
||||
$this->assertFileDoesNotExist($tempFile2);
|
||||
}
|
||||
|
||||
public function testCleanupHandlesNonExistentFiles(): void
|
||||
{
|
||||
// Should not throw an exception
|
||||
InvoiceAttachmentGenerator::cleanup(['/non/existent/file1', '/non/existent/file2']);
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
70
tests/Libraries/InvoiceAttachment/PdfAttachmentTest.php
Normal file
70
tests/Libraries/InvoiceAttachment/PdfAttachmentTest.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Libraries\InvoiceAttachment;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Libraries\InvoiceAttachment\PdfAttachment;
|
||||
|
||||
class PdfAttachmentTest extends CIUnitTestCase
|
||||
{
|
||||
private PdfAttachment $attachment;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->attachment = new PdfAttachment();
|
||||
}
|
||||
|
||||
public function testGetFileExtensionReturnsPdf(): void
|
||||
{
|
||||
$this->assertEquals('pdf', $this->attachment->getFileExtension());
|
||||
}
|
||||
|
||||
public function testGetEnabledConfigValuesReturnsCorrectArray(): void
|
||||
{
|
||||
$values = $this->attachment->getEnabledConfigValues();
|
||||
|
||||
$this->assertIsArray($values);
|
||||
$this->assertContains('pdf_only', $values);
|
||||
$this->assertContains('both', $values);
|
||||
$this->assertCount(2, $values);
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsTrueForInvoice(): void
|
||||
{
|
||||
$this->assertTrue($this->attachment->isApplicableForType('invoice', []));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsTrueForTaxInvoice(): void
|
||||
{
|
||||
$this->assertTrue($this->attachment->isApplicableForType('tax_invoice', []));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsTrueForQuote(): void
|
||||
{
|
||||
$this->assertTrue($this->attachment->isApplicableForType('quote', []));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsTrueForWorkOrder(): void
|
||||
{
|
||||
$this->assertTrue($this->attachment->isApplicableForType('work_order', []));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsTrueForReceipt(): void
|
||||
{
|
||||
$this->assertTrue($this->attachment->isApplicableForType('receipt', []));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsTrueForAnyType(): void
|
||||
{
|
||||
// PDF should work for any document type
|
||||
$this->assertTrue($this->attachment->isApplicableForType('random_type', []));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeIgnoresSaleData(): void
|
||||
{
|
||||
// PDF attachment doesn't depend on invoice_number
|
||||
$this->assertTrue($this->attachment->isApplicableForType('invoice', ['invoice_number' => null]));
|
||||
$this->assertTrue($this->attachment->isApplicableForType('invoice', ['invoice_number' => 'INV-001']));
|
||||
}
|
||||
}
|
||||
103
tests/Libraries/InvoiceAttachment/UblAttachmentTest.php
Normal file
103
tests/Libraries/InvoiceAttachment/UblAttachmentTest.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Libraries\InvoiceAttachment;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Libraries\InvoiceAttachment\UblAttachment;
|
||||
|
||||
class UblAttachmentTest extends CIUnitTestCase
|
||||
{
|
||||
private UblAttachment $attachment;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->attachment = new UblAttachment();
|
||||
}
|
||||
|
||||
public function testGetFileExtensionReturnsXml(): void
|
||||
{
|
||||
$this->assertEquals('xml', $this->attachment->getFileExtension());
|
||||
}
|
||||
|
||||
public function testGetEnabledConfigValuesReturnsCorrectArray(): void
|
||||
{
|
||||
$values = $this->attachment->getEnabledConfigValues();
|
||||
|
||||
$this->assertIsArray($values);
|
||||
$this->assertContains('ubl_only', $values);
|
||||
$this->assertContains('both', $values);
|
||||
$this->assertCount(2, $values);
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsTrueForInvoiceWithInvoiceNumber(): void
|
||||
{
|
||||
$saleData = ['invoice_number' => 'INV-001'];
|
||||
|
||||
$this->assertTrue($this->attachment->isApplicableForType('invoice', $saleData));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsTrueForTaxInvoiceWithInvoiceNumber(): void
|
||||
{
|
||||
$saleData = ['invoice_number' => 'INV-001'];
|
||||
|
||||
$this->assertTrue($this->attachment->isApplicableForType('tax_invoice', $saleData));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsFalseForInvoiceWithoutInvoiceNumber(): void
|
||||
{
|
||||
$saleData = ['invoice_number' => null];
|
||||
|
||||
$this->assertFalse($this->attachment->isApplicableForType('invoice', $saleData));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsFalseForInvoiceWithEmptyInvoiceNumber(): void
|
||||
{
|
||||
$saleData = ['invoice_number' => ''];
|
||||
|
||||
$this->assertFalse($this->attachment->isApplicableForType('invoice', $saleData));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsFalseForInvoiceWithoutInvoiceNumberKey(): void
|
||||
{
|
||||
$saleData = [];
|
||||
|
||||
$this->assertFalse($this->attachment->isApplicableForType('invoice', $saleData));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsFalseForQuoteEvenWithInvoiceNumber(): void
|
||||
{
|
||||
$saleData = ['invoice_number' => 'INV-001'];
|
||||
|
||||
$this->assertFalse($this->attachment->isApplicableForType('quote', $saleData));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsFalseForWorkOrderEvenWithInvoiceNumber(): void
|
||||
{
|
||||
$saleData = ['invoice_number' => 'INV-001'];
|
||||
|
||||
$this->assertFalse($this->attachment->isApplicableForType('work_order', $saleData));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsFalseForReceiptEvenWithInvoiceNumber(): void
|
||||
{
|
||||
$saleData = ['invoice_number' => 'INV-001'];
|
||||
|
||||
$this->assertFalse($this->attachment->isApplicableForType('receipt', $saleData));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsFalseForUnknownType(): void
|
||||
{
|
||||
$saleData = ['invoice_number' => 'INV-001'];
|
||||
|
||||
$this->assertFalse($this->attachment->isApplicableForType('unknown_type', $saleData));
|
||||
}
|
||||
|
||||
public function testGenerateReturnsNullForMissingConfig(): void
|
||||
{
|
||||
// Without proper sale_data, generate should fail gracefully
|
||||
$result = $this->attachment->generate([], 'invoice');
|
||||
|
||||
$this->assertNull($result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user