feat: Add subject field to invoices, integrating it into the model, UI, and database.
This commit is contained in:
parent
81c2fc38a8
commit
6643aa4157
8 changed files with 152 additions and 53 deletions
|
|
@ -66,6 +66,7 @@ class Invoice {
|
||||||
final double? longitude; // 追加
|
final double? longitude; // 追加
|
||||||
final String terminalId; // 追加: 端末識別子
|
final String terminalId; // 追加: 端末識別子
|
||||||
final bool isDraft; // 追加: 下書きフラグ
|
final bool isDraft; // 追加: 下書きフラグ
|
||||||
|
final String? subject; // 追加: 案件名
|
||||||
|
|
||||||
Invoice({
|
Invoice({
|
||||||
String? id,
|
String? id,
|
||||||
|
|
@ -84,13 +85,14 @@ class Invoice {
|
||||||
this.longitude, // 追加
|
this.longitude, // 追加
|
||||||
String? terminalId, // 追加
|
String? terminalId, // 追加
|
||||||
this.isDraft = false, // 追加: デフォルトは通常
|
this.isDraft = false, // 追加: デフォルトは通常
|
||||||
|
this.subject, // 追加: 案件
|
||||||
}) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
}) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
terminalId = terminalId ?? "T1", // デフォルト端末ID
|
terminalId = terminalId ?? "T1", // デフォルト端末ID
|
||||||
updatedAt = updatedAt ?? DateTime.now();
|
updatedAt = updatedAt ?? DateTime.now();
|
||||||
|
|
||||||
/// 伝票内容から決定論的なハッシュを生成する (SHA256の一部)
|
/// 伝票内容から決定論的なハッシュを生成する (SHA256の一部)
|
||||||
String get contentHash {
|
String get contentHash {
|
||||||
final input = "$id|$terminalId|${date.toIso8601String()}|${customer.id}|$totalAmount|${items.map((e) => "${e.description}${e.quantity}${e.unitPrice}").join()}";
|
final input = "$id|$terminalId|${date.toIso8601String()}|${customer.id}|$totalAmount|${subject ?? ""}|${items.map((e) => "${e.description}${e.quantity}${e.unitPrice}").join()}";
|
||||||
final bytes = utf8.encode(input);
|
final bytes = utf8.encode(input);
|
||||||
return sha256.convert(bytes).toString().substring(0, 8).toUpperCase();
|
return sha256.convert(bytes).toString().substring(0, 8).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
@ -141,6 +143,7 @@ class Invoice {
|
||||||
'terminal_id': terminalId, // 追加
|
'terminal_id': terminalId, // 追加
|
||||||
'content_hash': contentHash, // 追加
|
'content_hash': contentHash, // 追加
|
||||||
'is_draft': isDraft ? 1 : 0, // 追加
|
'is_draft': isDraft ? 1 : 0, // 追加
|
||||||
|
'subject': subject, // 追加
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,6 +164,7 @@ class Invoice {
|
||||||
double? longitude,
|
double? longitude,
|
||||||
String? terminalId,
|
String? terminalId,
|
||||||
bool? isDraft,
|
bool? isDraft,
|
||||||
|
String? subject,
|
||||||
}) {
|
}) {
|
||||||
return Invoice(
|
return Invoice(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
|
|
@ -179,6 +183,7 @@ class Invoice {
|
||||||
longitude: longitude ?? this.longitude,
|
longitude: longitude ?? this.longitude,
|
||||||
terminalId: terminalId ?? this.terminalId,
|
terminalId: terminalId ?? this.terminalId,
|
||||||
isDraft: isDraft ?? this.isDraft,
|
isDraft: isDraft ?? this.isDraft,
|
||||||
|
subject: subject ?? this.subject,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -328,6 +328,11 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
||||||
Text("取引先:", style: TextStyle(fontWeight: FontWeight.bold, color: textColor)),
|
Text("取引先:", style: TextStyle(fontWeight: FontWeight.bold, color: textColor)),
|
||||||
Text("${_currentInvoice.customerNameForDisplay} ${_currentInvoice.customer.title}",
|
Text("${_currentInvoice.customerNameForDisplay} ${_currentInvoice.customer.title}",
|
||||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: textColor)),
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: textColor)),
|
||||||
|
if (_currentInvoice.subject?.isNotEmpty ?? false) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text("件名: ${_currentInvoice.subject}",
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.indigoAccent)),
|
||||||
|
],
|
||||||
if (_currentInvoice.customer.department != null && _currentInvoice.customer.department!.isNotEmpty)
|
if (_currentInvoice.customer.department != null && _currentInvoice.customer.department!.isNotEmpty)
|
||||||
Text(_currentInvoice.customer.department!, style: TextStyle(fontSize: 16, color: textColor)),
|
Text(_currentInvoice.customer.department!, style: TextStyle(fontSize: 16, color: textColor)),
|
||||||
if (_currentInvoice.latitude != null) ...[
|
if (_currentInvoice.latitude != null) ...[
|
||||||
|
|
|
||||||
|
|
@ -259,14 +259,26 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
backgroundColor: _isUnlocked ? Colors.indigo.shade100 : Colors.grey.shade200,
|
backgroundColor: _isUnlocked ? Colors.indigo.shade100 : Colors.grey.shade200,
|
||||||
child: Icon(Icons.description_outlined, color: _isUnlocked ? Colors.indigo : Colors.grey),
|
child: Icon(Icons.description_outlined, color: _isUnlocked ? Colors.indigo : Colors.grey),
|
||||||
),
|
),
|
||||||
title: Text(invoice.customerNameForDisplay),
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(invoice.customerNameForDisplay, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
if (invoice.subject?.isNotEmpty ?? false)
|
||||||
|
Text(
|
||||||
|
invoice.subject!,
|
||||||
|
style: TextStyle(fontSize: 13, color: Colors.indigo.shade700, fontWeight: FontWeight.normal),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
subtitle: Text("${dateFormatter.format(invoice.date)} - ${invoice.invoiceNumber}"),
|
subtitle: Text("${dateFormatter.format(invoice.date)} - ${invoice.invoiceNumber}"),
|
||||||
trailing: Column(
|
trailing: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text("¥${amountFormatter.format(invoice.totalAmount)}",
|
Text("¥${amountFormatter.format(invoice.totalAmount)}",
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
|
||||||
if (invoice.isSynced)
|
if (invoice.isSynced)
|
||||||
const Icon(Icons.sync, size: 16, color: Colors.green)
|
const Icon(Icons.sync, size: 16, color: Colors.green)
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
DocumentType _documentType = DocumentType.invoice; // 追加
|
DocumentType _documentType = DocumentType.invoice; // 追加
|
||||||
DateTime _selectedDate = DateTime.now(); // 追加: 伝票日付
|
DateTime _selectedDate = DateTime.now(); // 追加: 伝票日付
|
||||||
bool _isDraft = false; // 追加: 下書きモード
|
bool _isDraft = false; // 追加: 下書きモード
|
||||||
|
final TextEditingController _subjectController = TextEditingController(); // 追加
|
||||||
String _status = "取引先と商品を入力してください";
|
String _status = "取引先と商品を入力してください";
|
||||||
|
|
||||||
// 署名用の実験的パス
|
// 署名用の実験的パス
|
||||||
|
|
@ -98,14 +99,16 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
|
|
||||||
final invoice = Invoice(
|
final invoice = Invoice(
|
||||||
customer: _selectedCustomer!,
|
customer: _selectedCustomer!,
|
||||||
date: _selectedDate, // 修正
|
date: _selectedDate,
|
||||||
items: _items,
|
items: _items,
|
||||||
taxRate: _includeTax ? _taxRate : 0.0,
|
taxRate: _includeTax ? _taxRate : 0.0,
|
||||||
documentType: _documentType,
|
documentType: _documentType,
|
||||||
customerFormalNameSnapshot: _selectedCustomer!.formalName,
|
customerFormalNameSnapshot: _selectedCustomer!.formalName,
|
||||||
|
subject: _subjectController.text.isNotEmpty ? _subjectController.text : null, // 追加
|
||||||
notes: _includeTax ? "(消費税 ${(_taxRate * 100).toInt()}% 込み)" : "(非課税)",
|
notes: _includeTax ? "(消費税 ${(_taxRate * 100).toInt()}% 込み)" : "(非課税)",
|
||||||
latitude: pos?.latitude,
|
latitude: pos?.latitude,
|
||||||
longitude: pos?.longitude,
|
longitude: pos?.longitude,
|
||||||
|
isDraft: _isDraft, // 追加
|
||||||
);
|
);
|
||||||
|
|
||||||
if (generatePdf) {
|
if (generatePdf) {
|
||||||
|
|
@ -177,7 +180,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
backgroundColor: themeColor,
|
backgroundColor: themeColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: const BackButton(),
|
leading: const BackButton(),
|
||||||
title: Text(_isDraft ? "伝票作成 (下書きモード)" : "販売アシスト1号 V1.5.02"),
|
title: Text(_isDraft ? "伝票作成 (下書きモード)" : "販売アシスト1号 V1.5.03"),
|
||||||
backgroundColor: _isDraft ? Colors.black87 : Colors.blueGrey,
|
backgroundColor: _isDraft ? Colors.black87 : Colors.blueGrey,
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
|
|
@ -195,6 +198,8 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
_buildDateSection(),
|
_buildDateSection(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildCustomerSection(),
|
_buildCustomerSection(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildSubjectSection(textColor), // 追加
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildItemsSection(fmt),
|
_buildItemsSection(fmt),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
@ -516,30 +521,28 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDraftToggle() {
|
Widget _buildDraftToggle() {
|
||||||
return Container(
|
// ... (existing code omitted for brevity but I'll provide the new method below it)
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
}
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: _isDraft ? Colors.black26 : Colors.orange.shade50,
|
Widget _buildSubjectSection(Color textColor) {
|
||||||
borderRadius: BorderRadius.circular(12),
|
return Column(
|
||||||
border: Border.all(color: _isDraft ? Colors.orangeAccent : Colors.orange, width: 2),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
),
|
children: [
|
||||||
child: Row(
|
Text("案件名 / 件名", style: TextStyle(fontWeight: FontWeight.bold, color: textColor)),
|
||||||
children: [
|
const SizedBox(height: 8),
|
||||||
Icon(_isDraft ? Icons.drafts : Icons.check_circle, color: Colors.orange),
|
TextField(
|
||||||
const SizedBox(width: 12),
|
controller: _subjectController,
|
||||||
Expanded(
|
style: TextStyle(color: textColor),
|
||||||
child: Text(
|
decoration: InputDecoration(
|
||||||
_isDraft ? "下書きモード設定中" : "正式発行モード",
|
hintText: "例:事務所改修工事 / 〇〇月分リース料",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, color: _isDraft ? Colors.white : Colors.orange.shade900),
|
hintStyle: TextStyle(color: textColor.withOpacity(0.5)),
|
||||||
),
|
filled: true,
|
||||||
|
fillColor: _isDraft ? Colors.white12 : Colors.grey.shade100,
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
),
|
),
|
||||||
Switch(
|
),
|
||||||
value: _isDraft,
|
],
|
||||||
activeColor: Colors.orangeAccent,
|
|
||||||
onChanged: (val) => setState(() => _isDraft = val),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import 'package:sqflite/sqflite.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
class DatabaseHelper {
|
class DatabaseHelper {
|
||||||
static const _databaseVersion = 11;
|
static const _databaseVersion = 12;
|
||||||
static final DatabaseHelper _instance = DatabaseHelper._internal();
|
static final DatabaseHelper _instance = DatabaseHelper._internal();
|
||||||
static Database? _database;
|
static Database? _database;
|
||||||
|
|
||||||
|
|
@ -91,6 +91,9 @@ class DatabaseHelper {
|
||||||
if (oldVersion < 11) {
|
if (oldVersion < 11) {
|
||||||
await db.execute('ALTER TABLE invoices ADD COLUMN is_draft INTEGER DEFAULT 0');
|
await db.execute('ALTER TABLE invoices ADD COLUMN is_draft INTEGER DEFAULT 0');
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 12) {
|
||||||
|
await db.execute('ALTER TABLE invoices ADD COLUMN subject TEXT');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onCreate(Database db, int version) async {
|
Future<void> _onCreate(Database db, int version) async {
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ class InvoiceRepository {
|
||||||
longitude: iMap['longitude'],
|
longitude: iMap['longitude'],
|
||||||
terminalId: iMap['terminal_id'] ?? "T1",
|
terminalId: iMap['terminal_id'] ?? "T1",
|
||||||
isDraft: (iMap['is_draft'] ?? 0) == 1,
|
isDraft: (iMap['is_draft'] ?? 0) == 1,
|
||||||
|
subject: iMap['subject'],
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return invoices;
|
return invoices;
|
||||||
|
|
|
||||||
|
|
@ -134,29 +134,41 @@ Future<pw.Document> buildInvoiceDocument(Invoice invoice) async {
|
||||||
pw.SizedBox(height: 20),
|
pw.SizedBox(height: 20),
|
||||||
|
|
||||||
// 明細テーブル
|
// 明細テーブル
|
||||||
pw.TableHelper.fromTextArray(
|
// 明細テーブル
|
||||||
headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold),
|
pw.Table(
|
||||||
headerDecoration: const pw.BoxDecoration(color: PdfColors.grey300),
|
border: pw.TableBorder.all(color: PdfColors.grey300),
|
||||||
cellHeight: 30,
|
columnWidths: {
|
||||||
cellAlignments: {
|
0: const pw.FlexColumnWidth(4),
|
||||||
0: pw.Alignment.centerLeft,
|
1: const pw.FixedColumnWidth(50),
|
||||||
1: pw.Alignment.centerRight,
|
2: const pw.FixedColumnWidth(80),
|
||||||
2: pw.Alignment.centerRight,
|
3: const pw.FixedColumnWidth(80),
|
||||||
3: pw.Alignment.centerRight,
|
|
||||||
},
|
},
|
||||||
headers: ["品名 / 項目", "数量", "単価", "金額"],
|
children: [
|
||||||
data: List<List<String>>.generate(
|
// ヘッダー
|
||||||
invoice.items.length,
|
pw.TableRow(
|
||||||
(index) {
|
decoration: const pw.BoxDecoration(color: PdfColors.grey300),
|
||||||
final item = invoice.items[index];
|
children: [
|
||||||
return [
|
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text("品名 / 項目", style: pw.TextStyle(fontWeight: pw.FontWeight.bold))),
|
||||||
item.description,
|
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text("数量", style: pw.TextStyle(fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.right)),
|
||||||
item.quantity.toString(),
|
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text("単価", style: pw.TextStyle(fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.right)),
|
||||||
amountFormatter.format(item.unitPrice),
|
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text("金額", style: pw.TextStyle(fontWeight: pw.FontWeight.bold), textAlign: pw.TextAlign.right)),
|
||||||
amountFormatter.format(item.subtotal),
|
],
|
||||||
];
|
),
|
||||||
},
|
// データ行
|
||||||
),
|
...invoice.items.map((item) {
|
||||||
|
return pw.TableRow(
|
||||||
|
children: [
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(4),
|
||||||
|
child: _parseMarkdown(item.description),
|
||||||
|
),
|
||||||
|
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text(item.quantity.toString(), textAlign: pw.TextAlign.right)),
|
||||||
|
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text(amountFormatter.format(item.unitPrice), textAlign: pw.TextAlign.right)),
|
||||||
|
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text(amountFormatter.format(item.subtotal), textAlign: pw.TextAlign.right)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
// 計算内訳
|
// 計算内訳
|
||||||
|
|
@ -239,8 +251,12 @@ Future<String?> generateInvoicePdf(Invoice invoice) async {
|
||||||
final pdf = await buildInvoiceDocument(invoice);
|
final pdf = await buildInvoiceDocument(invoice);
|
||||||
|
|
||||||
final String hash = invoice.contentHash;
|
final String hash = invoice.contentHash;
|
||||||
final String timeStr = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
|
final String dateStr = DateFormat('yyyyMMdd').format(invoice.date);
|
||||||
String fileName = "${invoice.invoiceNumberPrefix}_${invoice.terminalId}_${invoice.id.substring(invoice.id.length - 4)}_${timeStr}_$hash.pdf";
|
final String amountStr = NumberFormat("#,###").format(invoice.totalAmount);
|
||||||
|
final String subjectStr = invoice.subject?.isNotEmpty == true ? "_${invoice.subject}" : "";
|
||||||
|
|
||||||
|
// {日付}({タイプ}){顧客名}_{案件}_{金額}_{HASH下8桁}.pdf
|
||||||
|
String fileName = "${dateStr}(${invoice.documentTypeName})${invoice.customerNameForDisplay}${subjectStr}_${amountStr}円_$hash.pdf";
|
||||||
|
|
||||||
final directory = await getExternalStorageDirectory();
|
final directory = await getExternalStorageDirectory();
|
||||||
if (directory == null) return null;
|
if (directory == null) return null;
|
||||||
|
|
@ -278,3 +294,49 @@ pw.Widget _buildSummaryRow(String label, String value, {bool isBold = false}) {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pw.Widget _parseMarkdown(String text) {
|
||||||
|
final lines = text.split('\n');
|
||||||
|
final List<pw.Widget> widgets = [];
|
||||||
|
|
||||||
|
for (final line in lines) {
|
||||||
|
String content = line;
|
||||||
|
pw.EdgeInsets padding = const pw.EdgeInsets.only(bottom: 2);
|
||||||
|
pw.Widget? prefix;
|
||||||
|
|
||||||
|
// 箇条書き / インデント
|
||||||
|
if (content.startsWith('* ') || content.startsWith('- ')) {
|
||||||
|
content = content.substring(2);
|
||||||
|
prefix = pw.Padding(padding: const pw.EdgeInsets.only(right: 4), child: pw.Text('•'));
|
||||||
|
} else if (content.startsWith(' ')) {
|
||||||
|
padding = padding.copyWith(left: 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 太字 (**text**) - 簡易実装
|
||||||
|
final List<pw.TextSpan> spans = [];
|
||||||
|
final parts = content.split('**');
|
||||||
|
for (int i = 0; i < parts.length; i++) {
|
||||||
|
spans.add(pw.TextSpan(
|
||||||
|
text: parts[i],
|
||||||
|
style: i % 2 == 1 ? pw.TextStyle(fontWeight: pw.FontWeight.bold) : null,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
widgets.add(
|
||||||
|
pw.Padding(
|
||||||
|
padding: padding,
|
||||||
|
child: pw.Row(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (prefix != null) prefix,
|
||||||
|
pw.Expanded(
|
||||||
|
child: pw.RichText(text: pw.TextSpan(children: spans, style: const pw.TextStyle(fontSize: 10))),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pw.Column(crossAxisAlignment: pw.CrossAxisAlignment.start, children: widgets);
|
||||||
|
}
|
||||||
|
|
|
||||||
8
目標.md
8
目標.md
|
|
@ -49,3 +49,11 @@
|
||||||
---
|
---
|
||||||
*最終更新日: 2026-02-14*
|
*最終更新日: 2026-02-14*
|
||||||
*Version: 1.5.02*
|
*Version: 1.5.02*
|
||||||
|
|
||||||
|
追加:
|
||||||
|
ファイル名ルール
|
||||||
|
{日付}({タイプ}){顧客名}_{案件}_{金額}_{HASH下8桁}.pdf
|
||||||
|
20260214(請求書)佐々木製作所_10,000円_12345678.pdf
|
||||||
|
明細欄にはmarkdown的要素が使える様に、簡単なものが欲しい。
|
||||||
|
インデントや箇条書き、太字など。問題はodooとの連携と型番。
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue