feat: 财务结清、酒行信息、库存备注编辑、标签溯源、入库必填校验
后端 - 新增 shop handler:GET/PUT /shop/info(管理员权限) - 新增 finance CloseByRef:按单据 ref_type+ref_id 结清账款 - 新增 inventory UpdateRemark:PUT /inventory/:id/remark - 入库/出库审批自动生成财务应付/应收记录(去除金额>0限制) - 种子数据 S001-S003 补充真实门店信息 前端 - 设置页新增「酒行信息」Tab,管理员可编辑门店名称/地址/电话/负责人 - 入库单列表新增结清按钮(含确认弹窗),出库单同步 - 入库表单:规格、系列、生产日期、供应商、商品名称改为提交必填 - 入库/出库列表新增入库时间、出库时间、创建时间列 - 商品标签标题改为读取 shop 表门店名,扫码文案改为「扫码溯源 · TRACE」 - 标签页脚显示门店地址和电话(从 API 读取,不再依赖编译时 dart-define) - 库存备注支持点击编辑,超4字截断显示+Hover展示全文 - ApiClient 新增 patch() 方法(已改用 PUT 规避 CORS) 文档 - 新增 docs/user-manual.md 完整用户操作手册(12章) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// showDialog 的统一封装,自动在内容外层加 SelectionArea,
|
||||
/// 使对话框内的文字可以被选中/复制。
|
||||
Future<T?> showAppDialog<T>({
|
||||
required BuildContext context,
|
||||
required WidgetBuilder builder,
|
||||
bool barrierDismissible = true,
|
||||
Color? barrierColor,
|
||||
bool useRootNavigator = true,
|
||||
RouteSettings? routeSettings,
|
||||
}) {
|
||||
return showDialog<T>(
|
||||
context: context,
|
||||
barrierDismissible: barrierDismissible,
|
||||
barrierColor: barrierColor,
|
||||
useRootNavigator: useRootNavigator,
|
||||
routeSettings: routeSettings,
|
||||
builder: (ctx) => SelectionArea(child: builder(ctx)),
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,10 @@ Future<void> printProductLabel({
|
||||
String? series,
|
||||
String? batchNo,
|
||||
String? productionDate,
|
||||
String? remark,
|
||||
String shopName = '',
|
||||
String shopAddress = '',
|
||||
String shopPhone = '',
|
||||
}) =>
|
||||
printProductLabelImpl(
|
||||
qrBytes: qrBytes,
|
||||
@@ -21,6 +25,10 @@ Future<void> printProductLabel({
|
||||
series: series,
|
||||
batchNo: batchNo,
|
||||
productionDate: productionDate,
|
||||
remark: remark,
|
||||
shopName: shopName,
|
||||
shopAddress: shopAddress,
|
||||
shopPhone: shopPhone,
|
||||
);
|
||||
|
||||
Future<void> printStockInOrder(StockInOrder order) =>
|
||||
|
||||
@@ -5,6 +5,41 @@ import 'package:printing/printing.dart';
|
||||
import '../../models/stock_in.dart';
|
||||
import '../../models/stock_out.dart';
|
||||
|
||||
Future<pw.Font> _loadFont() async {
|
||||
final data = await rootBundle.load('assets/fonts/NotoSansSC-Regular.ttf');
|
||||
return pw.Font.ttf(data);
|
||||
}
|
||||
|
||||
Future<pw.Font> _loadBoldFont() async {
|
||||
// Fall back to regular if no bold variant
|
||||
final data = await rootBundle.load('assets/fonts/NotoSansSC-Regular.ttf');
|
||||
return pw.Font.ttf(data);
|
||||
}
|
||||
|
||||
// ── 设计色彩 token ──────────────────────────────────────────────────────────
|
||||
const _navy = PdfColor(0.122, 0.165, 0.227); // #1F2A3A header/chip
|
||||
const _cream = PdfColor(0.957, 0.925, 0.847); // #F4ECD8 footer/reversed text
|
||||
const _muted = PdfColor(0.533, 0.533, 0.533); // #888 field labels
|
||||
const _footnote = PdfColor(0.353, 0.306, 0.208); // #5A4E35 footer text
|
||||
const _ink = PdfColor(0.067, 0.067, 0.067); // #111 body text
|
||||
|
||||
pw.Widget _labelSpecRow(pw.Font font, String label, String value) =>
|
||||
pw.Row(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.SizedBox(
|
||||
width: 30,
|
||||
child: pw.Text(label,
|
||||
style: pw.TextStyle(font: font, fontSize: 5.5, color: _muted)),
|
||||
),
|
||||
pw.SizedBox(width: 4),
|
||||
pw.Expanded(
|
||||
child: pw.Text(value,
|
||||
style: pw.TextStyle(font: font, fontSize: 5.5, color: _ink)),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Future<void> printProductLabelImpl({
|
||||
required Uint8List qrBytes,
|
||||
required String name,
|
||||
@@ -13,58 +48,431 @@ Future<void> printProductLabelImpl({
|
||||
String? series,
|
||||
String? batchNo,
|
||||
String? productionDate,
|
||||
String? remark,
|
||||
String shopName = '',
|
||||
String shopAddress = '',
|
||||
String shopPhone = '',
|
||||
}) async {
|
||||
final fontData = await rootBundle.load('assets/fonts/NotoSansSC-Regular.ttf');
|
||||
final font = pw.Font.ttf(fontData);
|
||||
final font = await _loadFont();
|
||||
final doc = pw.Document();
|
||||
final qrImage = pw.MemoryImage(qrBytes);
|
||||
|
||||
// Label: 4 × 2 inch
|
||||
const labelW = 4.0 * PdfPageFormat.inch;
|
||||
const labelH = 2.0 * PdfPageFormat.inch;
|
||||
const headerH = labelH * 0.215;
|
||||
const footerH = labelH * 0.09;
|
||||
|
||||
final specVal = (spec ?? '').isNotEmpty ? spec! : '—';
|
||||
final seriesVal = (series ?? '').isNotEmpty ? series! : '—';
|
||||
final batchVal = (batchNo ?? '').isNotEmpty ? batchNo! : '—';
|
||||
final dateVal = (productionDate ?? '').isNotEmpty
|
||||
? (productionDate!.length > 10 ? productionDate.substring(0, 10) : productionDate)
|
||||
: '—';
|
||||
|
||||
final contact = [
|
||||
if (shopAddress.isNotEmpty) shopAddress,
|
||||
if (shopPhone.isNotEmpty) shopPhone,
|
||||
].join(' · ');
|
||||
final footerLeft = contact.isNotEmpty ? contact : shopName;
|
||||
|
||||
// 标签生成时间
|
||||
final now = DateTime.now();
|
||||
final genTime =
|
||||
'${now.year}-${now.month.toString().padLeft(2,'0')}-${now.day.toString().padLeft(2,'0')}'
|
||||
' ${now.hour.toString().padLeft(2,'0')}:${now.minute.toString().padLeft(2,'0')}';
|
||||
|
||||
doc.addPage(pw.Page(
|
||||
pageFormat: const PdfPageFormat(4 * PdfPageFormat.inch, 2 * PdfPageFormat.inch),
|
||||
margin: const pw.EdgeInsets.all(10),
|
||||
build: (_) => pw.Row(
|
||||
pageFormat: const PdfPageFormat(labelW, labelH),
|
||||
margin: pw.EdgeInsets.zero,
|
||||
build: (_) => pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: pw.Column(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.center,
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
|
||||
// ── Header:酒行名称 ─────────────────────────────────────────────────
|
||||
pw.Container(
|
||||
height: headerH,
|
||||
color: _navy,
|
||||
padding: const pw.EdgeInsets.symmetric(horizontal: 11),
|
||||
child: pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||
children: [
|
||||
pw.Text(name,
|
||||
pw.Flexible(
|
||||
child: pw.Text(shopName,
|
||||
style: pw.TextStyle(
|
||||
font: font, fontSize: 13,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
color: _cream, letterSpacing: 1.5,
|
||||
)),
|
||||
),
|
||||
pw.Text('Certificate of Authenticity',
|
||||
style: pw.TextStyle(
|
||||
font: font, fontSize: 14, fontWeight: pw.FontWeight.bold)),
|
||||
pw.SizedBox(height: 3),
|
||||
pw.Text('编号:$code',
|
||||
style: pw.TextStyle(font: font, fontSize: 10)),
|
||||
if ((series ?? '').isNotEmpty)
|
||||
pw.Text('系列:$series',
|
||||
style: pw.TextStyle(font: font, fontSize: 10)),
|
||||
if ((spec ?? '').isNotEmpty)
|
||||
pw.Text('规格:$spec',
|
||||
style: pw.TextStyle(font: font, fontSize: 10)),
|
||||
if ((batchNo ?? '').isNotEmpty)
|
||||
pw.Text('批次:$batchNo',
|
||||
style: pw.TextStyle(font: font, fontSize: 10)),
|
||||
if ((productionDate ?? '').isNotEmpty)
|
||||
pw.Text('生产日期:$productionDate',
|
||||
style: pw.TextStyle(font: font, fontSize: 10)),
|
||||
font: font, fontSize: 5,
|
||||
color: const PdfColor(0.82, 0.79, 0.72))),
|
||||
],
|
||||
),
|
||||
),
|
||||
pw.SizedBox(width: 8),
|
||||
pw.SizedBox(
|
||||
width: 1.2 * PdfPageFormat.inch,
|
||||
height: 1.2 * PdfPageFormat.inch,
|
||||
child: pw.Image(qrImage),
|
||||
|
||||
// ── Body ─────────────────────────────────────────────────────────────
|
||||
pw.Expanded(
|
||||
child: pw.Container(
|
||||
color: PdfColors.white,
|
||||
padding: const pw.EdgeInsets.fromLTRB(11, 8, 11, 6),
|
||||
child: pw.Row(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: pw.Column(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.center,
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 商品名
|
||||
pw.Text(name,
|
||||
style: pw.TextStyle(
|
||||
font: font, fontSize: 12,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
color: _ink, letterSpacing: 0.5)),
|
||||
pw.SizedBox(height: 6),
|
||||
// 规格 + 系列
|
||||
_label2ColRow(font, '规 格', specVal, '系 列', seriesVal),
|
||||
pw.SizedBox(height: 3),
|
||||
// 批号 + 生产日期
|
||||
_label2ColRow(font, '批 号', batchVal, '生产日期', dateVal),
|
||||
if ((remark ?? '').isNotEmpty) ...[
|
||||
pw.SizedBox(height: 3),
|
||||
_labelSpecRow(font, '备 注', remark!),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
pw.SizedBox(width: 8),
|
||||
// QR
|
||||
pw.Column(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.center,
|
||||
children: [
|
||||
pw.SizedBox(
|
||||
width: 60, height: 60,
|
||||
child: pw.Image(qrImage, fit: pw.BoxFit.contain),
|
||||
),
|
||||
pw.SizedBox(height: 3),
|
||||
pw.Text('扫码溯源',
|
||||
style: pw.TextStyle(
|
||||
font: font, fontSize: 4.5,
|
||||
color: PdfColors.grey600)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── Footer ───────────────────────────────────────────────────────────
|
||||
pw.Container(
|
||||
height: footerH,
|
||||
color: _cream,
|
||||
padding: const pw.EdgeInsets.symmetric(horizontal: 11),
|
||||
child: pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||
children: [
|
||||
pw.Flexible(
|
||||
child: pw.Text(footerLeft,
|
||||
style: pw.TextStyle(font: font, fontSize: 4, color: _footnote)),
|
||||
),
|
||||
pw.Text(genTime,
|
||||
style: pw.TextStyle(font: font, fontSize: 4, color: _footnote)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
|
||||
await Printing.layoutPdf(onLayout: (_) async => doc.save());
|
||||
}
|
||||
|
||||
pw.Widget _label2ColRow(
|
||||
pw.Font font, String lbl1, String val1, String lbl2, String val2) {
|
||||
return pw.Row(
|
||||
children: [
|
||||
pw.SizedBox(
|
||||
width: 28,
|
||||
child: pw.Text(lbl1,
|
||||
style: pw.TextStyle(font: font, fontSize: 5.5, color: _muted)),
|
||||
),
|
||||
pw.SizedBox(width: 3),
|
||||
pw.Expanded(
|
||||
child: pw.Text(val1,
|
||||
style: pw.TextStyle(font: font, fontSize: 5.5, color: _ink)),
|
||||
),
|
||||
pw.SizedBox(width: 6),
|
||||
pw.SizedBox(
|
||||
width: 28,
|
||||
child: pw.Text(lbl2,
|
||||
style: pw.TextStyle(font: font, fontSize: 5.5, color: _muted)),
|
||||
),
|
||||
pw.SizedBox(width: 3),
|
||||
pw.Expanded(
|
||||
child: pw.Text(val2,
|
||||
style: pw.TextStyle(font: font, fontSize: 5.5, color: _ink)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
pw.Widget _buildOrderDoc({
|
||||
required pw.Font font,
|
||||
required pw.Font bold,
|
||||
required String title,
|
||||
required String orderNo,
|
||||
required String? orderDate,
|
||||
required String? partnerLabel,
|
||||
required String? partnerName,
|
||||
required String? warehouseName,
|
||||
required String? operatorName,
|
||||
required String? reviewerName,
|
||||
required String? remark,
|
||||
required List<String> headers,
|
||||
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 metaStyle = pw.TextStyle(font: font, fontSize: 10);
|
||||
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();
|
||||
|
||||
return pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Center(
|
||||
child: pw.Text(title,
|
||||
style: pw.TextStyle(font: bold, fontSize: 20, fontWeight: pw.FontWeight.bold)),
|
||||
),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Container(
|
||||
decoration: const pw.BoxDecoration(
|
||||
border: pw.Border(bottom: pw.BorderSide(color: PdfColors.grey600)),
|
||||
),
|
||||
padding: const pw.EdgeInsets.only(bottom: 5),
|
||||
child: pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
pw.Text('$partnerLabel:${partnerName ?? ''}', style: metaStyle),
|
||||
pw.Text('日期:${orderDate ?? ''}', style: metaStyle),
|
||||
pw.Text('NO: $orderNo', style: boldStyle),
|
||||
],
|
||||
),
|
||||
),
|
||||
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] },
|
||||
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(),
|
||||
),
|
||||
...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,
|
||||
),
|
||||
);
|
||||
}
|
||||
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.Container(
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border.all(color: PdfColors.grey400),
|
||||
color: PdfColors.grey50,
|
||||
),
|
||||
padding: const pw.EdgeInsets.all(5),
|
||||
child: pw.Text(tipsText,
|
||||
style: pw.TextStyle(font: font, fontSize: 9, color: PdfColors.grey700)),
|
||||
),
|
||||
],
|
||||
pw.SizedBox(height: 36),
|
||||
pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_sig(font, bold, '收/出货人(签字)'),
|
||||
_sig(font, bold, '经办人:${operatorName ?? ''}'),
|
||||
_sig(font, bold, '制单人:${reviewerName ?? operatorName ?? ''}'),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> printStockInOrderImpl(StockInOrder order) async {
|
||||
// Native PDF printing not implemented; no-op on non-web platforms.
|
||||
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];
|
||||
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.unitPrice.toStringAsFixed(2),
|
||||
it.totalPrice.toStringAsFixed(2),
|
||||
it.productionDate ?? '',
|
||||
it.batchNo ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
final doc = pw.Document();
|
||||
doc.addPage(pw.MultiPage(
|
||||
pageFormat: PdfPageFormat.a4,
|
||||
margin: const pw.EdgeInsets.symmetric(horizontal: 15 * PdfPageFormat.mm, vertical: 12 * PdfPageFormat.mm),
|
||||
build: (_) => [
|
||||
_buildOrderDoc(
|
||||
font: font, bold: bold,
|
||||
title: '入 库 单',
|
||||
orderNo: order.orderNo,
|
||||
orderDate: order.orderDate,
|
||||
partnerLabel: '供应商',
|
||||
partnerName: order.partnerName,
|
||||
warehouseName: order.warehouseName,
|
||||
operatorName: order.operatorName,
|
||||
reviewerName: order.reviewerName,
|
||||
remark: order.remark,
|
||||
headers: ['序号', '商品编号', '商品名称', '系列', '规格', '数量', '单位', '单价', '金额', '生产日期', '批次'],
|
||||
rows: rows,
|
||||
totalQty: totalQty,
|
||||
totalAmt: totalAmt,
|
||||
),
|
||||
],
|
||||
));
|
||||
await Printing.layoutPdf(onLayout: (_) async => doc.save());
|
||||
}
|
||||
|
||||
Future<void> printStockOutOrderImpl(StockOutOrder order) async {
|
||||
// Native PDF printing not implemented; no-op on non-web platforms.
|
||||
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];
|
||||
totalQty += it.quantity;
|
||||
totalAmt += it.totalPrice;
|
||||
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),
|
||||
]);
|
||||
}
|
||||
|
||||
final doc = pw.Document();
|
||||
doc.addPage(pw.MultiPage(
|
||||
pageFormat: PdfPageFormat.a4,
|
||||
margin: const pw.EdgeInsets.symmetric(horizontal: 15 * PdfPageFormat.mm, vertical: 12 * PdfPageFormat.mm),
|
||||
build: (_) => [
|
||||
_buildOrderDoc(
|
||||
font: font, bold: bold,
|
||||
title: '出 库 单',
|
||||
orderNo: order.orderNo,
|
||||
orderDate: order.orderDate,
|
||||
partnerLabel: '客户',
|
||||
partnerName: order.partnerName,
|
||||
warehouseName: order.warehouseName,
|
||||
operatorName: order.operatorName,
|
||||
reviewerName: order.reviewerName,
|
||||
remark: order.remark,
|
||||
headers: ['序号', '商品编号', '商品名称', '系列', '规格', '单位', '数量', '单价', '金额'],
|
||||
rows: rows,
|
||||
totalQty: totalQty,
|
||||
totalAmt: totalAmt,
|
||||
tipsText: '温馨提示:签收时,请务必核对好酒品数量、年份和日期、批次及物流码,如有问题及时反馈,酒品无质量问题一经售出概不退换,谢谢合作。',
|
||||
),
|
||||
],
|
||||
));
|
||||
await Printing.layoutPdf(onLayout: (_) async => doc.save());
|
||||
}
|
||||
|
||||
@@ -5,6 +5,14 @@ import 'package:web/web.dart' as web;
|
||||
import '../../models/stock_in.dart';
|
||||
import '../../models/stock_out.dart';
|
||||
|
||||
void _openPrintWindow(String html) {
|
||||
final win = web.window.open('', '_blank');
|
||||
if (win != null) {
|
||||
win.document.write(html.toJS);
|
||||
win.document.close();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> printProductLabelImpl({
|
||||
required Uint8List qrBytes,
|
||||
required String name,
|
||||
@@ -13,206 +21,380 @@ Future<void> printProductLabelImpl({
|
||||
String? series,
|
||||
String? batchNo,
|
||||
String? productionDate,
|
||||
String? remark,
|
||||
String shopName = '',
|
||||
String shopAddress = '',
|
||||
String shopPhone = '',
|
||||
}) async {
|
||||
final base64Img = base64Encode(qrBytes);
|
||||
|
||||
final leftRows = StringBuffer();
|
||||
leftRows.write('<div class="name">$name</div>');
|
||||
leftRows.write('<div class="row">编号:$code</div>');
|
||||
if (series != null && series.isNotEmpty) leftRows.write('<div class="row">系列:$series</div>');
|
||||
if (spec != null && spec.isNotEmpty) leftRows.write('<div class="row">规格:$spec</div>');
|
||||
if (batchNo != null && batchNo.isNotEmpty) leftRows.write('<div class="row">批次:$batchNo</div>');
|
||||
if (productionDate != null && productionDate.isNotEmpty) {
|
||||
leftRows.write('<div class="row">生产日期:$productionDate</div>');
|
||||
}
|
||||
final specVal = (spec ?? '').isNotEmpty ? spec! : '—';
|
||||
final seriesVal = (series ?? '').isNotEmpty ? series! : '—';
|
||||
final batchVal = (batchNo ?? '').isNotEmpty ? batchNo! : '—';
|
||||
final dateVal = (productionDate ?? '').isNotEmpty
|
||||
? (productionDate!.length > 10 ? productionDate.substring(0, 10) : productionDate)
|
||||
: '—';
|
||||
|
||||
final footerContact = [
|
||||
if (shopAddress.isNotEmpty) shopAddress,
|
||||
if (shopPhone.isNotEmpty) shopPhone,
|
||||
].join(' · ');
|
||||
|
||||
// 标签生成时间
|
||||
final now = DateTime.now();
|
||||
final genTime =
|
||||
'${now.year}-${now.month.toString().padLeft(2,'0')}-${now.day.toString().padLeft(2,'0')}'
|
||||
' ${now.hour.toString().padLeft(2,'0')}:${now.minute.toString().padLeft(2,'0')}';
|
||||
|
||||
final remarkRow = (remark ?? '').isNotEmpty
|
||||
? '''<div class="spec-row">
|
||||
<span class="lbl">备 注</span>
|
||||
<span class="val-full">$remark</span>
|
||||
</div>'''
|
||||
: '';
|
||||
|
||||
final html = '''<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700&family=Noto+Serif+SC:wght@700&family=IBM+Plex+Mono:wght@400&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
@page{size:4in 2in;margin:0;}
|
||||
*{box-sizing:border-box;margin:0;padding:0;}
|
||||
body{display:flex;justify-content:center;align-items:center;width:4in;height:2in;
|
||||
font-family:"PingFang SC","Microsoft YaHei",sans-serif;overflow:hidden;}
|
||||
.label{display:flex;width:100%;height:100%;padding:10px;align-items:center;gap:8px;}
|
||||
.left{flex:1;display:flex;flex-direction:column;justify-content:center;gap:3px;overflow:hidden;}
|
||||
.name{font-size:15px;font-weight:bold;line-height:1.2;word-break:break-all;}
|
||||
.row{font-size:11px;color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
||||
.qr{width:1.2in;height:1.2in;flex-shrink:0;}
|
||||
img{width:100%;height:100%;display:block;}
|
||||
@page { size: 4in 2in; margin: 0; }
|
||||
* { box-sizing: border-box; margin: 0; padding: 0;
|
||||
-webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
body { width: 4in; height: 2in; overflow: hidden; background: #fff; }
|
||||
|
||||
.label {
|
||||
width: 4in; height: 2in;
|
||||
display: grid; grid-template-rows: 42px 1fr 18px;
|
||||
font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
/* ── Header:酒行名称 ── */
|
||||
.hdr {
|
||||
background: #1f2a3a; color: #f4ecd8;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 15px; overflow: hidden;
|
||||
}
|
||||
.hdr-name {
|
||||
font-family: "Noto Serif SC", serif;
|
||||
font-weight: 700; font-size: 17px; letter-spacing: 0.08em;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.hdr-tag {
|
||||
font-size: 6.5px; letter-spacing: 0.2em; opacity: 0.8;
|
||||
font-style: italic; white-space: nowrap; flex-shrink: 0; padding-left: 10px;
|
||||
}
|
||||
|
||||
/* ── Body ── */
|
||||
.body {
|
||||
background: #fff; padding: 9px 15px 8px;
|
||||
display: grid; grid-template-columns: 1fr 76px; gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 商品名 */
|
||||
.product-name {
|
||||
font-family: "Noto Serif SC", serif;
|
||||
font-weight: 700; font-size: 13px; letter-spacing: 0.04em;
|
||||
margin-bottom: 7px;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 规格行 */
|
||||
.spec-row {
|
||||
display: flex; gap: 6px; align-items: baseline;
|
||||
font-size: 6.5px; margin-bottom: 3.5px; flex-wrap: nowrap;
|
||||
}
|
||||
.lbl { color: #888; letter-spacing: 0.1em; white-space: nowrap; flex-shrink: 0; }
|
||||
.val { font-family: "IBM Plex Mono", monospace;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.val-full { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
color: #444; font-size: 6px; }
|
||||
|
||||
/* 同行双字段 */
|
||||
.spec-row-2 {
|
||||
display: grid; grid-template-columns: 28px 1fr 28px 1fr;
|
||||
gap: 0 7px; font-size: 6.5px; margin-bottom: 3.5px;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
/* QR */
|
||||
.qr-col { display: flex; flex-direction: column; align-items: center; gap: 3px; }
|
||||
.qr-col img { width: 68px; height: 68px; object-fit: contain; display: block; }
|
||||
.qr-cap { font-size: 5px; letter-spacing: 0.2em; color: #666; text-align: center; }
|
||||
|
||||
/* ── Footer:地址 + 生成时间 ── */
|
||||
.ftr {
|
||||
background: #f4ecd8;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 15px;
|
||||
font-family: "IBM Plex Mono", monospace;
|
||||
font-size: 5px; color: #5a4e35; letter-spacing: 0.04em; overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="label">
|
||||
<div class="left">$leftRows</div>
|
||||
<div class="qr"><img src="data:image/png;base64,$base64Img"/></div>
|
||||
<div class="label">
|
||||
|
||||
<div class="hdr">
|
||||
<span class="hdr-name">$shopName</span>
|
||||
<span class="hdr-tag">Certificate of Authenticity</span>
|
||||
</div>
|
||||
<script>window.onload=function(){window.print();};</script>
|
||||
|
||||
<div class="body">
|
||||
<div>
|
||||
<div class="product-name">$name</div>
|
||||
|
||||
<div class="spec-row-2">
|
||||
<span class="lbl">规 格</span>
|
||||
<span class="val">$specVal</span>
|
||||
<span class="lbl">系 列</span>
|
||||
<span class="val">$seriesVal</span>
|
||||
</div>
|
||||
|
||||
<div class="spec-row-2">
|
||||
<span class="lbl">批 号</span>
|
||||
<span class="val">$batchVal</span>
|
||||
<span class="lbl">生产日期</span>
|
||||
<span class="val">$dateVal</span>
|
||||
</div>
|
||||
|
||||
$remarkRow
|
||||
</div>
|
||||
|
||||
<div class="qr-col">
|
||||
<img src="data:image/png;base64,$base64Img" />
|
||||
<div class="qr-cap">扫码溯源 · TRACE</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ftr">
|
||||
<span>${footerContact.isNotEmpty ? footerContact : shopName}</span>
|
||||
<span>$genTime</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
if (document.fonts && document.fonts.ready) {
|
||||
document.fonts.ready.then(function() { window.print(); });
|
||||
} else {
|
||||
window.onload = function() { window.print(); };
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>''';
|
||||
|
||||
final win = web.window.open('', '_blank');
|
||||
if (win != null) {
|
||||
win.document.write(html.toJS);
|
||||
win.document.close();
|
||||
}
|
||||
_openPrintWindow(html);
|
||||
}
|
||||
|
||||
// ── 入库单 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> printStockInOrderImpl(StockInOrder order) async {
|
||||
final rows = StringBuffer();
|
||||
double total = 0;
|
||||
double totalQty = 0;
|
||||
double totalAmt = 0;
|
||||
for (int i = 0; i < order.items.length; i++) {
|
||||
final item = order.items[i];
|
||||
total += item.totalPrice;
|
||||
totalQty += item.quantity;
|
||||
totalAmt += item.totalPrice;
|
||||
rows.write('''<tr>
|
||||
<td style="text-align:center">${i + 1}</td>
|
||||
<td class="c">${i + 1}</td>
|
||||
<td>${item.productCode ?? ''}</td>
|
||||
<td>${item.productName ?? ''}</td>
|
||||
<td>${item.productSeries ?? ''}</td>
|
||||
<td>${item.productSpec ?? ''}</td>
|
||||
<td>${item.batchNo ?? ''}</td>
|
||||
<td style="text-align:right">${item.quantity.toStringAsFixed(0)}</td>
|
||||
<td style="text-align:center">${item.productUnit ?? ''}</td>
|
||||
<td style="text-align:right">${item.unitPrice.toStringAsFixed(2)}</td>
|
||||
<td style="text-align:right">${item.totalPrice.toStringAsFixed(2)}</td>
|
||||
<td class="c">${item.productSeries ?? ''}</td>
|
||||
<td class="c">${item.productSpec ?? ''}</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>
|
||||
</tr>''');
|
||||
}
|
||||
|
||||
final remarkLine = (order.remark ?? '').isNotEmpty
|
||||
? '<div class="remark">备注:${order.remark}</div>'
|
||||
: '';
|
||||
|
||||
final html = '''<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
@page{size:A4;margin:15mm;}
|
||||
*{box-sizing:border-box;margin:0;padding:0;}
|
||||
body{font-family:"PingFang SC","Microsoft YaHei",sans-serif;font-size:12px;color:#000;}
|
||||
h1{text-align:center;font-size:20px;margin-bottom:12px;}
|
||||
.grid{display:grid;grid-template-columns:1fr 1fr;gap:6px 24px;margin-bottom:12px;}
|
||||
.row{display:flex;gap:4px;}
|
||||
.lbl{color:#555;white-space:nowrap;}
|
||||
table{width:100%;border-collapse:collapse;margin-bottom:8px;}
|
||||
th,td{border:1px solid #ccc;padding:5px 6px;}
|
||||
th{background:#f5f5f5;font-weight:600;text-align:center;}
|
||||
.foot td{font-weight:bold;background:#f9f9f9;}
|
||||
.sigs{display:flex;justify-content:space-between;margin-top:32px;}
|
||||
.sig{text-align:center;width:30%;}
|
||||
.sig-line{border-top:1px solid #333;margin-bottom:4px;height:32px;}
|
||||
@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>
|
||||
</head>
|
||||
<body>
|
||||
<h1>入库单</h1>
|
||||
<div class="grid">
|
||||
<div class="row"><span class="lbl">单号:</span><span>${order.orderNo}</span></div>
|
||||
<div class="row"><span class="lbl">日期:</span><span>${order.orderDate ?? ''}</span></div>
|
||||
<div class="row"><span class="lbl">供应商:</span><span>${order.partnerName ?? ''}</span></div>
|
||||
<div class="row"><span class="lbl">仓库:</span><span>${order.warehouseName ?? ''}</span></div>
|
||||
<div class="row"><span class="lbl">采购人:</span><span>${order.operatorName ?? ''}</span></div>
|
||||
<div class="row"><span class="lbl">备注:</span><span>${order.remark ?? ''}</span></div>
|
||||
<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>
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>序号</th><th>商品名称</th><th>系列</th><th>规格</th><th>批次</th>
|
||||
<th>数量</th><th>单位</th><th>单价</th><th>金额</th>
|
||||
<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>
|
||||
<tbody>$rows</tbody>
|
||||
<tfoot><tr class="foot">
|
||||
<td colspan="8" style="text-align:right;">合计</td>
|
||||
<td style="text-align:right">${total.toStringAsFixed(2)}</td>
|
||||
<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"></div>审核人</div>
|
||||
<div class="sig"><div class="sig-line"></div>供应商签字</div>
|
||||
<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>
|
||||
<script>window.onload=function(){window.print();};</script>
|
||||
<div class="footer-note">酒库管理系统</div>
|
||||
<script>window.onload = function(){ window.print(); };</script>
|
||||
</body>
|
||||
</html>''';
|
||||
|
||||
final win = web.window.open('', '_blank');
|
||||
if (win != null) {
|
||||
win.document.write(html.toJS);
|
||||
win.document.close();
|
||||
}
|
||||
_openPrintWindow(html);
|
||||
}
|
||||
|
||||
// ── 出库单 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> printStockOutOrderImpl(StockOutOrder order) async {
|
||||
final rows = StringBuffer();
|
||||
double total = 0;
|
||||
double totalQty = 0;
|
||||
double totalAmt = 0;
|
||||
for (int i = 0; i < order.items.length; i++) {
|
||||
final item = order.items[i];
|
||||
total += item.totalPrice;
|
||||
totalQty += item.quantity;
|
||||
totalAmt += item.totalPrice;
|
||||
rows.write('''<tr>
|
||||
<td style="text-align:center">${i + 1}</td>
|
||||
<td class="c">${i + 1}</td>
|
||||
<td>${item.productCode ?? ''}</td>
|
||||
<td>${item.productName ?? ''}</td>
|
||||
<td>${item.productSeries ?? ''}</td>
|
||||
<td>${item.productSpec ?? ''}</td>
|
||||
<td style="text-align:right">${item.quantity.toStringAsFixed(0)}</td>
|
||||
<td style="text-align:center">${item.productUnit ?? ''}</td>
|
||||
<td style="text-align:right">${item.unitPrice.toStringAsFixed(2)}</td>
|
||||
<td style="text-align:right">${item.totalPrice.toStringAsFixed(2)}</td>
|
||||
<td class="c">${item.productSeries ?? ''}</td>
|
||||
<td class="c">${item.productSpec ?? ''}</td>
|
||||
<td class="c">${item.productUnit ?? ''}</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>
|
||||
</tr>''');
|
||||
}
|
||||
|
||||
final remarkLine = (order.remark ?? '').isNotEmpty
|
||||
? '<div class="remark">备注:${order.remark}</div>'
|
||||
: '';
|
||||
|
||||
final html = '''<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
@page{size:A4;margin:15mm;}
|
||||
*{box-sizing:border-box;margin:0;padding:0;}
|
||||
body{font-family:"PingFang SC","Microsoft YaHei",sans-serif;font-size:12px;color:#000;}
|
||||
.header{text-align:center;margin-bottom:16px;}
|
||||
h1{font-size:20px;margin-bottom:2px;}
|
||||
.subtitle{font-size:13px;color:#555;}
|
||||
.grid{display:grid;grid-template-columns:1fr 1fr;gap:6px 24px;margin-bottom:12px;}
|
||||
.row{display:flex;gap:4px;}
|
||||
.lbl{color:#555;white-space:nowrap;}
|
||||
table{width:100%;border-collapse:collapse;margin-bottom:8px;}
|
||||
th,td{border:1px solid #ccc;padding:5px 6px;}
|
||||
th{background:#f5f5f5;font-weight:600;text-align:center;}
|
||||
.foot td{font-weight:bold;background:#f9f9f9;}
|
||||
.sigs{display:flex;justify-content:space-between;margin-top:32px;}
|
||||
.sig{text-align:center;width:30%;}
|
||||
.sig-line{border-top:1px solid #333;margin-bottom:4px;height:32px;}
|
||||
@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>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>温馨提示</h1>
|
||||
<div class="subtitle">出库通知单</div>
|
||||
<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="grid">
|
||||
<div class="row"><span class="lbl">单号:</span><span>${order.orderNo}</span></div>
|
||||
<div class="row"><span class="lbl">日期:</span><span>${order.orderDate ?? ''}</span></div>
|
||||
<div class="row"><span class="lbl">客户:</span><span>${order.partnerName ?? ''}</span></div>
|
||||
<div class="row"><span class="lbl">仓库:</span><span>${order.warehouseName ?? ''}</span></div>
|
||||
<div class="row"><span class="lbl">经办人:</span><span>${order.operatorName ?? ''}</span></div>
|
||||
<div class="row"><span class="lbl">备注:</span><span>${order.remark ?? ''}</span></div>
|
||||
<div class="meta2">
|
||||
<span>仓库:${order.warehouseName ?? ''}</span>
|
||||
<span>经办人:${order.operatorName ?? ''}</span>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>序号</th><th>商品名称</th><th>系列</th><th>规格</th>
|
||||
<th>数量</th><th>单位</th><th>单价</th><th>金额</th>
|
||||
<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>
|
||||
<tbody>$rows</tbody>
|
||||
<tfoot><tr class="foot">
|
||||
<td colspan="7" style="text-align:right;">合计</td>
|
||||
<td style="text-align:right">${total.toStringAsFixed(2)}</td>
|
||||
<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"></div>审核人</div>
|
||||
<div class="sig"><div class="sig-line"></div>客户签字</div>
|
||||
<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>
|
||||
<script>window.onload=function(){window.print();};</script>
|
||||
<div class="footer-note">酒库管理系统</div>
|
||||
<script>window.onload = function(){ window.print(); };</script>
|
||||
</body>
|
||||
</html>''';
|
||||
|
||||
final win = web.window.open('', '_blank');
|
||||
if (win != null) {
|
||||
win.document.write(html.toJS);
|
||||
win.document.close();
|
||||
}
|
||||
_openPrintWindow(html);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user