feat: Implement invoice CSV export, add customer title field, and refine invoice item properties and copy method.
This commit is contained in:
parent
5e965b682a
commit
2459be9cca
3 changed files with 93 additions and 8 deletions
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
62
lib/screens/product_picker_modal.dart
Normal file
62
lib/screens/product_picker_modal.dart
Normal 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),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue