Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1063f0c319 | ||
|
|
c2da40ba4d | ||
|
|
6e45507b22 | ||
|
|
99d05680c5 | ||
|
|
d89d99ceff | ||
|
|
40be1b79f2 | ||
|
|
e807d6fa66 | ||
|
|
8ae8df6ef8 | ||
|
|
791f3572b6 | ||
|
|
ce8053ff2c | ||
|
|
f5f4c083f0 | ||
|
|
6800fc6c41
|
||
|
|
1f56757d30 | ||
|
|
73dc64a688 | ||
|
|
3b22037496 | ||
|
|
206c756ad7 | ||
|
|
a66d046b3f |
@@ -93,11 +93,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload to Google Play
|
- name: Upload to Google Play
|
||||||
uses: r0adkll/upload-google-play@v1
|
uses: r0adkll/upload-google-play@v1
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
# if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||||
packageName: net.helcel.cowspent
|
packageName: net.helcel.cowspent
|
||||||
releaseFiles: app/build/outputs/bundle/signedRelease/app-signedRelease.aab
|
releaseFiles: app/build/outputs/bundle/signedRelease/app-signedRelease.aab
|
||||||
tracks: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || 'alpha' }}
|
tracks: ${{ startsWith(github.ref, 'refs/tags/') && 'production' || 'internal' }}
|
||||||
status: completed
|
status: completed
|
||||||
whatsNewDirectory: tmp/whatsnew
|
whatsNewDirectory: tmp/whatsnew
|
||||||
+8
-8
@@ -1,7 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '9.3.2'
|
id 'com.android.application' version '9.3.2'
|
||||||
id 'org.jetbrains.kotlin.plugin.serialization' version '2.4.10'
|
id 'org.jetbrains.kotlin.plugin.serialization' version '2.4.20'
|
||||||
id 'org.jetbrains.kotlin.plugin.compose' version '2.4.10'
|
id 'org.jetbrains.kotlin.plugin.compose' version '2.4.20'
|
||||||
id 'jacoco'
|
id 'jacoco'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,12 +118,12 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.compose.foundation:foundation:1.12.0'
|
implementation 'androidx.compose.foundation:foundation:1.12.1'
|
||||||
implementation 'androidx.compose.runtime:runtime:1.12.0'
|
implementation 'androidx.compose.runtime:runtime:1.12.1'
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
|
||||||
|
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.11.2'
|
implementation 'androidx.work:work-runtime-ktx:2.12.0'
|
||||||
|
|
||||||
implementation 'com.google.zxing:core:3.5.4'
|
implementation 'com.google.zxing:core:3.5.4'
|
||||||
implementation 'androidx.camera:camera-camera2:1.6.2'
|
implementation 'androidx.camera:camera-camera2:1.6.2'
|
||||||
@@ -132,7 +132,7 @@ dependencies {
|
|||||||
implementation 'com.github.nextcloud:Android-SingleSignOn:1.3.4'
|
implementation 'com.github.nextcloud:Android-SingleSignOn:1.3.4'
|
||||||
implementation 'com.opencsv:opencsv:5.12.0'
|
implementation 'com.opencsv:opencsv:5.12.0'
|
||||||
|
|
||||||
implementation platform('androidx.compose:compose-bom:2026.08.00')
|
implementation platform('androidx.compose:compose-bom:2026.09.00')
|
||||||
implementation 'androidx.compose.ui:ui'
|
implementation 'androidx.compose.ui:ui'
|
||||||
implementation 'androidx.compose.material:material'
|
implementation 'androidx.compose.material:material'
|
||||||
implementation 'androidx.compose.material:material-icons-extended'
|
implementation 'androidx.compose.material:material-icons-extended'
|
||||||
@@ -147,12 +147,12 @@ dependencies {
|
|||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'io.mockk:mockk:1.14.11'
|
testImplementation 'io.mockk:mockk:1.14.11'
|
||||||
testImplementation 'org.robolectric:robolectric:4.16.1'
|
testImplementation 'org.robolectric:robolectric:4.17'
|
||||||
testImplementation 'androidx.test:core:1.7.0'
|
testImplementation 'androidx.test:core:1.7.0'
|
||||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0'
|
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0'
|
||||||
testImplementation 'androidx.compose.ui:ui-test-junit4'
|
testImplementation 'androidx.compose.ui:ui-test-junit4'
|
||||||
|
|
||||||
androidTestImplementation platform('androidx.compose:compose-bom:2026.08.00')
|
androidTestImplementation platform('androidx.compose:compose-bom:2026.09.00')
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
|
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
|
||||||
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
|
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
|
||||||
|
|||||||
+10
-2
@@ -48,8 +48,10 @@ import net.helcel.cowspent.model.Transaction
|
|||||||
import net.helcel.cowspent.model.UserItem
|
import net.helcel.cowspent.model.UserItem
|
||||||
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
||||||
import net.helcel.cowspent.util.SupportUtil
|
import net.helcel.cowspent.util.SupportUtil
|
||||||
|
import net.helcel.cowspent.util.SupportUtil.SETTLE_DIRECT
|
||||||
import net.helcel.cowspent.util.SupportUtil.SETTLE_OPTIMAL
|
import net.helcel.cowspent.util.SupportUtil.SETTLE_OPTIMAL
|
||||||
import net.helcel.cowspent.util.SupportUtil.settleBills
|
import net.helcel.cowspent.util.SupportUtil.settleBills
|
||||||
|
import net.helcel.cowspent.util.SupportUtil.settleBillsDirect
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProjectSettlementDialogContent(
|
fun ProjectSettlementDialogContent(
|
||||||
@@ -60,10 +62,12 @@ fun ProjectSettlementDialogContent(
|
|||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit
|
||||||
) {
|
) {
|
||||||
val centerNoneStr = stringResource(R.string.center_none)
|
val centerNoneStr = stringResource(R.string.center_none)
|
||||||
|
val centerDirectStr = stringResource(R.string.center_none_direct)
|
||||||
val memberList = remember(proj.id) { db.getMembersOfProject(proj.id, null) }
|
val memberList = remember(proj.id) { db.getMembersOfProject(proj.id, null) }
|
||||||
val userList = remember(memberList, centerNoneStr) {
|
val userList = remember(memberList, centerNoneStr, centerDirectStr) {
|
||||||
buildList {
|
buildList {
|
||||||
add(UserItem(SETTLE_OPTIMAL, centerNoneStr))
|
add(UserItem(SETTLE_OPTIMAL, centerNoneStr))
|
||||||
|
add(UserItem(SETTLE_DIRECT, centerDirectStr))
|
||||||
addAll(memberList.map { UserItem(it.id, it.name) })
|
addAll(memberList.map { UserItem(it.id, it.name) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +95,11 @@ fun ProjectSettlementDialogContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val transactions = remember(selectedMemberId, membersBalance, membersSortedByName) {
|
val transactions = remember(selectedMemberId, membersBalance, membersSortedByName) {
|
||||||
settleBills(membersSortedByName, membersBalance, selectedMemberId)
|
if (selectedMemberId == SETTLE_DIRECT) {
|
||||||
|
settleBillsDirect(membersSortedByName, db.getBillsOfProject(proj.id))
|
||||||
|
} else {
|
||||||
|
settleBills(membersSortedByName, membersBalance, selectedMemberId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ProjectSettlementUI(
|
ProjectSettlementUI(
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ object SupportUtil {
|
|||||||
membersBalance[b.payerId] = balPayer + amount
|
membersBalance[b.payerId] = balPayer + amount
|
||||||
val paid = membersPaid[b.payerId] ?: 0.0
|
val paid = membersPaid[b.payerId] ?: 0.0
|
||||||
membersPaid[b.payerId] = paid + amount
|
membersPaid[b.payerId] = paid + amount
|
||||||
|
|
||||||
var nbOwerShares = 0.0
|
var nbOwerShares = 0.0
|
||||||
for (bo in b.billOwers) {
|
for (bo in b.billOwers) {
|
||||||
nbOwerShares += membersWeight[bo.memberId] ?: 0.0
|
nbOwerShares += membersWeight[bo.memberId] ?: 0.0
|
||||||
@@ -167,6 +167,39 @@ object SupportUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const val SETTLE_OPTIMAL: Long = 0
|
const val SETTLE_OPTIMAL: Long = 0
|
||||||
|
const val SETTLE_DIRECT: Long = -1
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun settleBillsDirect(members: List<DBMember>, bills: List<DBBill>): List<Transaction> {
|
||||||
|
val membersWeight = members.associate { it.id to it.weight }
|
||||||
|
// owed[debtor][creditor] = total the debtor owes the creditor over all bills
|
||||||
|
val owed = HashMap<Long, HashMap<Long, Double>>()
|
||||||
|
|
||||||
|
for (b in bills) {
|
||||||
|
if (b.state == DBBill.STATE_DELETED) continue
|
||||||
|
val nbOwerShares = b.billOwers.sumOf { membersWeight[it.memberId] ?: 0.0 }
|
||||||
|
if (nbOwerShares <= 0.0) continue
|
||||||
|
for (bo in b.billOwers) {
|
||||||
|
if (bo.memberId == b.payerId) continue
|
||||||
|
val share = b.amount / nbOwerShares * (membersWeight[bo.memberId] ?: 0.0)
|
||||||
|
val row = owed.getOrPut(bo.memberId) { HashMap() }
|
||||||
|
row[b.payerId] = (row[b.payerId] ?: 0.0) + share
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val results: MutableList<Transaction> = ArrayList()
|
||||||
|
for (debtor in members) {
|
||||||
|
for (creditor in members) {
|
||||||
|
if (debtor.id == creditor.id) continue
|
||||||
|
val net = (owed[debtor.id]?.get(creditor.id) ?: 0.0) -
|
||||||
|
(owed[creditor.id]?.get(debtor.id) ?: 0.0)
|
||||||
|
if (round2(net) > 0.0) {
|
||||||
|
results.add(Transaction(debtor.id, creditor.id, round2(net)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun settleBills(
|
fun settleBills(
|
||||||
@@ -233,11 +266,11 @@ object SupportUtil {
|
|||||||
if (cd1.balance < cd2.balance) 1 else -1
|
if (cd1.balance < cd2.balance) 1 else -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (c in crediters) {
|
for (c in crediters) {
|
||||||
Log.e(SupportUtil::class.java.simpleName, "* " + c.memberId + " : " + c.balance)
|
Log.e(SupportUtil::class.java.simpleName, "* " + c.memberId + " : " + c.balance)
|
||||||
}
|
}
|
||||||
|
|
||||||
debiters.sortWith { cd2, cd1 ->
|
debiters.sortWith { cd2, cd1 ->
|
||||||
if (cd1.balance == cd2.balance) {
|
if (cd1.balance == cd2.balance) {
|
||||||
0
|
0
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
|
|
||||||
<!-- Settlement -->
|
<!-- Settlement -->
|
||||||
<string name="center_none">Keine (optimal)</string>
|
<string name="center_none">Keine (optimal)</string>
|
||||||
|
<string name="center_none_direct">Keine (direkt)</string>
|
||||||
<string name="settle_who">Wer zahlt</string>
|
<string name="settle_who">Wer zahlt</string>
|
||||||
<string name="settle_to_whom">An wen</string>
|
<string name="settle_to_whom">An wen</string>
|
||||||
<string name="settle_how_much">Betrag</string>
|
<string name="settle_how_much">Betrag</string>
|
||||||
|
|||||||
@@ -210,6 +210,7 @@
|
|||||||
|
|
||||||
<!-- Settlement -->
|
<!-- Settlement -->
|
||||||
<string name="center_none">Ninguno (óptimo)</string>
|
<string name="center_none">Ninguno (óptimo)</string>
|
||||||
|
<string name="center_none_direct">Ninguno (directo)</string>
|
||||||
<string name="settle_who">Quién paga</string>
|
<string name="settle_who">Quién paga</string>
|
||||||
<string name="settle_to_whom">A quién</string>
|
<string name="settle_to_whom">A quién</string>
|
||||||
<string name="settle_how_much">Importe</string>
|
<string name="settle_how_much">Importe</string>
|
||||||
|
|||||||
@@ -213,6 +213,7 @@
|
|||||||
|
|
||||||
<!-- Settlement -->
|
<!-- Settlement -->
|
||||||
<string name="center_none">Aucun (optimal)</string>
|
<string name="center_none">Aucun (optimal)</string>
|
||||||
|
<string name="center_none_direct">Aucun (direct)</string>
|
||||||
<string name="settle_who">Qui paie</string>
|
<string name="settle_who">Qui paie</string>
|
||||||
<string name="settle_to_whom">À qui</string>
|
<string name="settle_to_whom">À qui</string>
|
||||||
<string name="settle_how_much">Montant</string>
|
<string name="settle_how_much">Montant</string>
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
|
|
||||||
<!-- Settlement -->
|
<!-- Settlement -->
|
||||||
<string name="center_none">Нет (оптимально)</string>
|
<string name="center_none">Нет (оптимально)</string>
|
||||||
|
<string name="center_none_direct">Нет (напрямую)</string>
|
||||||
<string name="settle_who">Кто платит</string>
|
<string name="settle_who">Кто платит</string>
|
||||||
<string name="settle_to_whom">Кому</string>
|
<string name="settle_to_whom">Кому</string>
|
||||||
<string name="settle_how_much">Сумма</string>
|
<string name="settle_how_much">Сумма</string>
|
||||||
|
|||||||
@@ -247,6 +247,7 @@
|
|||||||
|
|
||||||
<!-- Settlement -->
|
<!-- Settlement -->
|
||||||
<string name="center_none">None (Optimal)</string>
|
<string name="center_none">None (Optimal)</string>
|
||||||
|
<string name="center_none_direct">None (Direct)</string>
|
||||||
<string name="settle_who">Who pays</string>
|
<string name="settle_who">Who pays</string>
|
||||||
<string name="settle_to_whom">To whom</string>
|
<string name="settle_to_whom">To whom</string>
|
||||||
<string name="settle_how_much">Amount</string>
|
<string name="settle_how_much">Amount</string>
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
retries=0
|
retries=0
|
||||||
retryBackOffMs=500
|
retryBackOffMs=500
|
||||||
|
|||||||
Reference in New Issue
Block a user