Add flexible receipt generator
This commit is contained in:
parent
d8a561ac0e
commit
5df113c70d
137
action.php
137
action.php
@ -144,41 +144,55 @@ switch ($VARS['action']) {
|
||||
|
||||
break;
|
||||
case "getreceipt":
|
||||
header("Content-Type: text/html");
|
||||
require_once __DIR__ . "/lib/receipts.php";
|
||||
$format = "html";
|
||||
$width = 64;
|
||||
if (isset($VARS['width']) && preg_match("/[0-9]+/", $VARS['width']) && (int) $VARS['width'] > 0) {
|
||||
$width = (int) $VARS['width'];
|
||||
}
|
||||
if (isset($VARS['format'])) {
|
||||
switch ($VARS['format']) {
|
||||
case "text":
|
||||
$format = "text";
|
||||
header("Content-Type: text/plain");
|
||||
break;
|
||||
case "json":
|
||||
$format = "json";
|
||||
header("Content-Type: application/json");
|
||||
break;
|
||||
default:
|
||||
$format = "html";
|
||||
header("Content-Type: text/html");
|
||||
}
|
||||
}
|
||||
if (!$database->has('transactions', ['txid' => $VARS['txid']])) {
|
||||
header("Content-Type: application/json");
|
||||
exit(json_encode(["status" => "ERROR", "txid" => null]));
|
||||
}
|
||||
|
||||
$receipt = new Receipt();
|
||||
$tx = $database->get('transactions', ['txid', 'txdate', 'customerid', 'type', 'cashier', 'discountpercent'], ['txid' => $VARS['txid']]);
|
||||
|
||||
// Info
|
||||
$txid = $tx['txid'];
|
||||
$datetime = date(DATETIME_FORMAT, strtotime($tx['txdate']));
|
||||
$type = $tx['type'];
|
||||
$cashier = getUserByID($tx['cashier'])['name'];
|
||||
$customerid = $tx['customerid'];
|
||||
$customerline = "";
|
||||
if (!is_null($customerid) && !empty($customerid)) {
|
||||
$customerline = "<br />Customer: " . $database->get('customers', 'name', ['customerid' => $customerid]);
|
||||
}
|
||||
|
||||
$itemhtml = "";
|
||||
// Items
|
||||
$itemlines = [];
|
||||
$items = $database->select('lines', ['amount', 'name', 'itemid', 'qty'], ['txid' => $txid]);
|
||||
$subtotal = 0.0;
|
||||
$paid = 0.0;
|
||||
foreach ($items as $i) {
|
||||
$itemhtml .= "\n";
|
||||
$itemhtml .= '<div class="flexrow">';
|
||||
$itemhtml .= '<div>' . $i['name'] . '</div>';
|
||||
$itemhtml .= '<div>$' . number_format($i['amount'], 2) . '</div>';
|
||||
$itemhtml .= '<div>x' . (float) $i['qty'] . '</div>';
|
||||
$itemhtml .= '<div>$' . number_format($i['qty'] * $i['amount'] * 1.0, 2) . '</div>';
|
||||
$itemhtml .= '</div>';
|
||||
$itemlines[] = new ReceiptLine(
|
||||
$i['name'], (float) $i['qty'] . '@' . number_format($i['amount'], 2), '$' . number_format($i['qty'] * $i['amount'] * 1.0, 2)
|
||||
);
|
||||
$subtotal += $i['qty'] * $i['amount'] * 1.0;
|
||||
}
|
||||
|
||||
// Payments
|
||||
$total = $subtotal * (1.0 - ((float) $tx['discountpercent'] / 100));
|
||||
|
||||
$paymenthtml = "";
|
||||
$paymentlines = [];
|
||||
$payments = $database->select('payments', [
|
||||
'[>]payment_types' => ['type' => 'typeid']
|
||||
], [
|
||||
@ -190,63 +204,54 @@ switch ($VARS['action']) {
|
||||
if ($p['amount'] < 0) {
|
||||
continue;
|
||||
}
|
||||
$paymenthtml .= "\n";
|
||||
$paymenthtml .= '<div class="flexrow">';
|
||||
$paymenthtml .= '<div>' . lang($p['text'], false) . '</div>';
|
||||
$paymenthtml .= '<div>$' . number_format($p['amount'] * 1.0, 2) . '</div>';
|
||||
$paymenthtml .= '</div>';
|
||||
$paymentlines[] = new ReceiptLine(lang($p['text'], false), "", '$' . number_format($p['amount'] * 1.0, 2));
|
||||
$paid += $p['amount'] * 1.0;
|
||||
}
|
||||
|
||||
$change = $paid - $total;
|
||||
if ($change <= 0) {
|
||||
$change = 0.0;
|
||||
}
|
||||
|
||||
$subtotalstr = number_format($subtotal, 2);
|
||||
$paidstr = number_format($paid, 2);
|
||||
$changestr = number_format($change, 2);
|
||||
$totalstr = $subtotalstr;
|
||||
$discountstr = "";
|
||||
if ($tx['discountpercent'] > 0) {
|
||||
$discountstr = '<div class="flexrow"><span>Discount: </span><span>' . (float) $tx['discountpercent'] . '% off</span></div>';
|
||||
$totalstr = number_format($total, 2);
|
||||
}
|
||||
// Totals
|
||||
$subtotalline = new ReceiptLine("Subtotal:", "", '$' . number_format($subtotal, 2));
|
||||
$paidline = new ReceiptLine("Paid:", "", '$' . number_format($paid, 2), ReceiptLine::LINEFORMAT_BOLD);
|
||||
$changeline = new ReceiptLine("Change:", "", '$' . number_format($change, 2), ReceiptLine::LINEFORMAT_BOLD);
|
||||
$totalline = new ReceiptLine("Total:", "", '$' . number_format($subtotal, 2), ReceiptLine::LINEFORMAT_BOLD);
|
||||
|
||||
$html = <<<END
|
||||
<!DOCTYPE html>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Tx #$txid</title>
|
||||
<style nonce="$SECURE_NONCE">
|
||||
.flexrow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<hr />
|
||||
Date: $datetime<br />
|
||||
Tx. ID: $txid<br />
|
||||
Cashier: $cashier
|
||||
$customerline
|
||||
<hr />
|
||||
<div id="items">
|
||||
$itemhtml
|
||||
</div>
|
||||
<hr />
|
||||
<div class="flexrow"><span>Subtotal: </span><span>$$subtotalstr</span></div>
|
||||
$discountstr
|
||||
<b class="flexrow"><span>Total: </span><span>$$totalstr</span></b>
|
||||
<hr />
|
||||
<div id="payments">
|
||||
$paymenthtml
|
||||
</div>
|
||||
<hr />
|
||||
<b class="flexrow"><span>Paid: </span><span>$$paidstr</span></b>
|
||||
<b class="flexrow"><span>Change: </span><span>$$changestr</span></b>
|
||||
END;
|
||||
exit($html);
|
||||
$receipt->appendLine(new ReceiptLine("Date: $datetime"));
|
||||
$receipt->appendLine(new ReceiptLine("Tx. ID: $txid"));
|
||||
$receipt->appendLine(new ReceiptLine("Cashier: $cashier"));
|
||||
if (!is_null($customerid) && !empty($customerid)) {
|
||||
$customer = $database->get('customers', 'name', ['customerid' => $customerid]);
|
||||
$receipt->appendLine(new ReceiptLine("Customer: $customer"));
|
||||
}
|
||||
$receipt->appendBreak();
|
||||
$receipt->appendLines($itemlines);
|
||||
$receipt->appendBreak();
|
||||
$receipt->appendLine($subtotalline);
|
||||
if ($tx['discountpercent'] > 0) {
|
||||
$receipt->appendLine(new ReceiptLine("Discount:", "", (float) $tx['discountpercent'] . '% off'));
|
||||
$totalline = new ReceiptLine("Total:", "", '$' . number_format($total, 2), ReceiptLine::LINEFORMAT_BOLD);
|
||||
}
|
||||
$receipt->appendLine($totalline);
|
||||
$receipt->appendBreak();
|
||||
$receipt->appendLines($paymentlines);
|
||||
$receipt->appendBreak();
|
||||
$receipt->appendLine($paidline);
|
||||
$receipt->appendLine($changeline);
|
||||
|
||||
$output = "";
|
||||
switch ($format) {
|
||||
case "text":
|
||||
$output = $receipt->getPlainText($width);
|
||||
break;
|
||||
case "json":
|
||||
$output = $receipt->getJson($width);
|
||||
break;
|
||||
default:
|
||||
$output = $receipt->getHtml("Tx. #$txid");
|
||||
}
|
||||
exit($output);
|
||||
break;
|
||||
case "itemsearch":
|
||||
header("Content-Type: application/json");
|
||||
|
240
lib/receipts.php
Normal file
240
lib/receipts.php
Normal file
@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A single variable-width line on a receipt, with different formatting options
|
||||
*/
|
||||
class ReceiptLine {
|
||||
|
||||
const LINEFORMAT_PLAIN = 1;
|
||||
const LINEFORMAT_BOLD = 2;
|
||||
const LINEFORMAT_HR = 4;
|
||||
const LINEFORMAT_CENTER = 8;
|
||||
|
||||
private $left = "";
|
||||
private $middle = "";
|
||||
private $right = "";
|
||||
private $format;
|
||||
|
||||
function __construct($l = "", $m = "", $r = "", $f = ReceiptLine::LINEFORMAT_PLAIN) {
|
||||
$this->left = $l;
|
||||
$this->middle = $m;
|
||||
$this->right = $r;
|
||||
$this->format = $f;
|
||||
}
|
||||
|
||||
function getLeft() {
|
||||
return $this->left;
|
||||
}
|
||||
|
||||
function getMiddle() {
|
||||
return $this->middle;
|
||||
}
|
||||
|
||||
function getRight() {
|
||||
return $this->right;
|
||||
}
|
||||
|
||||
function hasFormat($format) {
|
||||
return ($this->format & $format);
|
||||
}
|
||||
|
||||
function getHtml() {
|
||||
if ($this->hasFormat($this::LINEFORMAT_HR)) {
|
||||
return "<hr />";
|
||||
}
|
||||
$html = "";
|
||||
if (!empty($this->left)) {
|
||||
$html .= '<span>' . htmlspecialchars($this->left) . ' </span>';
|
||||
}
|
||||
if (!empty($this->middle)) {
|
||||
$html .= '<span>' . htmlspecialchars($this->middle) . ' </span>';
|
||||
}
|
||||
if (!empty($this->right)) {
|
||||
$html .= '<span>' . htmlspecialchars($this->right) . '</span>';
|
||||
}
|
||||
$classes = ["flex"];
|
||||
if ($this->hasFormat($this::LINEFORMAT_BOLD)) {
|
||||
$classes[] = "bold";
|
||||
}
|
||||
if ($this->hasFormat($this::LINEFORMAT_CENTER)) {
|
||||
$classes[] = "centered";
|
||||
}
|
||||
$classstr = implode(" ", $classes);
|
||||
return "<div class=\"$classstr\">$html</div>";
|
||||
}
|
||||
|
||||
function getPlainText($width) {
|
||||
if ($this->hasFormat($this::LINEFORMAT_HR)) {
|
||||
return str_repeat("-", $width);
|
||||
}
|
||||
$left = $this->left;
|
||||
$middle = $this->middle;
|
||||
$right = $this->right;
|
||||
$leftln = strlen($left);
|
||||
$middleln = strlen($middle);
|
||||
$rightln = strlen($right);
|
||||
if ($middleln > 0) {
|
||||
$middleln++;
|
||||
$middle = " " . $middle;
|
||||
}
|
||||
if ($rightln > 0) {
|
||||
$rightln++;
|
||||
$right = " " . $right;
|
||||
}
|
||||
$strln = $leftln + $middleln + $rightln;
|
||||
if ($strln < $width) {
|
||||
if ($this->hasFormat($this::LINEFORMAT_CENTER)) {
|
||||
return str_pad($left . $middle . $right, $width, " ", STR_PAD_BOTH);
|
||||
} else {
|
||||
$middle = str_pad($middle, $width - $leftln - $rightln, " ", STR_PAD_BOTH);
|
||||
}
|
||||
} else if ($strln > $width) {
|
||||
$loseln = $strln - $width;
|
||||
$left = substr($this->left, 0, $leftln - $loseln);
|
||||
}
|
||||
return $left . $middle . $right;
|
||||
}
|
||||
|
||||
function getArray($width = 64) {
|
||||
$data = [
|
||||
"left" => $this->left,
|
||||
"middle" => $this->middle,
|
||||
"right" => $this->right,
|
||||
"text" => $this->getPlainText($width),
|
||||
"format" => []
|
||||
];
|
||||
if ($this->hasFormat($this::LINEFORMAT_PLAIN)) {
|
||||
$data['format'][] = "plain";
|
||||
}
|
||||
if ($this->hasFormat($this::LINEFORMAT_BOLD)) {
|
||||
$data['format'][] = "bold";
|
||||
}
|
||||
if ($this->hasFormat($this::LINEFORMAT_HR)) {
|
||||
$data['format'][] = "hr";
|
||||
}
|
||||
if ($this->hasFormat($this::LINEFORMAT_CENTER)) {
|
||||
$data['format'][] = "center";
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Receipt {
|
||||
|
||||
private $lines = [];
|
||||
private $header = [];
|
||||
private $footer = [];
|
||||
|
||||
function __construct() {
|
||||
|
||||
}
|
||||
|
||||
function appendLine(ReceiptLine $line) {
|
||||
$this->lines[] = $line;
|
||||
}
|
||||
|
||||
function appendLines($lines) {
|
||||
foreach ($lines as $l) {
|
||||
$this->lines[] = $l;
|
||||
}
|
||||
}
|
||||
|
||||
function appendHeader(ReceiptLine $line) {
|
||||
$this->header[] = $line;
|
||||
}
|
||||
|
||||
function appendFooter(ReceiptLine $line) {
|
||||
$this->footer[] = $line;
|
||||
}
|
||||
|
||||
function appendBreak() {
|
||||
$this->lines[] = new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR);
|
||||
}
|
||||
|
||||
function getHtml($title = "") {
|
||||
global $SECURE_NONCE;
|
||||
$html = <<<END
|
||||
<!DOCTYPE html>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>$title</title>
|
||||
<style nonce="$SECURE_NONCE">
|
||||
.flex {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
||||
}
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.centered {
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
END;
|
||||
if (count($this->header) > 0) {
|
||||
foreach ($this->header as $line) {
|
||||
$html .= $line->getHtml() . "\n";
|
||||
}
|
||||
$html .= (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getHtml();
|
||||
}
|
||||
foreach ($this->lines as $line) {
|
||||
$html .= $line->getHtml() . "\n";
|
||||
}
|
||||
if (count($this->footer) > 0) {
|
||||
$html .= (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getHtml();
|
||||
foreach ($this->footer as $line) {
|
||||
$html .= $line->getHtml() . "\n";
|
||||
}
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
function getPlainText($width) {
|
||||
$lines = [];
|
||||
if (count($this->header) > 0) {
|
||||
foreach ($this->header as $line) {
|
||||
$lines[] = $line->getPlainText($width);
|
||||
}
|
||||
$lines[] = (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getPlainText($width);
|
||||
}
|
||||
foreach ($this->lines as $line) {
|
||||
$lines[] = $line->getPlainText($width);
|
||||
}
|
||||
if (count($this->footer) > 0) {
|
||||
$lines[] = (new ReceiptLine("", "", "", ReceiptLine::LINEFORMAT_HR))->getPlainText($width);
|
||||
foreach ($this->footer as $line) {
|
||||
$lines[] = $line->getPlainText($width);
|
||||
}
|
||||
}
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
function getArray($width = 64) {
|
||||
$header = [];
|
||||
$lines = [];
|
||||
$footer = [];
|
||||
foreach ($this->header as $line) {
|
||||
$header[] = $line->getArray($width);
|
||||
}
|
||||
foreach ($this->lines as $line) {
|
||||
$lines[] = $line->getArray($width);
|
||||
}
|
||||
foreach ($this->footer as $line) {
|
||||
$footer[] = $line->getArray($width);
|
||||
}
|
||||
return ["header" => $header, "lines" => $lines, "footer" => $footer];
|
||||
}
|
||||
|
||||
function getJson($width = 64) {
|
||||
return json_encode($this->getArray($width));
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user