first commit
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:esc_pos_utils_plus/esc_pos_utils_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:mobile_pos/Const/api_config.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:mobile_pos/thermal%20priting%20invoices/model/print_transaction_model.dart';
|
||||
import '../../thermer/thermer.dart' as thermer;
|
||||
|
||||
class DueThermalInvoiceTemplate {
|
||||
DueThermalInvoiceTemplate({
|
||||
required this.printDueTransactionModel,
|
||||
required this.is58mm,
|
||||
required this.context,
|
||||
required this.isRTL,
|
||||
});
|
||||
|
||||
final PrintDueTransactionModel printDueTransactionModel;
|
||||
final bool is58mm;
|
||||
final BuildContext context;
|
||||
final bool isRTL;
|
||||
|
||||
// --- Helpers: Styles & Formats ---
|
||||
|
||||
thermer.TextStyle _commonStyle({double fontSize = 24, bool isBold = false}) {
|
||||
return thermer.TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: isBold ? thermer.FontWeight.bold : thermer.FontWeight.w500,
|
||||
color: thermer.Colors.black,
|
||||
);
|
||||
}
|
||||
|
||||
String formatPointNumber(num number, {bool addComma = false}) {
|
||||
if (addComma) return NumberFormat("#,###.##", "en_US").format(number);
|
||||
return number.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
// --- Main Generator ---
|
||||
|
||||
Future<List<int>> get template async {
|
||||
final _lang = lang.S.of(context);
|
||||
final _profile = await CapabilityProfile.load();
|
||||
final _generator = Generator(is58mm ? PaperSize.mm58 : PaperSize.mm80, _profile);
|
||||
|
||||
final _imageBytes = await _generateLayout(_lang);
|
||||
final _image = img.decodeImage(_imageBytes);
|
||||
|
||||
if (_image == null) throw Exception('Failed to generate receipt.');
|
||||
|
||||
List<int> _bytes = [];
|
||||
_bytes += _generator.image(_image);
|
||||
_bytes += _generator.cut();
|
||||
return _bytes;
|
||||
}
|
||||
|
||||
Future<Uint8List> _generateLayout(lang.S _lang) async {
|
||||
final data = printDueTransactionModel.dueTransactionModel;
|
||||
final info = printDueTransactionModel.personalInformationModel.data;
|
||||
|
||||
// 1. Prepare Logo
|
||||
thermer.ThermerImage? _logo;
|
||||
if (info?.thermalInvoiceLogo != null && info?.showThermalInvoiceLogo == 1) {
|
||||
try {
|
||||
_logo = await thermer.ThermerImage.network(
|
||||
"${APIConfig.domain}${info?.thermalInvoiceLogo}",
|
||||
width: is58mm ? 120 : 184,
|
||||
height: is58mm ? 120 : 184,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
//qr logo
|
||||
thermer.ThermerImage? _qrLogo;
|
||||
if (info?.invoiceScannerLogo != null && info?.showInvoiceScannerLogo == 1) {
|
||||
try {
|
||||
_qrLogo = await thermer.ThermerImage.network(
|
||||
APIConfig.domain + info!.invoiceScannerLogo!,
|
||||
width: is58mm ? 120 : 140,
|
||||
height: is58mm ? 120 : 140,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// 2. Prepare Payment Labels
|
||||
final paymentLabels = _buildPaymentLabels();
|
||||
|
||||
// 3. Build Layout
|
||||
final _layout = thermer.ThermerLayout(
|
||||
paperSize: is58mm ? thermer.PaperSize.mm58 : thermer.PaperSize.mm80,
|
||||
textDirection: isRTL ? thermer.TextDirection.rtl : thermer.TextDirection.ltr,
|
||||
widgets: [
|
||||
// --- Header ---
|
||||
if (_logo != null) ...[thermer.ThermerAlign(child: _logo), thermer.ThermerSizedBox(height: 16)],
|
||||
|
||||
if (info?.meta?.showCompanyName == 1)
|
||||
thermer.ThermerText(
|
||||
info?.companyName ?? '',
|
||||
style: _commonStyle(fontSize: is58mm ? 46 : 54),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
if (data?.branch?.name != null)
|
||||
thermer.ThermerText('Branch: ${data?.branch?.name}',
|
||||
style: _commonStyle(), textAlign: thermer.TextAlign.center),
|
||||
|
||||
if (info?.meta?.showAddress == 1)
|
||||
if (data?.branch?.address != null || info?.address != null)
|
||||
thermer.ThermerText(
|
||||
'${_lang.address}: ${data?.branch?.address ?? info?.address ?? ''}',
|
||||
style: _commonStyle(),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
if (info?.meta?.showPhoneNumber == 1)
|
||||
if (data?.branch?.phone != null || info?.phoneNumber != null)
|
||||
thermer.ThermerText(
|
||||
'${_lang.mobile} ${data?.branch?.phone ?? info?.phoneNumber ?? ''}',
|
||||
style: _commonStyle(),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
if (info?.meta?.showVat == 1)
|
||||
if (info?.vatNo != null && info?.meta?.showVat == 1)
|
||||
thermer.ThermerText(
|
||||
"${info?.vatName ?? _lang.vatNumber}: ${info?.vatNo}",
|
||||
style: _commonStyle(),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
thermer.ThermerText(
|
||||
_lang.receipt, // Due collection is usually a Receipt
|
||||
style: _commonStyle(fontSize: is58mm ? 30 : 48, isBold: true)
|
||||
.copyWith(decoration: thermer.TextDecoration.underline),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
|
||||
// --- Info Section ---
|
||||
..._buildInfoSection(_lang),
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
|
||||
// --- Data Table (Single Row for Due Context) ---
|
||||
thermer.ThermerTable(
|
||||
header: thermer.ThermerTableRow([
|
||||
if (!is58mm) thermer.ThermerText(_lang.sl, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.invoice, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.dueAmount, textAlign: thermer.TextAlign.end, style: _commonStyle(isBold: true)),
|
||||
]),
|
||||
data: [
|
||||
thermer.ThermerTableRow([
|
||||
if (!is58mm) thermer.ThermerText('1', style: _commonStyle()),
|
||||
thermer.ThermerText(data?.invoiceNumber ?? '', style: _commonStyle()),
|
||||
thermer.ThermerText(formatPointNumber(data?.totalDue ?? 0, addComma: true),
|
||||
textAlign: thermer.TextAlign.end, style: _commonStyle()),
|
||||
])
|
||||
],
|
||||
cellWidths: is58mm ? {0: null, 1: 0.3} : {0: 0.1, 1: null, 2: 0.3},
|
||||
),
|
||||
thermer.ThermerDivider.horizontal(),
|
||||
|
||||
// --- Calculations ---
|
||||
_buildCalculationColumn(_lang),
|
||||
|
||||
thermer.ThermerDivider.horizontal(),
|
||||
thermer.ThermerSizedBox(height: 8),
|
||||
|
||||
// --- Payment Info ---
|
||||
thermer.ThermerText(
|
||||
"${_lang.paidVia} : ${paymentLabels.join(', ')}",
|
||||
style: _commonStyle(),
|
||||
textAlign: thermer.TextAlign.left,
|
||||
),
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
|
||||
// --- Footer ---
|
||||
if (info?.gratitudeMessage != null && info?.showGratitudeMsg == 1)
|
||||
thermer.ThermerText(info?.gratitudeMessage ?? '',
|
||||
textAlign: thermer.TextAlign.center, style: _commonStyle(isBold: true)),
|
||||
|
||||
if (data?.paymentDate != null)
|
||||
thermer.ThermerText(
|
||||
DateFormat('M/d/yyyy h:mm a').format(DateTime.parse(data!.paymentDate!)),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
style: _commonStyle(),
|
||||
),
|
||||
|
||||
if (info?.showNote == 1)
|
||||
thermer.ThermerText(
|
||||
'${info?.invoiceNoteLevel ?? _lang.note}: ${info?.invoiceNote ?? ''}',
|
||||
textAlign: thermer.TextAlign.left,
|
||||
style: _commonStyle(),
|
||||
),
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
if (_qrLogo != null) ...[thermer.ThermerAlign(child: _qrLogo), thermer.ThermerSizedBox(height: 1)],
|
||||
|
||||
if (info?.developBy != null)
|
||||
thermer.ThermerText(
|
||||
'${info?.developByLevel ?? _lang.developedBy}: ${info?.developBy}',
|
||||
textAlign: thermer.TextAlign.center,
|
||||
style: _commonStyle(),
|
||||
),
|
||||
|
||||
thermer.ThermerSizedBox(height: 200), // Cutter Space
|
||||
],
|
||||
);
|
||||
|
||||
return _layout.toUint8List();
|
||||
}
|
||||
|
||||
// --- Sub-Builders ---
|
||||
|
||||
List<thermer.ThermerWidget> _buildInfoSection(lang.S _lang) {
|
||||
final data = printDueTransactionModel.dueTransactionModel;
|
||||
final dateStr = data?.paymentDate != null ? DateFormat.yMd().format(DateTime.parse(data!.paymentDate!)) : '';
|
||||
final timeStr = data?.paymentDate != null ? DateFormat.jm().format(DateTime.parse(data!.paymentDate!)) : '';
|
||||
|
||||
final receiptText = '${_lang.receipt}: ${data?.invoiceNumber ?? 'Not Provided'}';
|
||||
final dateText = '${_lang.date}: $dateStr';
|
||||
final timeText = '${_lang.time}: $timeStr';
|
||||
final nameText = '${_lang.receivedFrom}: ${data?.party?.name ?? ''}';
|
||||
final mobileText = '${_lang.mobile} ${data?.party?.phone ?? ''}';
|
||||
final receivedByText =
|
||||
'${_lang.receivedBy}: ${data?.user?.role == "shop-owner" ? 'Admin' : data?.user?.name ?? ''}';
|
||||
|
||||
if (is58mm) {
|
||||
// 58mm: Stacked
|
||||
return [
|
||||
thermer.ThermerText(receiptText, style: _commonStyle()),
|
||||
if (data?.paymentDate != null) thermer.ThermerText("$dateText $timeStr", style: _commonStyle()),
|
||||
thermer.ThermerText(nameText, style: _commonStyle()),
|
||||
thermer.ThermerText(mobileText, style: _commonStyle()),
|
||||
thermer.ThermerText(receivedByText, style: _commonStyle()),
|
||||
];
|
||||
} else {
|
||||
// 80mm: Two Columns
|
||||
return [
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(receiptText, style: _commonStyle()),
|
||||
if (data?.paymentDate != null) thermer.ThermerText(dateText, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(nameText, style: _commonStyle()),
|
||||
thermer.ThermerText(timeText, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(mobileText, style: _commonStyle()),
|
||||
thermer.ThermerText(receivedByText, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
thermer.ThermerColumn _buildCalculationColumn(lang.S _lang) {
|
||||
final data = printDueTransactionModel.dueTransactionModel;
|
||||
|
||||
thermer.ThermerRow calcRow(String label, num value, {bool bold = false, bool isCurrency = true}) {
|
||||
return thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(label, style: _commonStyle(isBold: bold)),
|
||||
thermer.ThermerText(
|
||||
isCurrency ? formatPointNumber(value, addComma: true) : value.toString(),
|
||||
textAlign: thermer.TextAlign.end,
|
||||
style: _commonStyle(isBold: bold),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return thermer.ThermerColumn(children: [
|
||||
calcRow('${_lang.totalDue}:', data?.totalDue ?? 0),
|
||||
calcRow('${_lang.paymentsAmount}:', data?.payDueAmount ?? 0, bold: true),
|
||||
calcRow('${_lang.remainingDue}:', data?.dueAmountAfterPay ?? 0, bold: true),
|
||||
]);
|
||||
}
|
||||
|
||||
List<String> _buildPaymentLabels() {
|
||||
final transactions = printDueTransactionModel.dueTransactionModel?.transactions ?? [];
|
||||
List<String> labels = [];
|
||||
|
||||
for (var item in transactions) {
|
||||
String label = item.paymentType?.name ?? 'n/a';
|
||||
if (item.transactionType == 'cash_payment') label = lang.S.of(context).cash;
|
||||
if (item.transactionType == 'cheque_payment') label = lang.S.of(context).cheque;
|
||||
if (item.transactionType == 'wallet_payment') label = lang.S.of(context).wallet;
|
||||
labels.add(label);
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:esc_pos_utils_plus/esc_pos_utils_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Const/api_config.dart';
|
||||
import 'package:mobile_pos/Screens/Products/add%20product/modle/create_product_model.dart';
|
||||
import 'package:mobile_pos/Screens/Purchase/Model/purchase_transaction_model.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:mobile_pos/thermal%20priting%20invoices/model/print_transaction_model.dart';
|
||||
|
||||
import '../../thermer/thermer.dart' as thermer;
|
||||
|
||||
class PurchaseThermalInvoiceTemplate {
|
||||
PurchaseThermalInvoiceTemplate({
|
||||
required this.printTransactionModel,
|
||||
required this.productList,
|
||||
required this.is58mm,
|
||||
required this.context,
|
||||
required this.isRTL,
|
||||
});
|
||||
|
||||
final PrintPurchaseTransactionModel printTransactionModel;
|
||||
final List<PurchaseDetails>? productList;
|
||||
final bool is58mm;
|
||||
final BuildContext context;
|
||||
final bool isRTL;
|
||||
|
||||
// --- Helpers: Styles & Formats ---
|
||||
|
||||
thermer.TextStyle _commonStyle({double fontSize = 24, bool isBold = false}) {
|
||||
return thermer.TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: isBold ? thermer.FontWeight.bold : thermer.FontWeight.w500,
|
||||
color: thermer.Colors.black,
|
||||
);
|
||||
}
|
||||
|
||||
String formatPointNumber(num number, {bool addComma = false}) {
|
||||
if (addComma) return NumberFormat("#,###.##", "en_US").format(number);
|
||||
return number.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
// --- Data Logic (Adapted from your provided code) ---
|
||||
|
||||
num _getProductPrice(num detailsId) {
|
||||
return productList!.where((element) => element.id == detailsId).first.productPurchasePrice ?? 0;
|
||||
}
|
||||
|
||||
String _getProductName(num detailsId) {
|
||||
final details = printTransactionModel.purchaseTransitionModel?.details?.firstWhere(
|
||||
(element) => element.id == detailsId,
|
||||
orElse: () => PurchaseDetails(),
|
||||
);
|
||||
String name = details?.product?.productName ?? '';
|
||||
if (details?.product?.productType == ProductType.variant.name) {
|
||||
name += ' [${details?.stock?.batchNo ?? ''}]';
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
num _getProductQuantity(num detailsId) {
|
||||
num totalQuantity = productList?.where((element) => element.id == detailsId).first.quantities ?? 0;
|
||||
|
||||
// Add returned quantities logic
|
||||
if (printTransactionModel.purchaseTransitionModel?.purchaseReturns?.isNotEmpty ?? false) {
|
||||
for (var returns in printTransactionModel.purchaseTransitionModel!.purchaseReturns!) {
|
||||
if (returns.purchaseReturnDetails?.isNotEmpty ?? false) {
|
||||
for (var details in returns.purchaseReturnDetails!) {
|
||||
if (details.purchaseDetailId == detailsId) {
|
||||
totalQuantity += details.returnQty ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return totalQuantity;
|
||||
}
|
||||
|
||||
num _getTotalForOldInvoice() {
|
||||
num total = 0;
|
||||
if (productList != null) {
|
||||
for (var element in productList!) {
|
||||
num productPrice = element.productPurchasePrice ?? 0;
|
||||
num productQuantity = _getProductQuantity(element.id ?? 0);
|
||||
total += productPrice * productQuantity;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
num _getReturnedDiscountAmount() {
|
||||
num totalReturnDiscount = 0;
|
||||
if (printTransactionModel.purchaseTransitionModel?.purchaseReturns?.isNotEmpty ?? false) {
|
||||
for (var returns in printTransactionModel.purchaseTransitionModel!.purchaseReturns!) {
|
||||
if (returns.purchaseReturnDetails?.isNotEmpty ?? false) {
|
||||
for (var details in returns.purchaseReturnDetails!) {
|
||||
totalReturnDiscount += ((_getProductPrice(details.purchaseDetailId ?? 0) * (details.returnQty ?? 0)) -
|
||||
((details.returnAmount ?? 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return totalReturnDiscount;
|
||||
}
|
||||
|
||||
num _getTotalReturnedAmount() {
|
||||
num totalReturn = 0;
|
||||
if (printTransactionModel.purchaseTransitionModel?.purchaseReturns?.isNotEmpty ?? false) {
|
||||
for (var returns in printTransactionModel.purchaseTransitionModel!.purchaseReturns!) {
|
||||
if (returns.purchaseReturnDetails?.isNotEmpty ?? false) {
|
||||
for (var details in returns.purchaseReturnDetails!) {
|
||||
totalReturn += details.returnAmount ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return totalReturn;
|
||||
}
|
||||
|
||||
// --- Main Generator ---
|
||||
|
||||
Future<List<int>> get template async {
|
||||
final _profile = await CapabilityProfile.load();
|
||||
final _generator = Generator(is58mm ? PaperSize.mm58 : PaperSize.mm80, _profile);
|
||||
|
||||
final _imageBytes = await _generateLayout();
|
||||
final _image = img.decodeImage(_imageBytes);
|
||||
|
||||
if (_image == null) throw Exception('Failed to generate invoice.');
|
||||
|
||||
List<int> _bytes = [];
|
||||
_bytes += _generator.image(_image);
|
||||
_bytes += _generator.cut();
|
||||
return _bytes;
|
||||
}
|
||||
|
||||
Future<Uint8List> _generateLayout() async {
|
||||
final data = printTransactionModel.purchaseTransitionModel;
|
||||
final info = printTransactionModel.personalInformationModel.data;
|
||||
final _lang = lang.S.of(context);
|
||||
|
||||
// 1. Prepare Logo
|
||||
thermer.ThermerImage? _logo;
|
||||
if (info?.thermalInvoiceLogo != null && info?.showThermalInvoiceLogo == 1) {
|
||||
try {
|
||||
_logo = await thermer.ThermerImage.network(
|
||||
"${APIConfig.domain}${info?.thermalInvoiceLogo}",
|
||||
width: is58mm ? 120 : 184,
|
||||
height: is58mm ? 120 : 184,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
//qr logo
|
||||
thermer.ThermerImage? _qrLogo;
|
||||
if (info?.invoiceScannerLogo != null && info?.showInvoiceScannerLogo == 1) {
|
||||
try {
|
||||
_qrLogo = await thermer.ThermerImage.network(
|
||||
APIConfig.domain + info!.invoiceScannerLogo!,
|
||||
width: is58mm ? 120 : 140,
|
||||
height: is58mm ? 120 : 140,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// 2. Prepare Product Rows
|
||||
final productRows = _buildProductRows();
|
||||
|
||||
// 3. Prepare Returns
|
||||
final returnWidgets = _buildReturnSection(_lang);
|
||||
|
||||
// 4. Build Layout
|
||||
final _layout = thermer.ThermerLayout(
|
||||
paperSize: is58mm ? thermer.PaperSize.mm58 : thermer.PaperSize.mm80,
|
||||
textDirection: isRTL ? thermer.TextDirection.rtl : thermer.TextDirection.ltr,
|
||||
widgets: [
|
||||
// --- Header ---
|
||||
if (_logo != null) ...[thermer.ThermerAlign(child: _logo), thermer.ThermerSizedBox(height: 16)],
|
||||
|
||||
if (info?.meta?.showCompanyName == 1)
|
||||
thermer.ThermerText(
|
||||
info?.companyName ?? '',
|
||||
style: _commonStyle(fontSize: is58mm ? 46 : 54),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
if (data?.branch?.name != null)
|
||||
thermer.ThermerText('${_lang.branch}: ${data?.branch?.name}',
|
||||
style: _commonStyle(), textAlign: thermer.TextAlign.center),
|
||||
|
||||
if (info?.meta?.showAddress == 1)
|
||||
if (data?.branch?.address != null || info?.address != null)
|
||||
thermer.ThermerText(
|
||||
'${_lang.address}: ${data?.branch?.address ?? info?.address ?? ''}',
|
||||
style: _commonStyle(),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
if (info?.meta?.showPhoneNumber == 1)
|
||||
if (data?.branch?.phone != null || info?.phoneNumber != null)
|
||||
thermer.ThermerText(
|
||||
'${_lang.mobile} ${data?.branch?.phone ?? info?.phoneNumber ?? ''}',
|
||||
style: _commonStyle(),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
if (info?.meta?.showVat == 1)
|
||||
if (info?.vatNo != null && info?.meta?.showVat == 1)
|
||||
thermer.ThermerText(
|
||||
"${info?.vatName ?? _lang.vatNumber}: ${info?.vatNo}",
|
||||
style: _commonStyle(),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
thermer.ThermerText(
|
||||
_lang.invoice,
|
||||
style: _commonStyle(fontSize: is58mm ? 30 : 48, isBold: true)
|
||||
.copyWith(decoration: thermer.TextDecoration.underline),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
|
||||
// --- Info Section ---
|
||||
..._buildInfoSection(_lang),
|
||||
|
||||
thermer.ThermerSizedBox(height: 8),
|
||||
|
||||
// --- Product Table ---
|
||||
thermer.ThermerTable(
|
||||
header: thermer.ThermerTableRow([
|
||||
if (!is58mm) thermer.ThermerText(_lang.sl, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.item, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.qty, textAlign: thermer.TextAlign.center, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.price, textAlign: thermer.TextAlign.center, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.total, textAlign: thermer.TextAlign.end, style: _commonStyle(isBold: true)),
|
||||
]),
|
||||
data: productRows,
|
||||
cellWidths: is58mm
|
||||
? {0: null, 1: 0.2, 2: 0.2, 3: 0.25} // 58mm
|
||||
: {0: 0.1, 1: null, 2: 0.15, 3: 0.15, 4: 0.2}, // 80mm
|
||||
columnSpacing: 10.0,
|
||||
rowSpacing: 3.0,
|
||||
),
|
||||
thermer.ThermerDivider.horizontal(),
|
||||
|
||||
// --- Calculations ---
|
||||
if (!is58mm)
|
||||
thermer.ThermerRow(
|
||||
children: [
|
||||
thermer.ThermerExpanded(flex: 4, child: thermer.ThermerAlign(child: _buildPaymentInfoText(_lang))),
|
||||
thermer.ThermerExpanded(flex: 6, child: _buildCalculationColumn(_lang)),
|
||||
],
|
||||
)
|
||||
else ...[
|
||||
_buildCalculationColumn(_lang),
|
||||
thermer.ThermerDivider.horizontal(),
|
||||
_buildPaymentInfoText(_lang),
|
||||
],
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
|
||||
// --- Returns ---
|
||||
...returnWidgets,
|
||||
|
||||
// --- Footer ---
|
||||
if (info?.gratitudeMessage != null && info?.showGratitudeMsg == 1)
|
||||
thermer.ThermerText(info?.gratitudeMessage ?? '',
|
||||
textAlign: thermer.TextAlign.center, style: _commonStyle(isBold: true)),
|
||||
|
||||
if (data?.purchaseDate != null)
|
||||
thermer.ThermerText(
|
||||
DateFormat('M/d/yyyy h:mm a').format(DateTime.parse(data!.purchaseDate!)),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
style: _commonStyle(),
|
||||
),
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
|
||||
if (info?.showNote == 1)
|
||||
thermer.ThermerText(
|
||||
'${info?.invoiceNoteLevel ?? _lang.note}: ${info?.invoiceNote ?? ''}',
|
||||
textAlign: thermer.TextAlign.left,
|
||||
style: _commonStyle(),
|
||||
),
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
if (_qrLogo != null) ...[thermer.ThermerAlign(child: _qrLogo), thermer.ThermerSizedBox(height: 1)],
|
||||
// if (info?.developByLink != null)
|
||||
// thermer.ThermerAlign(child: thermer.ThermerQRCode(data: info?.developByLink ?? '', size: 120)),
|
||||
|
||||
if (info?.developBy != null)
|
||||
thermer.ThermerText(
|
||||
'${info?.developByLevel ?? _lang.developedBy}: ${info?.developBy}',
|
||||
textAlign: thermer.TextAlign.center,
|
||||
style: _commonStyle(),
|
||||
),
|
||||
|
||||
thermer.ThermerSizedBox(height: 200), // Cutter Space
|
||||
],
|
||||
);
|
||||
|
||||
return _layout.toUint8List();
|
||||
}
|
||||
|
||||
// --- Sub-Builders ---
|
||||
|
||||
List<thermer.ThermerWidget> _buildInfoSection(lang.S _lang) {
|
||||
final data = printTransactionModel.purchaseTransitionModel;
|
||||
final dateStr = data?.purchaseDate != null ? DateFormat.yMd().format(DateTime.parse(data!.purchaseDate!)) : '';
|
||||
final timeStr = data?.purchaseDate != null ? DateFormat.jm().format(DateTime.parse(data!.purchaseDate!)) : '';
|
||||
|
||||
final invText = '${_lang.invoice}: ${data?.invoiceNumber ?? 'Not Provided'}';
|
||||
final dateText = '${_lang.date}: $dateStr';
|
||||
final timeText = '${_lang.time}: $timeStr';
|
||||
final nameText = '${_lang.name}: ${data?.party?.name ?? 'Guest'}';
|
||||
final mobileText = '${_lang.mobile} ${data?.party?.phone ?? ''}';
|
||||
final purchaseByText = '${_lang.purchaseBy} ${data?.user?.role == "shop-owner" ? 'Admin' : data?.user?.name ?? ''}';
|
||||
|
||||
if (is58mm) {
|
||||
return [
|
||||
thermer.ThermerText(invText, style: _commonStyle()),
|
||||
if (data?.purchaseDate != null) thermer.ThermerText("$dateText $timeStr", style: _commonStyle()),
|
||||
thermer.ThermerText(nameText, style: _commonStyle()),
|
||||
thermer.ThermerText(mobileText, style: _commonStyle()),
|
||||
thermer.ThermerText(purchaseByText, style: _commonStyle()),
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(invText, style: _commonStyle()),
|
||||
if (data?.purchaseDate != null) thermer.ThermerText(dateText, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(nameText, style: _commonStyle()),
|
||||
thermer.ThermerText(timeText, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(mobileText, style: _commonStyle()),
|
||||
thermer.ThermerText(purchaseByText, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
List<thermer.ThermerTableRow> _buildProductRows() {
|
||||
List<thermer.ThermerTableRow> rows = [];
|
||||
if (productList == null) return rows;
|
||||
|
||||
for (var index = 0; index < productList!.length; index++) {
|
||||
final item = productList![index];
|
||||
final qty = _getProductQuantity(item.id ?? 0);
|
||||
final price = item.productPurchasePrice ?? 0;
|
||||
final amount = price * qty;
|
||||
|
||||
rows.add(thermer.ThermerTableRow([
|
||||
if (!is58mm) thermer.ThermerText('${index + 1}', style: _commonStyle()),
|
||||
thermer.ThermerText(_getProductName(item.id ?? 0), style: _commonStyle()),
|
||||
thermer.ThermerText(formatPointNumber(qty, addComma: true),
|
||||
textAlign: thermer.TextAlign.center, style: _commonStyle()),
|
||||
thermer.ThermerText(formatPointNumber(price, addComma: true),
|
||||
textAlign: thermer.TextAlign.center, style: _commonStyle()),
|
||||
thermer.ThermerText(formatPointNumber(amount, addComma: true),
|
||||
textAlign: thermer.TextAlign.end, style: _commonStyle()),
|
||||
]));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
thermer.ThermerColumn _buildCalculationColumn(lang.S _lang) {
|
||||
final data = printTransactionModel.purchaseTransitionModel;
|
||||
|
||||
thermer.ThermerRow calcRow(String label, num value, {bool bold = false, bool isCurrency = true}) {
|
||||
return thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(label, style: _commonStyle(isBold: bold)),
|
||||
thermer.ThermerText(
|
||||
isCurrency ? formatPointNumber(value, addComma: true) : value.toString(),
|
||||
textAlign: thermer.TextAlign.end,
|
||||
style: _commonStyle(isBold: bold),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return thermer.ThermerColumn(children: [
|
||||
calcRow('${_lang.subTotal}:', _getTotalForOldInvoice()),
|
||||
calcRow('${_lang.discount}:', (data?.discountAmount ?? 0) + _getReturnedDiscountAmount()),
|
||||
calcRow('${data?.vat?.name ?? _lang.vat}:', data?.vatAmount ?? 0),
|
||||
thermer.ThermerDivider.horizontal(),
|
||||
if (_getTotalReturnedAmount() > 0) calcRow('${_lang.returnAmount}:', _getTotalReturnedAmount()),
|
||||
calcRow('${_lang.totalPayable}:', data?.totalAmount ?? 0, bold: true),
|
||||
calcRow('${_lang.paidAmount}:', ((data?.totalAmount ?? 0) - (data?.dueAmount ?? 0)) + (data?.changeAmount ?? 0)),
|
||||
if ((data?.dueAmount ?? 0) > 0) calcRow('${_lang.dueAmount}', data?.dueAmount ?? 0),
|
||||
if ((data?.changeAmount ?? 0) > 0) calcRow('${_lang.changeAmount}:', data?.changeAmount ?? 0),
|
||||
]);
|
||||
}
|
||||
|
||||
thermer.ThermerText _buildPaymentInfoText(lang.S _lang) {
|
||||
final transactions = printTransactionModel.purchaseTransitionModel?.transactions ?? [];
|
||||
List<String> labels = [];
|
||||
|
||||
for (var item in transactions) {
|
||||
String label = item.paymentType?.name ?? 'n/a';
|
||||
if (item.transactionType == 'cash_payment') label = _lang.cash;
|
||||
if (item.transactionType == 'cheque_payment') label = _lang.cheque;
|
||||
if (item.transactionType == 'wallet_payment') label = _lang.wallet;
|
||||
labels.add(label);
|
||||
}
|
||||
return thermer.ThermerText(
|
||||
"${_lang.paidVia}: ${labels.join(', ')}",
|
||||
style: _commonStyle(),
|
||||
textAlign: is58mm ? thermer.TextAlign.left : thermer.TextAlign.left,
|
||||
);
|
||||
}
|
||||
|
||||
List<thermer.ThermerWidget> _buildReturnSection(lang.S _lang) {
|
||||
final returns = printTransactionModel.purchaseTransitionModel?.purchaseReturns;
|
||||
if (returns?.isEmpty ?? true) return [];
|
||||
|
||||
List<thermer.ThermerWidget> widgets = [];
|
||||
List<String> processedDates = [];
|
||||
|
||||
for (var i = 0; i < (returns?.length ?? 0); i++) {
|
||||
final dateStr = returns![i].returnDate?.substring(0, 10);
|
||||
if (dateStr != null && !processedDates.contains(dateStr)) {
|
||||
processedDates.add(dateStr);
|
||||
widgets.add(thermer.ThermerDivider.horizontal());
|
||||
|
||||
// Return Header
|
||||
widgets.add(thermer.ThermerRow(
|
||||
children: [
|
||||
if (!is58mm) thermer.ThermerText(_lang.sl, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText('${_lang.retur}-${DateFormat.yMd().format(DateTime.parse(returns[i].returnDate!))}',
|
||||
style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.qty, textAlign: thermer.TextAlign.center, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.total, textAlign: thermer.TextAlign.end, style: _commonStyle(isBold: true)),
|
||||
],
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
));
|
||||
}
|
||||
|
||||
widgets.add(thermer.ThermerTable(
|
||||
data: (returns[i].purchaseReturnDetails ?? []).map((d) {
|
||||
// Re-using index logic might be tricky here for SL, simplify if needed
|
||||
return thermer.ThermerTableRow([
|
||||
if (!is58mm) thermer.ThermerText('*', style: _commonStyle()), // Bullet for return items or dynamic index
|
||||
thermer.ThermerText(_getProductName(d.purchaseDetailId ?? 0), style: _commonStyle()),
|
||||
thermer.ThermerText('${d.returnQty ?? 0}', textAlign: thermer.TextAlign.center, style: _commonStyle()),
|
||||
thermer.ThermerText(formatPointNumber(d.returnAmount ?? 0, addComma: true),
|
||||
textAlign: thermer.TextAlign.end, style: _commonStyle()),
|
||||
]);
|
||||
}).toList(),
|
||||
cellWidths: is58mm ? {0: null, 1: 0.2, 2: 0.25} : {0: 0.1, 1: null, 2: 0.15, 3: 0.2},
|
||||
));
|
||||
}
|
||||
|
||||
// Add Total Return Footer inside Calculation Column generally,
|
||||
// but if you want separate divider:
|
||||
widgets.add(thermer.ThermerDivider.horizontal());
|
||||
|
||||
return widgets;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:esc_pos_utils_plus/esc_pos_utils_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobile_pos/Const/api_config.dart';
|
||||
import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
import 'package:mobile_pos/model/business_info_model.dart';
|
||||
import 'package:mobile_pos/model/sale_transaction_model.dart';
|
||||
import '../../thermer/thermer.dart' as thermer;
|
||||
|
||||
class SaleThermalInvoiceTemplate {
|
||||
SaleThermalInvoiceTemplate({
|
||||
required this.saleInvoice,
|
||||
required this.is58mm,
|
||||
required this.business,
|
||||
required this.context,
|
||||
required this.isRTL,
|
||||
});
|
||||
|
||||
final SalesTransactionModel saleInvoice;
|
||||
final BusinessInformationModel business;
|
||||
final bool is58mm;
|
||||
final bool isRTL;
|
||||
final BuildContext context;
|
||||
|
||||
// --- Helpers: Styles & Formats ---
|
||||
|
||||
/// Centralized Text Style to ensure Black Color everywhere
|
||||
thermer.TextStyle _commonStyle({double fontSize = 24, bool isBold = false}) {
|
||||
return thermer.TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: isBold ? thermer.FontWeight.bold : thermer.FontWeight.w500,
|
||||
color: thermer.Colors.black,
|
||||
);
|
||||
}
|
||||
|
||||
String formatPointNumber(num number, {bool addComma = false}) {
|
||||
if (addComma) return NumberFormat("#,###.##", "en_US").format(number);
|
||||
return number.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
// --- Data Logic ---
|
||||
|
||||
String _getProductName(num detailsId) {
|
||||
final details = saleInvoice.salesDetails?.firstWhere(
|
||||
(e) => e.id == detailsId,
|
||||
orElse: () => SalesDetails(),
|
||||
);
|
||||
String name = details?.product?.productName ?? '';
|
||||
if (details?.product?.productType == 'variant' && details?.stock?.batchNo != null) {
|
||||
name += ' [${details!.stock!.batchNo}]';
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
num _getProductQty(num detailsId) {
|
||||
num totalQty =
|
||||
saleInvoice.salesDetails?.firstWhere((e) => e.id == detailsId, orElse: () => SalesDetails()).quantities ?? 0;
|
||||
|
||||
// Add returned quantities back logic
|
||||
if (saleInvoice.salesReturns?.isNotEmpty ?? false) {
|
||||
for (var ret in saleInvoice.salesReturns!) {
|
||||
for (var det in ret.salesReturnDetails ?? []) {
|
||||
if (det.saleDetailId == detailsId) totalQty += det.returnQty ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return totalQty;
|
||||
}
|
||||
|
||||
// --- Main Generator ---
|
||||
|
||||
@override
|
||||
Future<List<int>> get template async {
|
||||
final _profile = await CapabilityProfile.load();
|
||||
final _generator = Generator(is58mm ? PaperSize.mm58 : PaperSize.mm80, _profile);
|
||||
|
||||
// Generate Layout
|
||||
final _imageBytes = await _generateLayout();
|
||||
final _image = img.decodeImage(_imageBytes);
|
||||
|
||||
if (_image == null) throw Exception('Failed to generate invoice.');
|
||||
|
||||
List<int> _bytes = [];
|
||||
_bytes += _generator.image(_image);
|
||||
_bytes += _generator.cut();
|
||||
return _bytes;
|
||||
}
|
||||
|
||||
Future<Uint8List> _generateLayout() async {
|
||||
final _lang = lang.S.of(context);
|
||||
// 1. Prepare Logo
|
||||
thermer.ThermerImage? _logo;
|
||||
if (business.data?.thermalInvoiceLogo != null && business.data?.showThermalInvoiceLogo == 1) {
|
||||
try {
|
||||
_logo = await thermer.ThermerImage.network(
|
||||
APIConfig.domain + business.data!.thermalInvoiceLogo!,
|
||||
width: is58mm ? 120 : 200,
|
||||
height: is58mm ? 120 : 200,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
//qr logo
|
||||
thermer.ThermerImage? _qrLogo;
|
||||
if (business.data?.invoiceScannerLogo != null && business.data?.showInvoiceScannerLogo == 1) {
|
||||
try {
|
||||
_qrLogo = await thermer.ThermerImage.network(
|
||||
APIConfig.domain + business.data!.invoiceScannerLogo!,
|
||||
width: is58mm ? 120 : 140,
|
||||
height: is58mm ? 120 : 140,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// 2. Prepare Product Rows
|
||||
final productRows = _buildProductRows();
|
||||
|
||||
// 3. Prepare Return Section
|
||||
final returnWidgets = _buildReturnSection(context);
|
||||
|
||||
// 4. Build Layout
|
||||
final _layout = thermer.ThermerLayout(
|
||||
textDirection: isRTL ? thermer.TextDirection.rtl : thermer.TextDirection.ltr,
|
||||
paperSize: is58mm ? thermer.PaperSize.mm58 : thermer.PaperSize.mm80,
|
||||
widgets: [
|
||||
// --- Header Section ---
|
||||
if (_logo != null) ...[thermer.ThermerAlign(child: _logo), thermer.ThermerSizedBox(height: 16)],
|
||||
|
||||
if (business.data?.meta?.showCompanyName == 1)
|
||||
thermer.ThermerText(
|
||||
business.data?.companyName ?? "N/A",
|
||||
style: _commonStyle(fontSize: is58mm ? 46 : 54, isBold: false),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
if (saleInvoice.branch?.name != null)
|
||||
thermer.ThermerText('${_lang.branch}: ${saleInvoice.branch?.name}',
|
||||
style: _commonStyle(), textAlign: thermer.TextAlign.center),
|
||||
|
||||
if (business.data?.meta?.showAddress == 1)
|
||||
if (business.data?.address != null || saleInvoice.branch?.address != null)
|
||||
thermer.ThermerText(
|
||||
saleInvoice.branch?.address ?? business.data?.address ?? 'N/A',
|
||||
style: _commonStyle(),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
if (business.data?.meta?.showPhoneNumber == 1)
|
||||
if (business.data?.phoneNumber != null || saleInvoice.branch?.phone != null)
|
||||
thermer.ThermerText(
|
||||
'${_lang.mobile} ${saleInvoice.branch?.phone ?? business.data?.phoneNumber ?? "N/A"}',
|
||||
style: _commonStyle(),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
|
||||
if (business.data?.vatName != null && business.data?.meta?.showVat == 1)
|
||||
thermer.ThermerText("${business.data?.vatName}: ${business.data?.vatNo}",
|
||||
style: _commonStyle(), textAlign: thermer.TextAlign.center),
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
thermer.ThermerText(
|
||||
_lang.invoice,
|
||||
style: _commonStyle(fontSize: is58mm ? 30 : 48, isBold: true)
|
||||
.copyWith(decoration: thermer.TextDecoration.underline),
|
||||
textAlign: thermer.TextAlign.center,
|
||||
),
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
|
||||
// --- Info Section (Layout adjusted based on is58mm) ---
|
||||
..._buildInfoSection(_lang),
|
||||
|
||||
thermer.ThermerSizedBox(height: 8),
|
||||
|
||||
// --- Product Table ---
|
||||
thermer.ThermerTable(
|
||||
header: thermer.ThermerTableRow([
|
||||
if (!is58mm) thermer.ThermerText(_lang.sl, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.item, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.qty, textAlign: thermer.TextAlign.center, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.price, textAlign: thermer.TextAlign.center, style: _commonStyle(isBold: true)),
|
||||
thermer.ThermerText(_lang.amount, textAlign: thermer.TextAlign.end, style: _commonStyle(isBold: true)),
|
||||
]),
|
||||
data: productRows,
|
||||
cellWidths: is58mm
|
||||
? {0: null, 1: 0.2, 2: 0.15, 3: 0.2} // 58mm layout
|
||||
: {0: 0.1, 1: null, 2: 0.15, 3: 0.2, 4: 0.2}, // 80mm layout
|
||||
columnSpacing: 15.0,
|
||||
rowSpacing: 3.0,
|
||||
),
|
||||
thermer.ThermerDivider.horizontal(),
|
||||
|
||||
// --- Totals Section ---
|
||||
if (!is58mm)
|
||||
// 80mm Split Layout
|
||||
thermer.ThermerRow(
|
||||
children: [
|
||||
thermer.ThermerExpanded(flex: 4, child: thermer.ThermerAlign(child: _buildPaymentInfoText(_lang))),
|
||||
thermer.ThermerExpanded(flex: 6, child: _buildCalculationColumn(_lang)),
|
||||
],
|
||||
)
|
||||
else ...[
|
||||
// 58mm Stacked Layout
|
||||
_buildCalculationColumn(_lang),
|
||||
thermer.ThermerDivider.horizontal(),
|
||||
_buildPaymentInfoText(_lang),
|
||||
],
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
|
||||
// --- Returns ---
|
||||
...returnWidgets,
|
||||
|
||||
// --- Footer ---
|
||||
if (business.data?.gratitudeMessage != null && business.data?.showGratitudeMsg == 1)
|
||||
thermer.ThermerText(business.data?.gratitudeMessage ?? '',
|
||||
textAlign: thermer.TextAlign.center, style: _commonStyle(isBold: true)),
|
||||
|
||||
if (business.data?.showNote == 1)
|
||||
thermer.ThermerText('${business.data?.invoiceNoteLevel ?? _lang.note}: ${business.data?.invoiceNote}',
|
||||
textAlign: thermer.TextAlign.center, style: _commonStyle()),
|
||||
|
||||
thermer.ThermerSizedBox(height: 16),
|
||||
if (_qrLogo != null) ...[thermer.ThermerAlign(child: _qrLogo), thermer.ThermerSizedBox(height: 1)],
|
||||
// if (business.data?.developByLink != null)
|
||||
// thermer.ThermerAlign(child: thermer.ThermerQRCode(data: business.data?.developByLink ?? '', size: 120)),
|
||||
|
||||
if (business.data?.developBy != null)
|
||||
thermer.ThermerText('${business.data?.developByLevel ?? _lang.developedBy} ${business.data?.developBy}',
|
||||
textAlign: thermer.TextAlign.center, style: _commonStyle()),
|
||||
|
||||
thermer.ThermerSizedBox(height: 200), // Cutter space
|
||||
],
|
||||
);
|
||||
|
||||
return _layout.toUint8List();
|
||||
}
|
||||
|
||||
// --- Sub-Builders ---
|
||||
|
||||
List<thermer.ThermerWidget> _buildInfoSection(lang.S _lang) {
|
||||
DateTime? saleDateTime;
|
||||
|
||||
if (saleInvoice.saleDate != null && saleInvoice.saleDate!.isNotEmpty) {
|
||||
saleDateTime = DateTime.tryParse(saleInvoice.saleDate!);
|
||||
}
|
||||
|
||||
final formattedDate = saleDateTime != null
|
||||
? DateFormat('dd MMMM yyyy').format(saleDateTime) // 25 January 2026
|
||||
: '';
|
||||
|
||||
final formattedTime = saleDateTime != null
|
||||
? DateFormat('hh:mm a').format(saleDateTime).toLowerCase() // 12:55 pm
|
||||
: '';
|
||||
|
||||
final invText = '${_lang.invoice}: ${saleInvoice.invoiceNumber ?? ''}';
|
||||
final dateText = '${_lang.date}: ${formattedDate ?? ''}';
|
||||
final timeText = "${_lang.time}: ${formattedTime ?? ''}";
|
||||
final nameText = '${_lang.name}: ${saleInvoice.party?.name ?? 'Guest'}';
|
||||
final mobileText = '${_lang.mobile} ${saleInvoice.party?.phone ?? 'N/A'}';
|
||||
final salesByText =
|
||||
"${_lang.salesBy} ${saleInvoice.user?.role == 'shop-owner' ? _lang.admin : saleInvoice.user?.role ?? "N/A"}";
|
||||
|
||||
if (is58mm) {
|
||||
// 58mm: Vertical Stack (One below another)
|
||||
return [
|
||||
thermer.ThermerText(
|
||||
invText,
|
||||
style: _commonStyle(),
|
||||
),
|
||||
if (saleInvoice.saleDate != null) thermer.ThermerText(dateText, style: _commonStyle()),
|
||||
thermer.ThermerText(nameText, style: _commonStyle()),
|
||||
thermer.ThermerText(mobileText, style: _commonStyle()),
|
||||
thermer.ThermerText(salesByText, style: _commonStyle()),
|
||||
];
|
||||
} else {
|
||||
// 80mm: Two columns (Side by side)
|
||||
return [
|
||||
// Row 1: Invoice | Date
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(invText, style: _commonStyle()),
|
||||
if (saleInvoice.saleDate != null) thermer.ThermerText(dateText, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
// Row 2: Name | Time
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(nameText, style: _commonStyle()),
|
||||
thermer.ThermerText(timeText, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
// Row 3: Mobile | Sales By
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(mobileText, style: _commonStyle()),
|
||||
thermer.ThermerText(salesByText, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
List<thermer.ThermerTableRow> _buildProductRows() {
|
||||
List<thermer.ThermerTableRow> rows = [];
|
||||
if (saleInvoice.salesDetails == null) return rows;
|
||||
|
||||
for (var index = 0; index < saleInvoice.salesDetails!.length; index++) {
|
||||
final item = saleInvoice.salesDetails![index];
|
||||
final qty = _getProductQty(item.id ?? 0);
|
||||
final price = item.price ?? 0;
|
||||
final discount = item.discount ?? 0;
|
||||
final amount = (price * qty) - (discount * qty);
|
||||
|
||||
// Main Row
|
||||
rows.add(thermer.ThermerTableRow([
|
||||
if (!is58mm) thermer.ThermerText((index + 1).toString(), style: _commonStyle()),
|
||||
thermer.ThermerText(_getProductName(item.id ?? 0), style: _commonStyle()),
|
||||
thermer.ThermerText(formatPointNumber(qty), textAlign: thermer.TextAlign.center, style: _commonStyle()),
|
||||
thermer.ThermerText('$price', textAlign: thermer.TextAlign.center, style: _commonStyle()),
|
||||
thermer.ThermerText(formatPointNumber(amount), textAlign: thermer.TextAlign.end, style: _commonStyle()),
|
||||
]));
|
||||
|
||||
// Warranty/Guarantee
|
||||
final w = item.warrantyInfo;
|
||||
if (w?.warrantyDuration != null) {
|
||||
rows.add(_buildInfoRow("${lang.S.of(context).warranty} : ${w!.warrantyDuration} ${w.warrantyUnit}"));
|
||||
}
|
||||
if (w?.guaranteeDuration != null) {
|
||||
rows.add(_buildInfoRow("${lang.S.of(context).guarantee} : ${w!.guaranteeDuration} ${w.guaranteeUnit}"));
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
thermer.ThermerTableRow _buildInfoRow(String text) {
|
||||
return thermer.ThermerTableRow([
|
||||
if (!is58mm) thermer.ThermerText(""),
|
||||
thermer.ThermerText(text, style: _commonStyle(fontSize: 20)),
|
||||
thermer.ThermerText(""),
|
||||
thermer.ThermerText(""),
|
||||
thermer.ThermerText(""),
|
||||
]);
|
||||
}
|
||||
|
||||
thermer.ThermerColumn _buildCalculationColumn(lang.S _lang) {
|
||||
thermer.ThermerRow calcRow(String label, num value, {bool bold = false, bool isCurrency = true}) {
|
||||
return thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(label, style: _commonStyle(isBold: bold)),
|
||||
thermer.ThermerText(
|
||||
isCurrency ? formatPointNumber(value, addComma: true) : value.toString(),
|
||||
textAlign: thermer.TextAlign.end,
|
||||
style: _commonStyle(isBold: bold),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
num subTotal = 0;
|
||||
num totalDiscount = 0;
|
||||
if (saleInvoice.salesDetails != null) {
|
||||
for (var e in saleInvoice.salesDetails!) {
|
||||
final q = _getProductQty(e.id ?? 0);
|
||||
subTotal += ((e.price ?? 0) * q) - ((e.discount ?? 0) * q);
|
||||
totalDiscount += (e.discount ?? 0) * q;
|
||||
}
|
||||
}
|
||||
|
||||
num returnDiscount = 0;
|
||||
if (saleInvoice.salesReturns != null) {
|
||||
for (var ret in saleInvoice.salesReturns!) {
|
||||
for (var det in ret.salesReturnDetails ?? []) {
|
||||
final price = saleInvoice.salesDetails
|
||||
?.firstWhere((e) => e.id == det.saleDetailId, orElse: () => SalesDetails())
|
||||
.price ??
|
||||
0;
|
||||
returnDiscount += ((price * (det.returnQty ?? 0)) - (det.returnAmount ?? 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return thermer.ThermerColumn(
|
||||
children: [
|
||||
calcRow('${_lang.subTotal}: ', subTotal),
|
||||
calcRow('${_lang.discount}: ', (saleInvoice.discountAmount ?? 0) + returnDiscount + totalDiscount),
|
||||
calcRow("${saleInvoice.vat?.name ?? _lang.vat}: ", saleInvoice.vatAmount ?? 0, isCurrency: false),
|
||||
calcRow('${_lang.shippingCharge}:', saleInvoice.shippingCharge ?? 0, isCurrency: false),
|
||||
if ((saleInvoice.roundingAmount ?? 0) != 0) ...[
|
||||
calcRow('${_lang.total}:', saleInvoice.actualTotalAmount ?? 0),
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText('${_lang.rounding}:', style: _commonStyle()),
|
||||
thermer.ThermerText(
|
||||
"${!(saleInvoice.roundingAmount?.isNegative ?? true) ? '+' : ''}${formatPointNumber(saleInvoice.roundingAmount ?? 0)}",
|
||||
textAlign: thermer.TextAlign.end,
|
||||
style: _commonStyle(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
thermer.ThermerDivider.horizontal(),
|
||||
calcRow('${_lang.totalPayable}: ', saleInvoice.totalAmount ?? 0, bold: true),
|
||||
calcRow('${_lang.paidAmount}: ',
|
||||
((saleInvoice.totalAmount ?? 0) - (saleInvoice.dueAmount ?? 0)) + (saleInvoice.changeAmount ?? 0)),
|
||||
if ((saleInvoice.dueAmount ?? 0) > 0) calcRow('${_lang.dueAmount}: ', saleInvoice.dueAmount ?? 0),
|
||||
if ((saleInvoice.changeAmount ?? 0) > 0) calcRow('${_lang.changeAmount}: ', saleInvoice.changeAmount ?? 0),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
thermer.ThermerText _buildPaymentInfoText(lang.S _lang) {
|
||||
List<String> labels = [];
|
||||
if (saleInvoice.transactions != null) {
|
||||
for (var item in saleInvoice.transactions!) {
|
||||
String label = item.paymentType?.name ?? 'n/a';
|
||||
if (item.transactionType == 'cash_payment') label = _lang.cash;
|
||||
if (item.transactionType == 'cheque_payment') label = _lang.cheque;
|
||||
if (item.transactionType == 'wallet_payment') label = _lang.wallet;
|
||||
labels.add(label);
|
||||
}
|
||||
}
|
||||
return thermer.ThermerText(
|
||||
"${_lang.paidVia} : ${labels.join(', ')}",
|
||||
style: _commonStyle(),
|
||||
textAlign: is58mm ? thermer.TextAlign.center : thermer.TextAlign.start,
|
||||
);
|
||||
}
|
||||
|
||||
List<thermer.ThermerWidget> _buildReturnSection(BuildContext context) {
|
||||
final _lang = lang.S.of(context);
|
||||
if (saleInvoice.salesReturns?.isEmpty ?? true) return [];
|
||||
|
||||
List<thermer.ThermerWidget> widgets = [];
|
||||
List<String> processedDates = [];
|
||||
num totalReturnedAmount = 0;
|
||||
|
||||
for (var ret in saleInvoice.salesReturns!) {
|
||||
final dateStr = ret.returnDate?.substring(0, 10);
|
||||
if (dateStr != null && !processedDates.contains(dateStr)) {
|
||||
processedDates.add(dateStr);
|
||||
widgets.add(thermer.ThermerDivider.horizontal());
|
||||
widgets.add(thermer.ThermerText('${_lang.retur}-$dateStr', style: _commonStyle(isBold: true)));
|
||||
}
|
||||
|
||||
widgets.add(thermer.ThermerTable(
|
||||
header: thermer.ThermerTableRow([
|
||||
thermer.ThermerText(_lang.item, style: _commonStyle(fontSize: 22, isBold: true)),
|
||||
thermer.ThermerText(_lang.qty,
|
||||
textAlign: thermer.TextAlign.center, style: _commonStyle(fontSize: 22, isBold: true)),
|
||||
thermer.ThermerText(_lang.total,
|
||||
textAlign: thermer.TextAlign.end, style: _commonStyle(fontSize: 22, isBold: true)),
|
||||
]),
|
||||
data: (ret.salesReturnDetails ?? []).map((d) {
|
||||
totalReturnedAmount += d.returnAmount ?? 0;
|
||||
return thermer.ThermerTableRow([
|
||||
thermer.ThermerText(_getProductName(d.saleDetailId ?? 0), style: _commonStyle(fontSize: 22)),
|
||||
thermer.ThermerText('${d.returnQty ?? 0}',
|
||||
textAlign: thermer.TextAlign.center, style: _commonStyle(fontSize: 22)),
|
||||
thermer.ThermerText('${d.returnAmount ?? 0}',
|
||||
textAlign: thermer.TextAlign.end, style: _commonStyle(fontSize: 22)),
|
||||
]);
|
||||
}).toList(),
|
||||
cellWidths: {0: null, 1: 0.2, 2: 0.25},
|
||||
));
|
||||
}
|
||||
|
||||
widgets.add(thermer.ThermerDivider.horizontal());
|
||||
widgets.add(
|
||||
thermer.ThermerRow(
|
||||
mainAxisAlignment: thermer.ThermerMainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
thermer.ThermerText(_lang.returnAmount, style: _commonStyle()),
|
||||
thermer.ThermerText(formatPointNumber(totalReturnedAmount),
|
||||
textAlign: thermer.TextAlign.end, style: _commonStyle()),
|
||||
],
|
||||
),
|
||||
);
|
||||
widgets.add(thermer.ThermerSizedBox(height: 10));
|
||||
|
||||
return widgets;
|
||||
}
|
||||
}
|
||||
69
lib/service/thermal_print/src/templates/templates.dart
Normal file
69
lib/service/thermal_print/src/templates/templates.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
// import 'dart:typed_data' show Uint8List;
|
||||
|
||||
// import 'package:esc_pos_utils_plus/esc_pos_utils_plus.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_riverpod/flutter_riverpod.dart' as riverpod;
|
||||
// import 'package:image/image.dart' as img;
|
||||
// import 'package:intl/intl.dart';
|
||||
// import 'package:mobile_pos/generated/l10n.dart' as lang;
|
||||
// import 'package:mobile_pos/generated/l10n.dart';
|
||||
// import 'package:mobile_pos/model/business_info_model.dart';
|
||||
// import 'package:mobile_pos/model/sale_transaction_model.dart';
|
||||
// import '../../thermer/thermer.dart' as thermer;
|
||||
|
||||
// // part '_purchase_invoice_template.dart';
|
||||
// part '_sale_invoice_template.dart';
|
||||
// // part '_kot_ticket_template.dart';
|
||||
// // part '_due_collection_invoice_template.dart';
|
||||
|
||||
// abstract class ThermalInvoiceTemplateBase {
|
||||
// ThermalInvoiceTemplateBase(this.ref);
|
||||
// final riverpod.Ref ref;
|
||||
|
||||
// thermer.TextDirection get textDirection {
|
||||
// final _rtlLang = ['ar', 'ar-bh', 'eg-ar', 'fa', 'prs', 'ps', 'ur'];
|
||||
|
||||
// // if (_rtlLang.contains(ref.read(GlobalContextHolder.localeProvider).languageCode)) {
|
||||
// // return thermer.TextDirection.rtl;
|
||||
// // }
|
||||
|
||||
// return thermer.TextDirection.ltr;
|
||||
// }
|
||||
|
||||
// Future<List<int>> get template;
|
||||
// // Future<img.Image?> getNetworkImage(
|
||||
// // String? url, {
|
||||
// // int width = 100,
|
||||
// // int height = 100,
|
||||
// // }) async {
|
||||
// // if (url == null) return null;
|
||||
|
||||
// // try {
|
||||
// // final _response = await dio.Dio().get<List<int>>(
|
||||
// // url,
|
||||
// // options: dio.Options(responseType: dio.ResponseType.bytes),
|
||||
// // );
|
||||
|
||||
// // final _image = img.decodeImage(Uint8List.fromList(_response.data!));
|
||||
// // if (_image == null) return null;
|
||||
|
||||
// // return img.copyResize(
|
||||
// // _image,
|
||||
// // width: width,
|
||||
// // height: height,
|
||||
// // interpolation: img.Interpolation.average,
|
||||
// // );
|
||||
// // } catch (e) {
|
||||
// // return null;
|
||||
// // }
|
||||
// // }
|
||||
// }
|
||||
|
||||
// // extension ThermalPrinterPaperSizeExt {
|
||||
// // PaperSize get escPosSize {
|
||||
// // return switch (this) {
|
||||
// // ThermalPrinterPaperSize.mm582Inch => PaperSize.mm58,
|
||||
// // ThermalPrinterPaperSize.mm803Inch => PaperSize.mm80,
|
||||
// // };
|
||||
// // }
|
||||
// // }
|
||||
Reference in New Issue
Block a user