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 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 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, // 領収 } class Invoice { final String id; final Customer customer; final DateTime date; final List 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; 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, }) : 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 "領収書"; } } 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 => customerFormalNameSnapshot ?? customer.formalName; int get subtotal => items.fold(0, (sum, item) => sum + item.subtotal); int get tax => (subtotal * taxRate).floor(); int get totalAmount => subtotal + tax; Map 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, }; } Invoice copyWith({ String? id, Customer? customer, DateTime? date, List? 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, }) { 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, ); } 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(); } }