feat: Implement barcode scanning for products, add sample product generation, and update dependencies.
This commit is contained in:
parent
70c902885a
commit
4bc682f887
25 changed files with 655 additions and 49 deletions
|
|
@ -18,12 +18,30 @@
|
|||
@import geolocator_apple;
|
||||
#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>)
|
||||
#import <open_filex/OpenFilePlugin.h>
|
||||
#else
|
||||
@import open_filex;
|
||||
#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>)
|
||||
#import <permission_handler_apple/PermissionHandlerPlugin.h>
|
||||
#else
|
||||
|
|
@ -53,7 +71,10 @@
|
|||
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
|
||||
[FlutterContactsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterContactsPlugin"]];
|
||||
[GeolocatorPlugin registerWithRegistrar:[registry registrarForPlugin:@"GeolocatorPlugin"]];
|
||||
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];
|
||||
[MobileScannerPlugin registerWithRegistrar:[registry registrarForPlugin:@"MobileScannerPlugin"]];
|
||||
[OpenFilePlugin registerWithRegistrar:[registry registrarForPlugin:@"OpenFilePlugin"]];
|
||||
[FPPPackageInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPPackageInfoPlusPlugin"]];
|
||||
[PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]];
|
||||
[FPPSharePlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPSharePlusPlugin"]];
|
||||
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
|
||||
|
|
|
|||
58
lib/models/company_model.dart
Normal file
58
lib/models/company_model.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +43,8 @@ class Invoice {
|
|||
final List<InvoiceItem> items;
|
||||
final String? notes;
|
||||
final String? filePath;
|
||||
final double taxRate; // 追加
|
||||
final double taxRate;
|
||||
final String? customerFormalNameSnapshot; // 追加
|
||||
final String? odooId;
|
||||
final bool isSynced;
|
||||
final DateTime updatedAt;
|
||||
|
|
@ -56,6 +57,7 @@ class Invoice {
|
|||
this.notes,
|
||||
this.filePath,
|
||||
this.taxRate = 0.10, // デフォルト10%
|
||||
this.customerFormalNameSnapshot, // 追加
|
||||
this.odooId,
|
||||
this.isSynced = false,
|
||||
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 customerNameForDisplay => customerFormalNameSnapshot ?? customer.formalName;
|
||||
|
||||
int get subtotal => items.fold(0, (sum, item) => sum + item.subtotal);
|
||||
int get tax => (subtotal * taxRate).floor(); // taxRateを使用
|
||||
int get totalAmount => subtotal + tax;
|
||||
|
|
@ -77,6 +82,7 @@ class Invoice {
|
|||
'file_path': filePath,
|
||||
'total_amount': totalAmount,
|
||||
'tax_rate': taxRate, // 追加
|
||||
'customer_formal_name': customerFormalNameSnapshot ?? customer.formalName, // 追加
|
||||
'odoo_id': odooId,
|
||||
'is_synced': isSynced ? 1 : 0,
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
|
|
@ -91,7 +97,7 @@ class Invoice {
|
|||
|
||||
StringBuffer buffer = StringBuffer();
|
||||
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("品名,数量,単価,小計");
|
||||
|
||||
|
|
@ -110,6 +116,7 @@ class Invoice {
|
|||
String? notes,
|
||||
String? filePath,
|
||||
double? taxRate,
|
||||
String? customerFormalNameSnapshot,
|
||||
String? odooId,
|
||||
bool? isSynced,
|
||||
DateTime? updatedAt,
|
||||
|
|
@ -122,6 +129,7 @@ class Invoice {
|
|||
notes: notes ?? this.notes,
|
||||
filePath: filePath ?? this.filePath,
|
||||
taxRate: taxRate ?? this.taxRate,
|
||||
customerFormalNameSnapshot: customerFormalNameSnapshot ?? this.customerFormalNameSnapshot,
|
||||
odooId: odooId ?? this.odooId,
|
||||
isSynced: isSynced ?? this.isSynced,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ class Product {
|
|||
final String id;
|
||||
final String name;
|
||||
final int defaultUnitPrice;
|
||||
final String? barcode;
|
||||
final String? odooId;
|
||||
|
||||
Product({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.defaultUnitPrice = 0,
|
||||
this.barcode,
|
||||
this.odooId,
|
||||
});
|
||||
|
||||
|
|
@ -16,6 +18,7 @@ class Product {
|
|||
'id': id,
|
||||
'name': name,
|
||||
'default_unit_price': defaultUnitPrice,
|
||||
'barcode': barcode,
|
||||
'odoo_id': odooId,
|
||||
};
|
||||
}
|
||||
|
|
@ -25,6 +28,7 @@ class Product {
|
|||
id: map['id'],
|
||||
name: map['name'],
|
||||
defaultUnitPrice: map['default_unit_price'] ?? 0,
|
||||
barcode: map['barcode'],
|
||||
odooId: map['odoo_id'],
|
||||
);
|
||||
}
|
||||
|
|
@ -33,6 +37,7 @@ class Product {
|
|||
String? id,
|
||||
String? name,
|
||||
int? defaultUnitPrice,
|
||||
String? barcode,
|
||||
String? odooId,
|
||||
}) {
|
||||
return Product(
|
||||
|
|
|
|||
27
lib/screens/barcode_scanner_screen.dart
Normal file
27
lib/screens/barcode_scanner_screen.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
128
lib/screens/company_info_screen.dart
Normal file
128
lib/screens/company_info_screen.dart
Normal 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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -205,7 +205,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
decoration: const InputDecoration(labelText: "備考", border: OutlineInputBorder()),
|
||||
),
|
||||
] else ...[
|
||||
Text("${_currentInvoice.customer.formalName} ${_currentInvoice.customer.title}",
|
||||
Text("${_currentInvoice.customerNameForDisplay} ${_currentInvoice.customer.title}",
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||
if (_currentInvoice.customer.department != null && _currentInvoice.customer.department!.isNotEmpty)
|
||||
Text(_currentInvoice.customer.department!, style: const TextStyle(fontSize: 16)),
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ import '../services/invoice_repository.dart';
|
|||
import '../services/customer_repository.dart';
|
||||
import 'invoice_detail_page.dart';
|
||||
import 'management_screen.dart';
|
||||
import 'company_info_screen.dart';
|
||||
import '../widgets/slide_to_unlock.dart';
|
||||
import '../main.dart'; // InvoiceFlowScreen 用
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
class InvoiceHistoryScreen extends StatefulWidget {
|
||||
const InvoiceHistoryScreen({Key? key}) : super(key: key);
|
||||
|
|
@ -27,11 +29,20 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
|||
String _sortBy = "date"; // "date", "amount", "customer"
|
||||
DateTime? _startDate;
|
||||
DateTime? _endDate;
|
||||
String _appVersion = "1.0.0";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadData();
|
||||
_loadVersion();
|
||||
}
|
||||
|
||||
Future<void> _loadVersion() async {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
setState(() {
|
||||
_appVersion = packageInfo.version;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadData() async {
|
||||
|
|
@ -49,7 +60,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
|||
setState(() {
|
||||
_filteredInvoices = _invoices.where((inv) {
|
||||
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.notes?.toLowerCase().contains(query) ?? false);
|
||||
|
||||
|
|
@ -65,7 +76,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
|||
} else if (_sortBy == "amount") {
|
||||
_filteredInvoices.sort((a, b) => b.totalAmount.compareTo(a.totalAmount));
|
||||
} 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(
|
||||
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,
|
||||
actions: [
|
||||
if (_isUnlocked)
|
||||
|
|
@ -240,7 +259,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
|||
backgroundColor: _isUnlocked ? Colors.indigo.shade100 : Colors.grey.shade200,
|
||||
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}"),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
@ -280,7 +299,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
|||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text("伝票の削除"),
|
||||
content: Text("「${invoice.customer.formalName}」の伝票(${invoice.invoiceNumber})を削除しますか?\nこの操作は取り消せません。"),
|
||||
content: Text("「${invoice.customerNameForDisplay}」の伝票(${invoice.invoiceNumber})を削除しますか?\nこの操作は取り消せません。"),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("キャンセル")),
|
||||
TextButton(
|
||||
|
|
|
|||
|
|
@ -78,7 +78,8 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
customer: _selectedCustomer!,
|
||||
date: DateTime.now(),
|
||||
items: _items,
|
||||
taxRate: _includeTax ? _taxRate : 0.0, // 追加
|
||||
taxRate: _includeTax ? _taxRate : 0.0,
|
||||
customerFormalNameSnapshot: _selectedCustomer!.formalName, // 追加
|
||||
notes: _includeTax ? "(消費税 ${(_taxRate * 100).toInt()}% 込み)" : "(非課税)",
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:uuid/uuid.dart';
|
||||
import '../models/product_model.dart';
|
||||
import '../services/product_repository.dart';
|
||||
import 'barcode_scanner_screen.dart';
|
||||
|
||||
class ProductMasterScreen extends StatefulWidget {
|
||||
const ProductMasterScreen({Key? key}) : super(key: key);
|
||||
|
|
@ -34,41 +35,70 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
|
|||
final isEdit = product != null;
|
||||
final nameController = TextEditingController(text: product?.name ?? "");
|
||||
final priceController = TextEditingController(text: product?.defaultUnitPrice.toString() ?? "0");
|
||||
final barcodeController = TextEditingController(text: product?.barcode ?? "");
|
||||
|
||||
final result = await showDialog<Product>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(isEdit ? "商品を編集" : "商品を新規登録"),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: nameController,
|
||||
decoration: const InputDecoration(labelText: "商品名"),
|
||||
),
|
||||
TextField(
|
||||
controller: priceController,
|
||||
decoration: const InputDecoration(labelText: "初期単価"),
|
||||
keyboardType: TextInputType.number,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: Text(isEdit ? "商品を編集" : "商品を新規登録"),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: nameController,
|
||||
decoration: const InputDecoration(labelText: "商品名"),
|
||||
),
|
||||
TextField(
|
||||
controller: priceController,
|
||||
decoration: const InputDecoration(labelText: "初期単価"),
|
||||
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: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (nameController.text.isEmpty) return;
|
||||
final newProduct = Product(
|
||||
id: product?.id ?? const Uuid().v4(),
|
||||
name: nameController.text,
|
||||
defaultUnitPrice: int.tryParse(priceController.text) ?? 0,
|
||||
barcode: barcodeController.text.isEmpty ? null : barcodeController.text,
|
||||
odooId: product?.odooId,
|
||||
);
|
||||
Navigator.pop(context, newProduct);
|
||||
},
|
||||
child: const Text("保存"),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text("キャンセル")),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (nameController.text.isEmpty) return;
|
||||
final newProduct = Product(
|
||||
id: product?.id ?? const Uuid().v4(),
|
||||
name: nameController.text,
|
||||
defaultUnitPrice: int.tryParse(priceController.text) ?? 0,
|
||||
odooId: product?.odooId,
|
||||
);
|
||||
Navigator.pop(context, newProduct);
|
||||
},
|
||||
child: const Text("保存"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
26
lib/services/company_repository.dart
Normal file
26
lib/services/company_repository.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:sqflite/sqflite.dart';
|
||||
import '../models/customer_model.dart';
|
||||
import 'database_helper.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class CustomerRepository {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper();
|
||||
|
|
@ -8,9 +9,33 @@ class CustomerRepository {
|
|||
Future<List<Customer>> getAllCustomers() async {
|
||||
final db = await _dbHelper.database;
|
||||
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]));
|
||||
}
|
||||
|
||||
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 {
|
||||
final db = await _dbHelper.database;
|
||||
await db.insert(
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class DatabaseHelper {
|
|||
String path = join(await getDatabasesPath(), 'gemi_invoice.db');
|
||||
return await openDatabase(
|
||||
path,
|
||||
version: 2,
|
||||
version: 5,
|
||||
onCreate: _onCreate,
|
||||
onUpgrade: _onUpgrade,
|
||||
);
|
||||
|
|
@ -29,6 +29,26 @@ class DatabaseHelper {
|
|||
if (oldVersion < 2) {
|
||||
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 {
|
||||
|
|
@ -66,6 +86,7 @@ class DatabaseHelper {
|
|||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
default_unit_price INTEGER,
|
||||
barcode TEXT,
|
||||
odoo_id TEXT
|
||||
)
|
||||
''');
|
||||
|
|
@ -80,6 +101,7 @@ class DatabaseHelper {
|
|||
file_path TEXT,
|
||||
total_amount INTEGER,
|
||||
tax_rate REAL DEFAULT 0.10,
|
||||
customer_formal_name TEXT,
|
||||
odoo_id TEXT,
|
||||
is_synced INTEGER DEFAULT 0,
|
||||
updated_at TEXT NOT NULL,
|
||||
|
|
@ -98,5 +120,18 @@ class DatabaseHelper {
|
|||
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
|
||||
)
|
||||
''');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,8 @@ class InvoiceRepository {
|
|||
items: items,
|
||||
notes: iMap['notes'],
|
||||
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'],
|
||||
isSynced: iMap['is_synced'] == 1,
|
||||
updatedAt: DateTime.parse(iMap['updated_at']),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart' show debugPrint;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:pdf/pdf.dart';
|
||||
|
|
@ -8,6 +7,7 @@ import 'package:path_provider/path_provider.dart';
|
|||
import 'package:crypto/crypto.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../models/invoice_models.dart';
|
||||
import 'company_repository.dart';
|
||||
|
||||
/// A4サイズのプロフェッショナルな請求書PDFを生成し、保存する
|
||||
Future<String?> generateInvoicePdf(Invoice invoice) async {
|
||||
|
|
@ -22,6 +22,20 @@ Future<String?> generateInvoicePdf(Invoice invoice) async {
|
|||
final dateFormatter = DateFormat('yyyy年MM月dd日');
|
||||
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(
|
||||
pw.MultiPage(
|
||||
pageFormat: PdfPageFormat.a4,
|
||||
|
|
@ -68,13 +82,27 @@ Future<String?> generateInvoicePdf(Invoice invoice) async {
|
|||
),
|
||||
),
|
||||
pw.Expanded(
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
||||
child: pw.Stack(
|
||||
alignment: pw.Alignment.topRight,
|
||||
children: [
|
||||
pw.Text("自社名が入ります", style: pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold)),
|
||||
pw.Text("〒000-0000"),
|
||||
pw.Text("住所がここに入ります"),
|
||||
pw.Text("TEL: 00-0000-0000"),
|
||||
pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
||||
children: [
|
||||
pw.Text(companyInfo.name, style: pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold)),
|
||||
if (companyInfo.zipCode != null) pw.Text("〒${companyInfo.zipCode}"),
|
||||
if (companyInfo.address != null) pw.Text(companyInfo.address!),
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:sqflite/sqflite.dart';
|
||||
import '../models/product_model.dart';
|
||||
import 'database_helper.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ProductRepository {
|
||||
final DatabaseHelper _dbHelper = DatabaseHelper();
|
||||
|
|
@ -8,9 +9,33 @@ class ProductRepository {
|
|||
Future<List<Product>> getAllProducts() async {
|
||||
final db = await _dbHelper.database;
|
||||
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]));
|
||||
}
|
||||
|
||||
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 {
|
||||
final db = await _dbHelper.database;
|
||||
await db.insert(
|
||||
|
|
|
|||
1
linux/flutter/ephemeral/.plugin_symlinks/file_selector_linux
Symbolic link
1
linux/flutter/ephemeral/.plugin_symlinks/file_selector_linux
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/home/user/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.4/
|
||||
1
linux/flutter/ephemeral/.plugin_symlinks/image_picker_linux
Symbolic link
1
linux/flutter/ephemeral/.plugin_symlinks/image_picker_linux
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/home/user/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.2/
|
||||
1
linux/flutter/ephemeral/.plugin_symlinks/package_info_plus
Symbolic link
1
linux/flutter/ephemeral/.plugin_symlinks/package_info_plus
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
/home/user/.pub-cache/hosted/pub.dev/package_info_plus-9.0.0/
|
||||
|
|
@ -6,9 +6,13 @@
|
|||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
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 =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,13 +5,19 @@
|
|||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import file_selector_macos
|
||||
import geolocator_apple
|
||||
import mobile_scanner
|
||||
import package_info_plus
|
||||
import share_plus
|
||||
import sqflite_darwin
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
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"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
|
|
|
|||
144
pubspec.lock
144
pubspec.lock
|
|
@ -121,6 +121,38 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -150,6 +182,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
|
@ -224,6 +264,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -232,6 +288,70 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -312,6 +432,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -336,6 +464,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# 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.
|
||||
version: 1.0.0+1
|
||||
version: 1.0.1+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.7
|
||||
|
|
@ -47,6 +47,9 @@ dependencies:
|
|||
path: ^1.8.3
|
||||
geolocator: ^13.0.1
|
||||
uuid: ^4.5.1
|
||||
image_picker: ^1.2.1
|
||||
mobile_scanner: ^7.1.4
|
||||
package_info_plus: ^9.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
8
目標.md
8
目標.md
|
|
@ -24,4 +24,12 @@
|
|||
- 伝票入力はあれもこれも盛り込みたいので実験的に色んなのを実装
|
||||
− 商品マスター編集画面の実装
|
||||
- 顧客マスター編集画面の実装
|
||||
- 各種マスターは内容を編集した時に伝票と整合性を保つ仕組みを作る
|
||||
- 各種マスターはodoo側の編集作業により影響を受けるのでその対策を考える
|
||||
- 各種マスターはデータが空の場合サンプルを10個入れておく
|
||||
- アプリタイトルのバージョンは常に最新にする小数点第3位を最小バージョン単位に
|
||||
− 自社情報編集はタイトルを長押しで表示
|
||||
- 自社情報編集の画面で消費税を設定可能にする
|
||||
- 自社情報編集で印鑑を撮影出来る様にする
|
||||
- 商品マスター等でバーコードQRコードのスキャンが可能でありたい
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue