feat(client): 出入库单打印按新版式重做 + 单价口径修正
- 版式参照单据样板:公司抬头 + 两行三列表头 + 统一明细列 (商品编号/名称/系列/规格/批次号/生产日期/数量/单价/金额/备注) - 单据总计独立成行(仅数量+金额,右对齐高亮) - 出库单保留温馨提示,入库单去掉 - 签字行:制单人=当前登录用户(OrderPrintMeta),采购员/销售员=经办人; 公司名取门店(shopInfoProvider) - 出库单单价=售价(sale_price),金额=售价×数量;历史单 sale_price=0 时回退 unit_price/total_price。入库单单价=进价(unit_price) - 桌面 PDF 与 Web HTML 两端同构 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
/// 单据打印的「门店抬头 + 制单人」元信息。
|
||||
///
|
||||
/// - 门店信息(公司名称/地址/电话)来自 `shopInfoProvider`,作为单据顶部抬头。
|
||||
/// - [makerName] = 制单人 = **当前登录、点击打印的人**(非单据经办人/审核人)。
|
||||
/// 入库单的「采购员」「出库单的「销售员」才是单据经办人(operator)。
|
||||
class OrderPrintMeta {
|
||||
final String shopName;
|
||||
final String shopAddress;
|
||||
final String shopPhone;
|
||||
|
||||
/// 制单人 = 当前登录用户(打印此单的人)。
|
||||
final String makerName;
|
||||
|
||||
const OrderPrintMeta({
|
||||
this.shopName = '',
|
||||
this.shopAddress = '',
|
||||
this.shopPhone = '',
|
||||
this.makerName = '',
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import '../../models/stock_out.dart';
|
||||
import '../errors/error_reporter.dart';
|
||||
import 'label_data.dart';
|
||||
export 'label_data.dart';
|
||||
import 'order_print_meta.dart';
|
||||
export 'order_print_meta.dart';
|
||||
import 'print_util_stub.dart'
|
||||
if (dart.library.js_interop) 'print_util_web.dart';
|
||||
|
||||
@@ -47,19 +49,20 @@ Future<List<String>> listLabelPrinters() => listLabelPrintersImpl();
|
||||
/// 自动检测默认热敏打印机(名字含 deli/dl-888);找不到或 Web 返回 null。
|
||||
Future<String?> detectDefaultPrinter() => detectDefaultPrinterImpl();
|
||||
|
||||
Future<void> printStockInOrder(StockInOrder order) =>
|
||||
printStockInOrderImpl(order);
|
||||
Future<void> printStockInOrder(StockInOrder order, OrderPrintMeta meta) =>
|
||||
printStockInOrderImpl(order, meta);
|
||||
|
||||
Future<void> printStockOutOrder(StockOutOrder order) =>
|
||||
printStockOutOrderImpl(order);
|
||||
Future<void> printStockOutOrder(StockOutOrder order, OrderPrintMeta meta) =>
|
||||
printStockOutOrderImpl(order, meta);
|
||||
|
||||
/// 生成入库单 PDF 字节(桌面端真实排版,供打印预览光栅化用);Web 端不支持。
|
||||
Future<Uint8List> buildStockInOrderPdf(StockInOrder order) =>
|
||||
buildStockInOrderPdfImpl(order);
|
||||
Future<Uint8List> buildStockInOrderPdf(StockInOrder order, OrderPrintMeta meta) =>
|
||||
buildStockInOrderPdfImpl(order, meta);
|
||||
|
||||
/// 生成出库单 PDF 字节(桌面端真实排版,供打印预览光栅化用);Web 端不支持。
|
||||
Future<Uint8List> buildStockOutOrderPdf(StockOutOrder order) =>
|
||||
buildStockOutOrderPdfImpl(order);
|
||||
Future<Uint8List> buildStockOutOrderPdf(
|
||||
StockOutOrder order, OrderPrintMeta meta) =>
|
||||
buildStockOutOrderPdfImpl(order, meta);
|
||||
|
||||
/// 统一打印入口:catch 任意异常,上报并弹 SnackBar 给用户。
|
||||
/// 用法:await safePrint(context, () => printStockInOrder(order));
|
||||
|
||||
@@ -25,6 +25,7 @@ import 'package:printing/printing.dart';
|
||||
import '../../models/stock_in.dart';
|
||||
import '../../models/stock_out.dart';
|
||||
import 'label_data.dart';
|
||||
import 'order_print_meta.dart';
|
||||
|
||||
Future<pw.Font> _loadFont() async {
|
||||
final data = await rootBundle.load('assets/fonts/NotoSansSC-Regular.ttf');
|
||||
@@ -644,184 +645,263 @@ Future<List<String>> listLabelPrintersImpl() async {
|
||||
/// 自动检测默认热敏打印机(名字含 deli/dl-888);找不到返回 null。
|
||||
Future<String?> detectDefaultPrinterImpl() => _findThermalPrinter();
|
||||
|
||||
/// 截取日期前 10 位(yyyy-MM-dd),空值返回空串。
|
||||
String _d10(String? s) {
|
||||
if (s == null || s.isEmpty) return '';
|
||||
return s.length > 10 ? s.substring(0, 10) : s;
|
||||
}
|
||||
|
||||
/// 数量格式化:整数不带小数,否则保留 3 位。
|
||||
String _qtyStr(double q) =>
|
||||
q % 1 == 0 ? q.toStringAsFixed(0) : q.toStringAsFixed(3);
|
||||
|
||||
/// 单据明细列(入库/出库统一):商品编号→备注。
|
||||
const _orderHeaders = [
|
||||
'商品编号', '商品名称', '系列', '规格', '批次号',
|
||||
'生产日期', '数量', '单价', '金额', '备注',
|
||||
];
|
||||
const _orderColFlex = <double>[1.5, 2.0, 1.1, 1.3, 1.6, 1.3, 0.7, 1.1, 1.2, 0.9];
|
||||
|
||||
/// 温馨提示文案(入库/出库统一)。
|
||||
const _kTipsText =
|
||||
'温馨提示:签收时,请务必核对好酒品数量、年份和日期、批次及物流码,'
|
||||
'如有问题及时反馈,酒品无质量问题一经售出概不退换,谢谢合作。';
|
||||
|
||||
/// 各明细列对齐:系列/规格/批次号/生产日期居中,数量/单价/金额右对齐,其余左对齐。
|
||||
pw.TextAlign _colAlign(int i) {
|
||||
switch (i) {
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
return pw.TextAlign.center;
|
||||
case 6:
|
||||
case 7:
|
||||
case 8:
|
||||
return pw.TextAlign.right;
|
||||
default:
|
||||
return pw.TextAlign.left;
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建入库单/出库单 A4 版式(参照「锐浪报表」单据:公司抬头 + 两行三列表头
|
||||
/// + 明细表 + 单据总计 + 温馨提示 + 签字行)。入库/出库仅称谓不同。
|
||||
pw.Widget _buildOrderDoc({
|
||||
required pw.Font font,
|
||||
required pw.Font bold,
|
||||
required String title,
|
||||
required String shopName, // 公司名称(顶部抬头)
|
||||
required String title, // 入 库 单 / 出 库 单
|
||||
required String orderNo,
|
||||
required String? orderDate,
|
||||
required String? partnerLabel,
|
||||
required String noPrefix, // 'NO:' / 'NO.'
|
||||
required String partnerLabel, // 单位名称 / 客户名称
|
||||
required String? partnerName,
|
||||
required String dateLabel, // 入库日期 / 出库日期
|
||||
required String? orderDate,
|
||||
required String addressLabel, // 单位地址 / 送货地址
|
||||
required String? warehouseName,
|
||||
required String signerLabel, // 出货人(签字)/ 收货人(签字)
|
||||
required String operatorRoleLabel, // 采购员 / 销售员(= 经办人)
|
||||
required String? operatorName,
|
||||
required String? reviewerName,
|
||||
required String? makerName, // 制单人 = 打印人
|
||||
required String? remark,
|
||||
required List<String> headers,
|
||||
required bool showTips,
|
||||
required List<List<String>> rows,
|
||||
required double totalQty,
|
||||
required double totalAmt,
|
||||
String? tipsText,
|
||||
}) {
|
||||
final headerStyle = pw.TextStyle(font: bold, fontSize: 10, fontWeight: pw.FontWeight.bold);
|
||||
final cellStyle = pw.TextStyle(font: font, fontSize: 9.5);
|
||||
final cellStyle = pw.TextStyle(font: font, fontSize: 9);
|
||||
final headStyle =
|
||||
pw.TextStyle(font: bold, fontSize: 9.5, fontWeight: pw.FontWeight.bold);
|
||||
final metaStyle = pw.TextStyle(font: font, fontSize: 10);
|
||||
final boldStyle = pw.TextStyle(font: bold, fontSize: 10, fontWeight: pw.FontWeight.bold);
|
||||
final boldStyle =
|
||||
pw.TextStyle(font: bold, fontSize: 10, fontWeight: pw.FontWeight.bold);
|
||||
|
||||
final colWidths = headers.map((h) {
|
||||
if (h == '商品名称') return const pw.FlexColumnWidth(2.2);
|
||||
if (h == '系列' || h == '规格') return const pw.FlexColumnWidth(1.2);
|
||||
if (h == '商品编号') return const pw.FlexColumnWidth(1.2);
|
||||
return const pw.FlexColumnWidth(0.9);
|
||||
}).toList();
|
||||
pw.Widget metaText(String label, String value, pw.TextAlign align) =>
|
||||
pw.Text('$label:$value', style: metaStyle, textAlign: align);
|
||||
|
||||
return pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// ── 公司名称 ──────────────────────────────────────────────────────────
|
||||
pw.Center(
|
||||
child: pw.Text(shopName,
|
||||
style: pw.TextStyle(
|
||||
font: bold, fontSize: 18, fontWeight: pw.FontWeight.bold)),
|
||||
),
|
||||
pw.SizedBox(height: 4),
|
||||
// ── 单据类型 ──────────────────────────────────────────────────────────
|
||||
pw.Center(
|
||||
child: pw.Text(title,
|
||||
style: pw.TextStyle(font: bold, fontSize: 20, fontWeight: pw.FontWeight.bold)),
|
||||
style: pw.TextStyle(
|
||||
font: bold,
|
||||
fontSize: 15,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
letterSpacing: 2)),
|
||||
),
|
||||
pw.SizedBox(height: 8),
|
||||
// ── 抬头信息(两行三列)──────────────────────────────────────────────
|
||||
pw.Container(
|
||||
decoration: const pw.BoxDecoration(
|
||||
border: pw.Border(bottom: pw.BorderSide(color: PdfColors.grey600)),
|
||||
border: pw.Border(bottom: pw.BorderSide(color: PdfColors.grey700)),
|
||||
),
|
||||
padding: const pw.EdgeInsets.only(bottom: 5),
|
||||
child: pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
child: pw.Column(
|
||||
children: [
|
||||
pw.Text('$partnerLabel:${partnerName ?? ''}', style: metaStyle),
|
||||
pw.Text('日期:${orderDate ?? ''}', style: metaStyle),
|
||||
pw.Text('NO: $orderNo', style: boldStyle),
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Expanded(
|
||||
flex: 5,
|
||||
child: metaText(
|
||||
partnerLabel, partnerName ?? '', pw.TextAlign.left)),
|
||||
pw.Expanded(
|
||||
flex: 4,
|
||||
child: metaText(
|
||||
dateLabel, _d10(orderDate), pw.TextAlign.left)),
|
||||
pw.Expanded(
|
||||
flex: 3,
|
||||
child: pw.Text('$noPrefix$orderNo',
|
||||
style: boldStyle, textAlign: pw.TextAlign.right)),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 3),
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Expanded(
|
||||
flex: 5,
|
||||
child: metaText('联系电话', '', pw.TextAlign.left)),
|
||||
pw.Expanded(
|
||||
flex: 4,
|
||||
child: metaText(addressLabel, '', pw.TextAlign.left)),
|
||||
pw.Expanded(
|
||||
flex: 3,
|
||||
child: metaText(
|
||||
'仓库', warehouseName ?? '', pw.TextAlign.right)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 4),
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Text('仓库:${warehouseName ?? ''}', style: metaStyle),
|
||||
pw.SizedBox(width: 24),
|
||||
pw.Text('经办人:${operatorName ?? ''}', style: metaStyle),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(height: 8),
|
||||
// ── 明细表 + 单据总计 ────────────────────────────────────────────────
|
||||
pw.Table(
|
||||
border: pw.TableBorder.all(color: PdfColors.grey500),
|
||||
columnWidths: { for (var i = 0; i < colWidths.length; i++) i: colWidths[i] },
|
||||
border: pw.TableBorder.all(color: PdfColors.grey600, width: 0.5),
|
||||
columnWidths: {
|
||||
for (var i = 0; i < _orderColFlex.length; i++)
|
||||
i: pw.FlexColumnWidth(_orderColFlex[i])
|
||||
},
|
||||
children: [
|
||||
pw.TableRow(
|
||||
decoration: const pw.BoxDecoration(color: PdfColors.grey200),
|
||||
children: headers.map((h) => pw.Padding(
|
||||
padding: const pw.EdgeInsets.symmetric(horizontal: 3, vertical: 3),
|
||||
child: pw.Text(h, textAlign: pw.TextAlign.center, style: headerStyle),
|
||||
)).toList(),
|
||||
children: [
|
||||
for (final h in _orderHeaders)
|
||||
pw.Padding(
|
||||
padding:
|
||||
const pw.EdgeInsets.symmetric(horizontal: 2, vertical: 3),
|
||||
child: pw.Text(h,
|
||||
textAlign: pw.TextAlign.center, style: headStyle),
|
||||
),
|
||||
],
|
||||
),
|
||||
...rows.map((row) => pw.TableRow(
|
||||
children: row.map((cell) => pw.Padding(
|
||||
padding: const pw.EdgeInsets.symmetric(horizontal: 3, vertical: 2),
|
||||
child: pw.Text(cell, style: cellStyle),
|
||||
)).toList(),
|
||||
)),
|
||||
pw.TableRow(
|
||||
decoration: const pw.BoxDecoration(color: PdfColors.grey100),
|
||||
children: List.generate(headers.length, (i) {
|
||||
if (i == headers.length - 3) {
|
||||
return pw.Padding(
|
||||
padding: const pw.EdgeInsets.symmetric(horizontal: 3, vertical: 2),
|
||||
child: pw.Text('单据总计', textAlign: pw.TextAlign.right, style: boldStyle),
|
||||
);
|
||||
}
|
||||
if (i == headers.length - 2) {
|
||||
return pw.Padding(
|
||||
padding: const pw.EdgeInsets.symmetric(horizontal: 3, vertical: 2),
|
||||
child: pw.Text(
|
||||
totalQty % 1 == 0 ? totalQty.toStringAsFixed(0) : totalQty.toStringAsFixed(3),
|
||||
textAlign: pw.TextAlign.right, style: boldStyle,
|
||||
for (final row in rows)
|
||||
pw.TableRow(
|
||||
children: [
|
||||
for (var i = 0; i < row.length; i++)
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.symmetric(
|
||||
horizontal: 2, vertical: 2.5),
|
||||
child: pw.Text(row[i],
|
||||
textAlign: _colAlign(i), style: cellStyle),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (i == headers.length - 1) {
|
||||
return pw.Padding(
|
||||
padding: const pw.EdgeInsets.symmetric(horizontal: 3, vertical: 2),
|
||||
child: pw.Text(totalAmt.toStringAsFixed(2),
|
||||
textAlign: pw.TextAlign.right, style: boldStyle),
|
||||
);
|
||||
}
|
||||
return pw.SizedBox();
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if ((remark ?? '').isNotEmpty) ...[
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Text('备注:$remark', style: metaStyle),
|
||||
],
|
||||
if (tipsText != null) ...[
|
||||
pw.SizedBox(height: 6),
|
||||
// ── 单据总计:独立一行,仅数量 + 金额,右对齐高亮 ──────────────────
|
||||
pw.SizedBox(height: 6),
|
||||
pw.Container(
|
||||
width: double.infinity,
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border.all(color: PdfColors.grey600, width: 0.5),
|
||||
color: PdfColors.grey100,
|
||||
),
|
||||
padding: const pw.EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
child: pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.end,
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||
children: [
|
||||
pw.Text('单据总计',
|
||||
style: pw.TextStyle(
|
||||
font: bold, fontSize: 10.5, fontWeight: pw.FontWeight.bold)),
|
||||
pw.SizedBox(width: 30),
|
||||
pw.Text('数量', style: metaStyle),
|
||||
pw.SizedBox(width: 6),
|
||||
pw.Text(_qtyStr(totalQty),
|
||||
style: pw.TextStyle(
|
||||
font: bold, fontSize: 11, fontWeight: pw.FontWeight.bold)),
|
||||
pw.SizedBox(width: 30),
|
||||
pw.Text('金额', style: metaStyle),
|
||||
pw.SizedBox(width: 6),
|
||||
pw.Text(totalAmt.toStringAsFixed(2),
|
||||
style: pw.TextStyle(
|
||||
font: bold, fontSize: 12, fontWeight: pw.FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
// ── 温馨提示(仅出库单)────────────────────────────────────────────
|
||||
if (showTips) ...[
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Container(
|
||||
width: double.infinity,
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border.all(color: PdfColors.grey400),
|
||||
border: pw.Border.all(color: PdfColors.grey400, width: 0.5),
|
||||
color: PdfColors.grey50,
|
||||
),
|
||||
padding: const pw.EdgeInsets.all(5),
|
||||
child: pw.Text(tipsText,
|
||||
style: pw.TextStyle(font: font, fontSize: 9, color: PdfColors.grey700)),
|
||||
child: pw.Text(_kTipsText,
|
||||
style: pw.TextStyle(
|
||||
font: font, fontSize: 8.5, color: PdfColors.grey800)),
|
||||
),
|
||||
],
|
||||
pw.SizedBox(height: 36),
|
||||
pw.SizedBox(height: 26),
|
||||
// ── 签字行:签收人 / 经办人 / 制单人 ────────────────────────────────
|
||||
pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_sig(font, bold, '收/出货人(签字)'),
|
||||
_sig(font, bold, '经办人:${operatorName ?? ''}'),
|
||||
_sig(font, bold, '制单人:${reviewerName ?? operatorName ?? ''}'),
|
||||
pw.Text('$signerLabel:', style: metaStyle),
|
||||
pw.Text('$operatorRoleLabel:${operatorName ?? ''}', style: metaStyle),
|
||||
pw.Text('制单人:${makerName ?? ''}', style: metaStyle),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
pw.Widget _sig(pw.Font font, pw.Font bold, String label) {
|
||||
return pw.Column(
|
||||
children: [
|
||||
pw.Container(width: 120, height: 28),
|
||||
pw.Container(
|
||||
width: 120,
|
||||
decoration: const pw.BoxDecoration(
|
||||
border: pw.Border(top: pw.BorderSide(color: PdfColors.grey700)),
|
||||
),
|
||||
padding: const pw.EdgeInsets.only(top: 3),
|
||||
child: pw.Text(label,
|
||||
textAlign: pw.TextAlign.center,
|
||||
style: pw.TextStyle(font: font, fontSize: 10)),
|
||||
),
|
||||
if ((remark ?? '').isNotEmpty) ...[
|
||||
pw.SizedBox(height: 10),
|
||||
pw.Text('备注:$remark', style: metaStyle),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 生成入库单 A4 PDF 字节(与实际打印同一套排版,供预览光栅化 + 打印共用)。
|
||||
Future<Uint8List> buildStockInOrderPdfImpl(StockInOrder order) async {
|
||||
Future<Uint8List> buildStockInOrderPdfImpl(
|
||||
StockInOrder order, OrderPrintMeta meta) async {
|
||||
final font = await _loadFont();
|
||||
final bold = await _loadBoldFont();
|
||||
|
||||
double totalQty = 0, totalAmt = 0;
|
||||
final rows = <List<String>>[];
|
||||
for (int i = 0; i < order.items.length; i++) {
|
||||
final it = order.items[i];
|
||||
for (final it in order.items) {
|
||||
totalQty += it.quantity;
|
||||
totalAmt += it.totalPrice;
|
||||
rows.add([
|
||||
'${i + 1}',
|
||||
it.productCode ?? '',
|
||||
it.productName ?? '',
|
||||
it.productSeries ?? '',
|
||||
it.productSpec ?? '',
|
||||
it.quantity % 1 == 0 ? it.quantity.toStringAsFixed(0) : it.quantity.toStringAsFixed(3),
|
||||
it.productUnit ?? '',
|
||||
it.batchNo ?? '',
|
||||
_d10(it.productionDate),
|
||||
_qtyStr(it.quantity),
|
||||
it.unitPrice.toStringAsFixed(2),
|
||||
it.totalPrice.toStringAsFixed(2),
|
||||
it.productionDate ?? '',
|
||||
it.batchNo ?? '',
|
||||
'',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -832,16 +912,22 @@ Future<Uint8List> buildStockInOrderPdfImpl(StockInOrder order) async {
|
||||
build: (_) => [
|
||||
_buildOrderDoc(
|
||||
font: font, bold: bold,
|
||||
shopName: meta.shopName,
|
||||
title: '入 库 单',
|
||||
orderNo: order.orderNo,
|
||||
orderDate: order.orderDate,
|
||||
partnerLabel: '供应商',
|
||||
noPrefix: 'NO:',
|
||||
partnerLabel: '单位名称',
|
||||
partnerName: order.partnerName,
|
||||
dateLabel: '入库日期',
|
||||
orderDate: order.orderDate,
|
||||
addressLabel: '单位地址',
|
||||
warehouseName: order.warehouseName,
|
||||
signerLabel: '出货人(签字)',
|
||||
operatorRoleLabel: '采购员',
|
||||
operatorName: order.operatorName,
|
||||
reviewerName: order.reviewerName,
|
||||
makerName: meta.makerName,
|
||||
remark: order.remark,
|
||||
headers: ['序号', '商品编号', '商品名称', '系列', '规格', '数量', '单位', '单价', '金额', '生产日期', '批次'],
|
||||
showTips: false,
|
||||
rows: rows,
|
||||
totalQty: totalQty,
|
||||
totalAmt: totalAmt,
|
||||
@@ -851,9 +937,10 @@ Future<Uint8List> buildStockInOrderPdfImpl(StockInOrder order) async {
|
||||
return doc.save();
|
||||
}
|
||||
|
||||
Future<void> printStockInOrderImpl(StockInOrder order) async {
|
||||
Future<void> printStockInOrderImpl(
|
||||
StockInOrder order, OrderPrintMeta meta) async {
|
||||
try {
|
||||
final bytes = await buildStockInOrderPdfImpl(order);
|
||||
final bytes = await buildStockInOrderPdfImpl(order, meta);
|
||||
if (!kIsWeb && Platform.isMacOS) {
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
}
|
||||
@@ -868,26 +955,31 @@ Future<void> printStockInOrderImpl(StockInOrder order) async {
|
||||
}
|
||||
|
||||
/// 生成出库单 A4 PDF 字节(与实际打印同一套排版,供预览光栅化 + 打印共用)。
|
||||
Future<Uint8List> buildStockOutOrderPdfImpl(StockOutOrder order) async {
|
||||
Future<Uint8List> buildStockOutOrderPdfImpl(
|
||||
StockOutOrder order, OrderPrintMeta meta) async {
|
||||
final font = await _loadFont();
|
||||
final bold = await _loadBoldFont();
|
||||
|
||||
double totalQty = 0, totalAmt = 0;
|
||||
final rows = <List<String>>[];
|
||||
for (int i = 0; i < order.items.length; i++) {
|
||||
final it = order.items[i];
|
||||
for (final it in order.items) {
|
||||
// 出库单「单价」= 售价:App 单据存 sale_price;历史导入单 sale_price=0、
|
||||
// 实际售价存在 unit_price/total_price,故 sale_price 为空时回退到 unit_price。
|
||||
final unit = it.salePrice > 0 ? it.salePrice : it.unitPrice;
|
||||
final amt = it.salePrice > 0 ? it.salePrice * it.quantity : it.totalPrice;
|
||||
totalQty += it.quantity;
|
||||
totalAmt += it.totalPrice;
|
||||
totalAmt += amt;
|
||||
rows.add([
|
||||
'${i + 1}',
|
||||
it.productCode ?? '',
|
||||
it.productName ?? '',
|
||||
it.productSeries ?? '',
|
||||
it.productSpec ?? '',
|
||||
it.productUnit ?? '',
|
||||
it.quantity % 1 == 0 ? it.quantity.toStringAsFixed(0) : it.quantity.toStringAsFixed(3),
|
||||
it.unitPrice.toStringAsFixed(2),
|
||||
it.totalPrice.toStringAsFixed(2),
|
||||
it.batchNo ?? '',
|
||||
_d10(it.productionDate),
|
||||
_qtyStr(it.quantity),
|
||||
unit.toStringAsFixed(2),
|
||||
amt.toStringAsFixed(2),
|
||||
'',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -898,29 +990,35 @@ Future<Uint8List> buildStockOutOrderPdfImpl(StockOutOrder order) async {
|
||||
build: (_) => [
|
||||
_buildOrderDoc(
|
||||
font: font, bold: bold,
|
||||
shopName: meta.shopName,
|
||||
title: '出 库 单',
|
||||
orderNo: order.orderNo,
|
||||
orderDate: order.orderDate,
|
||||
partnerLabel: '客户',
|
||||
noPrefix: 'NO.',
|
||||
partnerLabel: '客户名称',
|
||||
partnerName: order.partnerName,
|
||||
dateLabel: '出库日期',
|
||||
orderDate: order.orderDate,
|
||||
addressLabel: '送货地址',
|
||||
warehouseName: order.warehouseName,
|
||||
signerLabel: '收货人(签字)',
|
||||
operatorRoleLabel: '销售员',
|
||||
operatorName: order.operatorName,
|
||||
reviewerName: order.reviewerName,
|
||||
makerName: meta.makerName,
|
||||
remark: order.remark,
|
||||
headers: ['序号', '商品编号', '商品名称', '系列', '规格', '单位', '数量', '单价', '金额'],
|
||||
showTips: true,
|
||||
rows: rows,
|
||||
totalQty: totalQty,
|
||||
totalAmt: totalAmt,
|
||||
tipsText: '温馨提示:签收时,请务必核对好酒品数量、年份和日期、批次及物流码,如有问题及时反馈,酒品无质量问题一经售出概不退换,谢谢合作。',
|
||||
),
|
||||
],
|
||||
));
|
||||
return doc.save();
|
||||
}
|
||||
|
||||
Future<void> printStockOutOrderImpl(StockOutOrder order) async {
|
||||
Future<void> printStockOutOrderImpl(
|
||||
StockOutOrder order, OrderPrintMeta meta) async {
|
||||
try {
|
||||
final bytes = await buildStockOutOrderPdfImpl(order);
|
||||
final bytes = await buildStockOutOrderPdfImpl(order, meta);
|
||||
if (!kIsWeb && Platform.isMacOS) {
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../models/stock_in.dart';
|
||||
import '../../models/stock_out.dart';
|
||||
import '../errors/error_reporter.dart';
|
||||
import 'label_data.dart';
|
||||
import 'order_print_meta.dart';
|
||||
|
||||
void _openPrintWindow(String html) {
|
||||
final win = web.window.open('', '_blank');
|
||||
@@ -172,34 +173,81 @@ body { width: 38mm; height: 20mm; overflow: hidden; background: #fff; }
|
||||
|
||||
// ── 单据 PDF 字节(Web 走 HTML 打印路径,不生成 PDF;预览弹窗仅桌面端调用)──────
|
||||
|
||||
Future<Uint8List> buildStockInOrderPdfImpl(StockInOrder order) async =>
|
||||
Future<Uint8List> buildStockInOrderPdfImpl(
|
||||
StockInOrder order, OrderPrintMeta meta) async =>
|
||||
throw UnsupportedError('Web 端入库单走 HTML 打印,不生成 PDF');
|
||||
|
||||
Future<Uint8List> buildStockOutOrderPdfImpl(StockOutOrder order) async =>
|
||||
Future<Uint8List> buildStockOutOrderPdfImpl(
|
||||
StockOutOrder order, OrderPrintMeta meta) async =>
|
||||
throw UnsupportedError('Web 端出库单走 HTML 打印,不生成 PDF');
|
||||
|
||||
/// 单据打印共用样式(入库/出库一致)。
|
||||
const String _orderPrintCss = '''
|
||||
@page { size: A4; margin: 12mm 15mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: "PingFang SC", "Microsoft YaHei", "SimSun", sans-serif; font-size: 11px; color: #000; }
|
||||
.shop { text-align: center; font-size: 20px; font-weight: bold; }
|
||||
.title { text-align: center; font-size: 16px; font-weight: bold; letter-spacing: 4px; margin: 4px 0 8px; }
|
||||
.head-block { border-bottom: 1px solid #555; padding-bottom: 5px; margin-bottom: 8px; }
|
||||
.meta-row { display: flex; font-size: 11px; }
|
||||
.meta-row + .meta-row { margin-top: 3px; }
|
||||
.meta-row .c1 { flex: 5; } .meta-row .c2 { flex: 4; } .meta-row .c3 { flex: 3; text-align: right; }
|
||||
.meta-row .no { font-weight: bold; font-size: 12px; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { border: 1px solid #777; padding: 4px 4px; font-size: 10.5px; }
|
||||
th { background: #eee; font-weight: 600; text-align: center; }
|
||||
.c { text-align: center; } .r { text-align: right; }
|
||||
.subtotal-bar { display: flex; justify-content: flex-end; align-items: baseline; gap: 30px; border: 1px solid #777; background: #f3f3f3; padding: 7px 10px; margin-top: 6px; font-size: 11px; }
|
||||
.subtotal-bar .lbl-total { font-weight: bold; font-size: 12px; margin-right: auto; }
|
||||
.subtotal-bar b { font-size: 13px; }
|
||||
.tips { border: 1px solid #ddd; background: #fafafa; padding: 6px 8px; margin: 8px 0; font-size: 10px; color: #555; line-height: 1.6; }
|
||||
.sigs { display: flex; justify-content: space-between; margin-top: 26px; font-size: 11px; }
|
||||
.remark { margin-top: 10px; font-size: 11px; color: #333; }
|
||||
''';
|
||||
|
||||
const String _orderTipsHtml =
|
||||
'<div class="tips">温馨提示:签收时,请务必核对好酒品数量、年份和日期、批次及物流码,'
|
||||
'如有问题及时反馈,酒品无质量问题一经售出概不退换,谢谢合作。</div>';
|
||||
|
||||
/// 截取日期前 10 位(yyyy-MM-dd)。
|
||||
String _d10(String? s) =>
|
||||
(s == null || s.isEmpty) ? '' : (s.length > 10 ? s.substring(0, 10) : s);
|
||||
|
||||
/// 单据明细表头(入库/出库一致)。
|
||||
const String _orderTableHead = '''<thead><tr>
|
||||
<th style="width:12%">商品编号</th>
|
||||
<th style="width:16%">商品名称</th>
|
||||
<th style="width:9%">系列</th>
|
||||
<th style="width:10%">规格</th>
|
||||
<th style="width:14%">批次号</th>
|
||||
<th style="width:11%">生产日期</th>
|
||||
<th style="width:5%">数量</th>
|
||||
<th style="width:8%">单价</th>
|
||||
<th style="width:9%">金额</th>
|
||||
<th style="width:6%">备注</th>
|
||||
</tr></thead>''';
|
||||
|
||||
// ── 入库单 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> printStockInOrderImpl(StockInOrder order) async {
|
||||
Future<void> printStockInOrderImpl(
|
||||
StockInOrder order, OrderPrintMeta meta) async {
|
||||
final rows = StringBuffer();
|
||||
double totalQty = 0;
|
||||
double totalAmt = 0;
|
||||
for (int i = 0; i < order.items.length; i++) {
|
||||
final item = order.items[i];
|
||||
for (final item in order.items) {
|
||||
totalQty += item.quantity;
|
||||
totalAmt += item.totalPrice;
|
||||
rows.write('''<tr>
|
||||
<td class="c">${i + 1}</td>
|
||||
<td>${item.productCode ?? ''}</td>
|
||||
<td>${item.productName ?? ''}</td>
|
||||
<td class="c">${item.productSeries ?? ''}</td>
|
||||
<td class="c">${item.productSpec ?? ''}</td>
|
||||
<td class="c">${item.batchNo ?? ''}</td>
|
||||
<td class="c">${_d10(item.productionDate)}</td>
|
||||
<td class="r">${item.quantity % 1 == 0 ? item.quantity.toStringAsFixed(0) : item.quantity.toStringAsFixed(3)}</td>
|
||||
<td class="c">${item.productUnit ?? ''}</td>
|
||||
<td class="r">${item.unitPrice.toStringAsFixed(2)}</td>
|
||||
<td class="r">${item.totalPrice.toStringAsFixed(2)}</td>
|
||||
<td class="c">${item.productionDate ?? ''}</td>
|
||||
<td class="c">${item.batchNo ?? ''}</td>
|
||||
<td></td>
|
||||
</tr>''');
|
||||
}
|
||||
|
||||
@@ -211,72 +259,38 @@ Future<void> printStockInOrderImpl(StockInOrder order) async {
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
@page { size: A4; margin: 12mm 15mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: "PingFang SC", "Microsoft YaHei", "SimSun", sans-serif; font-size: 11px; color: #000; }
|
||||
.hdr { text-align: center; margin-bottom: 10px; }
|
||||
.hdr .title { font-size: 22px; font-weight: bold; margin-bottom: 4px; }
|
||||
.meta { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 6px; border-bottom: 1px solid #666; padding-bottom: 5px; }
|
||||
.meta .left, .meta .center, .meta .right { flex: 1; }
|
||||
.meta .center { text-align: center; }
|
||||
.meta .right { text-align: right; font-weight: bold; font-size: 12px; }
|
||||
.meta2 { display: flex; gap: 24px; margin-bottom: 8px; font-size: 11px; }
|
||||
table { width: 100%; border-collapse: collapse; margin-bottom: 4px; }
|
||||
th, td { border: 1px solid #999; padding: 4px 5px; }
|
||||
th { background: #f0f0f0; font-weight: 600; text-align: center; font-size: 11px; }
|
||||
td { font-size: 10.5px; }
|
||||
.c { text-align: center; }
|
||||
.r { text-align: right; }
|
||||
.subtotal { font-weight: bold; background: #f7f7f7; }
|
||||
.remark { margin: 6px 0; font-size: 11px; color: #333; }
|
||||
.sigs { display: flex; justify-content: space-between; margin-top: 28px; }
|
||||
.sig { text-align: center; min-width: 120px; }
|
||||
.sig-line { border-top: 1px solid #555; padding-top: 4px; margin-top: 28px; font-size: 11px; }
|
||||
.footer-note { text-align: right; font-size: 9px; color: #aaa; margin-top: 6px; }
|
||||
</style>
|
||||
<style>$_orderPrintCss</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="hdr"><div class="title">入 库 单</div></div>
|
||||
<div class="meta">
|
||||
<div class="left">供应商:${order.partnerName ?? ''}</div>
|
||||
<div class="center">入库日期:${order.orderDate ?? ''}</div>
|
||||
<div class="right">NO: ${order.orderNo}</div>
|
||||
</div>
|
||||
<div class="meta2">
|
||||
<span>仓库:${order.warehouseName ?? ''}</span>
|
||||
<span>采购人:${order.operatorName ?? ''}</span>
|
||||
<div class="shop">${meta.shopName}</div>
|
||||
<div class="title">入 库 单</div>
|
||||
<div class="head-block">
|
||||
<div class="meta-row">
|
||||
<div class="c1">单位名称:${order.partnerName ?? ''}</div>
|
||||
<div class="c2">入库日期:${_d10(order.orderDate)}</div>
|
||||
<div class="c3 no">NO:${order.orderNo}</div>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<div class="c1">联系电话:</div>
|
||||
<div class="c2">单位地址:</div>
|
||||
<div class="c3">仓库:${order.warehouseName ?? ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th style="width:3%">序号</th>
|
||||
<th style="width:9%">商品编号</th>
|
||||
<th style="width:18%">商品名称</th>
|
||||
<th style="width:10%">系列</th>
|
||||
<th style="width:9%">规格</th>
|
||||
<th style="width:5%">数量</th>
|
||||
<th style="width:5%">单位</th>
|
||||
<th style="width:8%">单价</th>
|
||||
<th style="width:8%">金额</th>
|
||||
<th style="width:10%">生产日期</th>
|
||||
<th style="width:10%">批次号</th>
|
||||
</tr></thead>
|
||||
$_orderTableHead
|
||||
<tbody>$rows</tbody>
|
||||
<tfoot><tr class="subtotal">
|
||||
<td colspan="5" class="r">单据总计</td>
|
||||
<td class="r">${totalQty % 1 == 0 ? totalQty.toStringAsFixed(0) : totalQty.toStringAsFixed(3)}</td>
|
||||
<td></td><td></td>
|
||||
<td class="r">${totalAmt.toStringAsFixed(2)}</td>
|
||||
<td colspan="2"></td>
|
||||
</tr></tfoot>
|
||||
</table>
|
||||
$remarkLine
|
||||
<div class="sigs">
|
||||
<div class="sig"><div class="sig-line">出货人(签字)</div></div>
|
||||
<div class="sig"><div class="sig-line">采购员:${order.operatorName ?? ''}</div></div>
|
||||
<div class="sig"><div class="sig-line">制单人:${order.reviewerName ?? order.operatorName ?? ''}</div></div>
|
||||
<div class="subtotal-bar">
|
||||
<span class="lbl-total">单据总计</span>
|
||||
<span>数量 <b>${totalQty % 1 == 0 ? totalQty.toStringAsFixed(0) : totalQty.toStringAsFixed(3)}</b></span>
|
||||
<span>金额 <b>${totalAmt.toStringAsFixed(2)}</b></span>
|
||||
</div>
|
||||
<div class="footer-note">酒库管理系统</div>
|
||||
<div class="sigs">
|
||||
<span>出货人(签字):</span>
|
||||
<span>采购员:${order.operatorName ?? ''}</span>
|
||||
<span>制单人:${meta.makerName}</span>
|
||||
</div>
|
||||
$remarkLine
|
||||
<script>window.onload = function(){ window.print(); };</script>
|
||||
</body>
|
||||
</html>''';
|
||||
@@ -299,24 +313,30 @@ Future<String?> detectDefaultPrinterImpl() async => null;
|
||||
|
||||
// ── 出库单 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> printStockOutOrderImpl(StockOutOrder order) async {
|
||||
Future<void> printStockOutOrderImpl(
|
||||
StockOutOrder order, OrderPrintMeta meta) async {
|
||||
final rows = StringBuffer();
|
||||
double totalQty = 0;
|
||||
double totalAmt = 0;
|
||||
for (int i = 0; i < order.items.length; i++) {
|
||||
final item = order.items[i];
|
||||
for (final item in order.items) {
|
||||
// 出库单「单价」= 售价:App 单据存 sale_price;历史导入单 sale_price=0、
|
||||
// 实际售价存在 unit_price/total_price,故 sale_price 为空时回退到 unit_price。
|
||||
final unit = item.salePrice > 0 ? item.salePrice : item.unitPrice;
|
||||
final amt =
|
||||
item.salePrice > 0 ? item.salePrice * item.quantity : item.totalPrice;
|
||||
totalQty += item.quantity;
|
||||
totalAmt += item.totalPrice;
|
||||
totalAmt += amt;
|
||||
rows.write('''<tr>
|
||||
<td class="c">${i + 1}</td>
|
||||
<td>${item.productCode ?? ''}</td>
|
||||
<td>${item.productName ?? ''}</td>
|
||||
<td class="c">${item.productSeries ?? ''}</td>
|
||||
<td class="c">${item.productSpec ?? ''}</td>
|
||||
<td class="c">${item.productUnit ?? ''}</td>
|
||||
<td class="c">${item.batchNo ?? ''}</td>
|
||||
<td class="c">${_d10(item.productionDate)}</td>
|
||||
<td class="r">${item.quantity % 1 == 0 ? item.quantity.toStringAsFixed(0) : item.quantity.toStringAsFixed(3)}</td>
|
||||
<td class="r">${item.unitPrice.toStringAsFixed(2)}</td>
|
||||
<td class="r">${item.totalPrice.toStringAsFixed(2)}</td>
|
||||
<td class="r">${unit.toStringAsFixed(2)}</td>
|
||||
<td class="r">${amt.toStringAsFixed(2)}</td>
|
||||
<td></td>
|
||||
</tr>''');
|
||||
}
|
||||
|
||||
@@ -328,71 +348,39 @@ Future<void> printStockOutOrderImpl(StockOutOrder order) async {
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
@page { size: A4; margin: 12mm 15mm; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: "PingFang SC", "Microsoft YaHei", "SimSun", sans-serif; font-size: 11px; color: #000; }
|
||||
.hdr { text-align: center; margin-bottom: 10px; }
|
||||
.hdr .title { font-size: 22px; font-weight: bold; margin-bottom: 4px; }
|
||||
.meta { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 6px; border-bottom: 1px solid #666; padding-bottom: 5px; }
|
||||
.meta .left, .meta .center, .meta .right { flex: 1; }
|
||||
.meta .center { text-align: center; }
|
||||
.meta .right { text-align: right; font-weight: bold; font-size: 12px; }
|
||||
.meta2 { display: flex; gap: 24px; margin-bottom: 8px; font-size: 11px; }
|
||||
table { width: 100%; border-collapse: collapse; margin-bottom: 4px; }
|
||||
th, td { border: 1px solid #999; padding: 4px 5px; }
|
||||
th { background: #f0f0f0; font-weight: 600; text-align: center; font-size: 11px; }
|
||||
td { font-size: 10.5px; }
|
||||
.c { text-align: center; }
|
||||
.r { text-align: right; }
|
||||
.subtotal { font-weight: bold; background: #f7f7f7; }
|
||||
.remark { margin: 6px 0; font-size: 11px; color: #333; }
|
||||
.tips { border: 1px solid #ddd; background: #fafafa; padding: 6px 8px; margin: 8px 0; font-size: 10px; color: #555; line-height: 1.6; }
|
||||
.sigs { display: flex; justify-content: space-between; margin-top: 28px; }
|
||||
.sig { text-align: center; min-width: 120px; }
|
||||
.sig-line { border-top: 1px solid #555; padding-top: 4px; margin-top: 28px; font-size: 11px; }
|
||||
.footer-note { text-align: right; font-size: 9px; color: #aaa; margin-top: 6px; }
|
||||
</style>
|
||||
<style>$_orderPrintCss</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="hdr"><div class="title">出 库 单</div></div>
|
||||
<div class="meta">
|
||||
<div class="left">客户:${order.partnerName ?? ''}</div>
|
||||
<div class="center">出库日期:${order.orderDate ?? ''}</div>
|
||||
<div class="right">NO.${order.orderNo}</div>
|
||||
</div>
|
||||
<div class="meta2">
|
||||
<span>仓库:${order.warehouseName ?? ''}</span>
|
||||
<span>经办人:${order.operatorName ?? ''}</span>
|
||||
<div class="shop">${meta.shopName}</div>
|
||||
<div class="title">出 库 单</div>
|
||||
<div class="head-block">
|
||||
<div class="meta-row">
|
||||
<div class="c1">客户名称:${order.partnerName ?? ''}</div>
|
||||
<div class="c2">出库日期:${_d10(order.orderDate)}</div>
|
||||
<div class="c3 no">NO.${order.orderNo}</div>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<div class="c1">联系电话:</div>
|
||||
<div class="c2">送货地址:</div>
|
||||
<div class="c3">仓库:${order.warehouseName ?? ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th style="width:3%">序号</th>
|
||||
<th style="width:9%">商品编号</th>
|
||||
<th style="width:20%">商品名称</th>
|
||||
<th style="width:11%">系列</th>
|
||||
<th style="width:10%">规格</th>
|
||||
<th style="width:5%">单位</th>
|
||||
<th style="width:5%">数量</th>
|
||||
<th style="width:9%">单价</th>
|
||||
<th style="width:9%">金额</th>
|
||||
</tr></thead>
|
||||
$_orderTableHead
|
||||
<tbody>$rows</tbody>
|
||||
<tfoot><tr class="subtotal">
|
||||
<td colspan="6" class="r">单据总计</td>
|
||||
<td class="r">${totalQty % 1 == 0 ? totalQty.toStringAsFixed(0) : totalQty.toStringAsFixed(3)}</td>
|
||||
<td></td>
|
||||
<td class="r">${totalAmt.toStringAsFixed(2)}</td>
|
||||
</tr></tfoot>
|
||||
</table>
|
||||
$remarkLine
|
||||
<div class="tips">温馨提示:签收时,请务必核对好酒品数量、年份和日期、批次及物流码,如有问题及时反馈,酒品无质量问题一经售出概不退换,谢谢合作。</div>
|
||||
<div class="sigs">
|
||||
<div class="sig"><div class="sig-line">收货人(签字)</div></div>
|
||||
<div class="sig"><div class="sig-line">销售员:${order.operatorName ?? ''}</div></div>
|
||||
<div class="sig"><div class="sig-line">制单人:${order.reviewerName ?? order.operatorName ?? ''}</div></div>
|
||||
<div class="subtotal-bar">
|
||||
<span class="lbl-total">单据总计</span>
|
||||
<span>数量 <b>${totalQty % 1 == 0 ? totalQty.toStringAsFixed(0) : totalQty.toStringAsFixed(3)}</b></span>
|
||||
<span>金额 <b>${totalAmt.toStringAsFixed(2)}</b></span>
|
||||
</div>
|
||||
<div class="footer-note">酒库管理系统</div>
|
||||
$_orderTipsHtml
|
||||
<div class="sigs">
|
||||
<span>收货人(签字):</span>
|
||||
<span>销售员:${order.operatorName ?? ''}</span>
|
||||
<span>制单人:${meta.makerName}</span>
|
||||
</div>
|
||||
$remarkLine
|
||||
<script>window.onload = function(){ window.print(); };</script>
|
||||
</body>
|
||||
</html>''';
|
||||
|
||||
@@ -227,7 +227,7 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
||||
|
||||
Future<void> _printOrder() async {
|
||||
if (_loadedOrder == null) return;
|
||||
await showStockInOrderPrint(context, _loadedOrder!);
|
||||
await showStockInOrderPrint(context, ref, _loadedOrder!);
|
||||
}
|
||||
|
||||
Future<void> _submit(bool asDraft) async {
|
||||
|
||||
@@ -530,7 +530,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
onPrint: () async {
|
||||
final order = await ref.read(stockInRepositoryProvider).get(o.id);
|
||||
if (context.mounted) {
|
||||
await showStockInOrderPrint(context, order);
|
||||
await showStockInOrderPrint(context, ref, order);
|
||||
}
|
||||
},
|
||||
afterPrint: [
|
||||
@@ -1075,7 +1075,7 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.print_outlined, color: AppTheme.primaryDark),
|
||||
tooltip: '打印',
|
||||
onPressed: () => showStockInOrderPrint(context, _loadedOrder!),
|
||||
onPressed: () => showStockInOrderPrint(context, ref, _loadedOrder!),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -258,7 +258,7 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
|
||||
Future<void> _printOrder() async {
|
||||
if (_loadedOrder == null) return;
|
||||
await showStockOutOrderPrint(context, _loadedOrder!);
|
||||
await showStockOutOrderPrint(context, ref, _loadedOrder!);
|
||||
}
|
||||
|
||||
Future<void> _submit(bool asDraft) async {
|
||||
|
||||
@@ -558,7 +558,7 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
onPrint: () async {
|
||||
final order = await ref.read(stockOutRepositoryProvider).get(o.id);
|
||||
if (context.mounted) {
|
||||
await showStockOutOrderPrint(context, order);
|
||||
await showStockOutOrderPrint(context, ref, order);
|
||||
}
|
||||
},
|
||||
onSettle: () => _confirmSettle(context, o.id, 'stock_out'),
|
||||
@@ -986,7 +986,7 @@ class _StockOutDetailDialogState extends ConsumerState<_StockOutDetailDialog> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.print_outlined, color: AppTheme.primaryDark),
|
||||
tooltip: '打印',
|
||||
onPressed: () => showStockOutOrderPrint(context, _loadedOrder!),
|
||||
onPressed: () => showStockOutOrderPrint(context, ref, _loadedOrder!),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:printing/printing.dart';
|
||||
import '../core/auth/auth_state.dart';
|
||||
import '../core/errors/error_reporter.dart';
|
||||
import '../core/theme/app_theme.dart';
|
||||
import '../core/responsive/responsive.dart';
|
||||
import '../core/utils/print_util.dart';
|
||||
import '../models/shop.dart';
|
||||
import '../models/stock_in.dart';
|
||||
import '../models/stock_out.dart';
|
||||
import '../providers/shop_provider.dart';
|
||||
|
||||
/// 单据打印预览弹窗:把待打印的 A4 PDF 光栅化成图片(所见即所得)逐页展示,
|
||||
/// 页内点「打印」再调系统打印。解决部分 Windows 系统打印对话框无预览的问题。
|
||||
@@ -221,11 +225,29 @@ class _OrderPrintPreviewDialogState extends State<OrderPrintPreviewDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 组装单据打印抬头信息:公司名称(门店)+ 制单人(当前登录、点击打印的人)。
|
||||
Future<OrderPrintMeta> _buildPrintMeta(WidgetRef ref) async {
|
||||
ShopInfo? shop;
|
||||
try {
|
||||
shop = await ref.read(shopInfoProvider.future);
|
||||
} catch (_) {
|
||||
// 门店信息拉取失败(离线等)时抬头留空,不阻塞打印。
|
||||
}
|
||||
return OrderPrintMeta(
|
||||
shopName: shop?.name ?? '',
|
||||
shopAddress: shop?.address ?? '',
|
||||
shopPhone: shop?.phone ?? '',
|
||||
makerName: ref.read(authStateProvider).user?.realName ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/// 打印入库单:桌面端先弹应用内预览,Web 端走浏览器自带打印预览。
|
||||
Future<void> showStockInOrderPrint(
|
||||
BuildContext context, StockInOrder order) async {
|
||||
BuildContext context, WidgetRef ref, StockInOrder order) async {
|
||||
final meta = await _buildPrintMeta(ref);
|
||||
if (!context.mounted) return;
|
||||
if (kIsWeb) {
|
||||
await safePrint(context, () => printStockInOrder(order));
|
||||
await safePrint(context, () => printStockInOrder(order, meta));
|
||||
return;
|
||||
}
|
||||
await showDialog<void>(
|
||||
@@ -234,16 +256,18 @@ Future<void> showStockInOrderPrint(
|
||||
builder: (_) => OrderPrintPreviewDialog(
|
||||
title: '入库单打印预览',
|
||||
printName: '入库单',
|
||||
pdfBuilder: () => buildStockInOrderPdf(order),
|
||||
pdfBuilder: () => buildStockInOrderPdf(order, meta),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 打印出库单:桌面端先弹应用内预览,Web 端走浏览器自带打印预览。
|
||||
Future<void> showStockOutOrderPrint(
|
||||
BuildContext context, StockOutOrder order) async {
|
||||
BuildContext context, WidgetRef ref, StockOutOrder order) async {
|
||||
final meta = await _buildPrintMeta(ref);
|
||||
if (!context.mounted) return;
|
||||
if (kIsWeb) {
|
||||
await safePrint(context, () => printStockOutOrder(order));
|
||||
await safePrint(context, () => printStockOutOrder(order, meta));
|
||||
return;
|
||||
}
|
||||
await showDialog<void>(
|
||||
@@ -252,7 +276,7 @@ Future<void> showStockOutOrderPrint(
|
||||
builder: (_) => OrderPrintPreviewDialog(
|
||||
title: '出库单打印预览',
|
||||
printName: '出库单',
|
||||
pdfBuilder: () => buildStockOutOrderPdf(order),
|
||||
pdfBuilder: () => buildStockOutOrderPdf(order, meta),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user