initial commit (resetted)

This commit is contained in:
joe 2026-01-31 22:18:30 +09:00
commit 3128ab5579
138 changed files with 19148 additions and 0 deletions

View file

@ -0,0 +1,104 @@
// app/src/main/java/com/example/mobilepos/data/api/SyncApiService.kt
package com.example.mobilepos.data.api
import retrofit2.Response
import retrofit2.http.*
import com.example.mobilepos.data.models.*
import java.util.UUID
interface SyncApiService {
@POST("/api/v1/sync")
suspend fun syncDocuments(
@Header("X-API-Key") apiKey: String,
@Body request: SyncRequestDto
): Response<SyncResponseDto>
@GET("/api/v1/customers")
suspend fun getCustomers(
@Header("X-API-Key") apiKey: String
): Response<CustomerListResponse>
@GET("/api/v1/documents/{id}")
suspend fun getDocument(
@Header("X-API-Key") apiKey: String,
@Path("id") documentId: Int
): Response<DocumentDetailResponse>
@POST("/api/v1/receipts/{invoiceId}")
suspend fun createReceipt(
@Header("X-API-Key") apiKey: String,
@Path("invoiceId") invoiceId: Int
): Response<ReceiptCreateResponse>
@GET("/api/v1/health")
suspend fun healthCheck(
@Header("X-API-Key") apiKey: String
): Response<HealthCheckResponse>
}
// ========== API Response Models ==========
data class CustomerListResponse(
val status: String,
val customers: List<CustomerDto>
)
data class DocumentDetailResponse(
val status: String,
val document: DocumentDto
)
data class ReceiptCreateResponse(
val status: String,
val receiptId: Int,
val message: String
)
data class HealthCheckResponse(
val status: String,
val timestamp: Long
)
// ========== Retrofit Factory ==========
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import com.google.gson.GsonBuilder
object ApiClient {
private var retrofit: Retrofit? = null
fun getClient(baseUrl: String): Retrofit {
if (retrofit == null) {
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
.connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.build()
val gson = GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss")
.create()
retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
}
return retrofit!!
}
fun getSyncApiService(baseUrl: String): SyncApiService {
return getClient(baseUrl).create(SyncApiService::class.java)
}
}

109
claude/Android_DAO.kt Normal file
View file

@ -0,0 +1,109 @@
// app/src/main/java/com/example/mobilepos/data/dao/DocumentDao.kt
package com.example.mobilepos.data.dao
import androidx.room.*
import com.example.mobilepos.data.models.*
import kotlinx.coroutines.flow.Flow
@Dao
interface CustomerDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertCustomer(customer: CustomerEntity)
@Update
suspend fun updateCustomer(customer: CustomerEntity)
@Delete
suspend fun deleteCustomer(customer: CustomerEntity)
@Query("SELECT * FROM customers WHERE id = :id")
suspend fun getCustomerById(id: Int): CustomerEntity?
@Query("SELECT * FROM customers ORDER BY name ASC")
fun getAllCustomers(): Flow<List<CustomerEntity>>
@Query("SELECT * FROM customers WHERE synced = 0")
suspend fun getUnsyncedCustomers(): List<CustomerEntity>
@Query("UPDATE customers SET synced = 1 WHERE id = :id")
suspend fun markCustomerAsSynced(id: Int)
}
@Dao
interface DocumentDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertDocument(document: DocumentEntity): Long
@Update
suspend fun updateDocument(document: DocumentEntity)
@Delete
suspend fun deleteDocument(document: DocumentEntity)
@Query("SELECT * FROM documents WHERE id = :id")
suspend fun getDocumentById(id: Int): DocumentEntity?
@Query("SELECT * FROM documents WHERE customerId = :customerId ORDER BY documentDate DESC")
fun getDocumentsByCustomer(customerId: Int): Flow<List<DocumentEntity>>
@Query("SELECT * FROM documents WHERE docType = :docType ORDER BY documentDate DESC")
fun getDocumentsByType(docType: String): Flow<List<DocumentEntity>>
@Query("SELECT * FROM documents WHERE status = :status ORDER BY documentDate DESC")
fun getDocumentsByStatus(status: String): Flow<List<DocumentEntity>>
@Query("SELECT * FROM documents WHERE docType = 'invoice' AND paidDate IS NULL ORDER BY paymentDueDate ASC")
fun getUnpaidInvoices(): Flow<List<DocumentEntity>>
@Query("SELECT * FROM documents WHERE synced = 0")
suspend fun getUnsyncedDocuments(): List<DocumentEntity>
@Query("SELECT * FROM documents ORDER BY documentDate DESC")
fun getAllDocuments(): Flow<List<DocumentEntity>>
@Query("UPDATE documents SET synced = 1, syncTimestamp = :timestamp WHERE id = :id")
suspend fun markDocumentAsSynced(id: Int, timestamp: Long)
@Query("UPDATE documents SET status = :status, paidDate = :paidDate WHERE id = :id")
suspend fun updateDocumentStatus(id: Int, status: String, paidDate: Long?)
@Query("UPDATE documents SET paymentDueDate = :dueDate WHERE id = :id")
suspend fun updatePaymentDueDate(id: Int, dueDate: Long)
@Query("DELETE FROM documents WHERE docType = :docType")
suspend fun deleteDocumentsByType(docType: String)
}
@Dao
interface PaymentTermsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertPaymentTerms(terms: PaymentTermsEntity)
@Query("SELECT * FROM payment_terms WHERE documentId = :documentId")
suspend fun getPaymentTermsByDocument(documentId: Int): PaymentTermsEntity?
@Query("SELECT * FROM payment_terms ORDER BY createdAt DESC")
suspend fun getAllPaymentTerms(): List<PaymentTermsEntity>
@Delete
suspend fun deletePaymentTerms(terms: PaymentTermsEntity)
}
@Dao
interface SyncLogDao {
@Insert
suspend fun insertSyncLog(log: SyncLogEntity)
@Query("SELECT * FROM sync_logs ORDER BY timestamp DESC LIMIT :limit")
suspend fun getRecentSyncLogs(limit: Int = 10): List<SyncLogEntity>
@Query("SELECT * FROM sync_logs WHERE deviceId = :deviceId ORDER BY timestamp DESC LIMIT :limit")
suspend fun getSyncLogsByDevice(deviceId: String, limit: Int = 10): List<SyncLogEntity>
@Query("SELECT MAX(timestamp) FROM sync_logs WHERE operation = 'sync' AND status = 'success'")
suspend fun getLastSuccessfulSyncTime(): Long?
@Query("DELETE FROM sync_logs WHERE timestamp < :olderThanMillis")
suspend fun deleteSyncLogsOlderThan(olderThanMillis: Long)
}

View file

@ -0,0 +1,71 @@
// app/src/main/java/com/example/mobilepos/data/AppDatabase.kt
package com.example.mobilepos.data
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.example.mobilepos.data.dao.*
import com.example.mobilepos.data.models.*
@Database(
entities = [
CustomerEntity::class,
DocumentEntity::class,
PaymentTermsEntity::class,
SyncLogEntity::class
],
version = 2,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
abstract fun customerDao(): CustomerDao
abstract fun documentDao(): DocumentDao
abstract fun paymentTermsDao(): PaymentTermsDao
abstract fun syncLogDao(): SyncLogDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"mobile_pos_database"
)
.addMigrations(MIGRATION_1_2)
.build()
INSTANCE = instance
instance
}
}
// マイグレーション: v1 -> v2
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// syncTimestamp カラムを追加
database.execSQL(
"ALTER TABLE documents ADD COLUMN syncTimestamp INTEGER"
)
// インデックスを追加(パフォーマンス向上)
database.execSQL(
"CREATE INDEX IF NOT EXISTS idx_documents_customerId ON documents(customerId)"
)
database.execSQL(
"CREATE INDEX IF NOT EXISTS idx_documents_docType ON documents(docType)"
)
database.execSQL(
"CREATE INDEX IF NOT EXISTS idx_documents_status ON documents(status)"
)
database.execSQL(
"CREATE INDEX IF NOT EXISTS idx_documents_synced ON documents(synced)"
)
}
}
}
}

170
claude/Android_Models.kt Normal file
View file

@ -0,0 +1,170 @@
// app/src/main/java/com/example/mobilepos/data/models/Models.kt
package com.example.mobilepos.data.models
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import java.util.Date
// ========== Room Database Entities ==========
@Entity(tableName = "customers")
data class CustomerEntity(
@PrimaryKey
val id: Int,
val odooCustomerId: Int? = null,
val name: String,
val address: String? = null,
val phone: String? = null,
val email: String? = null,
val createdAt: Long = System.currentTimeMillis(),
val updatedAt: Long = System.currentTimeMillis(),
val synced: Boolean = false
)
@Entity(tableName = "documents")
data class DocumentEntity(
@PrimaryKey
val id: Int = 0,
val odooId: Int? = null,
val docType: String, // quotation, delivery, invoice, receipt
val customerId: Int,
val documentDate: Long,
val items: String, // JSON string
val subtotal: Double,
val tax: Double,
val total: Double,
val status: String, // draft, sent, confirmed, paid
val billingDate: Long? = null,
val paymentDueDate: Long,
val paymentMethod: String? = null,
val paidDate: Long? = null,
val notes: String? = null,
val createdAt: Long = System.currentTimeMillis(),
val updatedAt: Long = System.currentTimeMillis(),
val synced: Boolean = false,
val syncTimestamp: Long? = null
)
@Entity(tableName = "payment_terms")
data class PaymentTermsEntity(
@PrimaryKey
val id: Int = 0,
val documentId: Int,
val billingDate: Long? = null,
val paymentDueDate: Long,
val paymentMethod: String,
val createdAt: Long = System.currentTimeMillis()
)
@Entity(tableName = "sync_logs")
data class SyncLogEntity(
@PrimaryKey
val id: Int = 0,
val deviceId: String,
val operation: String, // sync, upload, download
val documentCount: Int,
val status: String, // success, failure
val message: String? = null,
val timestamp: Long = System.currentTimeMillis()
)
// ========== Data Transfer Objects (DTOs) ==========
data class CustomerDto(
val id: Int,
val name: String,
val address: String? = null,
val phone: String? = null,
val email: String? = null
)
data class DocumentItemDto(
val productName: String,
val quantity: Double,
val unitPrice: Double,
val subtotal: Double
)
data class PaymentTermsDto(
val billingDate: Long? = null,
val paymentDueDate: Long,
val paymentMethod: String
)
data class DocumentDto(
val id: Int = 0,
val docType: String,
val customerId: Int,
val documentDate: Long,
val items: List<DocumentItemDto>,
val subtotal: Double,
val tax: Double,
val total: Double,
val paymentTerms: PaymentTermsDto,
val status: String = "draft",
val notes: String? = null
)
data class SyncRequestDto(
val deviceId: String,
val lastSyncTimestamp: Long? = null,
val documents: List<DocumentDto>
)
data class SyncResponseDto(
val status: String,
val message: String,
val syncedDocuments: Int,
val newDocuments: List<Map<String, Any>>? = null
)
// ========== UI State Models ==========
data class DocumentUIState(
val id: Int = 0,
val docType: String = "quotation",
val customer: CustomerDto? = null,
val customerId: Int = 0,
val documentDate: Long = System.currentTimeMillis(),
val items: List<DocumentItemDto> = emptyList(),
val subtotal: Double = 0.0,
val tax: Double = 0.0,
val total: Double = 0.0,
val billingDate: Long? = null,
val paymentDueDate: Long = System.currentTimeMillis(),
val paymentMethod: String = "bank_transfer",
val status: String = "draft",
val notes: String = "",
val isSaving: Boolean = false,
val isLoading: Boolean = false,
val errorMessage: String? = null,
val showPaymentDatePicker: Boolean = false
)
data class DocumentListUIState(
val documents: List<DocumentEntity> = emptyList(),
val isLoading: Boolean = false,
val errorMessage: String? = null,
val filter: String = "all" // all, quotation, delivery, invoice, receipt
)
data class SyncUIState(
val isSyncing: Boolean = false,
val syncProgress: Int = 0,
val lastSyncTime: Long? = null,
val syncedCount: Int = 0,
val totalCount: Int = 0,
val errorMessage: String? = null,
val status: String = "ready"
)
// ========== Payment Term Patterns ==========
enum class PaymentPattern(val displayName: String, val days: Int? = null) {
IMMEDIATE("即支払い", 0),
END_OF_MONTH("末締め翌月末", null),
THIRTY_DAYS("30日後", 30),
SIXTY_DAYS("60日後", 60),
CUSTOM("カスタム", null)
}

View file

@ -0,0 +1,390 @@
// app/src/main/java/com/example/mobilepos/util/PdfGenerator.kt
package com.example.mobilepos.util
import android.content.Context
import android.graphics.pdf.PdfDocument
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Color
import android.graphics.Typeface
import java.io.File
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.*
import com.example.mobilepos.data.models.*
import com.google.gson.Gson
object PdfGenerator {
private const val PAGE_WIDTH = 595 // A4幅ポイント
private const val PAGE_HEIGHT = 842 // A4高さポイント
private const val MARGIN = 40
private const val CONTENT_WIDTH = PAGE_WIDTH - (MARGIN * 2)
fun generateQuotationPdf(
context: Context,
document: DocumentEntity,
customer: CustomerEntity,
fileName: String = "quotation_${document.id}.pdf"
): File? {
return try {
val pdfDocument = PdfDocument()
var pageHeight = PAGE_HEIGHT
var yPosition = MARGIN
val pageInfo = PdfDocument.PageInfo.Builder(PAGE_WIDTH, pageHeight, 1).create()
var page = pdfDocument.startPage(pageInfo)
var canvas = page.canvas
// ヘッダー
yPosition = drawHeader(canvas, yPosition, "見積書")
yPosition += 20
// 会社情報(左上)
yPosition = drawCompanyInfo(canvas, yPosition)
yPosition += 20
// 見積情報
canvas.drawText("見積日:${formatDate(document.documentDate)}", MARGIN.toFloat(), yPosition.toFloat(), getPaint())
yPosition += 20
canvas.drawText("見積番号QT-${document.id.toString().padStart(6, '0')}", MARGIN.toFloat(), yPosition.toFloat(), getPaint())
yPosition += 30
// 顧客情報
yPosition = drawCustomerInfo(canvas, yPosition, customer)
yPosition += 20
// 区切り線
canvas.drawLine(MARGIN.toFloat(), yPosition.toFloat(), (PAGE_WIDTH - MARGIN).toFloat(), yPosition.toFloat(), getPaint())
yPosition += 10
// 商品テーブルヘッダー
yPosition = drawTableHeader(canvas, yPosition)
yPosition += 5
// 商品行
val items = parseDocumentItems(document.items)
for (item in items) {
if (yPosition > PAGE_HEIGHT - 100) {
// 新しいページ
pdfDocument.finishPage(page)
val newPageInfo = PdfDocument.PageInfo.Builder(PAGE_WIDTH, pageHeight, pdfDocument.pages.size + 1).create()
page = pdfDocument.startPage(newPageInfo)
canvas = page.canvas
yPosition = MARGIN
}
yPosition = drawTableRow(canvas, yPosition, item)
}
yPosition += 10
canvas.drawLine(MARGIN.toFloat(), yPosition.toFloat(), (PAGE_WIDTH - MARGIN).toFloat(), yPosition.toFloat(), getPaint())
yPosition += 15
// 合計
yPosition = drawTotals(canvas, yPosition, document)
yPosition += 20
// 支払期限
val dueDate = formatDate(document.paymentDueDate)
canvas.drawText("お支払い期限:$dueDate", MARGIN.toFloat(), yPosition.toFloat(), getPaint(bold = true))
yPosition += 20
canvas.drawText("お支払い方法:${document.paymentMethod ?: "銀行振込"}", MARGIN.toFloat(), yPosition.toFloat(), getPaint())
// 備考
if (!document.notes.isNullOrEmpty()) {
yPosition += 20
canvas.drawText("備考:", MARGIN.toFloat(), yPosition.toFloat(), getPaint(bold = true))
yPosition += 15
val noteLines = document.notes!!.split("\n")
for (line in noteLines) {
canvas.drawText(line, MARGIN.toFloat(), yPosition.toFloat(), getPaint())
yPosition += 15
}
}
pdfDocument.finishPage(page)
// ファイル保存
val outputFile = File(context.getExternalFilesDir(null), fileName)
val fos = FileOutputStream(outputFile)
pdfDocument.writeTo(fos)
fos.close()
pdfDocument.close()
outputFile
} catch (e: Exception) {
e.printStackTrace()
null
}
}
fun generateInvoicePdf(
context: Context,
document: DocumentEntity,
customer: CustomerEntity,
fileName: String = "invoice_${document.id}.pdf"
): File? {
return try {
val pdfDocument = PdfDocument()
var pageHeight = PAGE_HEIGHT
var yPosition = MARGIN
val pageInfo = PdfDocument.PageInfo.Builder(PAGE_WIDTH, pageHeight, 1).create()
var page = pdfDocument.startPage(pageInfo)
var canvas = page.canvas
// ヘッダー
yPosition = drawHeader(canvas, yPosition, "請求書")
yPosition += 20
// 会社情報
yPosition = drawCompanyInfo(canvas, yPosition)
yPosition += 20
// 請求情報
canvas.drawText("請求日:${formatDate(document.documentDate)}", MARGIN.toFloat(), yPosition.toFloat(), getPaint())
yPosition += 20
canvas.drawText("請求番号INV-${document.id.toString().padStart(6, '0')}", MARGIN.toFloat(), yPosition.toFloat(), getPaint())
yPosition += 30
// 顧客情報
yPosition = drawCustomerInfo(canvas, yPosition, customer)
yPosition += 20
// 区切り線
canvas.drawLine(MARGIN.toFloat(), yPosition.toFloat(), (PAGE_WIDTH - MARGIN).toFloat(), yPosition.toFloat(), getPaint())
yPosition += 10
// 商品テーブル
yPosition = drawTableHeader(canvas, yPosition)
yPosition += 5
val items = parseDocumentItems(document.items)
for (item in items) {
if (yPosition > PAGE_HEIGHT - 150) {
pdfDocument.finishPage(page)
val newPageInfo = PdfDocument.PageInfo.Builder(PAGE_WIDTH, pageHeight, pdfDocument.pages.size + 1).create()
page = pdfDocument.startPage(newPageInfo)
canvas = page.canvas
yPosition = MARGIN
}
yPosition = drawTableRow(canvas, yPosition, item)
}
yPosition += 10
canvas.drawLine(MARGIN.toFloat(), yPosition.toFloat(), (PAGE_WIDTH - MARGIN).toFloat(), yPosition.toFloat(), getPaint())
yPosition += 15
// 合計
yPosition = drawTotals(canvas, yPosition, document)
yPosition += 20
// 支払期限・方法
val dueDate = formatDate(document.paymentDueDate)
canvas.drawText("お支払い期限:$dueDate", MARGIN.toFloat(), yPosition.toFloat(), getPaint(bold = true))
yPosition += 20
canvas.drawText("お支払い方法:${document.paymentMethod ?: "銀行振込"}", MARGIN.toFloat(), yPosition.toFloat(), getPaint())
pdfDocument.finishPage(page)
val outputFile = File(context.getExternalFilesDir(null), fileName)
val fos = FileOutputStream(outputFile)
pdfDocument.writeTo(fos)
fos.close()
pdfDocument.close()
outputFile
} catch (e: Exception) {
e.printStackTrace()
null
}
}
fun generateReceiptPdf(
context: Context,
document: DocumentEntity,
customer: CustomerEntity,
fileName: String = "receipt_${document.id}.pdf"
): File? {
return try {
val pdfDocument = PdfDocument()
var pageHeight = PAGE_HEIGHT
var yPosition = MARGIN
val pageInfo = PdfDocument.PageInfo.Builder(PAGE_WIDTH, pageHeight, 1).create()
val page = pdfDocument.startPage(pageInfo)
val canvas = page.canvas
// ヘッダー
yPosition = drawHeader(canvas, yPosition, "領収書")
yPosition += 20
// 会社情報
yPosition = drawCompanyInfo(canvas, yPosition)
yPosition += 20
// 領収情報
canvas.drawText("領収日:${formatDate(document.documentDate)}", MARGIN.toFloat(), yPosition.toFloat(), getPaint())
yPosition += 20
canvas.drawText("領収番号RCP-${document.id.toString().padStart(6, '0')}", MARGIN.toFloat(), yPosition.toFloat(), getPaint())
yPosition += 30
// 顧客情報
yPosition = drawCustomerInfo(canvas, yPosition, customer)
yPosition += 20
// 金額
canvas.drawText("お振込金額", MARGIN.toFloat(), yPosition.toFloat(), getPaint(bold = true))
yPosition += 20
val totalText = "¥${String.format("%,d", document.total.toLong())}"
val totalPaint = getPaint(bold = true, size = 36f)
canvas.drawText(totalText, MARGIN.toFloat(), yPosition.toFloat(), totalPaint)
yPosition += 40
// 摘要
canvas.drawText("摘要:商品・サービス提供代金", MARGIN.toFloat(), yPosition.toFloat(), getPaint())
yPosition += 20
// 支払い日
if (document.paidDate != null) {
canvas.drawText("お支払い日:${formatDate(document.paidDate!!)}", MARGIN.toFloat(), yPosition.toFloat(), getPaint())
}
pdfDocument.finishPage(page)
val outputFile = File(context.getExternalFilesDir(null), fileName)
val fos = FileOutputStream(outputFile)
pdfDocument.writeTo(fos)
fos.close()
pdfDocument.close()
outputFile
} catch (e: Exception) {
e.printStackTrace()
null
}
}
// ========== ヘルパー関数 ==========
private fun drawHeader(canvas: Canvas, yPosition: Int, title: String): Int {
val paint = getPaint(bold = true, size = 28f)
canvas.drawText(title, MARGIN.toFloat(), (yPosition + 25).toFloat(), paint)
return yPosition + 40
}
private fun drawCompanyInfo(canvas: Canvas, yPosition: Int): Int {
val paint = getPaint(size = 10f)
var y = yPosition
canvas.drawText("株式会社 ○○○○", MARGIN.toFloat(), y.toFloat(), paint)
y += 12
canvas.drawText("住所〒000-0000 ○○県○○市○○町1-1", MARGIN.toFloat(), y.toFloat(), paint)
y += 12
canvas.drawText("電話09X-XXXX-XXXX", MARGIN.toFloat(), y.toFloat(), paint)
y += 12
canvas.drawText("メールinfo@example.com", MARGIN.toFloat(), y.toFloat(), paint)
return y
}
private fun drawCustomerInfo(canvas: Canvas, yPosition: Int, customer: CustomerEntity): Int {
val paint = getPaint()
var y = yPosition
canvas.drawText("ご購入者様", MARGIN.toFloat(), y.toFloat(), paint)
y += 20
canvas.drawText(customer.name, MARGIN.toFloat(), y.toFloat(), paint)
y += 15
if (!customer.address.isNullOrEmpty()) {
canvas.drawText("住所:${customer.address}", MARGIN.toFloat(), y.toFloat(), paint)
y += 15
}
if (!customer.phone.isNullOrEmpty()) {
canvas.drawText("電話:${customer.phone}", MARGIN.toFloat(), y.toFloat(), paint)
y += 15
}
return y
}
private fun drawTableHeader(canvas: Canvas, yPosition: Int): Int {
val paint = getPaint(bold = true)
val smallPaint = getPaint(size = 10f)
var x = MARGIN
canvas.drawText("品目", x.toFloat(), yPosition.toFloat(), paint)
x += 200
canvas.drawText("数量", x.toFloat(), yPosition.toFloat(), paint)
x += 80
canvas.drawText("単価", x.toFloat(), yPosition.toFloat(), paint)
x += 80
canvas.drawText("小計", x.toFloat(), yPosition.toFloat(), paint)
return yPosition + 15
}
private fun drawTableRow(canvas: Canvas, yPosition: Int, item: DocumentItemDto): Int {
val paint = getPaint(size = 11f)
val productText = item.productName
val quantityText = String.format("%.2f", item.quantity)
val unitPriceText = "¥${String.format("%,d", item.unitPrice.toLong())}"
val subtotalText = "¥${String.format("%,d", item.subtotal.toLong())}"
var x = MARGIN
canvas.drawText(productText, x.toFloat(), yPosition.toFloat(), paint)
x += 200
canvas.drawText(quantityText, x.toFloat(), yPosition.toFloat(), paint)
x += 80
canvas.drawText(unitPriceText, x.toFloat(), yPosition.toFloat(), paint)
x += 80
canvas.drawText(subtotalText, x.toFloat(), yPosition.toFloat(), paint)
return yPosition + 15
}
private fun drawTotals(canvas: Canvas, yPosition: Int, document: DocumentEntity): Int {
val paint = getPaint()
val boldPaint = getPaint(bold = true)
var y = yPosition
val rightX = (PAGE_WIDTH - MARGIN - 100).toFloat()
// 小計
canvas.drawText("小計:", rightX - 80, y.toFloat(), paint)
canvas.drawText("¥${String.format("%,d", document.subtotal.toLong())}", rightX.toFloat(), y.toFloat(), paint)
y += 20
// 税金
canvas.drawText("税金:", rightX - 80, y.toFloat(), paint)
canvas.drawText("¥${String.format("%,d", document.tax.toLong())}", rightX.toFloat(), y.toFloat(), paint)
y += 20
// 合計
canvas.drawText("合計:", rightX - 80, y.toFloat(), boldPaint)
canvas.drawText("¥${String.format("%,d", document.total.toLong())}", rightX.toFloat(), y.toFloat(), boldPaint)
return y + 20
}
private fun getPaint(bold: Boolean = false, size: Float = 12f): Paint {
return Paint().apply {
this.typeface = if (bold) Typeface.create(Typeface.DEFAULT, Typeface.BOLD) else Typeface.DEFAULT
this.textSize = size
this.color = Color.BLACK
}
}
private fun formatDate(timestamp: Long): String {
val sdf = SimpleDateFormat("yyyy年MM月dd日", Locale.JAPAN)
return sdf.format(Date(timestamp))
}
private fun parseDocumentItems(itemsJson: String): List<DocumentItemDto> {
return try {
val gson = Gson()
gson.fromJson(itemsJson, Array<DocumentItemDto>::class.java).toList()
} catch (e: Exception) {
emptyList()
}
}
}

414
claude/README.md Normal file
View file

@ -0,0 +1,414 @@
# モバイルPOS・見積/納品/請求/領収書システム
## 概要
Proxmox CT上で動作するオフラインファーストのスマートフォンアプリケーション。
営業現場で完全スタンドアロンで見積・納品・請求・領収書を作成・管理し、
ネットワーク接続時にOdooと同期します。
## アーキテクチャ
```
┌─────────────────┐
│ Android App │ ← スマホ(完全オフライン対応)
│ SQLite DB │
│ PDF生成 │
└────────┬────────┘
│ (ネットワーク接続時)
┌────▼─────────────────────────┐
│ REST API コンテナ │
│ (Python FastAPI) │
│ - 同期エンドポイント │
│ - Odoo連携 │
└────┬────────────────┬─────────┘
│ │
┌────▼──────┐ ┌─────▼─────┐
│ PostgreSQL│ │ Odoo │
│ DB │ │ (Sales) │
└───────────┘ └───────────┘
```
## ディレクトリ構成
```
project_root/
├── docker-compose.yml # Odoo + API + DB
├── api/
│ ├── main.py # FastAPI メイン
│ ├── requirements.txt
│ └── Dockerfile
├── addons/ # Odooカスタムモジュール
├── scheduler/ # 定期同期スクリプト
└── android/
├── Models.kt # データモデル
├── DAO.kt # Room DAO
├── Database.kt # Room DB
├── ApiService.kt # Retrofit API
└── PdfGenerator.kt # PDF生成
```
## セットアップ手順
### 1. Docker環境構築Proxmox CT
```bash
# リポジトリクローン
git clone <your-repo-url>
cd project_root
# 環境変数設定
cp .env.example .env
# .envを編集API_SECRET_KEY等
# コンテナ起動
docker-compose up -d
# ログ確認
docker-compose logs -f api
docker-compose logs -f odoo
```
### 2. Odooの初期セットアップ
```bash
# Odooにアクセス
# http://localhost:8069
# 以下のモジュールを有効化
# - Sales (見積・受注管理)
# - Invoicing (請求・領収書)
# - Accounting (会計・売掛金)
# API認証設定
# Admin > 設定 > API キーを生成
```
### 3. REST APIの初期化
```bash
# DB テーブル作成
docker-compose exec api python -c "from main import Base, engine; Base.metadata.create_all(bind=engine)"
# テスト
curl -X GET http://localhost:8000/api/v1/health \
-H "X-API-Key: your_secret_key"
```
### 4. Androidアプリ開発
#### 依存パッケージ (build.gradle)
```gradle
dependencies {
// Room
implementation "androidx.room:room-runtime:2.6.0"
implementation "androidx.room:room-ktx:2.6.0"
kapt "androidx.room:room-compiler:2.6.0"
// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.11.0"
// Coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
// Jetpack Compose
implementation "androidx.compose.ui:ui:1.6.0"
implementation "androidx.compose.material3:material3:1.1.0"
// PDF (iText or Apache POI)
implementation "com.itextpdf:itext-core:8.0.0"
// Gson
implementation "com.google.code.gson:gson:2.10.1"
}
```
#### AndroidManifest.xml の設定
```xml
<!-- パーミッション -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
```
#### build.gradle 設定
```gradle
android {
compileSdk 34
defaultConfig {
applicationId "com.example.mobilepos"
minSdk 26
targetSdk 34
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.0"
}
}
```
## API エンドポイント
### 同期
**POST** `/api/v1/sync`
```bash
curl -X POST http://localhost:8000/api/v1/sync \
-H "X-API-Key: secret_key" \
-H "Content-Type: application/json" \
-d '{
"device_id": "device_001",
"last_sync_timestamp": null,
"documents": [
{
"doc_type": "quotation",
"customer_id": 1,
"document_date": "2026-01-31T10:00:00",
"items": [
{
"product_name": "商品A",
"quantity": 10,
"unit_price": 1000,
"subtotal": 10000
}
],
"subtotal": 10000,
"tax": 1000,
"total": 11000,
"payment_terms": {
"billing_date": "2026-01-31",
"payment_due_date": "2026-02-28",
"payment_method": "bank_transfer"
}
}
]
}'
```
### 顧客一覧
**GET** `/api/v1/customers`
```bash
curl http://localhost:8000/api/v1/customers \
-H "X-API-Key: secret_key"
```
### ドキュメント取得
**GET** `/api/v1/documents/{id}`
### 領収書自動生成
**POST** `/api/v1/receipts/{invoice_id}`
```bash
# 入金から1週間以内の請求書から領収書を生成
curl -X POST http://localhost:8000/api/v1/receipts/1 \
-H "X-API-Key: secret_key"
```
## Android実装ガイド
### 1. データベース初期化
```kotlin
val db = AppDatabase.getInstance(context)
val documentDao = db.documentDao()
val customerDao = db.customerDao()
```
### 2. ドキュメント作成・保存
```kotlin
val document = DocumentEntity(
docType = "quotation",
customerId = 1,
documentDate = System.currentTimeMillis(),
items = Gson().toJson(listOf(
DocumentItemDto("商品A", 10.0, 1000.0, 10000.0)
)),
subtotal = 10000.0,
tax = 1000.0,
total = 11000.0,
paymentDueDate = calculateDueDate(PaymentPattern.END_OF_MONTH),
synced = false
)
documentDao.insertDocument(document)
```
### 3. PDF生成
```kotlin
val file = PdfGenerator.generateQuotationPdf(
context = context,
document = document,
customer = customer,
fileName = "quotation_${document.id}.pdf"
)
// ファイル共有
val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "application/pdf"
putExtra(Intent.EXTRA_STREAM, uri)
}
startActivity(Intent.createChooser(shareIntent, "PDFを共有"))
```
### 4. 同期処理
```kotlin
suspend fun syncDocuments(context: Context) {
val apiKey = "your_secret_key"
val baseUrl = "http://your_api_server:8000"
val apiService = ApiClient.getSyncApiService(baseUrl)
val db = AppDatabase.getInstance(context)
val unsyncedDocs = db.documentDao().getUnsyncedDocuments()
val request = SyncRequestDto(
deviceId = getDeviceId(),
documents = unsyncedDocs.map { convertToDto(it) }
)
try {
val response = apiService.syncDocuments(apiKey, request)
if (response.isSuccessful && response.body()?.status == "success") {
response.body()?.newDocuments?.forEach { doc ->
db.documentDao().markDocumentAsSynced(doc["local_id"] as Int, System.currentTimeMillis())
}
}
} catch (e: Exception) {
Log.e("Sync", "Error: ${e.message}")
}
}
```
### 5. 支払期限の計算
```kotlin
fun calculatePaymentDueDate(billingDate: Long, pattern: PaymentPattern): Long {
val calendar = Calendar.getInstance().apply {
timeInMillis = billingDate
}
return when (pattern) {
PaymentPattern.IMMEDIATE -> calendar.timeInMillis
PaymentPattern.END_OF_MONTH -> {
calendar.set(Calendar.DAY_OF_MONTH, 1)
calendar.add(Calendar.MONTH, 1)
calendar.add(Calendar.DAY_OF_MONTH, -1)
calendar.timeInMillis
}
PaymentPattern.THIRTY_DAYS -> {
calendar.add(Calendar.DAY_OF_MONTH, 30)
calendar.timeInMillis
}
PaymentPattern.SIXTY_DAYS -> {
calendar.add(Calendar.DAY_OF_MONTH, 60)
calendar.timeInMillis
}
else -> calendar.timeInMillis
}
}
```
## 同期フロー
### オフライン時
1. スマホアプリで見積/納品/請求/領収書を作成
2. SQLiteに自動保存
3. PDF生成・送信メール等
### ネットワーク接続時
1. 未同期ドキュメントを検出
2. REST APIに送信
3. API が Odoo に登録
4. 同期完了後、ローカルの synced フラグを更新
5. Odoo で売掛金管理・レポート生成
## 支払条件パターン
| パターン | 説明 | 計算方式 |
|---------|------|--------|
| 即支払い | 当日支払い | 請求日 |
| 末締め翌月末 | 末締めで翌月末払い | 翌月末日 |
| 30日後 | 請求日から30日後 | 請求日 + 30日 |
| 60日後 | 請求日から60日後 | 請求日 + 60日 |
| カスタム | 任意設定 | ユーザーが指定 |
## セキュリティ
- API キーは環境変数で管理
- HTTPS通信を推奨本番環境
- トークン認証で API アクセス制限
- Odoo への認証も環境変数化
## トラブルシューティング
### API接続エラー
```bash
# ヘルスチェック
curl http://localhost:8000/api/v1/health -H "X-API-Key: your_key"
# ログ確認
docker-compose logs api
```
### Odoo連携エラー
```bash
# Odooログ
docker-compose logs odoo
# DBテーブル確認
docker-compose exec postgres psql -U odoo -d odoo -c "\dt"
```
### Android PDF生成エラー
- ストレージパーミッション確認
- 外部ストレージ空き容量確認
- iText/Apache POI の対応バージョン確認
## 今後の実装予定
- [ ] Odoo REST API 直接連携
- [ ] Nextcloud WebDAV バックアップ統合
- [ ] 複数ユーザー・デバイス同期
- [ ] オフライン時の競合解決
- [ ] 売掛金ダッシュボード
- [ ] Web UIPCから Odoo 管理用)
- [ ] カスタム領収書テンプレート
- [ ] 電子署名対応
## 開発者向け情報
### REST API テスト
```bash
# FastAPI ドキュメント
http://localhost:8000/docs
```
### DB マイグレーション
```bash
# 新しいテーブル追加時
# Android: Migrations クラスを追加
# API: SQLAlchemy モデルを追加 → DB再作成
```
## ライセンス
MIT License
## サポート
問題報告は Issues で。

374
claude/api_main.py Normal file
View file

@ -0,0 +1,374 @@
"""
Mobile Sync API - Odoo連携
見積/納品/請求/領収書のモバイル同期エンドポイント
"""
from fastapi import FastAPI, HTTPException, Depends, Header, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from datetime import datetime, timedelta
from typing import Optional, List
import sqlalchemy as sa
from sqlalchemy import create_engine, Column, Integer, String, DateTime, JSON, Numeric, Boolean, ForeignKey
from sqlalchemy.orm import sessionmaker, declarative_base, Session
from sqlalchemy.ext.declarative import declarative_base
import os
import json
import requests
from dateutil.relativedelta import relativedelta
import logging
# ========== 設定 ==========
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://odoo:odoo_secure_password@localhost:5432/odoo")
ODOO_URL = os.getenv("ODOO_URL", "http://localhost:8069")
ODOO_USER = os.getenv("ODOO_USER", "admin")
ODOO_PASSWORD = os.getenv("ODOO_PASSWORD", "admin")
API_SECRET_KEY = os.getenv("API_SECRET_KEY", "your_secret_key_here")
# ========== ログ設定 ==========
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ========== DB設定 ==========
engine = create_engine(DATABASE_URL, echo=False)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# ========== SQLAlchemy モデル ==========
class Customer(Base):
__tablename__ = "mobile_customers"
id = Column(Integer, primary_key=True)
odoo_customer_id = Column(Integer, unique=True, nullable=True)
name = Column(String(255))
address = Column(String(500), nullable=True)
phone = Column(String(20), nullable=True)
email = Column(String(255), nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
synced = Column(Boolean, default=False)
class Document(Base):
__tablename__ = "mobile_documents"
id = Column(Integer, primary_key=True)
odoo_id = Column(Integer, unique=True, nullable=True)
doc_type = Column(String(50)) # quotation, delivery, invoice, receipt
customer_id = Column(Integer, ForeignKey("mobile_customers.id"))
document_date = Column(DateTime)
items = Column(JSON) # [{product_name, quantity, unit_price, subtotal}, ...]
subtotal = Column(Numeric(12, 2))
tax = Column(Numeric(12, 2))
total = Column(Numeric(12, 2))
status = Column(String(50)) # draft, sent, confirmed, paid, etc.
billing_date = Column(DateTime, nullable=True)
payment_due_date = Column(DateTime, nullable=True)
payment_method = Column(String(100), nullable=True)
paid_date = Column(DateTime, nullable=True)
notes = Column(String(1000), nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
synced = Column(Boolean, default=False)
sync_timestamp = Column(DateTime, nullable=True)
class SyncLog(Base):
__tablename__ = "sync_logs"
id = Column(Integer, primary_key=True)
device_id = Column(String(255))
operation = Column(String(50)) # sync, upload, download
document_count = Column(Integer)
status = Column(String(50)) # success, failure
message = Column(String(500), nullable=True)
timestamp = Column(DateTime, default=datetime.utcnow)
# ========== Pydantic モデル ==========
class ItemModel(BaseModel):
product_name: str
quantity: float
unit_price: float
subtotal: float
class PaymentTermsModel(BaseModel):
billing_date: Optional[datetime] = None
payment_due_date: datetime
payment_method: str # bank_transfer, cash, etc.
class DocumentModel(BaseModel):
doc_type: str # quotation, delivery, invoice, receipt
customer_id: int
document_date: datetime
items: List[ItemModel]
subtotal: float
tax: float
total: float
payment_terms: PaymentTermsModel
status: str = "draft"
notes: Optional[str] = None
class SyncRequest(BaseModel):
device_id: str
last_sync_timestamp: Optional[datetime] = None
documents: List[DocumentModel]
class SyncResponse(BaseModel):
status: str
message: str
synced_documents: int
new_documents: Optional[List[dict]] = None
# ========== FastAPI アプリ ==========
app = FastAPI(title="Mobile Sync API", version="1.0.0")
# ========== DB セッション依存性 ==========
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# ========== 認証 ==========
async def verify_api_key(x_api_key: str = Header(None)):
if x_api_key != API_SECRET_KEY:
raise HTTPException(status_code=401, detail="Invalid API key")
return x_api_key
# ========== ヘルパー関数 ==========
def calculate_payment_due_date(billing_date: datetime, pattern: str) -> datetime:
"""
支払期限を計算
patterns:
- "immediate": 即支払い当日
- "end_of_month": 末締め翌月末
- "30days": 30日後
- "60days": 60日後
"""
if pattern == "immediate":
return billing_date
elif pattern == "end_of_month":
# 翌月末
next_month = billing_date + relativedelta(months=1)
return next_month.replace(day=1) - timedelta(days=1)
elif pattern == "30days":
return billing_date + timedelta(days=30)
elif pattern == "60days":
return billing_date + timedelta(days=60)
else:
# デフォルト30日後
return billing_date + timedelta(days=30)
def sync_to_odoo(db: Session, document: Document) -> dict:
"""
ドキュメントを Odoo に同期
"""
try:
# Odoo XML-RPC または REST API を使用してデータを送信
# ここでは簡略版
logger.info(f"Syncing document {document.id} to Odoo")
# TODO: Odoo API呼び出し
# response = odoo_api.create_document(...)
document.synced = True
document.sync_timestamp = datetime.utcnow()
db.commit()
return {"status": "success", "odoo_id": document.odoo_id}
except Exception as e:
logger.error(f"Error syncing to Odoo: {str(e)}")
return {"status": "error", "message": str(e)}
# ========== エンドポイント ==========
@app.post("/api/v1/sync", response_model=SyncResponse, dependencies=[Depends(verify_api_key)])
async def sync_documents(request: SyncRequest, db: Session = Depends(get_db)):
"""
モバイルアプリからのドキュメント同期
"""
try:
synced_count = 0
new_documents = []
for doc in request.documents:
# 顧客を確認
customer = db.query(Customer).filter(Customer.id == doc.customer_id).first()
if not customer:
logger.warning(f"Customer {doc.customer_id} not found")
continue
# ドキュメント作成
db_doc = Document(
doc_type=doc.doc_type,
customer_id=doc.customer_id,
document_date=doc.document_date,
items=json.dumps([item.model_dump() for item in doc.items]),
subtotal=doc.subtotal,
tax=doc.tax,
total=doc.total,
status=doc.status,
billing_date=doc.payment_terms.billing_date,
payment_due_date=doc.payment_terms.payment_due_date,
payment_method=doc.payment_terms.payment_method,
notes=doc.notes,
synced=False
)
db.add(db_doc)
db.commit()
db.refresh(db_doc)
# Odoo に同期
sync_result = sync_to_odoo(db, db_doc)
synced_count += 1
new_documents.append({
"local_id": db_doc.id,
"odoo_id": db_doc.odoo_id,
"doc_type": db_doc.doc_type,
"status": sync_result["status"]
})
# ログ記録
sync_log = SyncLog(
device_id=request.device_id,
operation="sync",
document_count=synced_count,
status="success",
message=f"Synced {synced_count} documents"
)
db.add(sync_log)
db.commit()
return SyncResponse(
status="success",
message=f"Successfully synced {synced_count} documents",
synced_documents=synced_count,
new_documents=new_documents
)
except Exception as e:
logger.error(f"Sync error: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/v1/customers", dependencies=[Depends(verify_api_key)])
async def get_customers(db: Session = Depends(get_db)):
"""
顧客一覧取得オンライン時にマスタ更新
"""
try:
customers = db.query(Customer).all()
return {
"status": "success",
"customers": [
{
"id": c.id,
"name": c.name,
"address": c.address,
"phone": c.phone,
"email": c.email
} for c in customers
]
}
except Exception as e:
logger.error(f"Error fetching customers: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/v1/documents/{doc_id}", dependencies=[Depends(verify_api_key)])
async def get_document(doc_id: int, db: Session = Depends(get_db)):
"""
特定ドキュメント取得
"""
try:
doc = db.query(Document).filter(Document.id == doc_id).first()
if not doc:
raise HTTPException(status_code=404, detail="Document not found")
return {
"status": "success",
"document": {
"id": doc.id,
"doc_type": doc.doc_type,
"customer_id": doc.customer_id,
"document_date": doc.document_date,
"items": json.loads(doc.items) if doc.items else [],
"total": float(doc.total),
"payment_due_date": doc.payment_due_date,
"status": doc.status
}
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching document: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/v1/receipts/{invoice_id}", dependencies=[Depends(verify_api_key)])
async def create_receipt(invoice_id: int, db: Session = Depends(get_db)):
"""
請求書から領収書を自動生成
入金1週間以内の場合に発行可能
"""
try:
invoice = db.query(Document).filter(
Document.id == invoice_id,
Document.doc_type == "invoice"
).first()
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
if not invoice.paid_date:
raise HTTPException(status_code=400, detail="Invoice not yet paid")
# 入金1週間以内かチェック
days_since_payment = (datetime.utcnow() - invoice.paid_date).days
if days_since_payment > 7:
raise HTTPException(status_code=400, detail="Receipt cannot be issued (payment older than 7 days)")
# 領収書作成
receipt = Document(
doc_type="receipt",
customer_id=invoice.customer_id,
document_date=datetime.utcnow(),
items=invoice.items,
subtotal=invoice.subtotal,
tax=invoice.tax,
total=invoice.total,
status="issued",
paid_date=invoice.paid_date,
synced=False
)
db.add(receipt)
db.commit()
db.refresh(receipt)
return {
"status": "success",
"receipt_id": receipt.id,
"message": "Receipt created successfully"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error creating receipt: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/v1/health", dependencies=[Depends(verify_api_key)])
async def health_check():
"""
ヘルスチェック
"""
return {"status": "ok", "timestamp": datetime.utcnow()}
# ========== DB初期化 ==========
@app.on_event("startup")
async def startup():
Base.metadata.create_all(bind=engine)
logger.info("Database tables created")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

96
claude/docker-compose.yml Normal file
View file

@ -0,0 +1,96 @@
version: '3.8'
services:
# PostgreSQLOdoo用
postgres:
image: postgres:15-alpine
container_name: odoo_db
environment:
POSTGRES_DB: odoo
POSTGRES_USER: odoo
POSTGRES_PASSWORD: odoo_secure_password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- odoo_network
restart: unless-stopped
# Odoo
odoo:
image: odoo:17.0
container_name: odoo_app
depends_on:
- postgres
environment:
HOST: postgres
USER: odoo
PASSWORD: odoo_secure_password
DB_NAME: odoo
volumes:
- odoo_data:/var/lib/odoo
- ./addons:/mnt/extra-addons
ports:
- "8069:8069"
networks:
- odoo_network
restart: unless-stopped
# REST APIモバイル同期用
api:
build:
context: ./api
dockerfile: Dockerfile
container_name: mobile_sync_api
depends_on:
- postgres
- odoo
environment:
DATABASE_URL: postgresql://odoo:odoo_secure_password@postgres:5432/odoo
ODOO_URL: http://odoo:8069
ODOO_USER: admin
ODOO_PASSWORD: admin
API_SECRET_KEY: your_secret_key_here_change_me
ports:
- "8000:8000"
volumes:
- ./api:/app
networks:
- odoo_network
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
restart: unless-stopped
# 同期・バックアップスクリプト(定期実行)
sync_scheduler:
build:
context: ./scheduler
dockerfile: Dockerfile
container_name: sync_scheduler
depends_on:
- postgres
- odoo
- api
environment:
DATABASE_URL: postgresql://odoo:odoo_secure_password@postgres:5432/odoo
ODOO_URL: http://odoo:8069
ODOO_USER: admin
ODOO_PASSWORD: admin
NEXTCLOUD_URL: https://your_nextcloud_url
NEXTCLOUD_USER: your_nextcloud_user
NEXTCLOUD_PASSWORD: your_nextcloud_password
volumes:
- ./scheduler:/app
- sync_logs:/var/log/sync
networks:
- odoo_network
restart: unless-stopped
volumes:
postgres_data:
odoo_data:
sync_logs:
networks:
odoo_network:
driver: bridge

334
claude/odoo_sync.py Normal file
View file

@ -0,0 +1,334 @@
# api/odoo_sync.py
"""
Odoo連携モジュール
REST API が受け取ったドキュメントを Odoo に同期
"""
import requests
import logging
from typing import Dict, List, Optional
from datetime import datetime
import json
logger = logging.getLogger(__name__)
class OdooClient:
"""Odoo XML-RPC クライアント"""
def __init__(self, odoo_url: str, db: str, username: str, password: str):
self.odoo_url = odoo_url
self.db = db
self.username = username
self.password = password
self.uid = None
self.authenticate()
def authenticate(self):
"""Odoo 認証"""
try:
import xmlrpc.client
common = xmlrpc.client.ServerProxy(f'{self.odoo_url}/xmlrpc/2/common')
self.uid = common.authenticate(self.db, self.username, self.password, {})
logger.info(f"Odoo authenticated: uid={self.uid}")
except Exception as e:
logger.error(f"Odoo authentication failed: {str(e)}")
raise
def create_customer(self, name: str, address: str = "", phone: str = "", email: str = "") -> int:
"""顧客を Odoo に作成"""
try:
import xmlrpc.client
models = xmlrpc.client.ServerProxy(f'{self.odoo_url}/xmlrpc/2/object')
partner_data = {
'name': name,
'street': address,
'phone': phone,
'email': email,
'customer_rank': 1,
}
partner_id = models.execute_kw(
self.db, self.uid, self.password,
'res.partner', 'create', [partner_data]
)
logger.info(f"Created Odoo customer: {partner_id}")
return partner_id
except Exception as e:
logger.error(f"Error creating customer: {str(e)}")
return 0
def create_quotation(self, customer_id: int, items: List[Dict],
payment_due_date: str, notes: str = "") -> int:
"""見積を Odoo に作成"""
try:
import xmlrpc.client
models = xmlrpc.client.ServerProxy(f'{self.odoo_url}/xmlrpc/2/object')
# 見積ラインの準備
order_lines = []
for item in items:
# 商品をOdooから検索簡略版
product_search = models.execute_kw(
self.db, self.uid, self.password,
'product.product', 'search',
[[('name', '=', item['product_name'])]]
)
product_id = product_search[0] if product_search else 1
line_data = (0, 0, {
'product_id': product_id,
'product_qty': item['quantity'],
'price_unit': item['unit_price'],
})
order_lines.append(line_data)
# 見積作成
quotation_data = {
'partner_id': customer_id,
'order_line': order_lines,
'date_order': datetime.now().isoformat(),
'payment_term_id': self._get_payment_term_id(payment_due_date),
'note': notes,
}
quotation_id = models.execute_kw(
self.db, self.uid, self.password,
'sale.order', 'create', [quotation_data]
)
logger.info(f"Created Odoo quotation: {quotation_id}")
return quotation_id
except Exception as e:
logger.error(f"Error creating quotation: {str(e)}")
return 0
def create_invoice(self, customer_id: int, items: List[Dict],
payment_due_date: str, notes: str = "") -> int:
"""請求書を Odoo に作成"""
try:
import xmlrpc.client
models = xmlrpc.client.ServerProxy(f'{self.odoo_url}/xmlrpc/2/object')
invoice_lines = []
for item in items:
product_search = models.execute_kw(
self.db, self.uid, self.password,
'product.product', 'search',
[[('name', '=', item['product_name'])]]
)
product_id = product_search[0] if product_search else 1
line_data = (0, 0, {
'product_id': product_id,
'quantity': item['quantity'],
'price_unit': item['unit_price'],
})
invoice_lines.append(line_data)
invoice_data = {
'partner_id': customer_id,
'invoice_line_ids': invoice_lines,
'invoice_date': datetime.now().date().isoformat(),
'invoice_date_due': payment_due_date,
'note': notes,
}
invoice_id = models.execute_kw(
self.db, self.uid, self.password,
'account.move', 'create', [invoice_data]
)
logger.info(f"Created Odoo invoice: {invoice_id}")
return invoice_id
except Exception as e:
logger.error(f"Error creating invoice: {str(e)}")
return 0
def record_payment(self, invoice_id: int, amount: float, payment_date: str) -> int:
"""支払いを記録"""
try:
import xmlrpc.client
models = xmlrpc.client.ServerProxy(f'{self.odoo_url}/xmlrpc/2/object')
payment_data = {
'move_id': invoice_id,
'amount': amount,
'payment_date': payment_date,
}
payment_id = models.execute_kw(
self.db, self.uid, self.password,
'account.payment', 'create', [payment_data]
)
logger.info(f"Recorded payment: {payment_id}")
return payment_id
except Exception as e:
logger.error(f"Error recording payment: {str(e)}")
return 0
def get_customer_by_email(self, email: str) -> Optional[int]:
"""メールアドレスで顧客を検索"""
try:
import xmlrpc.client
models = xmlrpc.client.ServerProxy(f'{self.odoo_url}/xmlrpc/2/object')
result = models.execute_kw(
self.db, self.uid, self.password,
'res.partner', 'search',
[[('email', '=', email)]]
)
return result[0] if result else None
except Exception as e:
logger.error(f"Error searching customer: {str(e)}")
return None
def _get_payment_term_id(self, due_date: str) -> int:
"""支払い条件を Odoo から取得"""
try:
import xmlrpc.client
models = xmlrpc.client.ServerProxy(f'{self.odoo_url}/xmlrpc/2/object')
# 簡略版「30日」の支払い条件 ID を取得
result = models.execute_kw(
self.db, self.uid, self.password,
'account.payment.term', 'search',
[[('name', 'like', '30')]]
)
return result[0] if result else 1
except Exception as e:
logger.warning(f"Could not get payment term: {str(e)}")
return 1
class SyncService:
"""REST API と Odoo の同期サービス"""
def __init__(self, odoo_client: OdooClient):
self.odoo = odoo_client
def sync_document(self, db_session, document_entity) -> Dict:
"""ドキュメントを Odoo に同期"""
# 顧客情報を取得
customer = db_session.query(Customer).filter(
Customer.id == document_entity.customer_id
).first()
if not customer:
return {"status": "error", "message": "Customer not found"}
# Odoo 顧客 ID を確認・作成
odoo_customer_id = customer.odoo_customer_id
if not odoo_customer_id:
odoo_customer_id = self.odoo.create_customer(
name=customer.name,
address=customer.address or "",
phone=customer.phone or "",
email=customer.email or ""
)
customer.odoo_customer_id = odoo_customer_id
db_session.commit()
if not odoo_customer_id:
return {"status": "error", "message": "Could not create/find Odoo customer"}
# ドキュメントタイプ別処理
items = json.loads(document_entity.items)
try:
if document_entity.doc_type == "quotation":
odoo_id = self.odoo.create_quotation(
customer_id=odoo_customer_id,
items=items,
payment_due_date=self._format_date(document_entity.payment_due_date),
notes=document_entity.notes or ""
)
elif document_entity.doc_type == "invoice":
odoo_id = self.odoo.create_invoice(
customer_id=odoo_customer_id,
items=items,
payment_due_date=self._format_date(document_entity.payment_due_date),
notes=document_entity.notes or ""
)
else:
odoo_id = 0
if odoo_id:
document_entity.odoo_id = odoo_id
return {"status": "success", "odoo_id": odoo_id}
else:
return {"status": "error", "message": "Failed to create Odoo document"}
except Exception as e:
logger.error(f"Sync error: {str(e)}")
return {"status": "error", "message": str(e)}
@staticmethod
def _format_date(timestamp: int) -> str:
"""タイムスタンプを ISO 形式に変換"""
from datetime import datetime
return datetime.fromtimestamp(timestamp / 1000).isoformat()
# FastAPI メインに統合される部分
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
app = FastAPI()
# グローバル Odoo クライアント
odoo_client = OdooClient(
odoo_url=os.getenv("ODOO_URL", "http://localhost:8069"),
db="odoo",
username=os.getenv("ODOO_USER", "admin"),
password=os.getenv("ODOO_PASSWORD", "admin")
)
sync_service = SyncService(odoo_client)
@app.post("/api/v1/sync")
async def sync_documents(request: SyncRequest, db: Session = Depends(get_db)):
"""ドキュメント同期エンドポイント"""
synced_count = 0
new_documents = []
for doc in request.documents:
# ドキュメント作成DB
db_doc = Document(...)
db.add(db_doc)
db.commit()
# Odoo に同期
result = sync_service.sync_document(db, db_doc)
if result["status"] == "success":
synced_count += 1
new_documents.append({
"local_id": db_doc.id,
"odoo_id": result.get("odoo_id"),
"doc_type": db_doc.doc_type
})
return SyncResponse(
status="success",
message=f"Synced {synced_count} documents",
synced_documents=synced_count,
new_documents=new_documents
)

View file

@ -0,0 +1,8 @@
fastapi==0.104.1
uvicorn==0.24.0
pydantic==2.5.0
sqlalchemy==2.0.23
psycopg2-binary==2.9.9
python-dateutil==2.8.2
requests==2.31.0
python-multipart==0.0.6

58
gemi_invoice/.gitignore vendored Normal file
View file

@ -0,0 +1,58 @@
# General
*.log
.DS_Store
# system-specific
*~
*.swp
# IDE configurations
.idea/
.vscode/
# Flutter/Dart specific
.dart_tool/
.flutter-plugins-dependencies
# ios/Pods/ is often ignored, but sometimes specific projects include it
# ios/Pods/
# For macOS desktop builds
macos/Runner/Flutter/AppFrameworkInfo.plist
macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
# For Windows desktop builds
windows/flutter/ephemeral/
# Android specific
android/.gradle/
android/gradle/wrapper/gradle-wrapper.properties
android/app/build.gradle.kts # Usually generated, but can be ignored if specific configurations are managed elsewhere or to prevent accidental commits
android/app/build/ # Build artifacts
android/captures/ # For Android Studio captures
android/gradle.properties # Usually fine to commit, but depends on project setup
# iOS specific
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json # Example for specific asset files, usually not ignored unless generated
ios/Runner.xcworkspace/contents.xcworkspacedata
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
# Web specific
web/icons/
web/manifest.json
# Build output
build/
# Dependency caching
.pub-cache/
# OS-generated files
.DS_Store
Thumbs.db
# IDE settings (IntelliJ IDEA)
*.iml
# Temporary files
*.tmp

45
gemi_invoice/.metadata Normal file
View file

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "67323de285b00232883f53b84095eb72be97d35c"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 67323de285b00232883f53b84095eb72be97d35c
base_revision: 67323de285b00232883f53b84095eb72be97d35c
- platform: android
create_revision: 67323de285b00232883f53b84095eb72be97d35c
base_revision: 67323de285b00232883f53b84095eb72be97d35c
- platform: ios
create_revision: 67323de285b00232883f53b84095eb72be97d35c
base_revision: 67323de285b00232883f53b84095eb72be97d35c
- platform: linux
create_revision: 67323de285b00232883f53b84095eb72be97d35c
base_revision: 67323de285b00232883f53b84095eb72be97d35c
- platform: macos
create_revision: 67323de285b00232883f53b84095eb72be97d35c
base_revision: 67323de285b00232883f53b84095eb72be97d35c
- platform: web
create_revision: 67323de285b00232883f53b84095eb72be97d35c
base_revision: 67323de285b00232883f53b84095eb72be97d35c
- platform: windows
create_revision: 67323de285b00232883f53b84095eb72be97d35c
base_revision: 67323de285b00232883f53b84095eb72be97d35c
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

16
gemi_invoice/README.md Normal file
View file

@ -0,0 +1,16 @@
# gemi_invoice
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

View file

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
gemi_invoice/android/.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View file

@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.gemi_invoice"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.gemi_invoice"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View file

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View file

@ -0,0 +1,56 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.gemi_invoice"
>
<uses-permission android:name="android.permission.READ_CONTACTS" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="file" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="content" />
</intent>
<intent>
<action android:name="android.intent.action.PICK" />
<data android:mimeType="vnd.android.cursor.dir/contact" />
</intent>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
<application
android:label="gemi_invoice"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
>
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
>
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<meta-data android:name="flutterEmbedding" android:value="2" />
</application>
</manifest>

View file

@ -0,0 +1,5 @@
package com.example.gemi_invoice
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View file

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View file

@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View file

@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View file

@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

Binary file not shown.

2478
gemi_invoice/dir.tree Normal file

File diff suppressed because it is too large Load diff

34
gemi_invoice/ios/.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View file

@ -0,0 +1 @@
#include "Generated.xcconfig"

View file

@ -0,0 +1 @@
#include "Generated.xcconfig"

View file

@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.gemiInvoice;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.gemiInvoice.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.gemiInvoice.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.gemiInvoice.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.gemiInvoice;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.gemiInvoice;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View file

@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View file

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View file

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Gemi Invoice</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>gemi_invoice</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View file

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View file

@ -0,0 +1,70 @@
// lib/main.dart
// version: 1.4.3c (Bug Fix: PDF layout error) - Refactored for modularity
import 'package:flutter/material.dart';
// --- ---
import 'models/invoice_models.dart'; // Invoice, InvoiceItem
import 'screens/invoice_input_screen.dart'; //
import 'screens/invoice_detail_page.dart'; //
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '販売アシスト1号',
theme: ThemeData(
primarySwatch: Colors.blueGrey,
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true,
),
home: const InvoiceFlowScreen(),
);
}
}
class InvoiceFlowScreen extends StatefulWidget {
const InvoiceFlowScreen({super.key});
@override
State<InvoiceFlowScreen> createState() => _InvoiceFlowScreenState();
}
class _InvoiceFlowScreenState extends State<InvoiceFlowScreen> {
//
Invoice? _lastGeneratedInvoice;
// PDF
void _handleInvoiceGenerated(Invoice generatedInvoice, String filePath) {
setState(() {
_lastGeneratedInvoice = generatedInvoice;
});
//
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InvoiceDetailPage(invoice: generatedInvoice),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("販売アシスト1号 V1.4.3c"),
backgroundColor: Colors.blueGrey,
),
//
body: InvoiceInputForm(
onInvoiceGenerated: _handleInvoiceGenerated,
),
);
}
}

View file

@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: .fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: .center,
children: [
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

View file

@ -0,0 +1,93 @@
import 'package:intl/intl.dart';
///
class InvoiceItem {
String description;
int quantity;
int unitPrice;
InvoiceItem({
required this.description,
required this.quantity,
required this.unitPrice,
});
// ( * )
int get subtotal => quantity * unitPrice;
//
InvoiceItem copyWith({
String? description,
int? quantity,
int? unitPrice,
}) {
return InvoiceItem(
description: description ?? this.description,
quantity: quantity ?? this.quantity,
unitPrice: unitPrice ?? this.unitPrice,
);
}
}
///
class Invoice {
String clientName;
DateTime date;
List<InvoiceItem> items;
String? filePath; // PDFのパス
String invoiceNumber; //
String? notes; //
Invoice({
required this.clientName,
required this.date,
required this.items,
this.filePath,
String? invoiceNumber,
this.notes,
}) : invoiceNumber = invoiceNumber ?? DateFormat('yyyyMMdd-HHmm').format(date);
//
int get subtotal {
return items.fold(0, (sum, item) => sum + item.subtotal);
}
// (10%)
int get tax {
return (subtotal * 0.1).floor();
}
//
int get totalAmount {
return subtotal + tax;
}
//
Invoice copyWith({
String? clientName,
DateTime? date,
List<InvoiceItem>? items,
String? filePath,
String? invoiceNumber,
String? notes,
}) {
return Invoice(
clientName: clientName ?? this.clientName,
date: date ?? this.date,
items: items ?? this.items,
filePath: filePath ?? this.filePath,
invoiceNumber: invoiceNumber ?? this.invoiceNumber,
notes: notes ?? this.notes,
);
}
// CSV形式への変換 (CSV編集用)
String toCsv() {
StringBuffer sb = StringBuffer();
sb.writeln("Description,Quantity,UnitPrice,Subtotal");
for (var item in items) {
sb.writeln("${item.description},${item.quantity},${item.unitPrice},${item.subtotal}");
}
return sb.toString();
}
}

View file

@ -0,0 +1,314 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:share_plus/share_plus.dart';
import 'package:open_filex/open_filex.dart';
import '../models/invoice_models.dart';
import '../services/pdf_generator.dart';
class InvoiceDetailPage extends StatefulWidget {
final Invoice invoice;
const InvoiceDetailPage({Key? key, required this.invoice}) : super(key: key);
@override
State<InvoiceDetailPage> createState() => _InvoiceDetailPageState();
}
class _InvoiceDetailPageState extends State<InvoiceDetailPage> {
late TextEditingController _clientController;
late TextEditingController _notesController;
late List<InvoiceItem> _items;
late bool _isEditing;
late Invoice _currentInvoice;
String? _currentFilePath;
@override
void initState() {
super.initState();
_currentInvoice = widget.invoice;
_currentFilePath = widget.invoice.filePath;
_clientController = TextEditingController(text: _currentInvoice.clientName);
_notesController = TextEditingController(text: _currentInvoice.notes ?? "");
_items = List.from(_currentInvoice.items);
_isEditing = false;
}
@override
void dispose() {
_clientController.dispose();
_notesController.dispose();
super.dispose();
}
void _addItem() {
setState(() {
_items.add(InvoiceItem(description: "新項目", quantity: 1, unitPrice: 0));
});
}
void _removeItem(int index) {
setState(() {
_items.removeAt(index);
});
}
Future<void> _saveChanges() async {
final String clientName = _clientController.text.trim();
if (clientName.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('取引先名を入力してください')),
);
return;
}
final updatedInvoice = _currentInvoice.copyWith(
clientName: clientName,
items: _items,
notes: _notesController.text,
);
setState(() => _isEditing = false);
final newPath = await generateInvoicePdf(updatedInvoice);
if (newPath != null) {
setState(() {
_currentInvoice = updatedInvoice.copyWith(filePath: newPath);
_currentFilePath = newPath;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('A4請求書PDFを更新しました')),
);
}
}
void _exportCsv() {
final csvData = _currentInvoice.toCsv();
//
Share.share(csvData, subject: '請求書データ_CSV');
}
@override
Widget build(BuildContext context) {
final amountFormatter = NumberFormat("#,###");
return Scaffold(
appBar: AppBar(
title: const Text("販売アシスト1号 請求書詳細"),
backgroundColor: Colors.blueGrey,
actions: [
if (!_isEditing) ...[
IconButton(icon: const Icon(Icons.grid_on), onPressed: _exportCsv, tooltip: "CSV出力"),
IconButton(icon: const Icon(Icons.edit), onPressed: () => setState(() => _isEditing = true)),
] else ...[
IconButton(icon: const Icon(Icons.save), onPressed: _saveChanges),
IconButton(icon: const Icon(Icons.cancel), onPressed: () => setState(() => _isEditing = false)),
]
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderSection(),
const Divider(height: 32),
const Text("明細一覧", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
_buildItemTable(amountFormatter),
if (_isEditing)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: ElevatedButton.icon(
onPressed: _addItem,
icon: const Icon(Icons.add),
label: const Text("行を追加"),
),
),
const SizedBox(height: 24),
_buildSummarySection(amountFormatter),
const SizedBox(height: 24),
_buildFooterActions(),
],
),
),
);
}
Widget _buildHeaderSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_isEditing) ...[
TextField(
controller: _clientController,
decoration: const InputDecoration(labelText: "取引先名", border: OutlineInputBorder()),
),
const SizedBox(height: 12),
TextField(
controller: _notesController,
maxLines: 2,
decoration: const InputDecoration(labelText: "備考", border: OutlineInputBorder()),
),
] else ...[
Text("宛名: ${_currentInvoice.clientName} 御中", style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text("請求番号: ${_currentInvoice.invoiceNumber}"),
if (_currentInvoice.notes?.isNotEmpty ?? false) ...[
const SizedBox(height: 8),
Text("備考: ${_currentInvoice.notes}", style: const TextStyle(color: Colors.black87)),
]
],
],
);
}
Widget _buildItemTable(NumberFormat formatter) {
return Table(
border: TableBorder.all(color: Colors.grey.shade300),
columnWidths: const {
0: FlexColumnWidth(4),
1: FixedColumnWidth(50),
2: FixedColumnWidth(80),
3: FlexColumnWidth(2),
4: FixedColumnWidth(40),
},
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [
TableRow(
decoration: BoxDecoration(color: Colors.grey.shade100),
children: const [
_TableCell("品名"), _TableCell("数量"), _TableCell("単価"), _TableCell("金額"), _TableCell(""),
],
),
..._items.asMap().entries.map((entry) {
int idx = entry.key;
InvoiceItem item = entry.value;
if (_isEditing) {
return TableRow(children: [
_EditableCell(
initialValue: item.description,
onChanged: (val) => item.description = val,
),
_EditableCell(
initialValue: item.quantity.toString(),
keyboardType: TextInputType.number,
onChanged: (val) => setState(() => item.quantity = int.tryParse(val) ?? 0),
),
_EditableCell(
initialValue: item.unitPrice.toString(),
keyboardType: TextInputType.number,
onChanged: (val) => setState(() => item.unitPrice = int.tryParse(val) ?? 0),
),
_TableCell(formatter.format(item.subtotal)),
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)),
const SizedBox(),
]);
}
}),
],
);
}
Widget _buildSummarySection(NumberFormat formatter) {
return Align(
alignment: Alignment.centerRight,
child: Container(
width: 200,
child: Column(
children: [
_SummaryRow("小計 (税抜)", formatter.format(_isEditing ? _calculateCurrentSubtotal() : _currentInvoice.subtotal)),
_SummaryRow("消費税 (10%)", formatter.format(_isEditing ? (_calculateCurrentSubtotal() * 0.1).floor() : _currentInvoice.tax)),
const Divider(),
_SummaryRow("合計 (税込)", "${formatter.format(_isEditing ? (_calculateCurrentSubtotal() * 1.1).floor() : _currentInvoice.totalAmount)}", isBold: true),
],
),
),
);
}
int _calculateCurrentSubtotal() {
return _items.fold(0, (sum, item) => sum + (item.quantity * item.unitPrice));
}
Widget _buildFooterActions() {
if (_isEditing || _currentFilePath == null) return const SizedBox();
return Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _openPdf,
icon: const Icon(Icons.launch),
label: const Text("PDFを開く"),
style: ElevatedButton.styleFrom(backgroundColor: Colors.orange, foregroundColor: Colors.white),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: _sharePdf,
icon: const Icon(Icons.share),
label: const Text("共有"),
style: ElevatedButton.styleFrom(backgroundColor: Colors.green, foregroundColor: Colors.white),
),
),
],
);
}
Future<void> _openPdf() async => await OpenFilex.open(_currentFilePath!);
Future<void> _sharePdf() async => await Share.shareXFiles([XFile(_currentFilePath!)], text: '請求書送付');
}
class _TableCell extends StatelessWidget {
final String text;
const _TableCell(this.text);
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.all(8.0),
child: Text(text, textAlign: TextAlign.right, style: const TextStyle(fontSize: 12)),
);
}
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});
@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,
),
);
}
class _SummaryRow extends StatelessWidget {
final String label, value;
final bool isBold;
const _SummaryRow(this.label, this.value, {this.isBold = false});
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label, style: TextStyle(fontSize: 12, fontWeight: isBold ? FontWeight.bold : null)),
Text(value, style: TextStyle(fontSize: 12, fontWeight: isBold ? FontWeight.bold : null)),
],
),
);
}

View file

@ -0,0 +1,254 @@
// lib/screens/invoice_input_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import '../models/invoice_models.dart';
import '../services/pdf_generator.dart';
///
class InvoiceInputForm extends StatefulWidget {
final Function(Invoice invoice, String filePath) onInvoiceGenerated;
const InvoiceInputForm({
Key? key,
required this.onInvoiceGenerated,
}) : super(key: key);
@override
State<InvoiceInputForm> createState() => _InvoiceInputFormState();
}
class _InvoiceInputFormState extends State<InvoiceInputForm> {
final _clientController = TextEditingController(text: "佐々木製作所");
final _amountController = TextEditingController(text: "250000");
String _status = "取引先と基本金額を入力してPDFを生成してください";
@override
void dispose() {
_clientController.dispose();
_amountController.dispose();
super.dispose();
}
//
Future<void> _pickContact() async {
setState(() => _status = "連絡先をスキャン中...");
try {
if (await FlutterContacts.requestPermission(readonly: true)) {
final List<Contact> contacts = await FlutterContacts.getContacts(
withProperties: false,
withThumbnail: false,
);
if (!mounted) return;
if (contacts.isEmpty) {
setState(() => _status = "連絡先が空、または取得できませんでした。");
return;
}
contacts.sort((a, b) => a.displayName.compareTo(b.displayName));
final Contact? selected = await showModalBottomSheet<Contact>(
context: context,
isScrollControlled: true,
builder: (BuildContext modalContext) => FractionallySizedBox(
heightFactor: 0.8,
child: ContactPickerModal(
contacts: contacts,
onContactSelected: (selectedContact) {
Navigator.pop(modalContext, selectedContact);
},
),
),
);
if (selected != null) {
setState(() {
_clientController.text = selected.displayName;
_status = "${selected.displayName}」をセットしました";
});
}
} else {
setState(() => _status = "電話帳の権限が拒否されています。");
}
} catch (e) {
setState(() => _status = "エラーが発生しました: $e");
}
}
// PDFを生成して保存する処理
Future<void> _handleInitialGenerate() async {
final clientName = _clientController.text.trim();
final unitPrice = int.tryParse(_amountController.text) ?? 0;
if (clientName.isEmpty) {
setState(() => _status = "取引先名を入力してください");
return;
}
// 1
final initialItems = [
InvoiceItem(
description: "ご請求分",
quantity: 1,
unitPrice: unitPrice,
)
];
final invoice = Invoice(
clientName: clientName,
date: DateTime.now(),
items: initialItems,
);
setState(() => _status = "A4請求書を生成中...");
final path = await generateInvoicePdf(invoice);
if (path != null) {
final updatedInvoice = invoice.copyWith(filePath: path);
widget.onInvoiceGenerated(updatedInvoice, path);
setState(() => _status = "PDFを生成しました。詳細ページで表編集が可能です。");
} else {
setState(() => _status = "PDFの生成に失敗しました");
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Column(children: [
Row(children: [
Expanded(
child: TextField(
controller: _clientController,
decoration: const InputDecoration(
labelText: "取引先名",
hintText: "会社名や個人名",
border: OutlineInputBorder(),
),
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.person_search, color: Colors.blue, size: 40),
onPressed: _pickContact,
),
]),
const SizedBox(height: 16),
TextField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: "基本金額 (税抜)",
hintText: "後で詳細ページで変更・追加できます",
border: OutlineInputBorder(),
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: _handleInitialGenerate,
icon: const Icon(Icons.description),
label: const Text("A4請求書を作成して詳細編集へ"),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 60),
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
),
),
const SizedBox(height: 24),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Text(
_status,
style: const TextStyle(fontSize: 12, color: Colors.black54),
textAlign: TextAlign.center,
),
),
]),
),
);
}
}
//
class ContactPickerModal extends StatefulWidget {
final List<Contact> contacts;
final Function(Contact) onContactSelected;
const ContactPickerModal({
Key? key,
required this.contacts,
required this.onContactSelected,
}) : super(key: key);
@override
State<ContactPickerModal> createState() => _ContactPickerModalState();
}
class _ContactPickerModalState extends State<ContactPickerModal> {
String _searchQuery = "";
List<Contact> _filteredContacts = [];
@override
void initState() {
super.initState();
_filteredContacts = widget.contacts;
}
void _filterContacts(String query) {
setState(() {
_searchQuery = query.toLowerCase();
_filteredContacts = widget.contacts
.where((c) => c.displayName.toLowerCase().contains(_searchQuery))
.toList();
});
}
@override
Widget build(BuildContext context) {
return Material(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"取引先を選択 (${_filteredContacts.length}件)",
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 12),
TextField(
decoration: InputDecoration(
hintText: "名前で検索...",
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
),
onChanged: _filterContacts,
),
],
),
),
Expanded(
child: ListView.builder(
itemCount: _filteredContacts.length,
itemBuilder: (c, i) => ListTile(
leading: const CircleAvatar(child: Icon(Icons.person)),
title: Text(_filteredContacts[i].displayName),
onTap: () => widget.onContactSelected(_filteredContacts[i]),
),
),
),
],
),
);
}
}

View file

@ -0,0 +1,198 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart' show debugPrint;
import 'package:flutter/services.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:path_provider/path_provider.dart';
import 'package:crypto/crypto.dart';
import 'package:intl/intl.dart';
import '../models/invoice_models.dart';
/// A4サイズのプロフェッショナルな請求書PDFを生成し
Future<String?> generateInvoicePdf(Invoice invoice) async {
try {
final pdf = pw.Document();
//
final fontData = await rootBundle.load("assets/fonts/ipaexg.ttf");
final ttf = pw.Font.ttf(fontData);
final boldTtf = pw.Font.ttf(fontData); // IPAexGはウェイトが1つなので同じものを使用
final dateFormatter = DateFormat('yyyy年MM月dd日');
final amountFormatter = NumberFormat("#,###");
pdf.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.all(32),
theme: pw.ThemeData.withFont(base: ttf, bold: boldTtf),
build: (context) => [
//
pw.Header(
level: 0,
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text("請求書", style: pw.TextStyle(fontSize: 28, fontWeight: pw.FontWeight.bold)),
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
pw.Text("請求番号: ${invoice.invoiceNumber}"),
pw.Text("発行日: ${dateFormatter.format(invoice.date)}"),
],
),
],
),
),
pw.SizedBox(height: 20),
//
pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Expanded(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Container(
decoration: const pw.BoxDecoration(
border: pw.Border(bottom: pw.BorderSide(width: 1)),
),
child: pw.Text("${invoice.clientName} 御中",
style: const pw.TextStyle(fontSize: 18)),
),
pw.SizedBox(height: 10),
pw.Text("下記の通り、ご請求申し上げます。"),
],
),
),
pw.Expanded(
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end,
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.SizedBox(height: 30),
//
pw.Container(
padding: const pw.EdgeInsets.all(8),
decoration: const pw.BoxDecoration(color: PdfColors.grey200),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text("ご請求金額合計 (税込)", style: const pw.TextStyle(fontSize: 16)),
pw.Text("${amountFormatter.format(invoice.totalAmount)} -",
style: pw.TextStyle(fontSize: 20, fontWeight: pw.FontWeight.bold)),
],
),
),
pw.SizedBox(height: 20),
//
pw.TableHelper.fromTextArray(
headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold),
headerDecoration: const pw.BoxDecoration(color: PdfColors.grey300),
cellHeight: 30,
cellAlignments: {
0: pw.Alignment.centerLeft,
1: pw.Alignment.centerRight,
2: pw.Alignment.centerRight,
3: pw.Alignment.centerRight,
},
headers: ["品名 / 項目", "数量", "単価", "金額"],
data: List<List<String>>.generate(
invoice.items.length,
(index) {
final item = invoice.items[index];
return [
item.description,
item.quantity.toString(),
amountFormatter.format(item.unitPrice),
amountFormatter.format(item.subtotal),
];
},
),
),
//
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.end,
children: [
pw.Container(
width: 200,
child: pw.Column(
children: [
pw.SizedBox(height: 10),
_buildSummaryRow("小計 (税抜)", amountFormatter.format(invoice.subtotal)),
_buildSummaryRow("消費税 (10%)", amountFormatter.format(invoice.tax)),
pw.Divider(),
_buildSummaryRow("合計", "${amountFormatter.format(invoice.totalAmount)}", isBold: true),
],
),
),
],
),
//
if (invoice.notes != null && invoice.notes!.isNotEmpty) ...[
pw.SizedBox(height: 40),
pw.Text("備考:", style: pw.TextStyle(fontWeight: pw.FontWeight.bold)),
pw.Container(
width: double.infinity,
padding: const pw.EdgeInsets.all(8),
decoration: pw.BoxDecoration(border: pw.Border.all(color: PdfColors.grey400)),
child: pw.Text(invoice.notes!),
),
],
],
footer: (context) => pw.Container(
alignment: pw.Alignment.centerRight,
margin: const pw.EdgeInsets.only(top: 16),
child: pw.Text(
"Page ${context.pageNumber} / ${context.pagesCount}",
style: const pw.TextStyle(color: PdfColors.grey),
),
),
),
);
//
final Uint8List bytes = await pdf.save();
final String hash = sha256.convert(bytes).toString().substring(0, 8);
final String dateFileStr = DateFormat('yyyyMMdd').format(invoice.date);
String fileName = "Invoice_${dateFileStr}_${invoice.clientName}_$hash.pdf";
final directory = await getExternalStorageDirectory();
if (directory == null) return null;
final file = File("${directory.path}/$fileName");
await file.writeAsBytes(bytes);
return file.path;
} catch (e) {
debugPrint("PDF Generation Error: $e");
return null;
}
}
pw.Widget _buildSummaryRow(String label, String value, {bool isBold = false}) {
final style = pw.TextStyle(fontSize: 12, fontWeight: isBold ? pw.FontWeight.bold : null);
return pw.Padding(
padding: const pw.EdgeInsets.symmetric(vertical: 2),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text(label, style: style),
pw.Text(value, style: style),
],
),
);
}

1
gemi_invoice/linux/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
flutter/ephemeral

View file

@ -0,0 +1,128 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "gemi_invoice")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.gemi_invoice")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View file

@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View file

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
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);
}

View file

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View file

@ -0,0 +1,24 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

View file

@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# Define the application target. To change its name, change BINARY_NAME in the
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
# work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add preprocessor definitions for the application ID.
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")

View file

@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

View file

@ -0,0 +1,148 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Called when first Flutter frame received.
static void first_frame_cb(MyApplication* self, FlView* view) {
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
}
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "gemi_invoice");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "gemi_invoice");
}
gtk_window_set_default_size(window, 1280, 720);
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(
project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
GdkRGBA background_color;
// Background defaults to black, override it here if necessary, e.g. #00000000
// for transparent.
gdk_rgba_parse(&background_color, "#000000");
fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
// Show the window when Flutter renders.
// Requires the view to be realized so we can start rendering.
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
self);
gtk_widget_realize(GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application,
gchar*** arguments,
int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line =
my_application_local_command_line;
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID, "flags",
G_APPLICATION_NON_UNIQUE, nullptr));
}

View file

@ -0,0 +1,21 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication,
my_application,
MY,
APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

7
gemi_invoice/macos/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/

View file

@ -0,0 +1 @@
#include "ephemeral/Flutter-Generated.xcconfig"

View file

@ -0,0 +1 @@
#include "ephemeral/Flutter-Generated.xcconfig"

View file

@ -0,0 +1,14 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import share_plus
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View file

@ -0,0 +1,705 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXAggregateTarget section */
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
isa = PBXAggregateTarget;
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
buildPhases = (
33CC111E2044C6BF0003C045 /* ShellScript */,
);
dependencies = (
);
name = "Flutter Assemble";
productName = FLX;
};
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC10EC2044A3C60003C045;
remoteInfo = Runner;
};
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
remoteInfo = FLX;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
33CC110E2044A8840003C045 /* Bundle Framework */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Bundle Framework";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* gemi_invoice.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "gemi_invoice.app"; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
331C80D2294CF70F00263BE5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EA2044A3C60003C045 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C80D6294CF71000263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C80D7294CF71000263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
33BA886A226E78AF003329D5 /* Configs */ = {
isa = PBXGroup;
children = (
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
);
path = Configs;
sourceTree = "<group>";
};
33CC10E42044A3C60003C045 = {
isa = PBXGroup;
children = (
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
);
sourceTree = "<group>";
};
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* gemi_invoice.app */,
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
);
name = Resources;
path = ..;
sourceTree = "<group>";
};
33CEB47122A05771004F2AC0 /* Flutter */ = {
isa = PBXGroup;
children = (
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
);
path = Flutter;
sourceTree = "<group>";
};
33FAB671232836740065AC1E /* Runner */ = {
isa = PBXGroup;
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */,
33CC11242044D66E0003C045 /* Resources */,
33BA886A226E78AF003329D5 /* Configs */,
);
path = Runner;
sourceTree = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C80D4294CF70F00263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C80DA294CF71000263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
33CC10EC2044A3C60003C045 /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
);
buildRules = (
);
dependencies = (
33CC11202044C79F0003C045 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* gemi_invoice.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
33CC10E52044A3C60003C045 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C80D4294CF70F00263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 33CC10EC2044A3C60003C045;
};
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
};
};
};
33CC111A2044C6BA0003C045 = {
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Manual;
};
};
};
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 33CC10E42044A3C60003C045;
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
33CC10EC2044A3C60003C045 /* Runner */,
331C80D4294CF70F00263BE5 /* RunnerTests */,
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C80D3294CF70F00263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EB2044A3C60003C045 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
};
33CC111E2044C6BF0003C045 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
Flutter/ephemeral/FlutterInputs.xcfilelist,
);
inputPaths = (
Flutter/ephemeral/tripwire,
);
outputFileListPaths = (
Flutter/ephemeral/FlutterOutputs.xcfilelist,
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C80D1294CF70F00263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10E92044A3C60003C045 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC10EC2044A3C60003C045 /* Runner */;
targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
};
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
isa = PBXVariantGroup;
children = (
33CC10F52044A3C60003C045 /* Base */,
);
name = MainMenu.xib;
path = Runner;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
331C80DB294CF71000263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.gemiInvoice.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/gemi_invoice.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/gemi_invoice";
};
name = Debug;
};
331C80DC294CF71000263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.gemiInvoice.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/gemi_invoice.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/gemi_invoice";
};
name = Release;
};
331C80DD294CF71000263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.gemiInvoice.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/gemi_invoice.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/gemi_invoice";
};
name = Profile;
};
338D0CE9231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Profile;
};
338D0CEA231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Profile;
};
338D0CEB231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Profile;
};
33CC10F92044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
33CC10FA2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
33CC10FC2044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
33CC10FD2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
33CC111C2044C6BA0003C045 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
33CC111D2044C6BA0003C045 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C80DB294CF71000263BE5 /* Debug */,
331C80DC294CF71000263BE5 /* Release */,
331C80DD294CF71000263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10F92044A3C60003C045 /* Debug */,
33CC10FA2044A3C60003C045 /* Release */,
338D0CE9231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10FC2044A3C60003C045 /* Debug */,
33CC10FD2044A3C60003C045 /* Release */,
338D0CEA231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC111C2044C6BA0003C045 /* Debug */,
33CC111D2044C6BA0003C045 /* Release */,
338D0CEB231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
}

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View file

@ -0,0 +1,13 @@
import Cocoa
import FlutterMacOS
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}

View file

@ -0,0 +1,68 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_16.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_64.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_1024.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Some files were not shown because too many files have changed in this diff Show more