facturas.prg
function Main()
local cAction := Upost( "action" )
local cId := Upost( "id" )
local cClientId := Upost( "client_id" )
local cDate := Upost( "date" )
local cStatus := Upost( "status" )
local cLinesData := Upost( "lines_data" ) // Format: PROD_ID,QTY;PROD_ID,QTY
// Search Parameter (Post)
local cSearch := Upost( "search" )
local cTmpDir := "c:/hix/testdir"
local cDbFact := cTmpDir + "/facturas_v2.dbf"
local cDbLines := cTmpDir + "/facturas_lines.dbf"
local cDbCli := cTmpDir + "/clientes_v2.dbf"
local cDbProd := cTmpDir + "/productos.dbf"
local cMessage := ""
local cTotal := "0.00"
// Ensure Directory Exists
if ! IsDirectory( cTmpDir )
DirMake( cTmpDir )
endif
// 1. Initialize Databases
InitDatabases( cDbFact, cDbLines, cDbCli, cDbProd )
// 2. Open Databases Shared
if !OpenDatabases()
return "Error opening databases."
endif
// 3. Seed Data if Empty
SeedData()
set deleted on
set date format to "yyyy-mm-dd"
do case
case cAction == "save"
if !Empty(cClientId)
select facturas
if Empty( cId )
// New Invoice
append blank
if !NetErr()
cId := GenerateID()
replace id with cId
replace client_id with cClientId
replace date with CToD( cDate )
replace status with cStatus
// Save Lines & Calculate Total
cTotal := SaveLines( cId, cLinesData, .T. )
replace total with Val( cTotal )
commit
unlock
cMessage := "Invoice saved successfully."
else
cMessage := "Error: Could not create invoice (Lock failed)."
endif
else
// Update existing
go top
locate for AllTrim(id) == AllTrim(cId)
if found()
if Rlock()
replace client_id with cClientId
replace date with CToD( cDate )
replace status with cStatus // Note: Stock logic implies checking status change, simplfied here
// Save Lines & Calculate Total
cTotal := SaveLines( cId, cLinesData, .F. )
replace total with Val( cTotal )
commit
unlock
cMessage := "Invoice updated successfully."
endif
endif
endif
// Handle Stock Deduction if Paid
if cStatus == "Paid"
UpdateStock( cId )
endif
endif
cId := ""
cClientId := ""
cDate := ""
cTotal := ""
cStatus := ""
cSearch := ""
case cAction == "delete"
if ! Empty( cId )
// Delete Lines first
select lines
go top
do while ! Eof()
if lines->invoice_id == cId
if Rlock()
delete
unlock
endif
endif
skip
enddo
// Delete Header
select facturas
go top
locate for AllTrim(id) == AllTrim(cId)
if found() .and. Rlock()
delete
unlock
commit
cMessage := "Invoice deleted successfully."
endif
endif
cId := ""
cSearch := ""
case cAction == "edit"
select facturas
go top
locate for AllTrim(id) == AllTrim(cId)
if found()
cClientId := facturas->client_id
cDate := DToC( facturas->date )
cTotal := Str( facturas->total )
cStatus := facturas->status
endif
case cAction == "cancel"
cId := ""
cSearch := ""
endcase
RenderView( cId, cClientId, cDate, cTotal, cStatus, cMessage, cSearch )
CloseDatabases()
return ""
// --------------------------------------------------------------------------------
// Database Helpers
// --------------------------------------------------------------------------------
function InitDatabases( cDbFact, cDbLines, cDbCli, cDbProd )
if ! File( cDbFact )
DbCreate( cDbFact, { ;
{ "ID", "C", 8, 0 }, ;
{ "CLIENT_ID", "C", 8, 0 }, ;
{ "DATE", "D", 8, 0 }, ;
{ "TOTAL", "N", 12, 2 }, ;
{ "STATUS", "C", 10, 0 } ;
} )
endif
if ! File( cDbLines )
DbCreate( cDbLines, { ;
{ "INVOICE_ID","C", 8, 0 }, ;
{ "PROD_ID", "C", 8, 0 }, ;
{ "QTY", "N", 10, 2 }, ;
{ "PRICE", "N", 10, 2 }, ;
{ "TOTAL", "N", 12, 2 } ;
} )
endif
if ! File( cDbCli )
DbCreate( cDbCli, { ;
{ "ID", "C", 8, 0 }, ;
{ "NAME", "C", 50, 0 } ;
} )
endif
if ! File( cDbProd )
DbCreate( cDbProd, { ;
{ "ID", "C", 8, 0 }, ;
{ "NAME", "C", 50, 0 }, ;
{ "PRICE", "N", 10, 2 }, ;
{ "STOCK", "N", 10, 2 } ;
} )
endif
return nil
function OpenDatabases()
if Select("facturas") == 0
use ("c:/hix/testdir/facturas_v2.dbf") shared new alias "facturas"
else
select facturas
endif
if Select("lines") == 0
use ("c:/hix/testdir/facturas_lines.dbf") shared new alias "lines"
else
select lines
endif
if Select("clients") == 0
use ("c:/hix/testdir/clientes.dbf") shared new alias "clients"
else
select clients
endif
if Select("products") == 0
use ("c:/hix/testdir/productos.dbf") shared new alias "products"
else
select products
endif
return .T.
function CloseDatabases()
close all
return nil
function SeedData()
select clients
if RecCount() == 0
append blank
replace id with "C1", name with "Acme Corp"
append blank
replace id with "C2", name with "Globex Inc."
append blank
replace id with "C3", name with "Wayne Ent."
commit
endif
select products
if RecCount() == 0
append blank
replace id with "P1", name with "Widget A", price with 10.00, stock with 100
append blank
replace id with "P2", name with "Widget B", price with 25.50, stock with 50
append blank
replace id with "P3", name with "Premium X", price with 99.99, stock with 10
commit
endif
return nil
// --------------------------------------------------------------------------------
// Logic Helpers
// --------------------------------------------------------------------------------
function SaveLines( cInvoiceId, cData, lNew )
local aItems := hb_ATokens( cData, ";" )
local aParts
local cItem
local nGrandTotal := 0
local nPrice := 0
local nQty := 0
local nRowTotal := 0
select lines
// If not new, delete old lines first
if !lNew
go top
do while !Eof()
if lines->invoice_id == cInvoiceId
if Rlock()
delete
unlock
endif
endif
skip
enddo
endif
// Add new lines
for each cItem in aItems
if !Empty(cItem)
aParts := hb_ATokens( cItem, "," ) // PROD_ID,QTY
if Len(aParts) >= 2
nQty := Val( aParts[2] )
// Lookup Price
select products
go top
locate for id == aParts[1]
if found()
nPrice := products->price
else
nPrice := 0
endif
nRowTotal := nQty * nPrice
nGrandTotal += nRowTotal
select lines
append blank
if !NetErr()
replace invoice_id with cInvoiceId
replace prod_id with aParts[1]
replace qty with nQty
replace price with nPrice
replace total with nRowTotal
unlock
endif
endif
endif
next
return AllTrim( Str( nGrandTotal ) )
function UpdateStock( cInvoiceId )
// Simple implementation: Iterate lines and subtract stock
// Real implementation should check if stock was already deducted
select lines
go top
do while ! Eof()
if lines->invoice_id == cInvoiceId
select products
go top
locate for id == lines->prod_id
if found() .and. Rlock()
replace stock with stock - lines->qty
unlock
endif
select lines
endif
skip
enddo
return nil
function GenerateID()
local cChars := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
local cNewId := ""
local i
for i := 1 to 8
cNewId += SubStr( cChars, Int( hb_Random() * Len(cChars) ) + 1, 1 )
next
return cNewId
// --------------------------------------------------------------------------------
// View Renderer
// --------------------------------------------------------------------------------
function RenderView( cId, cClientId, cDate, cTotal, cStatus, cMessage, cSearch )
local cHtml := MemoRead( "c:/hix/facturas.view" )
// 1. Build Client Options
local cClientOpts := "<option value=''>Select Client</option>"
local cSelected := ""
// 2. Build Product Data for JS
local cProdScript := "var products = {"
// 3. Build Invoice Rows vars
local cRows := ""
local cCliName := ""
local lMatch := .T.
local cFullData := ""
local cStatusColor := ""
// MESSAGE VAR
local cMsgHtml := ""
local cLineScript := "var existingLines = [];"
// LOGIC START
select clients
go top
do while ! Eof()
cSelected := iif( AllTrim(clients->id) == AllTrim(cClientId), "selected", "" )
cClientOpts += [<option value="] + AllTrim(clients->id) + ["] + cSelected + [>] + AllTrim(clients->name) + [</option>]
skip
enddo
select products
go top
do while ! Eof()
cProdScript += ["] + AllTrim(products->id) + [": {name: "] + AllTrim(products->name) + [", price: ] + AllTrim(Str(products->price)) + [},]
skip
enddo
cProdScript += "};"
select facturas
go top
do while ! Eof()
lMatch := .T.
// Lookup Client Name
select clients
go top
locate for id == facturas->client_id
if found()
cCliName := AllTrim( clients->name )
else
cCliName := facturas->client_id
endif
select facturas
// Search Filter
if !Empty( cSearch )
cFullData := Upper(cCliName + " " + DToC(facturas->date) + " " + Str(facturas->total) + " " + facturas->status)
if ! ( Upper(AllTrim(cSearch)) $ cFullData )
lMatch := .F.
endif
endif
if lMatch
cRows += [<tr>]
cRows += [<td>] + AllTrim(facturas->id) + [</td>]
cRows += [<td>] + cCliName + [</td>]
cRows += [<td>] + DToC( facturas->date ) + [</td>]
cRows += [<td>] + AllTrim( Str( facturas->total ) ) + [</td>]
cRows += [<td>] + AllTrim( facturas->status ) + [</td>]
cRows += [<td class="actions-cell">]
cRows += [<form method="POST" action="facturas.prg" style="display:inline;">]
cRows += [<input type="hidden" name="action" value="edit">]
cRows += [<input type="hidden" name="id" value="] + AllTrim(facturas->id) + [">]
cRows += [<button type="submit" class="btn btn-sm btn-edit"><i class="fa-solid fa-pen"></i></button>]
cRows += [</form> ]
cRows += [<form method="POST" action="facturas.prg" style="display:inline;" onsubmit="return confirm('Delete invoice?')">]
cRows += [<input type="hidden" name="action" value="delete">]
cRows += [<input type="hidden" name="id" value="] + AllTrim(facturas->id) + [">]
cRows += [<button type="submit" class="btn btn-sm btn-danger"><i class="fa-solid fa-trash"></i></button>]
cRows += [</form>]
cRows += [<form method="POST" action="factura_pdf.prg" target="_blank" style="display:inline; margin-left: 4px;">]
cRows += [<input type="hidden" name="id" value="] + AllTrim(facturas->id) + [">]
cRows += [<button type="submit" class="btn btn-sm btn-secondary" title="Download PDF"><i class="fa-solid fa-file-pdf"></i></button>]
cRows += [</form></td></tr>]
endif
skip
enddo
if !Empty( cId )
select lines
go top
do while ! Eof()
if lines->invoice_id == cId
cLineScript += [existingLines.push({prod_id: "] + AllTrim(lines->prod_id) + [", qty: ] + AllTrim(Str(lines->qty)) + [});]
endif
skip
enddo
endif
// Replacements
cHtml := StrTran( cHtml, "{{CLIENT_OPTIONS}}", cClientOpts )
cHtml := StrTran( cHtml, "{{PRODUCT_DATA}}", cProdScript )
cHtml := StrTran( cHtml, "{{EXISTING_LINES}}", cLineScript )
cHtml := StrTran( cHtml, "{{INVOICE_ROWS}}", cRows )
// Standard Fields
cHtml := StrTran( cHtml, "{{VAL_ID}}", iif( Empty(cId), "", cId ) )
cHtml := StrTran( cHtml, "{{VAL_DATE}}", iif( Empty(cDate), Date(), cDate ) )
cHtml := StrTran( cHtml, "{{VAL_TOTAL}}", iif( Empty(cTotal), "", AllTrim(cTotal) ) )
cHtml := StrTran( cHtml, "{{SEL_PAID}}", iif( cStatus == "Paid", "selected", "" ) )
cHtml := StrTran( cHtml, "{{SEL_PENDING}}", iif( cStatus == "Pending", "selected", "" ) )
cHtml := StrTran( cHtml, "{{SEL_CANCELLED}}", iif( cStatus == "Cancelled", "selected", "" ) )
// Message & Search
if !Empty( cMessage )
if Left( cMessage, 5 ) == "Error"
cMsgHtml := [<div class="alert alert-danger">] + cMessage + [</div>]
else
cMsgHtml := [<div class="alert alert-success">] + cMessage + [</div>]
endif
endif
cHtml := StrTran( cHtml, "{{MESSAGE}}", cMsgHtml )
cHtml := StrTran( cHtml, "{{VAL_SEARCH}}", iif( Empty(cSearch), "", cSearch ) )
if !Empty(cId)
cHtml := StrTran( cHtml, "{{FORM_TITLE}}", "Edit Invoice: " + cId )
cHtml := StrTran( cHtml, "{{CANCEL_BUTTON}}", [<a href="facturas.prg" class="btn btn-sm" style="background:#475569;margin-left:5px;">Cancel</a>] )
else
cHtml := StrTran( cHtml, "{{FORM_TITLE}}", "New Invoice" )
cHtml := StrTran( cHtml, "{{CANCEL_BUTTON}}", "" )
endif
UWrite( cHtml )
return ""facturas.view
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoices Management</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
body { background-color: #0f172a; color: #e0f2fe; font-family: 'Inter', sans-serif; }
.container-fluid { max-width: 1400px; padding: 20px; }
.card { background-color: #1e293b; border: 1px solid #3b82f6; border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.2); }
.card-header { background-color: #1d4ed8; color: #e0f2fe; border-bottom: 1px solid #1e40af; border-radius: 12px 12px 0 0 !important; padding: 1rem 1.5rem; }
/* Navbar Standardization */
.navbar { background-color: #172554; border-bottom: 1px solid #1e3a8a; padding-top: 0.75rem; padding-bottom: 0.75rem; min-height: 70px; }
.navbar-brand { font-weight: 600; color: #bae6fd !important; font-size: 1.25rem; }
.nav-link { color: #bfdbfe; margin-right: 15px; font-weight: 500; }
.nav-link:hover, .nav-link.active { color: #e0f2fe; font-weight: 600; }
.form-label { color: #bfdbfe; font-weight: 500; font-size: 0.875rem; }
.form-control, .form-select { background-color: #0f172a; border: 1px solid #3b82f6; color: #e0f2fe; border-radius: 8px; padding: 0.625rem 1rem; }
.form-control:focus, .form-select:focus { background-color: #172554; border-color: #60a5fa; box-shadow: 0 0 0 2px rgba(96,165,250,0.3); color: #e0f2fe; }
.btn-primary { background-color: #3b82f6; border: none; padding: 0.625rem 1.25rem; font-weight: 500; }
.btn-primary:hover { background-color: #2563eb; }
.btn-secondary { background-color: #1e40af; border: none; color: #e0f2fe; }
.btn-secondary:hover { background-color: #1e3a8a; }
.table { color: #0c4a6e; margin-bottom: 0; }
.table thead th { background-color: #1e3a8a; border-bottom: none; color: #fff; font-weight: 600; text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; padding: 1rem; }
.table tbody tr:nth-of-type(odd) { background-color: #e0f2fe; color: #0c4a6e; }
.table tbody tr:nth-of-type(even) { background-color: #bae6fd; color: #0c4a6e; }
.table td { border-bottom: 1px solid #7dd3fc; padding: 1rem; vertical-align: middle; border-top: none; }
.table tr:last-child td { border-bottom: none; }
.actions-cell { white-space: nowrap; width: 1%; }
.btn-edit { color: #0f172a; border: none; background: transparent; padding: 0.25rem 0.5rem; }
.btn-edit:hover { color: #3b82f6; }
.nav-link { color: #bae6fd; margin-right: 15px; }
.nav-link:hover, .nav-link.active { color: #e0f2fe; font-weight:600; }
.total-box { font-size: 1.5rem; font-weight: 600; text-align: right; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #1e40af; color: #60a5fa; }
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark mb-4">
<div class="container">
<a class="navbar-brand" href="#"><i class="fa-solid fa-file-invoice me-2"></i>HIX Invoices</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link active" href="facturas.prg">Invoices</a></li>
<li class="nav-item"><a class="nav-link" href="clientes.prg">Clients</a></li>
<li class="nav-item"><a class="nav-link" href="productos.prg">Products</a></li>
</ul>
<form class="d-flex" action="facturas.prg" method="POST">
<button class="btn btn-outline-success me-2" type="button" id="btnNew"><i class="fa-solid fa-plus me-1"></i>New</button>
<input type="hidden" name="action" value="search">
<input class="form-control me-2" type="search" name="search" placeholder="Search invoices..." value="{{VAL_SEARCH}}">
<button class="btn btn-outline-light me-2" type="submit">Search</button>
{{CANCEL_BUTTON}}
<a href="facturas_list_pdf.prg" target="_blank" class="btn btn-outline-info" title="Generar Listado PDF"><i class="fa-solid fa-file-pdf"></i></a>
</form>
</div>
</div>
</nav>
<div class="container">
<div class="row">
<!-- Left Side: Invoice Editor -->
<div class="col-md-5 mb-4" id="invoiceFormCard" style="display:none;">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0"><i class="fa-solid fa-file-invoice me-2"></i>{{FORM_TITLE}}</h5>
</div>
<div class="card-body">
{{MESSAGE}}
<form method="POST" action="facturas.prg" onsubmit="prepareSubmit()">
<input type="hidden" name="action" value="save">
<input type="hidden" name="id" value="{{VAL_ID}}">
<input type="hidden" name="lines_data" id="lines_data">
<div class="mb-3">
<label class="form-label">Client</label>
<select name="client_id" class="form-select" required>
{{CLIENT_OPTIONS}}
</select>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Date</label>
<input type="date" name="date" class="form-control" value="{{VAL_DATE}}" required>
</div>
<div class="col-md-6">
<label class="form-label">Status</label>
<select name="status" class="form-select">
<option value="Pending" {{SEL_PENDING}}>Pending</option>
<option value="Paid" {{SEL_PAID}}>Paid</option>
<option value="Cancelled" {{SEL_CANCELLED}}>Cancelled</option>
</select>
</div>
</div>
<h6 class="border-bottom border-secondary pb-2 mb-3 mt-4">Itens</h6>
<div class="table-responsive mb-3">
<table class="table table-sm" id="linesTable">
<thead>
<tr>
<th style="width: 45%;">Product</th>
<th style="width: 20%;">Qty</th>
<th style="width: 25%;">Price</th>
<th style="width: 10%;"></th>
</tr>
</thead>
<tbody id="linesBody">
<!-- JS Rows -->
</tbody>
</table>
</div>
<button type="button" class="btn btn-sm btn-secondary mb-3" onclick="addLine()">
<i class="fa-solid fa-plus me-1"></i> Add Item
</button>
<div class="total-box">
Total: $<span id="displayTotal">0.00</span>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end mt-4">
<button type="submit" class="btn btn-primary"><i class="fa-solid fa-save me-2"></i>Save Invoice</button>
</div>
</form>
</div>
</div>
</div>
<!-- Right Side: List -->
<div class="col-md-7" id="invoiceListColumn">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fa-solid fa-list me-2"></i>Existing Invoices</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>ID</th>
<th>Client</th>
<th>Date</th>
<th>Total</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{INVOICE_ROWS}}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Data injected from PRG
{{PRODUCT_DATA}}
// Format: var products = {"P1": {name: "Widget", price: 10}, ...};
{{EXISTING_LINES}}
// Format: var existingLines = [{prod_id: "P1", qty: 2}, ...];
function renderLines() {
const tbody = document.getElementById('linesBody');
tbody.innerHTML = '';
existingLines.forEach((line, index) => {
addLineRow(line.prod_id, line.qty, index);
});
if(existingLines.length === 0) {
// Add one empty row by default if new and register it
existingLines.push({prod_id: "", qty: 1});
addLineRow("", 1, 0);
}
calculateTotal();
}
function addLine() {
addLineRow("", 1, existingLines.length);
existingLines.push({prod_id: "", qty: 1});
}
function addLineRow(prodId, qty, index) {
const body = document.getElementById('linesBody');
const tr = document.createElement('tr');
// Build Options
let opts = '<option value="">Select...</option>';
for (const [key, val] of Object.entries(products)) {
const sel = (key === prodId) ? 'selected' : '';
opts += `<option value="${key}" ${sel}>${val.name}</option>`;
}
tr.innerHTML = `
<td>
<select class="form-select form-select-sm" onchange="updateLine(${index}, 'prod_id', this.value); calculateTotal();">
${opts}
</select>
</td>
<td>
<input type="number" class="form-control form-control-sm" value="${qty}" min="1" step="0.01" onchange="updateLine(${index}, 'qty', this.value); calculateTotal();">
</td>
<td id="price_${index}" class="text-end pe-3 pt-2">
${getProductPrice(prodId, qty)}
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger border-0" onclick="removeLine(${index})"><i class="fa-solid fa-times"></i></button>
</td>
`;
body.appendChild(tr);
}
function getProductPrice(id, qty) {
if(!id || !products[id]) return "0.00";
return (products[id].price * qty).toFixed(2);
}
function updateLine(index, field, value) {
if(existingLines[index]) {
existingLines[index][field] = value;
// Update specific row price display
const rowPrice = document.getElementById(`price_${index}`);
if(rowPrice) {
rowPrice.innerText = getProductPrice(existingLines[index].prod_id, existingLines[index].qty);
}
}
}
function removeLine(index) {
existingLines.splice(index, 1);
renderLines(); // Re-render to update indices
}
function calculateTotal() {
let total = 0;
existingLines.forEach(line => {
if(line.prod_id && products[line.prod_id]) {
total += products[line.prod_id].price * line.qty;
}
});
document.getElementById('displayTotal').innerText = total.toFixed(2);
}
function prepareSubmit() {
// Serialize lines to format: ID,QTY;ID,QTY
let data = "";
existingLines.forEach(line => {
if(line.prod_id) {
data += `${line.prod_id},${line.qty};`;
}
});
document.getElementById('lines_data').value = data;
}
// Initialize
renderLines();
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var formCard = document.getElementById('invoiceFormCard');
var btnNew = document.getElementById('btnNew');
var inputId = document.querySelector('input[name="id"]');
var listCol = document.getElementById('invoiceListColumn');
function updateLayout() {
if (formCard.style.display === "none") {
listCol.classList.remove('col-md-7');
listCol.classList.add('col-12');
} else {
listCol.classList.remove('col-12');
listCol.classList.add('col-md-7');
}
}
if (inputId && inputId.value.trim() !== "") {
formCard.style.display = "block";
updateLayout();
} else {
// Ensure initial state
updateLayout();
}
if (btnNew) {
btnNew.addEventListener('click', function(e) {
e.preventDefault();
if (formCard.style.display === "none") {
formCard.style.display = "block";
} else {
formCard.style.display = "none";
}
updateLayout();
});
}
});
</script>
</body>
</html>factura_pdf.prg
function Main()
local cId := Upost("id")
local cTmpDir := "c:/hix/testdir"
local cPdfFile := ""
local cContent := ""
local cBase64 := ""
// DB Vars
local cClientId := ""
local cDate := ""
local cStatus := ""
local nTotal := 0
local cCliName := ""
local cCliAddr := ""
local cCliCity := ""
local cCliZip := ""
local cCliVat := ""
local cCliPhone := ""
// Init Logic
cPdfFile := cTmpDir + "/invoice_" + cId + "_" + AllTrim(Str(Int(hb_Random() * 100000))) + ".pdf"
if Empty(cId)
UWrite("Error: No Invoice ID provided.")
return ""
endif
// Open Databases
if !OpenDatabases()
UWrite("Error opening databases.")
return ""
endif
// Find Invoice
select facturas
locate for AllTrim(id) == AllTrim(cId)
if !found()
UWrite("Error: Invoice not found.")
close all
return ""
endif
cClientId := facturas->client_id
cDate := DToC(facturas->date)
cStatus := facturas->status
nTotal := facturas->total
// Find Client
select clients
locate for AllTrim(id) == AllTrim(cClientId)
if found()
cCliName := AllTrim(clients->name)
cCliAddr := AllTrim(clients->address)
cCliCity := AllTrim(clients->city)
cCliZip := AllTrim(clients->zip)
cCliVat := AllTrim(clients->vat_id)
cCliPhone := AllTrim(clients->phone)
endif
// GENERATE PDF
GeneratePDF( cPdfFile, cId, cDate, cStatus, nTotal, cCliName, cCliAddr, cCliCity, cCliZip, cCliVat, cCliPhone )
close all
// READ AND OUTPUT
if File(cPdfFile)
cContent := MemoRead(cPdfFile)
cBase64 := hb_base64Encode(cContent)
UWrite( '<!DOCTYPE html>' )
UWrite( '<html><head><title>Factura ' + cId + '</title></head>' )
UWrite( '<body style="margin:0;padding:0;overflow:hidden;">' )
UWrite( '<iframe src="data:application/pdf;base64,' + cBase64 + '" width="100%" height="100%" style="border:none;height:100vh;"></iframe>' )
UWrite( '</body></html>' )
else
UWrite("Error: PDF Generation failed.")
endif
return ""
function GeneratePDF( cFile, cId, cDate, cStatus, nTotal, cName, cAddr, cCity, cZip, cVat, cPhone )
local oPdf, oPage, oFont, oFontBold
local nY := 750
local nRow := 0
local cProdName := ""
local nLPrice := 0
oPdf := HPDF_New()
HPDF_SetCompressionMode( oPdf, "HPDF_COMP_ALL" )
oPage := HPDF_AddPage( oPdf )
HPDF_Page_SetSize( oPage, "HPDF_PAGE_SIZE_A4", "HPDF_PAGE_PORTRAIT" )
oFont := HPDF_GetFont( oPdf, "Helvetica", "CP1252" )
oFontBold := HPDF_GetFont( oPdf, "Helvetica-Bold", "CP1252" )
// HEADER
HPDF_Page_SetFontAndSize( oPage, oFontBold, 24 )
HPDF_Page_BeginText( oPage )
HPDF_Page_TextOut( oPage, 50, nY, "INVOICE" )
HPDF_Page_EndText( oPage )
HPDF_Page_SetFontAndSize( oPage, oFont, 12 )
HPDF_Page_BeginText( oPage )
HPDF_Page_TextOut( oPage, 400, nY, "Invoice #: " + cId )
HPDF_Page_TextOut( oPage, 400, nY - 15, "Date: " + cDate )
HPDF_Page_TextOut( oPage, 400, nY - 30, "Status: " + cStatus )
HPDF_Page_EndText( oPage )
nY -= 80
// CLIENT INFO
HPDF_Page_SetFontAndSize( oPage, oFontBold, 14 )
HPDF_Page_BeginText( oPage )
HPDF_Page_TextOut( oPage, 50, nY, "Bill To:" )
HPDF_Page_EndText( oPage )
nY -= 20
HPDF_Page_SetFontAndSize( oPage, oFont, 12 )
HPDF_Page_BeginText( oPage )
HPDF_Page_TextOut( oPage, 50, nY, cName )
nY -= 15
if !Empty(cVat); HPDF_Page_TextOut( oPage, 50, nY, "VAT ID: " + cVat ); nY -= 15; endif
if !Empty(cAddr); HPDF_Page_TextOut( oPage, 50, nY, cAddr ); nY -= 15; endif
if !Empty(cCity); HPDF_Page_TextOut( oPage, 50, nY, AllTrim(cCity) + " " + AllTrim(cZip) ); nY -= 15; endif
if !Empty(cPhone); HPDF_Page_TextOut( oPage, 50, nY, "Phone: " + cPhone ); nY -= 15; endif
HPDF_Page_EndText( oPage )
nY -= 40
// TABLE HEADER
HPDF_Page_SetLineWidth( oPage, 1 )
HPDF_Page_Rectangle( oPage, 50, nY, 500, 25 )
HPDF_Page_Stroke( oPage )
HPDF_Page_SetFontAndSize( oPage, oFontBold, 11 )
HPDF_Page_BeginText( oPage )
HPDF_Page_TextOut( oPage, 60, nY + 8, "Product" )
HPDF_Page_TextOut( oPage, 300, nY + 8, "Qty" )
HPDF_Page_TextOut( oPage, 380, nY + 8, "Price" )
HPDF_Page_TextOut( oPage, 480, nY + 8, "Total" )
HPDF_Page_EndText( oPage )
nY -= 5
// ITEMS
select lines
go top
HPDF_Page_SetFontAndSize( oPage, oFont, 11 )
do while !Eof()
if lines->invoice_id == cId
cProdName := "Unknown"
nLPrice := lines->price
select products
locate for id == lines->prod_id
if found(); cProdName := AllTrim(products->name); endif
select lines
nY -= 20
HPDF_Page_BeginText( oPage )
HPDF_Page_TextOut( oPage, 60, nY, cProdName )
HPDF_Page_TextOut( oPage, 300, nY, AllTrim(Str(lines->qty)) )
HPDF_Page_TextOut( oPage, 380, nY, AllTrim(Str(nLPrice)) )
HPDF_Page_TextOut( oPage, 480, nY, AllTrim(Str(lines->total)) )
HPDF_Page_EndText( oPage )
endif
skip
enddo
// GRAND TOTAL
nY -= 40
HPDF_Page_SetFontAndSize( oPage, oFontBold, 14 )
HPDF_Page_BeginText( oPage )
HPDF_Page_TextOut( oPage, 350, nY, "Grand Total: " + AllTrim(Str(nTotal)) )
HPDF_Page_EndText( oPage )
HPDF_SaveToFile( oPdf, cFile )
HPDF_Free( oPdf )
return nil
function OpenDatabases()
local cTmpDir := "c:/hix/testdir"
if Select("facturas") == 0; use (cTmpDir+"/facturas_v2.dbf") shared new alias "facturas"; endif
if Select("lines") == 0; use (cTmpDir+"/facturas_lines.dbf") shared new alias "lines"; endif
if Select("clients") == 0; use (cTmpDir+"/clientes_v2.dbf") shared new alias "clients"; endif
if Select("products") == 0; use (cTmpDir+"/productos.dbf") shared new alias "products"; endif
return .T.facturas_list_pdf.prg
function Main()
local cTmpDir := "c:/hix/testdir"
local cPdfFile := cTmpDir + "/invoices_list_" + AllTrim(Str(Int(hb_Random() * 100000))) + ".pdf"
local cContent, cBase64
// Open Databases
if !OpenDatabases()
UWrite("Error opening databases.")
return ""
endif
// GENERATE PDF
GeneratePDF( cPdfFile )
close all
// READ AND OUTPUT
if File(cPdfFile)
cContent := MemoRead(cPdfFile)
cBase64 := hb_base64Encode(cContent)
UWrite( '<!DOCTYPE html>' )
UWrite( '<html><head><title>Listado de Facturas</title></head>' )
UWrite( '<body style="margin:0;padding:0;overflow:hidden;">' )
UWrite( '<iframe src="data:application/pdf;base64,' + cBase64 + '" width="100%" height="100%" style="border:none;height:100vh;"></iframe>' )
UWrite( '</body></html>' )
else
UWrite("Error: PDF Generation failed.")
endif
return ""
function GeneratePDF( cFile )
local oPdf, oPage, oFont, oFontBold
local nY := 750
local nRow := 0
local cCliName := ""
local cCliId := ""
HPDF_SetCompressionMode( oPdf, "HPDF_COMP_ALL" )
oPage := HPDF_AddPage( oPdf )
HPDF_Page_SetSize( oPage, "HPDF_PAGE_SIZE_A4", "HPDF_PAGE_PORTRAIT" )
set deleted on
oFont := HPDF_GetFont( oPdf, "Helvetica", "CP1252" )
oFontBold := HPDF_GetFont( oPdf, "Helvetica-Bold", "CP1252" )
// HEADER
HPDF_Page_SetFontAndSize( oPage, oFontBold, 18 )
HPDF_Page_BeginText( oPage )
HPDF_Page_TextOut( oPage, 50, nY, "LISTADO DE FACTURAS" )
HPDF_Page_EndText( oPage )
nY -= 30
// TABLE HEADER
HPDF_Page_SetLineWidth( oPage, 1 )
HPDF_Page_Rectangle( oPage, 50, nY, 500, 20 )
HPDF_Page_Stroke( oPage )
HPDF_Page_SetFontAndSize( oPage, oFontBold, 10 )
HPDF_Page_BeginText( oPage )
HPDF_Page_TextOut( oPage, 55, nY + 6, "ID" )
HPDF_Page_TextOut( oPage, 120, nY + 6, "Date" )
HPDF_Page_TextOut( oPage, 200, nY + 6, "Client" )
HPDF_Page_TextOut( oPage, 380, nY + 6, "Status" )
HPDF_Page_TextOut( oPage, 460, nY + 6, "Total" )
HPDF_Page_EndText( oPage )
nY -= 5
// ITEMS
select facturas
go top
HPDF_Page_SetFontAndSize( oPage, oFont, 10 )
do while !Eof()
// Lookup Client Name
cCliName := "Unknown"
cCliId := facturas->client_id
select clients
go top
locate for AllTrim(clients->id) == AllTrim(cCliId)
if found()
cCliName := AllTrim(clients->name)
endif
select facturas
nY -= 15
if nY < 50
HPDF_Page_EndText( oPage )
oPage := HPDF_AddPage( oPdf )
HPDF_Page_SetSize( oPage, "HPDF_PAGE_SIZE_A4", "HPDF_PAGE_PORTRAIT" )
HPDF_Page_SetFontAndSize( oPage, oFont, 10 )
HPDF_Page_BeginText( oPage )
nY := 750
else
HPDF_Page_BeginText( oPage )
endif
HPDF_Page_TextOut( oPage, 55, nY, AllTrim(facturas->id) )
HPDF_Page_TextOut( oPage, 120, nY, DToC(facturas->date) )
HPDF_Page_TextOut( oPage, 200, nY, SubStr(cCliName, 1, 30) )
HPDF_Page_TextOut( oPage, 380, nY, AllTrim(facturas->status) )
HPDF_Page_TextOut( oPage, 460, nY, AllTrim(Str(facturas->total)) )
HPDF_Page_EndText( oPage )
skip
enddo
HPDF_SaveToFile( oPdf, cFile )
HPDF_Free( oPdf )
return nil
function OpenDatabases()
local cTmpDir := "c:/hix/testdir"
if Select("facturas") == 0; use (cTmpDir+"/facturas_v2.dbf") shared new alias "facturas"; endif
if Select("clients") == 0; use (cTmpDir+"/clientes_v2.dbf") shared new alias "clients"; endif
return .T.