feat: Implement invoice CSV export, add customer title field, and refine invoice item properties and copy method.

This commit is contained in:
joe 2026-02-14 18:59:59 +09:00
parent 5e965b682a
commit 2459be9cca
3 changed files with 93 additions and 8 deletions

View file

@ -4,6 +4,7 @@ class Customer {
final String id;
final String displayName; //
final String formalName; //
final String title; // 殿
final String? department; //
final String? address; //
@ -11,21 +12,24 @@ class Customer {
required this.id,
required this.displayName,
required this.formalName,
this.title = "",
this.department,
this.address,
});
String get invoiceName {
String name = formalName;
if (department != null && department!.isNotEmpty) {
return "$formalName\n$department";
name = "$formalName\n$department";
}
return formalName;
return "$name $title";
}
Customer copyWith({
String? id,
String? displayName,
String? formalName,
String? title,
String? department,
String? address,
}) {
@ -33,6 +37,7 @@ class Customer {
id: id ?? this.id,
displayName: displayName ?? this.displayName,
formalName: formalName ?? this.formalName,
title: title ?? this.title,
department: department ?? this.department,
address: address ?? this.address,
);

View file

@ -2,9 +2,9 @@ import 'customer_model.dart';
import 'package:intl/intl.dart';
class InvoiceItem {
final String description;
final num quantity;
final int unitPrice;
String description;
int quantity;
int unitPrice;
InvoiceItem({
required this.description,
@ -12,7 +12,7 @@ class InvoiceItem {
required this.unitPrice,
});
int get subtotal => (quantity * unitPrice).floor();
int get subtotal => quantity * unitPrice;
}
class Invoice {
@ -32,12 +32,30 @@ class Invoice {
this.filePath,
}) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString();
String get invoiceNumber => "INV-${DateFormat('yyyyMMdd').format(date)}-${id.substring(id.length - 4)}";
String get invoiceNumber => "INV-${DateFormat('yyyyMMdd').format(date)}-${id.substring(id.length > 4 ? id.length - 4 : 0)}";
int get subtotal => items.fold(0, (sum, item) => sum + item.subtotal);
int get tax => (subtotal * 0.1).floor();
int get totalAmount => subtotal + tax;
String toCsv() {
final dateFormatter = DateFormat('yyyy/MM/dd');
final amountFormatter = NumberFormat("###");
StringBuffer buffer = StringBuffer();
// ()
buffer.writeln("日付,請求番号,取引先,合計金額,備考");
buffer.writeln("${dateFormatter.format(date)},$invoiceNumber,${customer.formalName},$totalAmount,${notes ?? ""}");
buffer.writeln("");
buffer.writeln("品名,数量,単価,小計");
for (var item in items) {
buffer.writeln("${item.description},${item.quantity},${item.unitPrice},${item.subtotal}");
}
return buffer.toString();
}
Invoice copyWith({
String? id,
Customer? customer,
@ -50,7 +68,7 @@ class Invoice {
id: id ?? this.id,
customer: customer ?? this.customer,
date: date ?? this.date,
items: items ?? this.items,
items: items ?? List.from(this.items), //
notes: notes ?? this.notes,
filePath: filePath ?? this.filePath,
);

View file

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import '../models/invoice_models.dart';
///
class ProductPickerModal extends StatefulWidget {
final Function(InvoiceItem) onItemSelected;
const ProductPickerModal({Key? key, required this.onItemSelected}) : super(key: key);
@override
State<ProductPickerModal> createState() => _ProductPickerModalState();
}
class _ProductPickerModalState extends State<ProductPickerModal> {
//
final List<InvoiceItem> _masterProducts = [
InvoiceItem(description: "技術料", quantity: 1, unitPrice: 50000),
InvoiceItem(description: "部品代 A", quantity: 1, unitPrice: 15000),
InvoiceItem(description: "部品代 B", quantity: 1, unitPrice: 3000),
InvoiceItem(description: "出張費", quantity: 1, unitPrice: 10000),
InvoiceItem(description: "諸経費", quantity: 1, unitPrice: 5000),
];
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("商品・サービス選択", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context)),
],
),
),
const Divider(),
Expanded(
child: ListView.builder(
itemCount: _masterProducts.length,
itemBuilder: (context, index) {
final product = _masterProducts[index];
return ListTile(
leading: const Icon(Icons.inventory_2_outlined),
title: Text(product.description),
subtitle: Text("単価: ¥${product.unitPrice}"),
onTap: () => widget.onItemSelected(product),
);
},
),
),
],
),
);
}
}