diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 29a2967..80f01e4 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -4,6 +4,12 @@
>
+
+
+
+
+
+
diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html
index 0ac341c..491b9ef 100644
--- a/android/build/reports/problems/problems-report.html
+++ b/android/build/reports/problems/problems-report.html
@@ -650,7 +650,7 @@ code + .copy-button {
diff --git a/ios/Runner/GeneratedPluginRegistrant.m b/ios/Runner/GeneratedPluginRegistrant.m
index 3c55531..9242567 100644
--- a/ios/Runner/GeneratedPluginRegistrant.m
+++ b/ios/Runner/GeneratedPluginRegistrant.m
@@ -48,6 +48,18 @@
@import permission_handler_apple;
#endif
+#if __has_include()
+#import
+#else
+@import print_bluetooth_thermal;
+#endif
+
+#if __has_include()
+#import
+#else
+@import printing;
+#endif
+
#if __has_include()
#import
#else
@@ -76,6 +88,8 @@
[OpenFilePlugin registerWithRegistrar:[registry registrarForPlugin:@"OpenFilePlugin"]];
[FPPPackageInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPPackageInfoPlusPlugin"]];
[PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]];
+ [PrintBluetoothThermalPlugin registerWithRegistrar:[registry registrarForPlugin:@"PrintBluetoothThermalPlugin"]];
+ [PrintingPlugin registerWithRegistrar:[registry registrarForPlugin:@"PrintingPlugin"]];
[FPPSharePlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPSharePlusPlugin"]];
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
[URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];
diff --git a/lib/models/activity_log_model.dart b/lib/models/activity_log_model.dart
new file mode 100644
index 0000000..dc9dc20
--- /dev/null
+++ b/lib/models/activity_log_model.dart
@@ -0,0 +1,56 @@
+import 'package:uuid/uuid.dart';
+
+class ActivityLog {
+ final String id;
+ final String action; // 例: "CREATE", "UPDATE", "DELETE", "GENERATE_PDF"
+ final String targetType; // 例: "INVOICE", "CUSTOMER", "PRODUCT"
+ final String? targetId;
+ final String? details;
+ final DateTime timestamp;
+
+ ActivityLog({
+ required this.id,
+ required this.action,
+ required this.targetType,
+ this.targetId,
+ this.details,
+ DateTime? timestamp,
+ }) : timestamp = timestamp ?? DateTime.now();
+
+ Map toMap() {
+ return {
+ 'id': id,
+ 'action': action,
+ 'target_type': targetType,
+ 'target_id': targetId,
+ 'details': details,
+ 'timestamp': timestamp.toIso8601String(),
+ };
+ }
+
+ factory ActivityLog.fromMap(Map map) {
+ return ActivityLog(
+ id: map['id'],
+ action: map['action'],
+ targetType: map['target_type'],
+ targetId: map['target_id'],
+ details: map['details'],
+ timestamp: DateTime.parse(map['timestamp']),
+ );
+ }
+
+ static ActivityLog create({
+ required String action,
+ required String targetType,
+ String? targetId,
+ String? details,
+ }) {
+ return ActivityLog(
+ id: const Uuid().v4(),
+ action: action,
+ targetType: targetType,
+ targetId: targetId,
+ details: details,
+ );
+ }
+}
diff --git a/lib/models/company_model.dart b/lib/models/company_model.dart
index 96d8a69..ecd9bb9 100644
--- a/lib/models/company_model.dart
+++ b/lib/models/company_model.dart
@@ -5,6 +5,7 @@ class CompanyInfo {
final String? tel;
final double defaultTaxRate;
final String? sealPath; // 角印(印鑑)の画像パス
+ final String taxDisplayMode; // 'normal', 'hidden', 'text_only'
CompanyInfo({
required this.name,
@@ -13,6 +14,7 @@ class CompanyInfo {
this.tel,
this.defaultTaxRate = 0.10,
this.sealPath,
+ this.taxDisplayMode = 'normal',
});
Map toMap() {
@@ -24,6 +26,7 @@ class CompanyInfo {
'tel': tel,
'default_tax_rate': defaultTaxRate,
'seal_path': sealPath,
+ 'tax_display_mode': taxDisplayMode,
};
}
@@ -35,6 +38,7 @@ class CompanyInfo {
tel: map['tel'],
defaultTaxRate: map['default_tax_rate'] ?? 0.10,
sealPath: map['seal_path'],
+ taxDisplayMode: map['tax_display_mode'] ?? 'normal',
);
}
@@ -45,6 +49,7 @@ class CompanyInfo {
String? tel,
double? defaultTaxRate,
String? sealPath,
+ String? taxDisplayMode,
}) {
return CompanyInfo(
name: name ?? this.name,
@@ -53,6 +58,7 @@ class CompanyInfo {
tel: tel ?? this.tel,
defaultTaxRate: defaultTaxRate ?? this.defaultTaxRate,
sealPath: sealPath ?? this.sealPath,
+ taxDisplayMode: taxDisplayMode ?? this.taxDisplayMode,
);
}
}
diff --git a/lib/models/invoice_models.dart b/lib/models/invoice_models.dart
index 8ee2c15..786295f 100644
--- a/lib/models/invoice_models.dart
+++ b/lib/models/invoice_models.dart
@@ -3,12 +3,14 @@ import 'package:intl/intl.dart';
class InvoiceItem {
final String? id;
+ final String? productId; // 追加
String description;
int quantity;
int unitPrice;
InvoiceItem({
this.id,
+ this.productId, // 追加
required this.description,
required this.quantity,
required this.unitPrice,
@@ -20,6 +22,7 @@ class InvoiceItem {
return {
'id': id ?? DateTime.now().microsecondsSinceEpoch.toString(),
'invoice_id': invoiceId,
+ 'product_id': productId, // 追加
'description': description,
'quantity': quantity,
'unit_price': unitPrice,
@@ -29,6 +32,7 @@ class InvoiceItem {
factory InvoiceItem.fromMap(Map map) {
return InvoiceItem(
id: map['id'],
+ productId: map['product_id'], // 追加
description: map['description'],
quantity: map['quantity'],
unitPrice: map['unit_price'],
@@ -36,6 +40,13 @@ class InvoiceItem {
}
}
+enum DocumentType {
+ estimation, // 見積
+ delivery, // 納品
+ invoice, // 請求
+ receipt, // 領収
+}
+
class Invoice {
final String id;
final Customer customer;
@@ -44,10 +55,13 @@ class Invoice {
final String? notes;
final String? filePath;
final double taxRate;
- final String? customerFormalNameSnapshot; // 追加
+ final DocumentType documentType; // 追加
+ final String? customerFormalNameSnapshot;
final String? odooId;
final bool isSynced;
final DateTime updatedAt;
+ final double? latitude; // 追加
+ final double? longitude; // 追加
Invoice({
String? id,
@@ -56,21 +70,42 @@ class Invoice {
required this.items,
this.notes,
this.filePath,
- this.taxRate = 0.10, // デフォルト10%
- this.customerFormalNameSnapshot, // 追加
+ this.taxRate = 0.10,
+ this.documentType = DocumentType.invoice, // デフォルト請求書
+ this.customerFormalNameSnapshot,
this.odooId,
this.isSynced = false,
DateTime? updatedAt,
+ this.latitude, // 追加
+ this.longitude, // 追加
}) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString(),
updatedAt = updatedAt ?? DateTime.now();
- String get invoiceNumber => "INV-${DateFormat('yyyyMMdd').format(date)}-${id.substring(id.length > 4 ? id.length - 4 : 0)}";
+ String get documentTypeName {
+ switch (documentType) {
+ case DocumentType.estimation: return "見積書";
+ case DocumentType.delivery: return "納品書";
+ case DocumentType.invoice: return "請求書";
+ case DocumentType.receipt: return "領収書";
+ }
+ }
+
+ String get invoiceNumberPrefix {
+ switch (documentType) {
+ case DocumentType.estimation: return "EST";
+ case DocumentType.delivery: return "DEL";
+ case DocumentType.invoice: return "INV";
+ case DocumentType.receipt: return "REC";
+ }
+ }
+
+ String get invoiceNumber => "$invoiceNumberPrefix-${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 tax => (subtotal * taxRate).floor();
int get totalAmount => subtotal + tax;
Map toMap() {
@@ -81,33 +116,17 @@ class Invoice {
'notes': notes,
'file_path': filePath,
'total_amount': totalAmount,
- 'tax_rate': taxRate, // 追加
- 'customer_formal_name': customerFormalNameSnapshot ?? customer.formalName, // 追加
+ 'tax_rate': taxRate,
+ 'document_type': documentType.name, // 追加
+ 'customer_formal_name': customerFormalNameSnapshot ?? customer.formalName,
'odoo_id': odooId,
'is_synced': isSynced ? 1 : 0,
'updated_at': updatedAt.toIso8601String(),
+ 'latitude': latitude, // 追加
+ 'longitude': longitude, // 追加
};
}
- // 注: fromMap には Customer オブジェクトが必要なため、
- // リポジトリ層で構築することを想定し、ここでは factory は定義しません。
-
- String toCsv() {
- final dateFormatter = DateFormat('yyyy/MM/dd');
-
- StringBuffer buffer = StringBuffer();
- buffer.writeln("日付,請求番号,取引先,合計金額,備考");
- buffer.writeln("${dateFormatter.format(date)},$invoiceNumber,$customerNameForDisplay,$totalAmount,${notes ?? ""}");
- buffer.writeln("");
- buffer.writeln("品名,数量,単価,小計");
-
- for (var item in items) {
- buffer.writeln("${item.description},${item.quantity},${item.unitPrice},${item.subtotal}");
- }
-
- return buffer.toString();
- }
-
Invoice copyWith({
String? id,
Customer? customer,
@@ -116,10 +135,13 @@ class Invoice {
String? notes,
String? filePath,
double? taxRate,
+ DocumentType? documentType,
String? customerFormalNameSnapshot,
String? odooId,
bool? isSynced,
DateTime? updatedAt,
+ double? latitude,
+ double? longitude,
}) {
return Invoice(
id: id ?? this.id,
@@ -129,10 +151,25 @@ class Invoice {
notes: notes ?? this.notes,
filePath: filePath ?? this.filePath,
taxRate: taxRate ?? this.taxRate,
+ documentType: documentType ?? this.documentType,
customerFormalNameSnapshot: customerFormalNameSnapshot ?? this.customerFormalNameSnapshot,
odooId: odooId ?? this.odooId,
isSynced: isSynced ?? this.isSynced,
updatedAt: updatedAt ?? this.updatedAt,
+ latitude: latitude ?? this.latitude,
+ longitude: longitude ?? this.longitude,
);
}
+
+ String toCsv() {
+ final buffer = StringBuffer();
+ buffer.writeln('伝票種別,伝票番号,日付,取引先,合計金額,緯度,経度');
+ buffer.writeln('$documentTypeName,$invoiceNumber,${DateFormat('yyyy/MM/dd').format(date)},$customerNameForDisplay,$totalAmount,${latitude ?? ""},${longitude ?? ""}');
+ buffer.writeln('');
+ buffer.writeln('品名,数量,単価,小計');
+ for (var item in items) {
+ buffer.writeln('${item.description},${item.quantity},${item.unitPrice},${item.subtotal}');
+ }
+ return buffer.toString();
+ }
}
diff --git a/lib/models/product_model.dart b/lib/models/product_model.dart
index 35ed6a4..c5dfda0 100644
--- a/lib/models/product_model.dart
+++ b/lib/models/product_model.dart
@@ -3,6 +3,8 @@ class Product {
final String name;
final int defaultUnitPrice;
final String? barcode;
+ final String? category;
+ final int stockQuantity; // 追加
final String? odooId;
Product({
@@ -10,6 +12,8 @@ class Product {
required this.name,
this.defaultUnitPrice = 0,
this.barcode,
+ this.category,
+ this.stockQuantity = 0, // 追加
this.odooId,
});
@@ -19,6 +23,8 @@ class Product {
'name': name,
'default_unit_price': defaultUnitPrice,
'barcode': barcode,
+ 'category': category,
+ 'stock_quantity': stockQuantity, // 追加
'odoo_id': odooId,
};
}
@@ -29,6 +35,8 @@ class Product {
name: map['name'],
defaultUnitPrice: map['default_unit_price'] ?? 0,
barcode: map['barcode'],
+ category: map['category'],
+ stockQuantity: map['stock_quantity'] ?? 0, // 追加
odooId: map['odoo_id'],
);
}
diff --git a/lib/screens/activity_log_screen.dart b/lib/screens/activity_log_screen.dart
new file mode 100644
index 0000000..e685538
--- /dev/null
+++ b/lib/screens/activity_log_screen.dart
@@ -0,0 +1,127 @@
+import 'package:flutter/material.dart';
+import 'package:intl/intl.dart';
+import '../models/activity_log_model.dart';
+import '../services/activity_log_repository.dart';
+
+class ActivityLogScreen extends StatefulWidget {
+ const ActivityLogScreen({Key? key}) : super(key: key);
+
+ @override
+ State createState() => _ActivityLogScreenState();
+}
+
+class _ActivityLogScreenState extends State {
+ final ActivityLogRepository _logRepo = ActivityLogRepository();
+ List _logs = [];
+ bool _isLoading = true;
+
+ @override
+ void initState() {
+ super.initState();
+ _loadLogs();
+ }
+
+ Future _loadLogs() async {
+ setState(() => _isLoading = true);
+ final logs = await _logRepo.getAllLogs();
+ setState(() {
+ _logs = logs;
+ _isLoading = false;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final dateFormat = DateFormat('yyyy/MM/dd HH:mm:ss');
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text("アクティビティ履歴 (Gitログ風)"),
+ backgroundColor: Colors.blueGrey.shade800,
+ actions: [
+ IconButton(icon: const Icon(Icons.refresh), onPressed: _loadLogs),
+ ],
+ ),
+ body: _isLoading
+ ? const Center(child: CircularProgressIndicator())
+ : _logs.isEmpty
+ ? const Center(child: Text("履歴はありません"))
+ : ListView.builder(
+ itemCount: _logs.length,
+ itemBuilder: (context, index) {
+ final log = _logs[index];
+ return _buildLogTile(log, dateFormat);
+ },
+ ),
+ );
+ }
+
+ Widget _buildLogTile(ActivityLog log, DateFormat fmt) {
+ IconData icon;
+ Color color;
+
+ switch (log.action) {
+ case "SAVE_INVOICE":
+ case "SAVE_PRODUCT":
+ case "SAVE_CUSTOMER":
+ icon = Icons.save;
+ color = Colors.green;
+ break;
+ case "DELETE_INVOICE":
+ case "DELETE_PRODUCT":
+ case "DELETE_CUSTOMER":
+ icon = Icons.delete_forever;
+ color = Colors.red;
+ break;
+ case "GENERATE_PDF":
+ icon = Icons.picture_as_pdf;
+ color = Colors.orange;
+ break;
+ default:
+ icon = Icons.info_outline;
+ color = Colors.blueGrey;
+ }
+
+ return Card(
+ margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
+ elevation: 0,
+ shape: RoundedRectangleBorder(
+ side: BorderSide(color: Colors.grey.shade200),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: ListTile(
+ leading: CircleAvatar(
+ backgroundColor: color.withOpacity(0.1),
+ child: Icon(icon, color: color, size: 20),
+ ),
+ title: Text(
+ "${_getActionJapanese(log.action)} [${log.targetType}]",
+ style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
+ ),
+ subtitle: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (log.details != null)
+ Text(log.details!, style: const TextStyle(fontSize: 12, color: Colors.black87)),
+ const SizedBox(height: 4),
+ Text(fmt.format(log.timestamp), style: TextStyle(fontSize: 11, color: Colors.grey.shade600)),
+ ],
+ ),
+ isThreeLine: log.details != null,
+ ),
+ );
+ }
+
+ String _getActionJapanese(String action) {
+ switch (action) {
+ case "SAVE_INVOICE": return "伝票保存";
+ case "DELETE_INVOICE": return "伝票削除";
+ case "SAVE_PRODUCT": return "商品更新";
+ case "DELETE_PRODUCT": return "商品削除";
+ case "SAVE_CUSTOMER": return "顧客更新";
+ case "DELETE_CUSTOMER": return "顧客削除";
+ case "GENERATE_PDF": return "PDF発行";
+ default: return action;
+ }
+ }
+}
diff --git a/lib/screens/company_info_screen.dart b/lib/screens/company_info_screen.dart
index bce2060..b147a98 100644
--- a/lib/screens/company_info_screen.dart
+++ b/lib/screens/company_info_screen.dart
@@ -21,6 +21,7 @@ class _CompanyInfoScreenState extends State {
final _addressController = TextEditingController();
final _telController = TextEditingController();
double _taxRate = 0.10;
+ String _taxDisplayMode = 'normal';
@override
void initState() {
@@ -35,6 +36,7 @@ class _CompanyInfoScreenState extends State {
_addressController.text = _info.address ?? "";
_telController.text = _info.tel ?? "";
_taxRate = _info.defaultTaxRate;
+ _taxDisplayMode = _info.taxDisplayMode;
setState(() => _isLoading = false);
}
@@ -55,6 +57,7 @@ class _CompanyInfoScreenState extends State {
address: _addressController.text,
tel: _telController.text,
defaultTaxRate: _taxRate,
+ taxDisplayMode: _taxDisplayMode,
);
await _companyRepo.saveCompanyInfo(updated);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("自社情報を保存しました")));
@@ -94,6 +97,29 @@ class _CompanyInfoScreenState extends State {
ChoiceChip(label: const Text("8%"), selected: _taxRate == 0.08, onSelected: (_) => setState(() => _taxRate = 0.08)),
],
),
+ const SizedBox(height: 20),
+ const Text("消費税の表示設定(T番号非取得時など)", style: TextStyle(fontWeight: FontWeight.bold)),
+ const SizedBox(height: 8),
+ Wrap(
+ spacing: 8,
+ children: [
+ ChoiceChip(
+ label: const Text("通常表示"),
+ selected: _taxDisplayMode == 'normal',
+ onSelected: (_) => setState(() => _taxDisplayMode = 'normal'),
+ ),
+ ChoiceChip(
+ label: const Text("表示しない"),
+ selected: _taxDisplayMode == 'hidden',
+ onSelected: (_) => setState(() => _taxDisplayMode = 'hidden'),
+ ),
+ ChoiceChip(
+ label: const Text("「税別」と表示"),
+ selected: _taxDisplayMode == 'text_only',
+ onSelected: (_) => setState(() => _taxDisplayMode = 'text_only'),
+ ),
+ ],
+ ),
const SizedBox(height: 24),
const Text("印影(角印)撮影", style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
diff --git a/lib/screens/customer_picker_modal.dart b/lib/screens/customer_picker_modal.dart
index d51ab52..e3cbbd4 100644
--- a/lib/screens/customer_picker_modal.dart
+++ b/lib/screens/customer_picker_modal.dart
@@ -28,29 +28,18 @@ class _CustomerPickerModalState extends State {
@override
void initState() {
super.initState();
- _loadCustomers();
+ _onSearch(""); // 初期表示
}
- Future _loadCustomers() async {
+ Future _onSearch(String query) async {
setState(() => _isLoading = true);
- final customers = await _repository.getAllCustomers();
+ final customers = await _repository.searchCustomers(query);
setState(() {
- _allCustomers = customers;
_filteredCustomers = customers;
_isLoading = false;
});
}
- void _filterCustomers(String query) {
- setState(() {
- _searchQuery = query.toLowerCase();
- _filteredCustomers = _allCustomers.where((customer) {
- return customer.formalName.toLowerCase().contains(_searchQuery) ||
- customer.displayName.toLowerCase().contains(_searchQuery);
- }).toList();
- });
- }
-
/// 電話帳から取り込んで新規顧客として登録・編集するダイアログ
Future _importFromPhoneContacts() async {
setState(() => _isImportingFromContacts = true);
@@ -149,7 +138,7 @@ class _CustomerPickerModalState extends State {
await _repository.saveCustomer(updatedCustomer);
Navigator.pop(context); // エディットダイアログを閉じる
- _loadCustomers(); // リストを再読込
+ _onSearch(""); // リストを再読込
// 保存のついでに選択状態にするなら以下を有効化(今回は明示的にリストから選ばせる)
// widget.onCustomerSelected(updatedCustomer);
@@ -174,7 +163,7 @@ class _CustomerPickerModalState extends State {
onPressed: () async {
await _repository.deleteCustomer(customer.id);
Navigator.pop(context);
- _loadCustomers();
+ _onSearch("");
},
child: const Text("削除する", style: TextStyle(color: Colors.red)),
),
@@ -207,7 +196,7 @@ class _CustomerPickerModalState extends State {
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
- onChanged: _filterCustomers,
+ onChanged: _onSearch,
),
const SizedBox(height: 12),
SizedBox(
diff --git a/lib/screens/gps_history_screen.dart b/lib/screens/gps_history_screen.dart
new file mode 100644
index 0000000..454b281
--- /dev/null
+++ b/lib/screens/gps_history_screen.dart
@@ -0,0 +1,61 @@
+import 'package:flutter/material.dart';
+import 'package:intl/intl.dart';
+import '../services/gps_service.dart';
+
+class GpsHistoryScreen extends StatefulWidget {
+ const GpsHistoryScreen({Key? key}) : super(key: key);
+
+ @override
+ State createState() => _GpsHistoryScreenState();
+}
+
+class _GpsHistoryScreenState extends State {
+ final _gpsService = GpsService();
+ List