Co-authored-by: aider (ollama_chat/7b) <aider@aider.chat>
This commit is contained in:
parent
8ae6d1be62
commit
a8242c2a7e
19 changed files with 774 additions and 0 deletions
25
.github/workflows/flutter.yml
vendored
Normal file
25
.github/workflows/flutter.yml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
name: Flutter CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Flutter
|
||||||
|
uses: subosito/flutter-action@v1
|
||||||
|
with:
|
||||||
|
flutter-version: '3.x'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
- name: Run tests
|
||||||
|
run: flutter test
|
||||||
|
- name: Build the app for release
|
||||||
|
run: flutter build apk --release
|
||||||
111
create_invoice_screen.dart
Normal file
111
create_invoice_screen.dart
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'customer_provider.dart';
|
||||||
|
import 'product_provider.dart';
|
||||||
|
import 'invoice_provider.dart';
|
||||||
|
|
||||||
|
class CreateInvoiceScreen extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_CreateInvoiceScreenState createState() => _CreateInvoiceScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreateInvoiceScreenState extends State<CreateInvoiceScreen> {
|
||||||
|
final TextEditingController _quantityController = TextEditingController();
|
||||||
|
|
||||||
|
int? _selectedCustomerId;
|
||||||
|
int? _selectedProductId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final customerProvider = Provider.of<CustomerProvider>(context);
|
||||||
|
final productProvider = Provider.of<ProductProvider>(context);
|
||||||
|
final invoiceProvider = Provider.of<InvoiceProvider>(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('請求書作成'),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
DropdownButtonFormField<int>(
|
||||||
|
value: _selectedCustomerId,
|
||||||
|
items: customerProvider.customers.map((customer) {
|
||||||
|
return DropdownMenuItem<int>(
|
||||||
|
value: customer.id,
|
||||||
|
child: Text(customer.name),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_selectedCustomerId = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(labelText: '顧客選択'),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
DropdownButtonFormField<int>(
|
||||||
|
value: _selectedProductId,
|
||||||
|
items: productProvider.products.map((product) {
|
||||||
|
return DropdownMenuItem<int>(
|
||||||
|
value: product.id,
|
||||||
|
child: Text(product.name),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_selectedProductId = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(labelText: '商品選択'),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
TextField(
|
||||||
|
controller: _quantityController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
decoration: InputDecoration(labelText: '数量'),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
if (_selectedCustomerId != null &&
|
||||||
|
_selectedProductId != null &&
|
||||||
|
_quantityController.text.isNotEmpty) {
|
||||||
|
final quantity = int.parse(_quantityController.text);
|
||||||
|
final product = productProvider.products.firstWhere((p) => p.id == _selectedProductId);
|
||||||
|
|
||||||
|
final invoiceItem = InvoiceItem(
|
||||||
|
invoiceId: 0, // 後で生成される ID
|
||||||
|
productId: _selectedProductId!,
|
||||||
|
quantity: quantity,
|
||||||
|
unitPrice: product.unitPrice,
|
||||||
|
discount: product.discount,
|
||||||
|
);
|
||||||
|
|
||||||
|
final total = (product.unitPrice * quantity) - (product.unitPrice * quantity * product.discount);
|
||||||
|
final tax = total * 0.1; // 簡易的な税率
|
||||||
|
final discountTotal = product.unitPrice * quantity * product.discount;
|
||||||
|
|
||||||
|
final invoice = Invoice(
|
||||||
|
customerId: _selectedCustomerId!,
|
||||||
|
date: DateTime.now().toIso8601String(),
|
||||||
|
total: total,
|
||||||
|
tax: tax,
|
||||||
|
discountTotal: discountTotal,
|
||||||
|
);
|
||||||
|
|
||||||
|
await invoiceProvider.addInvoice(invoice);
|
||||||
|
// 追加の処理(PDF生成など)
|
||||||
|
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text('PDF生成'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
customer.dart
Normal file
35
customer.dart
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
class Customer {
|
||||||
|
final int? id;
|
||||||
|
final String name;
|
||||||
|
final String phone;
|
||||||
|
final String address;
|
||||||
|
final String email;
|
||||||
|
|
||||||
|
Customer({
|
||||||
|
this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.phone,
|
||||||
|
required this.address,
|
||||||
|
required this.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'phone': phone,
|
||||||
|
'address': address,
|
||||||
|
'email': email,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory Customer.fromMap(Map<String, dynamic> map) {
|
||||||
|
return Customer(
|
||||||
|
id: map['id'] as int?,
|
||||||
|
name: map['name'] as String,
|
||||||
|
phone: map['phone'] as String,
|
||||||
|
address: map['address'] as String,
|
||||||
|
email: map['email'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
customer_list_screen.dart
Normal file
35
customer_list_screen.dart
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'customer_provider.dart';
|
||||||
|
|
||||||
|
class CustomerListScreen extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final customerProvider = Provider.of<CustomerProvider>(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('顧客一覧'),
|
||||||
|
),
|
||||||
|
body: FutureBuilder<void>(
|
||||||
|
future: customerProvider.fetchCustomers(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return Center(child: CircularProgressIndicator());
|
||||||
|
} else {
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: customerProvider.customers.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final customer = customerProvider.customers[index];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(customer.name),
|
||||||
|
subtitle: Text(customer.phone),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
customer_provider.dart
Normal file
27
customer_provider.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
import 'db_helper.dart';
|
||||||
|
import 'customer.dart';
|
||||||
|
|
||||||
|
class CustomerProvider with ChangeNotifier {
|
||||||
|
List<Customer> _customers = [];
|
||||||
|
|
||||||
|
List<Customer> get customers => _customers;
|
||||||
|
|
||||||
|
Future<void> fetchCustomers() async {
|
||||||
|
final db = await DbHelper().database;
|
||||||
|
final List<Map<String, dynamic>> maps = await db.query('customers');
|
||||||
|
_customers = List.generate(maps.length, (i) {
|
||||||
|
return Customer.fromMap(maps[i]);
|
||||||
|
});
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addCustomer(Customer customer) async {
|
||||||
|
final db = await DbHelper().database;
|
||||||
|
await db.insert('customers', customer.toMap());
|
||||||
|
fetchCustomers();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加の CRUD メソッドを実装
|
||||||
|
}
|
||||||
41
customer_test.dart
Normal file
41
customer_test.dart
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'db_helper.dart';
|
||||||
|
import 'customer.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Customer Tests', () {
|
||||||
|
late Database db;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
final io.Directory documentsDirectory = await getApplicationDocumentsDirectory();
|
||||||
|
String path = '${documentsDirectory.path}/test.db';
|
||||||
|
db = await openDatabase(path, version: 1, onCreate: DbHelper()._onCreate);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await db.close();
|
||||||
|
final io.Directory documentsDirectory = await getApplicationDocumentsDirectory();
|
||||||
|
String path = '${documentsDirectory.path}/test.db';
|
||||||
|
await io.File(path).delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Add and fetch customer', () async {
|
||||||
|
final customer = Customer(
|
||||||
|
name: 'Test Customer',
|
||||||
|
phone: '1234567890',
|
||||||
|
address: 'Test Address',
|
||||||
|
email: 'test@example.com',
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.insert('customers', customer.toMap());
|
||||||
|
|
||||||
|
final List<Map<String, dynamic>> maps = await db.query('customers');
|
||||||
|
expect(maps.length, 1);
|
||||||
|
|
||||||
|
final fetchedCustomer = Customer.fromMap(maps.first);
|
||||||
|
expect(fetchedCustomer.name, 'Test Customer');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
63
db_helper.dart
Normal file
63
db_helper.dart
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'dart:io' as io;
|
||||||
|
|
||||||
|
class DbHelper {
|
||||||
|
static Database? _database;
|
||||||
|
|
||||||
|
Future<Database> get database async {
|
||||||
|
if (_database != null) return _database!;
|
||||||
|
_database = await initDb();
|
||||||
|
return _database!;
|
||||||
|
}
|
||||||
|
|
||||||
|
initDb() async {
|
||||||
|
io.Directory documentsDirectory = await getApplicationDocumentsDirectory();
|
||||||
|
String path = '${documentsDirectory.path}/invoice.db';
|
||||||
|
var db = await openDatabase(path, version: 1, onCreate: _onCreate);
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onCreate(Database db, int version) async {
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE customers (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT,
|
||||||
|
phone TEXT,
|
||||||
|
address TEXT,
|
||||||
|
email TEXT
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE products (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT,
|
||||||
|
unit_price REAL,
|
||||||
|
discount REAL
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE invoices (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
customer_id INTEGER,
|
||||||
|
date TEXT,
|
||||||
|
total REAL,
|
||||||
|
tax REAL,
|
||||||
|
discount_total REAL
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE invoice_items (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
invoice_id INTEGER,
|
||||||
|
product_id INTEGER,
|
||||||
|
quantity INTEGER,
|
||||||
|
unit_price REAL,
|
||||||
|
discount REAL
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
}
|
||||||
|
}
|
||||||
39
invoice.dart
Normal file
39
invoice.dart
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
class Invoice {
|
||||||
|
final int? id;
|
||||||
|
final int customerId;
|
||||||
|
final String date;
|
||||||
|
final double total;
|
||||||
|
final double tax;
|
||||||
|
final double discountTotal;
|
||||||
|
|
||||||
|
Invoice({
|
||||||
|
this.id,
|
||||||
|
required this.customerId,
|
||||||
|
required this.date,
|
||||||
|
required this.total,
|
||||||
|
required this.tax,
|
||||||
|
required this.discountTotal,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'customer_id': customerId,
|
||||||
|
'date': date,
|
||||||
|
'total': total,
|
||||||
|
'tax': tax,
|
||||||
|
'discount_total': discountTotal,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory Invoice.fromMap(Map<String, dynamic> map) {
|
||||||
|
return Invoice(
|
||||||
|
id: map['id'] as int?,
|
||||||
|
customerId: map['customer_id'] as int,
|
||||||
|
date: map['date'] as String,
|
||||||
|
total: map['total'] as double,
|
||||||
|
tax: map['tax'] as double,
|
||||||
|
discountTotal: map['discount_total'] as double,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
invoice_item.dart
Normal file
39
invoice_item.dart
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
class InvoiceItem {
|
||||||
|
final int? id;
|
||||||
|
final int invoiceId;
|
||||||
|
final int productId;
|
||||||
|
final int quantity;
|
||||||
|
final double unitPrice;
|
||||||
|
final double discount;
|
||||||
|
|
||||||
|
InvoiceItem({
|
||||||
|
this.id,
|
||||||
|
required this.invoiceId,
|
||||||
|
required this.productId,
|
||||||
|
required this.quantity,
|
||||||
|
required this.unitPrice,
|
||||||
|
required this.discount,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'invoice_id': invoiceId,
|
||||||
|
'product_id': productId,
|
||||||
|
'quantity': quantity,
|
||||||
|
'unit_price': unitPrice,
|
||||||
|
'discount': discount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory InvoiceItem.fromMap(Map<String, dynamic> map) {
|
||||||
|
return InvoiceItem(
|
||||||
|
id: map['id'] as int?,
|
||||||
|
invoiceId: map['invoice_id'] as int,
|
||||||
|
productId: map['product_id'] as int,
|
||||||
|
quantity: map['quantity'] as int,
|
||||||
|
unitPrice: map['unit_price'] as double,
|
||||||
|
discount: map['discount'] as double,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
invoice_provider.dart
Normal file
27
invoice_provider.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
import 'db_helper.dart';
|
||||||
|
import 'invoice.dart';
|
||||||
|
|
||||||
|
class InvoiceProvider with ChangeNotifier {
|
||||||
|
List<Invoice> _invoices = [];
|
||||||
|
|
||||||
|
List<Invoice> get invoices => _invoices;
|
||||||
|
|
||||||
|
Future<void> fetchInvoices() async {
|
||||||
|
final db = await DbHelper().database;
|
||||||
|
final List<Map<String, dynamic>> maps = await db.query('invoices');
|
||||||
|
_invoices = List.generate(maps.length, (i) {
|
||||||
|
return Invoice.fromMap(maps[i]);
|
||||||
|
});
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addInvoice(Invoice invoice) async {
|
||||||
|
final db = await DbHelper().database;
|
||||||
|
await db.insert('invoices', invoice.toMap());
|
||||||
|
fetchInvoices();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加の CRUD メソッドを実装
|
||||||
|
}
|
||||||
42
invoice_test.dart
Normal file
42
invoice_test.dart
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'db_helper.dart';
|
||||||
|
import 'invoice.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Invoice Tests', () {
|
||||||
|
late Database db;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
final io.Directory documentsDirectory = await getApplicationDocumentsDirectory();
|
||||||
|
String path = '${documentsDirectory.path}/test.db';
|
||||||
|
db = await openDatabase(path, version: 1, onCreate: DbHelper()._onCreate);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await db.close();
|
||||||
|
final io.Directory documentsDirectory = await getApplicationDocumentsDirectory();
|
||||||
|
String path = '${documentsDirectory.path}/test.db';
|
||||||
|
await io.File(path).delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Add and fetch invoice', () async {
|
||||||
|
final invoice = Invoice(
|
||||||
|
customerId: 1,
|
||||||
|
date: DateTime.now().toIso8601String(),
|
||||||
|
total: 90.0,
|
||||||
|
tax: 9.0,
|
||||||
|
discountTotal: 10.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.insert('invoices', invoice.toMap());
|
||||||
|
|
||||||
|
final List<Map<String, dynamic>> maps = await db.query('invoices');
|
||||||
|
expect(maps.length, 1);
|
||||||
|
|
||||||
|
final fetchedInvoice = Invoice.fromMap(maps.first);
|
||||||
|
expect(fetchedInvoice.customerId, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
66
main.dart
Normal file
66
main.dart
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'customer_provider.dart';
|
||||||
|
import 'product_provider.dart';
|
||||||
|
import 'invoice_provider.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(MyApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider(create: (_) => CustomerProvider()),
|
||||||
|
ChangeNotifierProvider(create: (_) => ProductProvider()),
|
||||||
|
ChangeNotifierProvider(create: (_) => InvoiceProvider()),
|
||||||
|
],
|
||||||
|
child: MaterialApp(
|
||||||
|
title: 'Invoice App',
|
||||||
|
theme: ThemeData(
|
||||||
|
primarySwatch: Colors.blue,
|
||||||
|
),
|
||||||
|
home: HomeScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HomeScreen extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Invoice App'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pushNamed(context, '/create_invoice');
|
||||||
|
},
|
||||||
|
child: Text('請求書作成'),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pushNamed(context, '/customer_list');
|
||||||
|
},
|
||||||
|
child: Text('顧客一覧'),
|
||||||
|
),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pushNamed(context, '/product_list');
|
||||||
|
},
|
||||||
|
child: Text('商品一覧'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
path/to/filename.js
Normal file
2
path/to/filename.js
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
// entire file content ...
|
||||||
|
// ... goes in between
|
||||||
67
pdf_generator.dart
Normal file
67
pdf_generator.dart
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:pdf/pdf.dart';
|
||||||
|
import 'package:printing/printing.dart';
|
||||||
|
|
||||||
|
class PdfGenerator {
|
||||||
|
static Future<void> generatePdf(Invoice invoice, List<InvoiceItem> items) async {
|
||||||
|
final pdf = pw.Document();
|
||||||
|
|
||||||
|
pdf.addPage(
|
||||||
|
pw.Page(
|
||||||
|
build: (pw.Context context) => pw.Column(
|
||||||
|
children: [
|
||||||
|
pw.Text('請求書'),
|
||||||
|
pw.SizedBox(height: 20),
|
||||||
|
pw.Text('顧客名: ${invoice.customerId}'), // 後で顧客名に変更
|
||||||
|
pw.Text('日付: ${invoice.date}'),
|
||||||
|
pw.SizedBox(height: 20),
|
||||||
|
pw.Table(
|
||||||
|
border: pw.TableBorder.all(),
|
||||||
|
children: [
|
||||||
|
pw.TableRow(children: [
|
||||||
|
pw.Text('商品名'),
|
||||||
|
pw.Text('数量'),
|
||||||
|
pw.Text('単価'),
|
||||||
|
pw.Text('値引き'),
|
||||||
|
pw.Text('小計'),
|
||||||
|
]),
|
||||||
|
for (var item in items)
|
||||||
|
pw.TableRow(children: [
|
||||||
|
pw.Text(item.productId.toString()), // 後で商品名に変更
|
||||||
|
pw.Text(item.quantity.toString()),
|
||||||
|
pw.Text(item.unitPrice.toStringAsFixed(2)),
|
||||||
|
pw.Text((item.discount * 100).toStringAsFixed(2) + '%'),
|
||||||
|
pw.Text((item.unitPrice * item.quantity - (item.unitPrice * item.quantity * item.discount)).toStringAsFixed(2)),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 20),
|
||||||
|
pw.Row(
|
||||||
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
pw.Text('合計'),
|
||||||
|
pw.Text(invoice.total.toStringAsFixed(2)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
pw.Row(
|
||||||
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
pw.Text('税額'),
|
||||||
|
pw.Text(invoice.tax.toStringAsFixed(2)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
pw.Row(
|
||||||
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
pw.Text('値引き合計'),
|
||||||
|
pw.Text(invoice.discountTotal.toStringAsFixed(2)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Printing.sharePdf(bytes: pdf.save(), filename: 'invoice.pdf');
|
||||||
|
}
|
||||||
|
}
|
||||||
31
product.dart
Normal file
31
product.dart
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
class Product {
|
||||||
|
final int? id;
|
||||||
|
final String name;
|
||||||
|
final double unitPrice;
|
||||||
|
final double discount;
|
||||||
|
|
||||||
|
Product({
|
||||||
|
this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.unitPrice,
|
||||||
|
required this.discount,
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'unit_price': unitPrice,
|
||||||
|
'discount': discount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory Product.fromMap(Map<String, dynamic> map) {
|
||||||
|
return Product(
|
||||||
|
id: map['id'] as int?,
|
||||||
|
name: map['name'] as String,
|
||||||
|
unitPrice: map['unit_price'] as double,
|
||||||
|
discount: map['discount'] as double,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
product_list_screen.dart
Normal file
35
product_list_screen.dart
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'product_provider.dart';
|
||||||
|
|
||||||
|
class ProductListScreen extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final productProvider = Provider.of<ProductProvider>(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('商品一覧'),
|
||||||
|
),
|
||||||
|
body: FutureBuilder<void>(
|
||||||
|
future: productProvider.fetchProducts(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return Center(child: CircularProgressIndicator());
|
||||||
|
} else {
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: productProvider.products.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final product = productProvider.products[index];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(product.name),
|
||||||
|
subtitle: Text('単価: ${product.unitPrice}, 値引き: ${product.discount}'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
product_provider.dart
Normal file
27
product_provider.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
import 'db_helper.dart';
|
||||||
|
import 'product.dart';
|
||||||
|
|
||||||
|
class ProductProvider with ChangeNotifier {
|
||||||
|
List<Product> _products = [];
|
||||||
|
|
||||||
|
List<Product> get products => _products;
|
||||||
|
|
||||||
|
Future<void> fetchProducts() async {
|
||||||
|
final db = await DbHelper().database;
|
||||||
|
final List<Map<String, dynamic>> maps = await db.query('products');
|
||||||
|
_products = List.generate(maps.length, (i) {
|
||||||
|
return Product.fromMap(maps[i]);
|
||||||
|
});
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addProduct(Product product) async {
|
||||||
|
final db = await DbHelper().database;
|
||||||
|
await db.insert('products', product.toMap());
|
||||||
|
fetchProducts();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加の CRUD メソッドを実装
|
||||||
|
}
|
||||||
40
product_test.dart
Normal file
40
product_test.dart
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:sqflite/sqflite.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'db_helper.dart';
|
||||||
|
import 'product.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Product Tests', () {
|
||||||
|
late Database db;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
final io.Directory documentsDirectory = await getApplicationDocumentsDirectory();
|
||||||
|
String path = '${documentsDirectory.path}/test.db';
|
||||||
|
db = await openDatabase(path, version: 1, onCreate: DbHelper()._onCreate);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await db.close();
|
||||||
|
final io.Directory documentsDirectory = await getApplicationDocumentsDirectory();
|
||||||
|
String path = '${documentsDirectory.path}/test.db';
|
||||||
|
await io.File(path).delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Add and fetch product', () async {
|
||||||
|
final product = Product(
|
||||||
|
name: 'Test Product',
|
||||||
|
unitPrice: 100.0,
|
||||||
|
discount: 0.1,
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.insert('products', product.toMap());
|
||||||
|
|
||||||
|
final List<Map<String, dynamic>> maps = await db.query('products');
|
||||||
|
expect(maps.length, 1);
|
||||||
|
|
||||||
|
final fetchedProduct = Product.fromMap(maps.first);
|
||||||
|
expect(fetchedProduct.name, 'Test Product');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
22
pubspec.yaml
Normal file
22
pubspec.yaml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
name: invoice_app
|
||||||
|
description: A new Flutter project.
|
||||||
|
|
||||||
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
|
version: 1.0.0+1
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
sqflite: ^2.0.0+3
|
||||||
|
path_provider: ^2.0.11
|
||||||
|
pdf: ^3.6.0
|
||||||
|
printing: ^5.9.3
|
||||||
|
provider: ^6.0.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
Loading…
Reference in a new issue