feat: Implement barcode scanning for products, add sample product generation, and update dependencies.

This commit is contained in:
joe 2026-02-14 20:08:44 +09:00
parent 70c902885a
commit 4bc682f887
25 changed files with 655 additions and 49 deletions

View file

@ -18,12 +18,30 @@
@import geolocator_apple; @import geolocator_apple;
#endif #endif
#if __has_include(<image_picker_ios/FLTImagePickerPlugin.h>)
#import <image_picker_ios/FLTImagePickerPlugin.h>
#else
@import image_picker_ios;
#endif
#if __has_include(<mobile_scanner/MobileScannerPlugin.h>)
#import <mobile_scanner/MobileScannerPlugin.h>
#else
@import mobile_scanner;
#endif
#if __has_include(<open_filex/OpenFilePlugin.h>) #if __has_include(<open_filex/OpenFilePlugin.h>)
#import <open_filex/OpenFilePlugin.h> #import <open_filex/OpenFilePlugin.h>
#else #else
@import open_filex; @import open_filex;
#endif #endif
#if __has_include(<package_info_plus/FPPPackageInfoPlusPlugin.h>)
#import <package_info_plus/FPPPackageInfoPlusPlugin.h>
#else
@import package_info_plus;
#endif
#if __has_include(<permission_handler_apple/PermissionHandlerPlugin.h>) #if __has_include(<permission_handler_apple/PermissionHandlerPlugin.h>)
#import <permission_handler_apple/PermissionHandlerPlugin.h> #import <permission_handler_apple/PermissionHandlerPlugin.h>
#else #else
@ -53,7 +71,10 @@
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry { + (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
[FlutterContactsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterContactsPlugin"]]; [FlutterContactsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterContactsPlugin"]];
[GeolocatorPlugin registerWithRegistrar:[registry registrarForPlugin:@"GeolocatorPlugin"]]; [GeolocatorPlugin registerWithRegistrar:[registry registrarForPlugin:@"GeolocatorPlugin"]];
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];
[MobileScannerPlugin registerWithRegistrar:[registry registrarForPlugin:@"MobileScannerPlugin"]];
[OpenFilePlugin registerWithRegistrar:[registry registrarForPlugin:@"OpenFilePlugin"]]; [OpenFilePlugin registerWithRegistrar:[registry registrarForPlugin:@"OpenFilePlugin"]];
[FPPPackageInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPPackageInfoPlusPlugin"]];
[PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]]; [PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]];
[FPPSharePlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPSharePlusPlugin"]]; [FPPSharePlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPSharePlusPlugin"]];
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]]; [SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];

View file

@ -0,0 +1,58 @@
class CompanyInfo {
final String name;
final String? zipCode;
final String? address;
final String? tel;
final double defaultTaxRate;
final String? sealPath; //
CompanyInfo({
required this.name,
this.zipCode,
this.address,
this.tel,
this.defaultTaxRate = 0.10,
this.sealPath,
});
Map<String, dynamic> toMap() {
return {
'id': 1, // 1
'name': name,
'zip_code': zipCode,
'address': address,
'tel': tel,
'default_tax_rate': defaultTaxRate,
'seal_path': sealPath,
};
}
factory CompanyInfo.fromMap(Map<String, dynamic> map) {
return CompanyInfo(
name: map['name'] ?? "自社名未設定",
zipCode: map['zip_code'],
address: map['address'],
tel: map['tel'],
defaultTaxRate: map['default_tax_rate'] ?? 0.10,
sealPath: map['seal_path'],
);
}
CompanyInfo copyWith({
String? name,
String? zipCode,
String? address,
String? tel,
double? defaultTaxRate,
String? sealPath,
}) {
return CompanyInfo(
name: name ?? this.name,
zipCode: zipCode ?? this.zipCode,
address: address ?? this.address,
tel: tel ?? this.tel,
defaultTaxRate: defaultTaxRate ?? this.defaultTaxRate,
sealPath: sealPath ?? this.sealPath,
);
}
}

View file

@ -43,7 +43,8 @@ class Invoice {
final List<InvoiceItem> items; final List<InvoiceItem> items;
final String? notes; final String? notes;
final String? filePath; final String? filePath;
final double taxRate; // final double taxRate;
final String? customerFormalNameSnapshot; //
final String? odooId; final String? odooId;
final bool isSynced; final bool isSynced;
final DateTime updatedAt; final DateTime updatedAt;
@ -56,6 +57,7 @@ class Invoice {
this.notes, this.notes,
this.filePath, this.filePath,
this.taxRate = 0.10, // 10% this.taxRate = 0.10, // 10%
this.customerFormalNameSnapshot, //
this.odooId, this.odooId,
this.isSynced = false, this.isSynced = false,
DateTime? updatedAt, DateTime? updatedAt,
@ -64,6 +66,9 @@ class Invoice {
String get invoiceNumber => "INV-${DateFormat('yyyyMMdd').format(date)}-${id.substring(id.length > 4 ? id.length - 4 : 0)}"; String get invoiceNumber => "INV-${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 subtotal => items.fold(0, (sum, item) => sum + item.subtotal);
int get tax => (subtotal * taxRate).floor(); // taxRateを使用 int get tax => (subtotal * taxRate).floor(); // taxRateを使用
int get totalAmount => subtotal + tax; int get totalAmount => subtotal + tax;
@ -77,6 +82,7 @@ class Invoice {
'file_path': filePath, 'file_path': filePath,
'total_amount': totalAmount, 'total_amount': totalAmount,
'tax_rate': taxRate, // 'tax_rate': taxRate, //
'customer_formal_name': customerFormalNameSnapshot ?? customer.formalName, //
'odoo_id': odooId, 'odoo_id': odooId,
'is_synced': isSynced ? 1 : 0, 'is_synced': isSynced ? 1 : 0,
'updated_at': updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
@ -91,7 +97,7 @@ class Invoice {
StringBuffer buffer = StringBuffer(); StringBuffer buffer = StringBuffer();
buffer.writeln("日付,請求番号,取引先,合計金額,備考"); buffer.writeln("日付,請求番号,取引先,合計金額,備考");
buffer.writeln("${dateFormatter.format(date)},$invoiceNumber,${customer.formalName},$totalAmount,${notes ?? ""}"); buffer.writeln("${dateFormatter.format(date)},$invoiceNumber,$customerNameForDisplay,$totalAmount,${notes ?? ""}");
buffer.writeln(""); buffer.writeln("");
buffer.writeln("品名,数量,単価,小計"); buffer.writeln("品名,数量,単価,小計");
@ -110,6 +116,7 @@ class Invoice {
String? notes, String? notes,
String? filePath, String? filePath,
double? taxRate, double? taxRate,
String? customerFormalNameSnapshot,
String? odooId, String? odooId,
bool? isSynced, bool? isSynced,
DateTime? updatedAt, DateTime? updatedAt,
@ -122,6 +129,7 @@ class Invoice {
notes: notes ?? this.notes, notes: notes ?? this.notes,
filePath: filePath ?? this.filePath, filePath: filePath ?? this.filePath,
taxRate: taxRate ?? this.taxRate, taxRate: taxRate ?? this.taxRate,
customerFormalNameSnapshot: customerFormalNameSnapshot ?? this.customerFormalNameSnapshot,
odooId: odooId ?? this.odooId, odooId: odooId ?? this.odooId,
isSynced: isSynced ?? this.isSynced, isSynced: isSynced ?? this.isSynced,
updatedAt: updatedAt ?? this.updatedAt, updatedAt: updatedAt ?? this.updatedAt,

View file

@ -2,12 +2,14 @@ class Product {
final String id; final String id;
final String name; final String name;
final int defaultUnitPrice; final int defaultUnitPrice;
final String? barcode;
final String? odooId; final String? odooId;
Product({ Product({
required this.id, required this.id,
required this.name, required this.name,
this.defaultUnitPrice = 0, this.defaultUnitPrice = 0,
this.barcode,
this.odooId, this.odooId,
}); });
@ -16,6 +18,7 @@ class Product {
'id': id, 'id': id,
'name': name, 'name': name,
'default_unit_price': defaultUnitPrice, 'default_unit_price': defaultUnitPrice,
'barcode': barcode,
'odoo_id': odooId, 'odoo_id': odooId,
}; };
} }
@ -25,6 +28,7 @@ class Product {
id: map['id'], id: map['id'],
name: map['name'], name: map['name'],
defaultUnitPrice: map['default_unit_price'] ?? 0, defaultUnitPrice: map['default_unit_price'] ?? 0,
barcode: map['barcode'],
odooId: map['odoo_id'], odooId: map['odoo_id'],
); );
} }
@ -33,6 +37,7 @@ class Product {
String? id, String? id,
String? name, String? name,
int? defaultUnitPrice, int? defaultUnitPrice,
String? barcode,
String? odooId, String? odooId,
}) { }) {
return Product( return Product(

View file

@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class BarcodeScannerScreen extends StatelessWidget {
const BarcodeScannerScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("バーコードスキャン"),
backgroundColor: Colors.black,
),
body: MobileScanner(
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
if (barcodes.isNotEmpty) {
final String? code = barcodes.first.rawValue;
if (code != null) {
Navigator.pop(context, code);
}
}
},
),
);
}
}

View file

@ -0,0 +1,128 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import '../models/company_model.dart';
import '../services/company_repository.dart';
class CompanyInfoScreen extends StatefulWidget {
const CompanyInfoScreen({Key? key}) : super(key: key);
@override
State<CompanyInfoScreen> createState() => _CompanyInfoScreenState();
}
class _CompanyInfoScreenState extends State<CompanyInfoScreen> {
final CompanyRepository _companyRepo = CompanyRepository();
late CompanyInfo _info;
bool _isLoading = true;
final _nameController = TextEditingController();
final _zipController = TextEditingController();
final _addressController = TextEditingController();
final _telController = TextEditingController();
double _taxRate = 0.10;
@override
void initState() {
super.initState();
_loadInfo();
}
Future<void> _loadInfo() async {
_info = await _companyRepo.getCompanyInfo();
_nameController.text = _info.name;
_zipController.text = _info.zipCode ?? "";
_addressController.text = _info.address ?? "";
_telController.text = _info.tel ?? "";
_taxRate = _info.defaultTaxRate;
setState(() => _isLoading = false);
}
Future<void> _pickImage() async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.camera);
if (image != null) {
setState(() {
_info = _info.copyWith(sealPath: image.path);
});
}
}
Future<void> _save() async {
final updated = _info.copyWith(
name: _nameController.text,
zipCode: _zipController.text,
address: _addressController.text,
tel: _telController.text,
defaultTaxRate: _taxRate,
);
await _companyRepo.saveCompanyInfo(updated);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("自社情報を保存しました")));
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
if (_isLoading) return const Scaffold(body: Center(child: CircularProgressIndicator()));
return Scaffold(
appBar: AppBar(
title: const Text("自社設定"),
backgroundColor: Colors.indigo,
actions: [
IconButton(icon: const Icon(Icons.check), onPressed: _save),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTextField("自社名", _nameController),
const SizedBox(height: 12),
_buildTextField("郵便番号", _zipController),
const SizedBox(height: 12),
_buildTextField("住所", _addressController),
const SizedBox(height: 12),
_buildTextField("電話番号", _telController),
const SizedBox(height: 20),
const Text("デフォルト消費税率", style: TextStyle(fontWeight: FontWeight.bold)),
Row(
children: [
ChoiceChip(label: const Text("10%"), selected: _taxRate == 0.10, onSelected: (_) => setState(() => _taxRate = 0.10)),
const SizedBox(width: 8),
ChoiceChip(label: const Text("8%"), selected: _taxRate == 0.08, onSelected: (_) => setState(() => _taxRate = 0.08)),
],
),
const SizedBox(height: 24),
const Text("印影(角印)撮影", style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
GestureDetector(
onTap: _pickImage,
child: Container(
height: 150,
width: 150,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: _info.sealPath != null
? Image.file(File(_info.sealPath!), fit: BoxFit.contain)
: const Center(child: Icon(Icons.camera_alt, size: 50, color: Colors.grey)),
),
),
const SizedBox(height: 8),
const Text("白い紙に押した判子を真上から撮影してください", style: TextStyle(fontSize: 12, color: Colors.grey)),
],
),
),
);
}
Widget _buildTextField(String label, TextEditingController controller) {
return TextField(
controller: controller,
decoration: InputDecoration(labelText: label, border: const OutlineInputBorder()),
);
}
}

View file

@ -205,7 +205,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
decoration: const InputDecoration(labelText: "備考", border: OutlineInputBorder()), decoration: const InputDecoration(labelText: "備考", border: OutlineInputBorder()),
), ),
] else ...[ ] else ...[
Text("${_currentInvoice.customer.formalName} ${_currentInvoice.customer.title}", Text("${_currentInvoice.customerNameForDisplay} ${_currentInvoice.customer.title}",
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
if (_currentInvoice.customer.department != null && _currentInvoice.customer.department!.isNotEmpty) if (_currentInvoice.customer.department != null && _currentInvoice.customer.department!.isNotEmpty)
Text(_currentInvoice.customer.department!, style: const TextStyle(fontSize: 16)), Text(_currentInvoice.customer.department!, style: const TextStyle(fontSize: 16)),

View file

@ -6,8 +6,10 @@ import '../services/invoice_repository.dart';
import '../services/customer_repository.dart'; import '../services/customer_repository.dart';
import 'invoice_detail_page.dart'; import 'invoice_detail_page.dart';
import 'management_screen.dart'; import 'management_screen.dart';
import 'company_info_screen.dart';
import '../widgets/slide_to_unlock.dart'; import '../widgets/slide_to_unlock.dart';
import '../main.dart'; // InvoiceFlowScreen import '../main.dart'; // InvoiceFlowScreen
import 'package:package_info_plus/package_info_plus.dart';
class InvoiceHistoryScreen extends StatefulWidget { class InvoiceHistoryScreen extends StatefulWidget {
const InvoiceHistoryScreen({Key? key}) : super(key: key); const InvoiceHistoryScreen({Key? key}) : super(key: key);
@ -27,11 +29,20 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
String _sortBy = "date"; // "date", "amount", "customer" String _sortBy = "date"; // "date", "amount", "customer"
DateTime? _startDate; DateTime? _startDate;
DateTime? _endDate; DateTime? _endDate;
String _appVersion = "1.0.0";
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadData(); _loadData();
_loadVersion();
}
Future<void> _loadVersion() async {
final packageInfo = await PackageInfo.fromPlatform();
setState(() {
_appVersion = packageInfo.version;
});
} }
Future<void> _loadData() async { Future<void> _loadData() async {
@ -49,7 +60,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
setState(() { setState(() {
_filteredInvoices = _invoices.where((inv) { _filteredInvoices = _invoices.where((inv) {
final query = _searchQuery.toLowerCase(); final query = _searchQuery.toLowerCase();
final matchesQuery = inv.customer.formalName.toLowerCase().contains(query) || final matchesQuery = inv.customerNameForDisplay.toLowerCase().contains(query) ||
inv.invoiceNumber.toLowerCase().contains(query) || inv.invoiceNumber.toLowerCase().contains(query) ||
(inv.notes?.toLowerCase().contains(query) ?? false); (inv.notes?.toLowerCase().contains(query) ?? false);
@ -65,7 +76,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
} else if (_sortBy == "amount") { } else if (_sortBy == "amount") {
_filteredInvoices.sort((a, b) => b.totalAmount.compareTo(a.totalAmount)); _filteredInvoices.sort((a, b) => b.totalAmount.compareTo(a.totalAmount));
} else if (_sortBy == "customer") { } else if (_sortBy == "customer") {
_filteredInvoices.sort((a, b) => a.customer.formalName.compareTo(b.customer.formalName)); _filteredInvoices.sort((a, b) => a.customerNameForDisplay.compareTo(b.customerNameForDisplay));
} }
}); });
} }
@ -88,7 +99,15 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("伝票マスター一覧"), title: GestureDetector(
onLongPress: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const CompanyInfoScreen()),
).then((_) => _loadData());
},
child: Text("伝票マスター v$_appVersion"),
),
backgroundColor: _isUnlocked ? Colors.blueGrey : Colors.blueGrey.shade800, backgroundColor: _isUnlocked ? Colors.blueGrey : Colors.blueGrey.shade800,
actions: [ actions: [
if (_isUnlocked) if (_isUnlocked)
@ -240,7 +259,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
backgroundColor: _isUnlocked ? Colors.indigo.shade100 : Colors.grey.shade200, backgroundColor: _isUnlocked ? Colors.indigo.shade100 : Colors.grey.shade200,
child: Icon(Icons.description_outlined, color: _isUnlocked ? Colors.indigo : Colors.grey), child: Icon(Icons.description_outlined, color: _isUnlocked ? Colors.indigo : Colors.grey),
), ),
title: Text(invoice.customer.formalName), title: Text(invoice.customerNameForDisplay),
subtitle: Text("${dateFormatter.format(invoice.date)} - ${invoice.invoiceNumber}"), subtitle: Text("${dateFormatter.format(invoice.date)} - ${invoice.invoiceNumber}"),
trailing: Column( trailing: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -280,7 +299,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text("伝票の削除"), title: const Text("伝票の削除"),
content: Text("${invoice.customer.formalName}」の伝票(${invoice.invoiceNumber})を削除しますか?\nこの操作は取り消せません。"), content: Text("${invoice.customerNameForDisplay}」の伝票(${invoice.invoiceNumber})を削除しますか?\nこの操作は取り消せません。"),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("キャンセル")), TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("キャンセル")),
TextButton( TextButton(

View file

@ -78,7 +78,8 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
customer: _selectedCustomer!, customer: _selectedCustomer!,
date: DateTime.now(), date: DateTime.now(),
items: _items, items: _items,
taxRate: _includeTax ? _taxRate : 0.0, // taxRate: _includeTax ? _taxRate : 0.0,
customerFormalNameSnapshot: _selectedCustomer!.formalName, //
notes: _includeTax ? "(消費税 ${(_taxRate * 100).toInt()}% 込み)" : "(非課税)", notes: _includeTax ? "(消費税 ${(_taxRate * 100).toInt()}% 込み)" : "(非課税)",
); );

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../models/product_model.dart'; import '../models/product_model.dart';
import '../services/product_repository.dart'; import '../services/product_repository.dart';
import 'barcode_scanner_screen.dart';
class ProductMasterScreen extends StatefulWidget { class ProductMasterScreen extends StatefulWidget {
const ProductMasterScreen({Key? key}) : super(key: key); const ProductMasterScreen({Key? key}) : super(key: key);
@ -34,10 +35,12 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
final isEdit = product != null; final isEdit = product != null;
final nameController = TextEditingController(text: product?.name ?? ""); final nameController = TextEditingController(text: product?.name ?? "");
final priceController = TextEditingController(text: product?.defaultUnitPrice.toString() ?? "0"); final priceController = TextEditingController(text: product?.defaultUnitPrice.toString() ?? "0");
final barcodeController = TextEditingController(text: product?.barcode ?? "");
final result = await showDialog<Product>( final result = await showDialog<Product>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(isEdit ? "商品を編集" : "商品を新規登録"), title: Text(isEdit ? "商品を編集" : "商品を新規登録"),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -51,6 +54,31 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
decoration: const InputDecoration(labelText: "初期単価"), decoration: const InputDecoration(labelText: "初期単価"),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
), ),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: TextField(
controller: barcodeController,
decoration: const InputDecoration(labelText: "バーコード"),
),
),
IconButton(
icon: const Icon(Icons.qr_code_scanner),
onPressed: () async {
final code = await Navigator.push<String>(
context,
MaterialPageRoute(builder: (context) => const BarcodeScannerScreen()),
);
if (code != null) {
setDialogState(() {
barcodeController.text = code;
});
}
},
),
],
),
], ],
), ),
actions: [ actions: [
@ -62,6 +90,7 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
id: product?.id ?? const Uuid().v4(), id: product?.id ?? const Uuid().v4(),
name: nameController.text, name: nameController.text,
defaultUnitPrice: int.tryParse(priceController.text) ?? 0, defaultUnitPrice: int.tryParse(priceController.text) ?? 0,
barcode: barcodeController.text.isEmpty ? null : barcodeController.text,
odooId: product?.odooId, odooId: product?.odooId,
); );
Navigator.pop(context, newProduct); Navigator.pop(context, newProduct);
@ -70,6 +99,7 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
), ),
], ],
), ),
),
); );
if (result != null) { if (result != null) {

View file

@ -0,0 +1,26 @@
import 'package:sqflite/sqflite.dart';
import '../models/company_model.dart';
import 'database_helper.dart';
class CompanyRepository {
final DatabaseHelper _dbHelper = DatabaseHelper();
Future<CompanyInfo> getCompanyInfo() async {
final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query('company_info', where: 'id = 1');
if (maps.isEmpty) {
//
return CompanyInfo(name: "販売アシスト1号 登録企業");
}
return CompanyInfo.fromMap(maps.first);
}
Future<void> saveCompanyInfo(CompanyInfo info) async {
final db = await _dbHelper.database;
await db.insert(
'company_info',
info.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
}

View file

@ -1,6 +1,7 @@
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import '../models/customer_model.dart'; import '../models/customer_model.dart';
import 'database_helper.dart'; import 'database_helper.dart';
import 'package:uuid/uuid.dart';
class CustomerRepository { class CustomerRepository {
final DatabaseHelper _dbHelper = DatabaseHelper(); final DatabaseHelper _dbHelper = DatabaseHelper();
@ -8,9 +9,33 @@ class CustomerRepository {
Future<List<Customer>> getAllCustomers() async { Future<List<Customer>> getAllCustomers() async {
final db = await _dbHelper.database; final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query('customers', orderBy: 'display_name ASC'); final List<Map<String, dynamic>> maps = await db.query('customers', orderBy: 'display_name ASC');
if (maps.isEmpty) {
await _generateSampleCustomers();
return getAllCustomers(); //
}
return List.generate(maps.length, (i) => Customer.fromMap(maps[i])); return List.generate(maps.length, (i) => Customer.fromMap(maps[i]));
} }
Future<void> _generateSampleCustomers() async {
final samples = [
Customer(id: const Uuid().v4(), displayName: "佐々木製作所", formalName: "株式会社 佐々木製作所", title: "御中"),
Customer(id: const Uuid().v4(), displayName: "田中商事", formalName: "田中商事 株式会社", title: ""),
Customer(id: const Uuid().v4(), displayName: "山田建材", formalName: "有限会社 山田建材", title: "御中"),
Customer(id: const Uuid().v4(), displayName: "鈴木運送", formalName: "鈴木運送 合同会社", title: ""),
Customer(id: const Uuid().v4(), displayName: "伊藤工務店", formalName: "伊藤工務店", title: ""),
Customer(id: const Uuid().v4(), displayName: "渡辺興業", formalName: "株式会社 渡辺興業", title: "御中"),
Customer(id: const Uuid().v4(), displayName: "高橋電気", formalName: "高橋電気工業所", title: ""),
Customer(id: const Uuid().v4(), displayName: "佐藤商店", formalName: "佐藤商店", title: ""),
Customer(id: const Uuid().v4(), displayName: "中村機械", formalName: "中村機械製作所", title: "殿"),
Customer(id: const Uuid().v4(), displayName: "小林産業", formalName: "小林産業 株式会社", title: "御中"),
];
for (var s in samples) {
await saveCustomer(s);
}
}
Future<void> saveCustomer(Customer customer) async { Future<void> saveCustomer(Customer customer) async {
final db = await _dbHelper.database; final db = await _dbHelper.database;
await db.insert( await db.insert(

View file

@ -19,7 +19,7 @@ class DatabaseHelper {
String path = join(await getDatabasesPath(), 'gemi_invoice.db'); String path = join(await getDatabasesPath(), 'gemi_invoice.db');
return await openDatabase( return await openDatabase(
path, path,
version: 2, version: 5,
onCreate: _onCreate, onCreate: _onCreate,
onUpgrade: _onUpgrade, onUpgrade: _onUpgrade,
); );
@ -29,6 +29,26 @@ class DatabaseHelper {
if (oldVersion < 2) { if (oldVersion < 2) {
await db.execute('ALTER TABLE invoices ADD COLUMN tax_rate REAL DEFAULT 0.10'); await db.execute('ALTER TABLE invoices ADD COLUMN tax_rate REAL DEFAULT 0.10');
} }
if (oldVersion < 3) {
await db.execute('''
CREATE TABLE company_info (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
zip_code TEXT,
address TEXT,
tel TEXT,
default_tax_rate REAL DEFAULT 0.10,
seal_path TEXT
)
''');
}
if (oldVersion < 4) {
await db.execute('ALTER TABLE products ADD COLUMN barcode TEXT');
}
if (oldVersion < 5) {
//
await db.execute('ALTER TABLE invoices ADD COLUMN customer_formal_name TEXT');
}
} }
Future<void> _onCreate(Database db, int version) async { Future<void> _onCreate(Database db, int version) async {
@ -66,6 +86,7 @@ class DatabaseHelper {
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
default_unit_price INTEGER, default_unit_price INTEGER,
barcode TEXT,
odoo_id TEXT odoo_id TEXT
) )
'''); ''');
@ -80,6 +101,7 @@ class DatabaseHelper {
file_path TEXT, file_path TEXT,
total_amount INTEGER, total_amount INTEGER,
tax_rate REAL DEFAULT 0.10, tax_rate REAL DEFAULT 0.10,
customer_formal_name TEXT,
odoo_id TEXT, odoo_id TEXT,
is_synced INTEGER DEFAULT 0, is_synced INTEGER DEFAULT 0,
updated_at TEXT NOT NULL, updated_at TEXT NOT NULL,
@ -98,5 +120,18 @@ class DatabaseHelper {
FOREIGN KEY (invoice_id) REFERENCES invoices (id) ON DELETE CASCADE FOREIGN KEY (invoice_id) REFERENCES invoices (id) ON DELETE CASCADE
) )
'''); ''');
//
await db.execute('''
CREATE TABLE company_info (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
zip_code TEXT,
address TEXT,
tel TEXT,
default_tax_rate REAL DEFAULT 0.10,
seal_path TEXT
)
''');
} }
} }

View file

@ -59,7 +59,8 @@ class InvoiceRepository {
items: items, items: items,
notes: iMap['notes'], notes: iMap['notes'],
filePath: iMap['file_path'], filePath: iMap['file_path'],
taxRate: iMap['tax_rate'] ?? 0.10, // taxRate: iMap['tax_rate'] ?? 0.10,
customerFormalNameSnapshot: iMap['customer_formal_name'], //
odooId: iMap['odoo_id'], odooId: iMap['odoo_id'],
isSynced: iMap['is_synced'] == 1, isSynced: iMap['is_synced'] == 1,
updatedAt: DateTime.parse(iMap['updated_at']), updatedAt: DateTime.parse(iMap['updated_at']),

View file

@ -1,5 +1,4 @@
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart' show debugPrint; import 'package:flutter/material.dart' show debugPrint;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pdf/pdf.dart'; import 'package:pdf/pdf.dart';
@ -8,6 +7,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../models/invoice_models.dart'; import '../models/invoice_models.dart';
import 'company_repository.dart';
/// A4サイズのプロフェッショナルな請求書PDFを生成し /// A4サイズのプロフェッショナルな請求書PDFを生成し
Future<String?> generateInvoicePdf(Invoice invoice) async { Future<String?> generateInvoicePdf(Invoice invoice) async {
@ -22,6 +22,20 @@ Future<String?> generateInvoicePdf(Invoice invoice) async {
final dateFormatter = DateFormat('yyyy年MM月dd日'); final dateFormatter = DateFormat('yyyy年MM月dd日');
final amountFormatter = NumberFormat("#,###"); final amountFormatter = NumberFormat("#,###");
//
final companyRepo = CompanyRepository();
final companyInfo = await companyRepo.getCompanyInfo();
//
pw.MemoryImage? sealImage;
if (companyInfo.sealPath != null) {
final file = File(companyInfo.sealPath!);
if (await file.exists()) {
final bytes = await file.readAsBytes();
sealImage = pw.MemoryImage(bytes);
}
}
pdf.addPage( pdf.addPage(
pw.MultiPage( pw.MultiPage(
pageFormat: PdfPageFormat.a4, pageFormat: PdfPageFormat.a4,
@ -68,13 +82,27 @@ Future<String?> generateInvoicePdf(Invoice invoice) async {
), ),
), ),
pw.Expanded( pw.Expanded(
child: pw.Column( child: pw.Stack(
alignment: pw.Alignment.topRight,
children: [
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end, crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [ children: [
pw.Text("自社名が入ります", style: pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold)), pw.Text(companyInfo.name, style: pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold)),
pw.Text("〒000-0000"), if (companyInfo.zipCode != null) pw.Text("${companyInfo.zipCode}"),
pw.Text("住所がここに入ります"), if (companyInfo.address != null) pw.Text(companyInfo.address!),
pw.Text("TEL: 00-0000-0000"), if (companyInfo.tel != null) pw.Text("TEL: ${companyInfo.tel}"),
],
),
if (sealImage != null)
pw.Positioned(
right: 10,
top: 0,
child: pw.Opacity(
opacity: 0.8,
child: pw.Image(sealImage, width: 40, height: 40),
),
),
], ],
), ),
), ),

View file

@ -1,6 +1,7 @@
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import '../models/product_model.dart'; import '../models/product_model.dart';
import 'database_helper.dart'; import 'database_helper.dart';
import 'package:uuid/uuid.dart';
class ProductRepository { class ProductRepository {
final DatabaseHelper _dbHelper = DatabaseHelper(); final DatabaseHelper _dbHelper = DatabaseHelper();
@ -8,9 +9,33 @@ class ProductRepository {
Future<List<Product>> getAllProducts() async { Future<List<Product>> getAllProducts() async {
final db = await _dbHelper.database; final db = await _dbHelper.database;
final List<Map<String, dynamic>> maps = await db.query('products', orderBy: 'name ASC'); final List<Map<String, dynamic>> maps = await db.query('products', orderBy: 'name ASC');
if (maps.isEmpty) {
await _generateSampleProducts();
return getAllProducts();
}
return List.generate(maps.length, (i) => Product.fromMap(maps[i])); return List.generate(maps.length, (i) => Product.fromMap(maps[i]));
} }
Future<void> _generateSampleProducts() async {
final samples = [
Product(id: const Uuid().v4(), name: "基本技術料", defaultUnitPrice: 50000),
Product(id: const Uuid().v4(), name: "出張診断費", defaultUnitPrice: 10000),
Product(id: const Uuid().v4(), name: "交換用ハードディスク (1TB)", defaultUnitPrice: 8500),
Product(id: const Uuid().v4(), name: "メモリ増設 (8GB)", defaultUnitPrice: 6000),
Product(id: const Uuid().v4(), name: "OSインストール作業", defaultUnitPrice: 15000),
Product(id: const Uuid().v4(), name: "データ復旧作業 (軽度)", defaultUnitPrice: 30000),
Product(id: const Uuid().v4(), name: "LANケーブル (5m)", defaultUnitPrice: 1200),
Product(id: const Uuid().v4(), name: "ウイルス除去作業", defaultUnitPrice: 20000),
Product(id: const Uuid().v4(), name: "液晶ディスプレイ (24インチ)", defaultUnitPrice: 25000),
Product(id: const Uuid().v4(), name: "定期保守契約料 (月額)", defaultUnitPrice: 5000),
];
for (var s in samples) {
await saveProduct(s);
}
}
Future<void> saveProduct(Product product) async { Future<void> saveProduct(Product product) async {
final db = await _dbHelper.database; final db = await _dbHelper.database;
await db.insert( await db.insert(

View file

@ -0,0 +1 @@
/home/user/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.4/

View file

@ -0,0 +1 @@
/home/user/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.2/

View file

@ -0,0 +1 @@
/home/user/.pub-cache/hosted/pub.dev/package_info_plus-9.0.0/

View file

@ -6,9 +6,13 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View file

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
url_launcher_linux url_launcher_linux
) )

View file

@ -5,13 +5,19 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import file_selector_macos
import geolocator_apple import geolocator_apple
import mobile_scanner
import package_info_plus
import share_plus import share_plus
import sqflite_darwin import sqflite_darwin
import url_launcher_macos import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))

View file

@ -121,6 +121,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "7.0.1"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
url: "https://pub.dev"
source: hosted
version: "0.9.4"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
url: "https://pub.dev"
source: hosted
version: "0.9.5"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
url: "https://pub.dev"
source: hosted
version: "2.7.0"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
url: "https://pub.dev"
source: hosted
version: "0.9.3+5"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -150,6 +182,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
url: "https://pub.dev"
source: hosted
version: "2.0.33"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -224,6 +264,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image: image:
dependency: transitive dependency: transitive
description: description:
@ -232,6 +288,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.4" version: "4.5.4"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "518a16108529fc18657a3e6dde4a043dc465d16596d20ab2abd49a4cac2e703d"
url: "https://pub.dev"
source: hosted
version: "0.8.13+13"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
url: "https://pub.dev"
source: hosted
version: "0.8.13+6"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
url: "https://pub.dev"
source: hosted
version: "0.2.2+1"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
url: "https://pub.dev"
source: hosted
version: "2.11.1"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
url: "https://pub.dev"
source: hosted
version: "0.2.2"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@ -312,6 +432,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: c6184bf2913dd66be244108c9c27ca04b01caf726321c44b0e7a7a1e32d41044
url: "https://pub.dev"
source: hosted
version: "7.1.4"
native_toolchain_c: native_toolchain_c:
dependency: transitive dependency: transitive
description: description:
@ -336,6 +464,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.7.0" version: "4.7.0"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
url: "https://pub.dev"
source: hosted
version: "9.0.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
path: path:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1 version: 1.0.1+1
environment: environment:
sdk: ^3.10.7 sdk: ^3.10.7
@ -47,6 +47,9 @@ dependencies:
path: ^1.8.3 path: ^1.8.3
geolocator: ^13.0.1 geolocator: ^13.0.1
uuid: ^4.5.1 uuid: ^4.5.1
image_picker: ^1.2.1
mobile_scanner: ^7.1.4
package_info_plus: ^9.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -24,4 +24,12 @@
- 伝票入力はあれもこれも盛り込みたいので実験的に色んなのを実装 - 伝票入力はあれもこれも盛り込みたいので実験的に色んなのを実装
商品マスター編集画面の実装 商品マスター編集画面の実装
- 顧客マスター編集画面の実装 - 顧客マスター編集画面の実装
- 各種マスターは内容を編集した時に伝票と整合性を保つ仕組みを作る
- 各種マスターはodoo側の編集作業により影響を受けるのでその対策を考える
- 各種マスターはデータが空の場合サンプルを10個入れておく
- アプリタイトルのバージョンは常に最新にする小数点第3位を最小バージョン単位に
自社情報編集はタイトルを長押しで表示
- 自社情報編集の画面で消費税を設定可能にする
- 自社情報編集で印鑑を撮影出来る様にする
- 商品マスター等でバーコードQRコードのスキャンが可能でありたい