feat: Implement invoice date selection and draft mode, update tax rate logic, and increment database schema.
This commit is contained in:
parent
25295fd619
commit
81c2fc38a8
11 changed files with 417 additions and 157 deletions
|
|
@ -1,5 +1,5 @@
|
|||
// lib/main.dart
|
||||
// version: 1.5.01 (Update: SHA256 & Management Features)
|
||||
// version: 1.5.02 (Update: Date selection & Tax fix)
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// --- 独自モジュールのインポート ---
|
||||
|
|
@ -56,7 +56,8 @@ class _InvoiceFlowScreenState extends State<InvoiceFlowScreen> {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("販売アシスト1号 V1.5.01"),
|
||||
leading: const BackButton(),
|
||||
title: const Text("販売アシスト1号 V1.5.02"),
|
||||
backgroundColor: Colors.blueGrey,
|
||||
),
|
||||
drawer: Drawer(
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ class Invoice {
|
|||
final double? latitude; // 追加
|
||||
final double? longitude; // 追加
|
||||
final String terminalId; // 追加: 端末識別子
|
||||
final bool isDraft; // 追加: 下書きフラグ
|
||||
|
||||
Invoice({
|
||||
String? id,
|
||||
|
|
@ -82,6 +83,7 @@ class Invoice {
|
|||
this.latitude, // 追加
|
||||
this.longitude, // 追加
|
||||
String? terminalId, // 追加
|
||||
this.isDraft = false, // 追加: デフォルトは通常
|
||||
}) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
terminalId = terminalId ?? "T1", // デフォルト端末ID
|
||||
updatedAt = updatedAt ?? DateTime.now();
|
||||
|
|
@ -138,6 +140,7 @@ class Invoice {
|
|||
'longitude': longitude, // 追加
|
||||
'terminal_id': terminalId, // 追加
|
||||
'content_hash': contentHash, // 追加
|
||||
'is_draft': isDraft ? 1 : 0, // 追加
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -156,6 +159,8 @@ class Invoice {
|
|||
DateTime? updatedAt,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
String? terminalId,
|
||||
bool? isDraft,
|
||||
}) {
|
||||
return Invoice(
|
||||
id: id ?? this.id,
|
||||
|
|
@ -172,6 +177,8 @@ class Invoice {
|
|||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
terminalId: terminalId ?? this.terminalId,
|
||||
isDraft: isDraft ?? this.isDraft,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ class _CustomerMasterScreenState extends State<CustomerMasterScreen> {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: const BackButton(),
|
||||
title: const Text("顧客マスター管理"),
|
||||
backgroundColor: Colors.blueGrey,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
late List<InvoiceItem> _items;
|
||||
late bool _isEditing;
|
||||
late Invoice _currentInvoice;
|
||||
late double _taxRate; // 追加
|
||||
late bool _includeTax; // 追加
|
||||
String? _currentFilePath;
|
||||
final _invoiceRepo = InvoiceRepository();
|
||||
final _customerRepo = CustomerRepository();
|
||||
|
|
@ -42,6 +44,8 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
_formalNameController = TextEditingController(text: _currentInvoice.customer.formalName);
|
||||
_notesController = TextEditingController(text: _currentInvoice.notes ?? "");
|
||||
_items = List.from(_currentInvoice.items);
|
||||
_taxRate = _currentInvoice.taxRate; // 初期化
|
||||
_includeTax = _currentInvoice.taxRate > 0; // 初期化
|
||||
_isEditing = false;
|
||||
_loadCompanyInfo();
|
||||
}
|
||||
|
|
@ -107,6 +111,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
customer: updatedCustomer,
|
||||
items: _items,
|
||||
notes: _notesController.text,
|
||||
taxRate: _includeTax ? _taxRate : 0.0, // 更新
|
||||
);
|
||||
|
||||
// データベースに保存
|
||||
|
|
@ -194,18 +199,35 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final amountFormatter = NumberFormat("#,###");
|
||||
final fmt = NumberFormat("#,###");
|
||||
final isDraft = _currentInvoice.isDraft;
|
||||
final themeColor = isDraft ? Colors.blueGrey.shade800 : Colors.white;
|
||||
final textColor = isDraft ? Colors.white : Colors.black87;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: themeColor,
|
||||
appBar: AppBar(
|
||||
title: const Text("販売アシスト1号 請求書詳細"),
|
||||
backgroundColor: Colors.blueGrey,
|
||||
leading: const BackButton(), // 常に表示
|
||||
title: Text(isDraft ? "伝票詳細 (下書き)" : "販売アシスト1号 伝票詳細"),
|
||||
backgroundColor: isDraft ? Colors.black87 : Colors.blueGrey,
|
||||
actions: [
|
||||
if (isDraft && !_isEditing)
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.check_circle_outline, color: Colors.orangeAccent),
|
||||
label: const Text("正式発行", style: TextStyle(color: Colors.orangeAccent)),
|
||||
onPressed: _showPromoteDialog,
|
||||
),
|
||||
if (!_isEditing) ...[
|
||||
IconButton(icon: const Icon(Icons.grid_on), onPressed: _exportCsv, tooltip: "CSV出力"),
|
||||
if (widget.isUnlocked)
|
||||
IconButton(icon: const Icon(Icons.edit), onPressed: () => setState(() => _isEditing = true)),
|
||||
] else ...[
|
||||
if (isDraft)
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.check_circle_outline, color: Colors.orangeAccent),
|
||||
label: const Text("正式発行", style: TextStyle(color: Colors.orangeAccent)),
|
||||
onPressed: _showPromoteDialog,
|
||||
),
|
||||
IconButton(icon: const Icon(Icons.save), onPressed: _saveChanges),
|
||||
IconButton(icon: const Icon(Icons.cancel), onPressed: () => setState(() => _isEditing = false)),
|
||||
]
|
||||
|
|
@ -216,11 +238,17 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeaderSection(),
|
||||
const Divider(height: 32),
|
||||
const Text("明細一覧", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
_buildHeaderSection(textColor),
|
||||
if (_isEditing) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildDraftToggleEdit(), // 編集用トグル
|
||||
const SizedBox(height: 16),
|
||||
_buildExperimentalSection(isDraft),
|
||||
],
|
||||
Divider(height: 32, color: isDraft ? Colors.white70 : Colors.grey),
|
||||
Text("明細一覧", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: textColor)),
|
||||
const SizedBox(height: 8),
|
||||
_buildItemTable(amountFormatter),
|
||||
_buildItemTable(fmt, textColor, isDraft),
|
||||
if (_isEditing)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
|
|
@ -246,7 +274,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildSummarySection(amountFormatter),
|
||||
_buildSummarySection(fmt, textColor, isDraft),
|
||||
const SizedBox(height: 24),
|
||||
_buildFooterActions(),
|
||||
],
|
||||
|
|
@ -255,7 +283,7 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderSection() {
|
||||
Widget _buildHeaderSection(Color textColor) {
|
||||
final dateFormatter = DateFormat('yyyy年MM月dd日');
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
@ -264,45 +292,72 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
TextField(
|
||||
controller: _formalNameController,
|
||||
decoration: const InputDecoration(labelText: "取引先 正式名称", border: OutlineInputBorder()),
|
||||
style: TextStyle(color: textColor),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _notesController,
|
||||
maxLines: 2,
|
||||
decoration: const InputDecoration(labelText: "備考", border: OutlineInputBorder()),
|
||||
style: TextStyle(color: textColor),
|
||||
),
|
||||
] else ...[
|
||||
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)),
|
||||
const SizedBox(height: 4),
|
||||
Text("請求番号: ${_currentInvoice.invoiceNumber}"),
|
||||
Text("発行日: ${dateFormatter.format(_currentInvoice.date)}"),
|
||||
if (_currentInvoice.latitude != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.location_on, size: 14, color: Colors.blueGrey),
|
||||
const SizedBox(width: 4),
|
||||
Text("座標: ${_currentInvoice.latitude!.toStringAsFixed(4)}, ${_currentInvoice.longitude!.toStringAsFixed(4)}",
|
||||
style: const TextStyle(fontSize: 12, color: Colors.blueGrey)),
|
||||
],
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"伝票番号: ${_currentInvoice.invoiceNumber}",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: textColor),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _currentInvoice.isDraft ? Colors.orange : Colors.green.shade700,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
_currentInvoice.isDraft ? "下書き" : "確定済",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
if (_currentInvoice.notes?.isNotEmpty ?? false) ...[
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text("日付: ${DateFormat('yyyy/MM/dd').format(_currentInvoice.date)}", style: TextStyle(color: textColor.withOpacity(0.8))),
|
||||
const SizedBox(height: 8),
|
||||
Text("取引先:", style: TextStyle(fontWeight: FontWeight.bold, color: textColor)),
|
||||
Text("${_currentInvoice.customerNameForDisplay} ${_currentInvoice.customer.title}",
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: textColor)),
|
||||
if (_currentInvoice.customer.department != null && _currentInvoice.customer.department!.isNotEmpty)
|
||||
Text(_currentInvoice.customer.department!, style: TextStyle(fontSize: 16, color: textColor)),
|
||||
if (_currentInvoice.latitude != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.location_on, size: 14, color: Colors.blueGrey),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
"座標: ${_currentInvoice.latitude!.toStringAsFixed(4)}, ${_currentInvoice.longitude!.toStringAsFixed(4)}",
|
||||
style: const TextStyle(fontSize: 12, color: Colors.blueGrey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (_currentInvoice.notes?.isNotEmpty ?? false) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text("備考: ${_currentInvoice.notes}", style: const TextStyle(color: Colors.black87)),
|
||||
]
|
||||
Text("備考: ${_currentInvoice.notes}", style: TextStyle(color: textColor.withOpacity(0.9))),
|
||||
],
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItemTable(NumberFormat formatter) {
|
||||
Widget _buildItemTable(NumberFormat formatter, Color textColor, bool isDraft) {
|
||||
return Table(
|
||||
border: TableBorder.all(color: Colors.grey.shade300),
|
||||
border: TableBorder.all(color: isDraft ? Colors.white24 : Colors.grey.shade300),
|
||||
columnWidths: const {
|
||||
0: FlexColumnWidth(4),
|
||||
1: FixedColumnWidth(50),
|
||||
|
|
@ -313,9 +368,13 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||
children: [
|
||||
TableRow(
|
||||
decoration: BoxDecoration(color: Colors.grey.shade100),
|
||||
children: const [
|
||||
_TableCell("品名"), _TableCell("数量"), _TableCell("単価"), _TableCell("金額"), _TableCell(""),
|
||||
decoration: BoxDecoration(color: isDraft ? Colors.black26 : Colors.grey.shade100),
|
||||
children: [
|
||||
_TableCell("品名", textColor: textColor),
|
||||
_TableCell("数量", textColor: textColor),
|
||||
_TableCell("単価", textColor: textColor),
|
||||
_TableCell("金額", textColor: textColor),
|
||||
const _TableCell(""),
|
||||
],
|
||||
),
|
||||
..._items.asMap().entries.map((entry) {
|
||||
|
|
@ -325,27 +384,30 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
return TableRow(children: [
|
||||
_EditableCell(
|
||||
initialValue: item.description,
|
||||
textColor: textColor,
|
||||
onChanged: (val) => item.description = val,
|
||||
),
|
||||
_EditableCell(
|
||||
initialValue: item.quantity.toString(),
|
||||
textColor: textColor,
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (val) => setState(() => item.quantity = int.tryParse(val) ?? 0),
|
||||
),
|
||||
_EditableCell(
|
||||
initialValue: item.unitPrice.toString(),
|
||||
textColor: textColor,
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (val) => setState(() => item.unitPrice = int.tryParse(val) ?? 0),
|
||||
),
|
||||
_TableCell(formatter.format(item.subtotal)),
|
||||
_TableCell(formatter.format(item.subtotal), textColor: textColor),
|
||||
IconButton(icon: const Icon(Icons.delete, size: 20, color: Colors.red), onPressed: () => _removeItem(idx)),
|
||||
]);
|
||||
} else {
|
||||
return TableRow(children: [
|
||||
_TableCell(item.description),
|
||||
_TableCell(item.quantity.toString()),
|
||||
_TableCell(formatter.format(item.unitPrice)),
|
||||
_TableCell(formatter.format(item.subtotal)),
|
||||
_TableCell(item.description, textColor: textColor),
|
||||
_TableCell(item.quantity.toString(), textColor: textColor),
|
||||
_TableCell(formatter.format(item.unitPrice), textColor: textColor),
|
||||
_TableCell(formatter.format(item.subtotal), textColor: textColor),
|
||||
const SizedBox(),
|
||||
]);
|
||||
}
|
||||
|
|
@ -354,27 +416,34 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildSummarySection(NumberFormat formatter) {
|
||||
final double currentTaxRate = _isEditing ? _currentInvoice.taxRate : _currentInvoice.taxRate; // 編集時も元の税率を維持
|
||||
Widget _buildSummarySection(NumberFormat formatter, Color textColor, bool isDraft) {
|
||||
final double currentTaxRate = _isEditing ? (_includeTax ? _taxRate : 0.0) : _currentInvoice.taxRate;
|
||||
final int subtotal = _isEditing ? _calculateCurrentSubtotal() : _currentInvoice.subtotal;
|
||||
final int tax = (subtotal * currentTaxRate).floor();
|
||||
final int total = subtotal + tax;
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Container(
|
||||
width: 200,
|
||||
child: Column(
|
||||
children: [
|
||||
_SummaryRow("小計 (税抜)", formatter.format(subtotal)),
|
||||
if (_companyInfo?.taxDisplayMode == 'normal')
|
||||
_SummaryRow("消費税 (${(currentTaxRate * 100).toInt()}%)", formatter.format(tax)),
|
||||
if (_companyInfo?.taxDisplayMode == 'text_only')
|
||||
_SummaryRow("消費税", "(税別)"),
|
||||
const Divider(),
|
||||
_SummaryRow("合計", "¥${formatter.format(total)}", isBold: true),
|
||||
],
|
||||
),
|
||||
return Column(
|
||||
children: [
|
||||
_buildSummaryRow("小計", formatter.format(subtotal), textColor),
|
||||
if (_companyInfo?.taxDisplayMode == 'normal')
|
||||
_buildSummaryRow("消費税 (${(currentTaxRate * 100).toInt()}%)", formatter.format(tax), textColor),
|
||||
if (_companyInfo?.taxDisplayMode == 'text_only')
|
||||
_buildSummaryRow("消費税", "(税別)", textColor),
|
||||
const Divider(color: Colors.grey),
|
||||
_buildSummaryRow("合計金額", "¥${formatter.format(total)}", textColor, isTotal: true),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummaryRow(String label, String value, Color textColor, {bool isTotal = false}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: TextStyle(fontSize: isTotal ? 18 : 16, fontWeight: isTotal ? FontWeight.bold : FontWeight.normal, color: textColor)),
|
||||
Text(value, style: TextStyle(fontSize: isTotal ? 20 : 16, fontWeight: isTotal ? FontWeight.bold : FontWeight.normal, color: isTotal ? Colors.orangeAccent : textColor)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -383,6 +452,46 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
return _items.fold(0, (sum, item) => sum + (item.quantity * item.unitPrice));
|
||||
}
|
||||
|
||||
Widget _buildExperimentalSection(bool isDraft) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isDraft ? Colors.black45 : Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.orange, width: 1),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("税率設定 (編集用)", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.orange)),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Text("消費税: ", style: TextStyle(color: isDraft ? Colors.white70 : Colors.black87)),
|
||||
ChoiceChip(
|
||||
label: const Text("10%"),
|
||||
selected: _taxRate == 0.10,
|
||||
onSelected: (val) => setState(() => _taxRate = 0.10),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ChoiceChip(
|
||||
label: const Text("8%"),
|
||||
selected: _taxRate == 0.08,
|
||||
onSelected: (val) => setState(() => _taxRate = 0.08),
|
||||
),
|
||||
const Spacer(),
|
||||
Switch(
|
||||
value: _includeTax,
|
||||
onChanged: (val) => setState(() => _includeTax = val),
|
||||
),
|
||||
Text(_includeTax ? "税込表示" : "非課税"),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFooterActions() {
|
||||
if (_isEditing || _currentFilePath == null) return const SizedBox();
|
||||
return Row(
|
||||
|
|
@ -417,6 +526,61 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _showPromoteDialog() async {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text("正式発行"),
|
||||
content: const Text("この下書き伝票を「確定」として正式に発行しますか?\n(下書きモードが解除され、通常の背景に戻ります)"),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text("キャンセル")),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.orange),
|
||||
child: const Text("正式発行する"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm == true) {
|
||||
final promoted = _currentInvoice.copyWith(isDraft: false);
|
||||
await _invoiceRepo.updateInvoice(promoted);
|
||||
setState(() {
|
||||
_currentInvoice = promoted;
|
||||
});
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("伝票を正式発行しました")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDraftToggleEdit() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: _currentInvoice.isDraft ? Colors.black26 : Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.orange, width: 2),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.drafts, color: Colors.orange),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(child: Text("下書き状態として保持", style: TextStyle(fontWeight: FontWeight.bold))),
|
||||
Switch(
|
||||
value: _currentInvoice.isDraft,
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_currentInvoice = _currentInvoice.copyWith(isDraft: val);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openPdf() async => await OpenFilex.open(_currentFilePath!);
|
||||
Future<void> _sharePdf() async {
|
||||
if (_currentFilePath != null) {
|
||||
|
|
@ -427,31 +591,44 @@ class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
|
|||
|
||||
class _TableCell extends StatelessWidget {
|
||||
final String text;
|
||||
const _TableCell(this.text);
|
||||
final Color? textColor;
|
||||
const _TableCell(this.text, {this.textColor});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(text, textAlign: TextAlign.right, style: const TextStyle(fontSize: 12)),
|
||||
);
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(text, textAlign: TextAlign.right, style: TextStyle(fontSize: 12, color: textColor)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EditableCell extends StatelessWidget {
|
||||
final String initialValue;
|
||||
final TextInputType keyboardType;
|
||||
final Function(String) onChanged;
|
||||
const _EditableCell({required this.initialValue, this.keyboardType = TextInputType.text, required this.onChanged});
|
||||
final Color? textColor;
|
||||
|
||||
const _EditableCell({
|
||||
required this.initialValue,
|
||||
required this.onChanged,
|
||||
this.keyboardType = TextInputType.text,
|
||||
this.textColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: TextFormField(
|
||||
initialValue: initialValue,
|
||||
keyboardType: keyboardType,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: const InputDecoration(isDense: true, contentPadding: EdgeInsets.all(8)),
|
||||
onChanged: onChanged,
|
||||
),
|
||||
);
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: TextField(
|
||||
controller: TextEditingController(text: initialValue),
|
||||
keyboardType: keyboardType,
|
||||
style: TextStyle(fontSize: 14, color: textColor),
|
||||
onChanged: onChanged,
|
||||
decoration: const InputDecoration(isDense: true, contentPadding: EdgeInsets.all(8)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SummaryRow extends StatelessWidget {
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
|||
SlideToUnlock(
|
||||
isLocked: !_isUnlocked,
|
||||
onUnlocked: _toggleUnlock,
|
||||
text: "スライドして編集モード解除",
|
||||
text: "スライドでロック解除",
|
||||
),
|
||||
Expanded(
|
||||
child: _isLoading
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
bool _includeTax = true;
|
||||
CompanyInfo? _companyInfo;
|
||||
DocumentType _documentType = DocumentType.invoice; // 追加
|
||||
DateTime _selectedDate = DateTime.now(); // 追加: 伝票日付
|
||||
bool _isDraft = false; // 追加: 下書きモード
|
||||
String _status = "取引先と商品を入力してください";
|
||||
|
||||
// 署名用の実験的パス
|
||||
|
|
@ -96,7 +98,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
|
||||
final invoice = Invoice(
|
||||
customer: _selectedCustomer!,
|
||||
date: DateTime.now(),
|
||||
date: _selectedDate, // 修正
|
||||
items: _items,
|
||||
taxRate: _includeTax ? _taxRate : 0.0,
|
||||
documentType: _documentType,
|
||||
|
|
@ -126,7 +128,7 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
if (_selectedCustomer == null) return;
|
||||
final invoice = Invoice(
|
||||
customer: _selectedCustomer!,
|
||||
date: DateTime.now(),
|
||||
date: _selectedDate, // 修正
|
||||
items: _items,
|
||||
taxRate: _includeTax ? _taxRate : 0.0,
|
||||
documentType: _documentType,
|
||||
|
|
@ -168,32 +170,46 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fmt = NumberFormat("#,###");
|
||||
final themeColor = _isDraft ? Colors.blueGrey.shade800 : Colors.white;
|
||||
final textColor = _isDraft ? Colors.white : Colors.black87;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDocumentTypeSection(), // 追加
|
||||
const SizedBox(height: 16),
|
||||
_buildCustomerSection(),
|
||||
const SizedBox(height: 20),
|
||||
_buildItemsSection(fmt),
|
||||
const SizedBox(height: 20),
|
||||
_buildExperimentalSection(),
|
||||
const SizedBox(height: 20),
|
||||
_buildSummarySection(fmt),
|
||||
const SizedBox(height: 20),
|
||||
_buildSignatureSection(),
|
||||
],
|
||||
return Scaffold(
|
||||
backgroundColor: themeColor,
|
||||
appBar: AppBar(
|
||||
leading: const BackButton(),
|
||||
title: Text(_isDraft ? "伝票作成 (下書きモード)" : "販売アシスト1号 V1.5.02"),
|
||||
backgroundColor: _isDraft ? Colors.black87 : Colors.blueGrey,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDraftToggle(), // 追加
|
||||
const SizedBox(height: 16),
|
||||
_buildDocumentTypeSection(),
|
||||
const SizedBox(height: 16),
|
||||
_buildDateSection(),
|
||||
const SizedBox(height: 16),
|
||||
_buildCustomerSection(),
|
||||
const SizedBox(height: 20),
|
||||
_buildItemsSection(fmt),
|
||||
const SizedBox(height: 20),
|
||||
_buildExperimentalSection(),
|
||||
const SizedBox(height: 20),
|
||||
_buildSummarySection(fmt),
|
||||
const SizedBox(height: 20),
|
||||
_buildSignatureSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildBottomActionBar(),
|
||||
],
|
||||
_buildBottomActionBar(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -242,6 +258,32 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildDateSection() {
|
||||
final fmt = DateFormat('yyyy年MM月dd日');
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: Colors.blueGrey.shade50,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.calendar_today, color: Colors.blueGrey),
|
||||
title: Text("伝票日付: ${fmt.format(_selectedDate)}", style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: const Text("タップして日付を変更"),
|
||||
trailing: const Icon(Icons.edit, size: 20),
|
||||
onTap: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _selectedDate,
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() => _selectedDate = picked);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCustomerSection() {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
|
|
@ -472,6 +514,34 @@ class _InvoiceInputFormState extends State<InvoiceInputForm> {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDraftToggle() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: _isDraft ? Colors.black26 : Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: _isDraft ? Colors.orangeAccent : Colors.orange, width: 2),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(_isDraft ? Icons.drafts : Icons.check_circle, color: Colors.orange),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_isDraft ? "下書きモード設定中" : "正式発行モード",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, color: _isDraft ? Colors.white : Colors.orange.shade900),
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: _isDraft,
|
||||
activeColor: Colors.orangeAccent,
|
||||
onChanged: (val) => setState(() => _isDraft = val),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SignaturePainter extends CustomPainter {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ class ManagementScreen extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: const BackButton(),
|
||||
title: const Text("マスター管理・同期"),
|
||||
backgroundColor: Colors.blueGrey,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ class _ProductMasterScreenState extends State<ProductMasterScreen> {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: const BackButton(),
|
||||
title: const Text("商品マスター"),
|
||||
backgroundColor: Colors.blueGrey,
|
||||
bottom: PreferredSize(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import 'package:sqflite/sqflite.dart';
|
|||
import 'package:path/path.dart';
|
||||
|
||||
class DatabaseHelper {
|
||||
static const _databaseVersion = 10;
|
||||
static const _databaseVersion = 11;
|
||||
static final DatabaseHelper _instance = DatabaseHelper._internal();
|
||||
static Database? _database;
|
||||
|
||||
|
|
@ -88,6 +88,9 @@ class DatabaseHelper {
|
|||
await db.execute('ALTER TABLE invoices ADD COLUMN terminal_id TEXT DEFAULT "T1"');
|
||||
await db.execute('ALTER TABLE invoices ADD COLUMN content_hash TEXT');
|
||||
}
|
||||
if (oldVersion < 11) {
|
||||
await db.execute('ALTER TABLE invoices ADD COLUMN is_draft INTEGER DEFAULT 0');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onCreate(Database db, int version) async {
|
||||
|
|
@ -151,6 +154,7 @@ class DatabaseHelper {
|
|||
longitude REAL,
|
||||
terminal_id TEXT DEFAULT "T1",
|
||||
content_hash TEXT,
|
||||
is_draft INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (customer_id) REFERENCES customers (id)
|
||||
)
|
||||
''');
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@ class InvoiceRepository {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> updateInvoice(Invoice invoice) async {
|
||||
await saveInvoice(invoice);
|
||||
}
|
||||
|
||||
Future<List<Invoice>> getAllInvoices(List<Customer> customers) async {
|
||||
final db = await _dbHelper.database;
|
||||
final List<Map<String, dynamic>> invoiceMaps = await db.query('invoices', orderBy: 'date DESC');
|
||||
|
|
@ -108,6 +112,7 @@ class InvoiceRepository {
|
|||
latitude: iMap['latitude'],
|
||||
longitude: iMap['longitude'],
|
||||
terminalId: iMap['terminal_id'] ?? "T1",
|
||||
isDraft: (iMap['is_draft'] ?? 0) == 1,
|
||||
));
|
||||
}
|
||||
return invoices;
|
||||
|
|
|
|||
105
目標.md
105
目標.md
|
|
@ -1,58 +1,51 @@
|
|||
### サンプルを編集できる状態ですが
|
||||
- 顧客マスターを実装します
|
||||
- 伝票マスターを実装します
|
||||
- 将来odooと同期する時に配慮
|
||||
- 完全にスタンドアローンで稼働するのを強く意識する
|
||||
- GoogleDrive等にDBをバックアップする可能性
|
||||
- 伝票マスター一覧画面を実装する
|
||||
− 起動したら伝票マスター一覧がトップになる様にする
|
||||
- 伝票マスターを編集する画面を実装する
|
||||
- 伝票マスターを新規作成する画面を実装する
|
||||
- 伝票マスターを削除する画面を実装する
|
||||
- 伝票マスターを検索する画面を実装する
|
||||
- 伝票マスターをソートする画面を実装する
|
||||
- 伝票マスターをフィルタリングする画面を実装する
|
||||
- 伝票マスターをインポートする画面を実装する
|
||||
- 伝票マスターをエクスポートする画面を実装する
|
||||
- 伝票マスターをバックアップする画面を実装する
|
||||
- 伝票マスターをリストアする画面を実装する
|
||||
- 伝票マスターを同期する画面を実装する
|
||||
- 伝票マスターは保護しなければならないのでアンロックする仕組みを作る
|
||||
- ロックは鍵のマークを横に大きくスワイプして解除したい
|
||||
- 商品マスター管理画面の実装
|
||||
- 伝票入力画面の実装
|
||||
- 伝票入力はあれもこれも盛り込みたいので実験的に色んなのを実装
|
||||
− 商品マスター編集画面の実装
|
||||
- 顧客マスター編集画面の実装
|
||||
- 各種マスターは内容を編集した時に伝票と整合性を保つ仕組みを作る
|
||||
- 各種マスターはodoo側の編集作業により影響を受けるのでその対策を考える
|
||||
- 各種マスターはデータが空の場合サンプルを10個入れておく
|
||||
- アプリタイトルのバージョンは常に最新にする小数点第3位を最小バージョン単位に
|
||||
− 自社情報編集はタイトルを長押しで表示
|
||||
- 自社情報編集の画面で消費税を設定可能にする
|
||||
- 自社情報編集で印鑑を撮影出来る様にする
|
||||
- 商品マスター等でバーコードQRコードのスキャンが可能でありたい
|
||||
− ロック機能はご動作対策であって削除と編集機能以外は全部使える様にする
|
||||
- 顧客マスターの新規・編集・削除機能を実装する
|
||||
- PDF作成と保存と仮表示は別ボタンで実装
|
||||
− 各マスター情報は1万件を越えたりするのでカテゴライズ等工夫して迅速に検索可能にする
|
||||
- PDF出力したりPDF共有は日時情報を付けて保存する
|
||||
- データはgit的にアクティビティを残す様にする
|
||||
- 顧客や商品等一覧から選択する時は必ず数万単位の検索がある前提にする
|
||||
- 仮表示とはPDFプレビューの事です
|
||||
- 見積納品請求領収書の切り替えボタンの実装
|
||||
- 商品マスターには在庫管理機能をこっそり実装する
|
||||
- 伝票で在庫の増減を自動的に計算する仕組みを作る
|
||||
- 年次月次の管理を実装する
|
||||
- 将来的にスタンドアローンでも資金管理を可能にする
|
||||
- データにはGPS座標の履歴を最低10件保持する
|
||||
- アマゾンで売っていたプリンタでレシート印刷をするhttps://www.amazon.co.jp/dp/B0G33SSZV6?ref=ppx_yo2ov_dt_b_fed_asin_title&th=1
|
||||
# インボイスシステム 開発目標リスト
|
||||
|
||||
- 伝票入力画面で消費税表示がシステム通りでないのを修正
|
||||
− 日時の自動入力と編集機能の実装
|
||||
- 管理番号端末UIDと伝票UIDの組み合わせで管理する
|
||||
- 管理番号の表示を画面とPDFにする
|
||||
- 伝票のコンテンツ情報に規則性を持たせSHA256をPDFに番号とQRで表示
|
||||
- PDFのファイル名にSHA256を含める
|
||||
## 📋 完了済みの項目 (V1.5.02)
|
||||
|
||||
-
|
||||
### 核心機能
|
||||
- [x] 顧客マスターの実装 (新規・編集・削除)
|
||||
- [x] 伝票マスター(履歴)の実装 (一覧・削除・ソート・フィルタ)
|
||||
- [x] 伝票入力画面の実装 (実験的オプション含む)
|
||||
- [x] 商品マスターの実装 (カテゴリー検索、バーコード対応)
|
||||
- [x] 在庫管理機能 (伝票保存/削除時の自動計算)
|
||||
|
||||
### データ整合性・セキュリティ
|
||||
- [x] 指定端末UIDと伝票UIDによる管理
|
||||
- [x] 規則性のあるコンテンツSHA256ハッシュ生成
|
||||
- [x] PDFファイル名へのハッシュ値埋め込み
|
||||
- [x] 誤動作防止の「スライドでロック解除」機能
|
||||
- [x] 各種マスター編集時の伝票整合性維持
|
||||
|
||||
### UI/UX
|
||||
- [x] アプリタイトルの小数点第3位まで含むバージョン表示
|
||||
- [x] 自社情報編集画面 (タイトル長押しで表示)
|
||||
- [x] 印鑑(社印)の撮影・登録機能
|
||||
- [x] ロック中も「閲覧」は可能にする権限分離
|
||||
- [x] 画面左上への「常に戻る矢印」の配置
|
||||
- [x] **下書き(Draft)属性の実装**
|
||||
- [x] **下書き時のテーマ切り替え (ダーク/ライト)**
|
||||
|
||||
### PDF・出力
|
||||
- [x] PDF作成、保存、仮表示(プレビュー)の分離実装
|
||||
- [x] PDFへのアクティビティログ(git的履歴)の埋め込み
|
||||
- [x] PDFへのSHA256ハッシュ/QRコード表示
|
||||
- [x] 日時情報を付与したファイル保存
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 現在進行中・未完了の項目
|
||||
|
||||
### 連携・高度な機能
|
||||
- [ ] プリンター連携 (Amazon/Bluetoothレシートプリンタ)
|
||||
- URL: [Amazon.co.jp B0G33SSZV6](https://www.amazon.co.jp/dp/B0G33SSZV6)
|
||||
- [ ] Odooとの同期機能
|
||||
- [ ] GoogleDrive等への外部クラウドバックアップ
|
||||
- [ ] 年次・月次の売上統計および資金管理のグラフィカル表示
|
||||
|
||||
### メンテナンス・改善
|
||||
- [ ] サンプルデータ(10件程度)の自動生成機能
|
||||
- [ ] 1万件を超えるマスターデータのさらなる高速検索最適化
|
||||
|
||||
---
|
||||
*最終更新日: 2026-02-14*
|
||||
*Version: 1.5.02*
|
||||
|
|
|
|||
Loading…
Reference in a new issue