h-1.flutter.0/lib/models/invoice_models.dart
2026-03-04 14:55:40 +09:00

364 lines
12 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:intl/intl.dart';
import 'customer_model.dart';
class InvoiceItem {
final String? id;
final String? productId; // 追加
String description;
int quantity;
int unitPrice;
InvoiceItem({
this.id,
this.productId, // 追加
required this.description,
required this.quantity,
required this.unitPrice,
});
int get subtotal => quantity * unitPrice;
Map<String, dynamic> toMap(String invoiceId) {
return {
'id': id ?? DateTime.now().microsecondsSinceEpoch.toString(),
'invoice_id': invoiceId,
'product_id': productId, // 追加
'description': description,
'quantity': quantity,
'unit_price': unitPrice,
};
}
factory InvoiceItem.fromMap(Map<String, dynamic> map) {
return InvoiceItem(
id: map['id'],
productId: map['product_id'], // 追加
description: map['description'],
quantity: map['quantity'],
unitPrice: map['unit_price'],
);
}
InvoiceItem copyWith({
String? id, // Added this to be complete
String? description,
int? quantity,
int? unitPrice,
String? productId,
}) {
return InvoiceItem(
id: id ?? this.id, // Added this to be complete
description: description ?? this.description,
quantity: quantity ?? this.quantity,
unitPrice: unitPrice ?? this.unitPrice,
productId: productId ?? this.productId,
);
}
}
enum DocumentType {
estimation, // 見積
delivery, // 納品
invoice, // 請求
receipt, // 領収
}
extension DocumentTypeDisplay on DocumentType {
String get displayName {
switch (this) {
case DocumentType.estimation:
return '見積書';
case DocumentType.delivery:
return '納品書';
case DocumentType.invoice:
return '請求書';
case DocumentType.receipt:
return '領収書';
}
}
}
class Invoice {
static const String lockStatement =
'正式発行ボタン押下時にこの伝票はロックされ、以後の編集・削除はできません。ロック状態はハッシュチェーンで保護されます。';
static const String hashDescription =
'metaJson = JSON.stringify({id, invoiceNumber, customer, date, total, documentType, hash, lockStatement, companySnapshot, companySealHash}); metaHash = SHA-256(metaJson).';
final String id;
final Customer customer;
final DateTime date;
final List<InvoiceItem> items;
final String? notes;
final String? filePath;
final double taxRate;
final DocumentType documentType; // 追加
final String? customerFormalNameSnapshot;
final String? odooId;
final bool isSynced;
final DateTime updatedAt;
final double? latitude; // 追加
final double? longitude; // 追加
final String terminalId; // 追加: 端末識別子
final bool isDraft; // 追加: 下書きフラグ
final String? subject; // 追加: 案件名
final bool isLocked; // 追加: ロック
final int? contactVersionId; // 追加: 連絡先バージョン
final String? contactEmailSnapshot;
final String? contactTelSnapshot;
final String? contactAddressSnapshot;
final String? companySnapshot; // 追加: 発行時会社情報スナップショット
final String? companySealHash; // 追加: 角印画像ハッシュ
final String? metaJson;
final String? metaHash;
final String? previousChainHash;
final String? chainHash;
final String chainStatus;
Invoice({
String? id,
required this.customer,
required this.date,
required this.items,
this.notes,
this.filePath,
this.taxRate = 0.10,
this.documentType = DocumentType.invoice, // デフォルト請求書
this.customerFormalNameSnapshot,
this.odooId,
this.isSynced = false,
DateTime? updatedAt,
this.latitude, // 追加
this.longitude, // 追加
String? terminalId, // 追加
this.isDraft = false, // 追加: デフォルトは通常
this.subject, // 追加: 案件
this.isLocked = false,
this.contactVersionId,
this.contactEmailSnapshot,
this.contactTelSnapshot,
this.contactAddressSnapshot,
this.companySnapshot,
this.companySealHash,
this.metaJson,
this.metaHash,
this.previousChainHash,
this.chainHash,
this.chainStatus = 'pending',
}) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString(),
terminalId = terminalId ?? "T1", // デフォルト端末ID
updatedAt = updatedAt ?? DateTime.now();
/// 伝票内容から決定論的なハッシュを生成する (SHA256の一部)
String get contentHash {
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);
return sha256.convert(bytes).toString().substring(0, 8).toUpperCase();
}
String get documentTypeName {
switch (documentType) {
case DocumentType.estimation: return "見積書";
case DocumentType.delivery: return "納品書";
case DocumentType.invoice: return "請求書";
case DocumentType.receipt: return "領収書";
}
}
static const Map<DocumentType, String> _docTypeShortLabel = {
DocumentType.estimation: '見積',
DocumentType.delivery: '納品',
DocumentType.invoice: '請求',
DocumentType.receipt: '領収',
};
String get invoiceNumberPrefix {
switch (documentType) {
case DocumentType.estimation: return "EST";
case DocumentType.delivery: return "DEL";
case DocumentType.invoice: return "INV";
case DocumentType.receipt: return "REC";
}
}
String get invoiceNumber => "$invoiceNumberPrefix-$terminalId-${DateFormat('yyyyMMdd').format(date)}-${id.substring(id.length > 4 ? id.length - 4 : 0)}";
// 表示用の宛名(スナップショットがあれば優先)。必ず敬称を付与。
String get customerNameForDisplay {
final base = customerFormalNameSnapshot ?? customer.formalName;
final hasHonorific = RegExp(r'(様|御中|殿)$').hasMatch(base);
return hasHonorific ? base : '$base ${customer.title}';
}
int get subtotal => items.fold(0, (sum, item) => sum + item.subtotal);
int get tax => (subtotal * taxRate).floor();
int get totalAmount => subtotal + tax;
String get _projectLabel {
if (subject != null && subject!.trim().isNotEmpty) {
return subject!.trim();
}
return '案件';
}
String get mailTitleCore {
final dateStr = DateFormat('yyyyMMdd').format(date);
final docLabel = _docTypeShortLabel[documentType] ?? documentTypeName.replaceAll('', '');
final customerCompact = customerNameForDisplay.replaceAll(RegExp(r'\s+'), '');
final amountStr = NumberFormat('#,###').format(totalAmount);
final buffer = StringBuffer()
..write(dateStr)
..write('($docLabel)')
..write(_projectLabel)
..write('@')
..write(customerCompact)
..write('_')
..write(amountStr)
..write('');
final raw = buffer.toString();
return _sanitizeForFile(raw);
}
String get mailAttachmentFileName => '$mailTitleCore.PDF';
String get mailBodyText => '請求書をお送りします。ご確認ください。';
static String _sanitizeForFile(String input) {
var sanitized = input.replaceAll(RegExp(r'[\\/:*?"<>|]'), '-');
sanitized = sanitized.replaceAll(RegExp(r'[\r\n]+'), '');
sanitized = sanitized.replaceAll(' ', '');
sanitized = sanitized.replaceAll(' ', '');
return sanitized;
}
Map<String, dynamic> metaPayload() {
return {
'id': id,
'invoiceNumber': invoiceNumber,
'customer': customerNameForDisplay,
'date': date.toIso8601String(),
'total': totalAmount,
'documentType': documentType.name,
'hash': contentHash,
'lockStatement': lockStatement,
'hashDescription': hashDescription,
'companySnapshot': companySnapshot,
'companySealHash': companySealHash,
};
}
String get metaJsonValue => metaJson ?? jsonEncode(metaPayload());
String get metaHashValue => metaHash ?? sha256.convert(utf8.encode(metaJsonValue)).toString();
Map<String, dynamic> toMap() {
return {
'id': id,
'customer_id': customer.id,
'date': date.toIso8601String(),
'notes': notes,
'file_path': filePath,
'total_amount': totalAmount,
'tax_rate': taxRate,
'document_type': documentType.name, // 追加
'customer_formal_name': customerFormalNameSnapshot ?? customer.formalName,
'odoo_id': odooId,
'is_synced': isSynced ? 1 : 0,
'updated_at': updatedAt.toIso8601String(),
'latitude': latitude, // 追加
'longitude': longitude, // 追加
'terminal_id': terminalId, // 追加
'content_hash': contentHash, // 追加
'is_draft': isDraft ? 1 : 0, // 追加
'subject': subject, // 追加
'is_locked': isLocked ? 1 : 0,
'contact_version_id': contactVersionId,
'contact_email_snapshot': contactEmailSnapshot,
'contact_tel_snapshot': contactTelSnapshot,
'contact_address_snapshot': contactAddressSnapshot,
'company_snapshot': companySnapshot,
'company_seal_hash': companySealHash,
'meta_json': metaJsonValue,
'meta_hash': metaHashValue,
'previous_chain_hash': previousChainHash,
'chain_hash': chainHash,
'chain_status': chainStatus,
};
}
Invoice copyWith({
String? id,
Customer? customer,
DateTime? date,
List<InvoiceItem>? items,
String? notes,
String? filePath,
double? taxRate,
DocumentType? documentType,
String? customerFormalNameSnapshot,
String? odooId,
bool? isSynced,
DateTime? updatedAt,
double? latitude,
double? longitude,
String? terminalId,
bool? isDraft,
String? subject,
bool? isLocked,
int? contactVersionId,
String? contactEmailSnapshot,
String? contactTelSnapshot,
String? contactAddressSnapshot,
String? companySnapshot,
String? companySealHash,
String? metaJson,
String? metaHash,
String? previousChainHash,
String? chainHash,
String? chainStatus,
}) {
return Invoice(
id: id ?? this.id,
customer: customer ?? this.customer,
date: date ?? this.date,
items: items ?? List.from(this.items),
notes: notes ?? this.notes,
filePath: filePath ?? this.filePath,
taxRate: taxRate ?? this.taxRate,
documentType: documentType ?? this.documentType,
customerFormalNameSnapshot: customerFormalNameSnapshot ?? this.customerFormalNameSnapshot,
odooId: odooId ?? this.odooId,
isSynced: isSynced ?? this.isSynced,
updatedAt: updatedAt ?? this.updatedAt,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
terminalId: terminalId ?? this.terminalId,
isDraft: isDraft ?? this.isDraft,
subject: subject ?? this.subject,
isLocked: isLocked ?? this.isLocked,
contactVersionId: contactVersionId ?? this.contactVersionId,
contactEmailSnapshot: contactEmailSnapshot ?? this.contactEmailSnapshot,
contactTelSnapshot: contactTelSnapshot ?? this.contactTelSnapshot,
contactAddressSnapshot: contactAddressSnapshot ?? this.contactAddressSnapshot,
companySnapshot: companySnapshot ?? this.companySnapshot,
companySealHash: companySealHash ?? this.companySealHash,
metaJson: metaJson ?? this.metaJson,
metaHash: metaHash ?? this.metaHash,
previousChainHash: previousChainHash ?? this.previousChainHash,
chainHash: chainHash ?? this.chainHash,
chainStatus: chainStatus ?? this.chainStatus,
);
}
String toCsv() {
final buffer = StringBuffer();
buffer.writeln('伝票種別,伝票番号,日付,取引先,合計金額,緯度,経度');
buffer.writeln('$documentTypeName,$invoiceNumber,${DateFormat('yyyy/MM/dd').format(date)},$customerNameForDisplay,$totalAmount,${latitude ?? ""},${longitude ?? ""}');
buffer.writeln('');
buffer.writeln('品名,数量,単価,小計');
for (var item in items) {
buffer.writeln('${item.description},${item.quantity},${item.unitPrice},${item.subtotal}');
}
return buffer.toString();
}
}