Compare commits

...

418 Commits
v0.7.1 ... main

Author SHA1 Message Date
318fb5dc7b Bump version to 0.13.9
All checks were successful
Test / Run tests (push) Successful in 2m7s
Deploy / Build and Deploy (push) Successful in 1m55s
2025-05-05 10:41:18 +02:00
41c5288fc5 Elwig: Update dependencies
Some checks failed
Test / Run tests (push) Failing after 14s
2025-05-05 10:36:30 +02:00
e50e7337e6 Helpers/Utils: Automatically change URL to sync.elwig.at when applicable 2025-05-05 10:36:30 +02:00
ffe85d471c README: Add text 2025-04-24 16:05:41 +02:00
d1c07ee92a Installer/Setup: Update to WiX 6
All checks were successful
Test / Run tests (push) Successful in 2m43s
2025-04-24 14:35:40 +02:00
4af2fa256e Tests: Update dependencies 2025-04-24 14:31:36 +02:00
bf0db37872 Elwig: Update dependencies 2025-04-24 14:31:27 +02:00
3161351a30 Bump version to 0.13.8
All checks were successful
Test / Run tests (push) Successful in 2m19s
Deploy / Build and Deploy (push) Successful in 2m1s
2025-02-21 12:05:34 +01:00
aa98909c0a DeliveryAdminWindow: Fix Handlese/Gerebelt Gewogen input behaviour
All checks were successful
Test / Run tests (push) Successful in 1m51s
2025-02-20 17:06:40 +01:00
775bb08e95 Elwig: Update dependencies
All checks were successful
Test / Run tests (push) Successful in 2m29s
2025-02-20 16:08:52 +01:00
fe0a7dab2a Tests: Update dependencies 2025-02-20 16:08:42 +01:00
138dae715e DeliveryAdminWindow: Allow Gr.Inzersdorf to select LesewagenInput 2025-02-20 15:56:26 +01:00
28af7f8dd3 CHANGELOG: Fix unser anchor names 2025-01-22 16:19:24 +01:00
7cc2f75e7c Bump version to 0.13.7
All checks were successful
Test / Run tests (push) Successful in 2m15s
Deploy / Build and Deploy (push) Successful in 2m23s
2025-01-21 11:59:54 +01:00
c7a2f2241d Billing: Handle negative credit amount in following credits
Some checks failed
Test / Run tests (push) Has been cancelled
2025-01-21 11:58:08 +01:00
bd4ebb8c35 PaymentVariantsWindow: Allow user to change date
All checks were successful
Test / Run tests (push) Successful in 2m35s
2025-01-21 11:03:54 +01:00
6d88c5645c PaymentVariantsWindow: Warn user about negative credit exports 2025-01-21 11:01:56 +01:00
5d017cc8ea MailLogWindow: Fix crash when opening
All checks were successful
Test / Run tests (push) Successful in 2m52s
2025-01-19 17:00:31 +01:00
0b8a1b321f BillingData: Fix collapsing tested with permutations
All checks were successful
Test / Run tests (push) Successful in 3m7s
2025-01-18 12:19:33 +01:00
95927c3f1a ChartWindow: Remove old commented-out code
All checks were successful
Test / Run tests (push) Successful in 2m42s
2025-01-16 09:09:28 +01:00
f7297d313a Bump version to 0.13.6
All checks were successful
Deploy / Build and Deploy (push) Successful in 2m20s
2025-01-14 23:43:20 +01:00
80fec4473a Elwig: Update dependencies
All checks were successful
Test / Run tests (push) Successful in 2m3s
2025-01-14 22:43:53 +01:00
95ccb2627c Tests: Update dependencies 2025-01-14 22:43:04 +01:00
20e3e2a76b BillingData: Fix collapsing with cultivations/defaults
All checks were successful
Test / Run tests (push) Successful in 1m58s
2025-01-14 22:05:48 +01:00
b83df45e8f Bump version to 0.13.5
All checks were successful
Test / Run tests (push) Successful in 2m9s
Deploy / Build and Deploy (push) Successful in 2m22s
2025-01-02 17:41:51 +01:00
c24b1ca2b9 DeliveryAdminWindow: Add WineLocalityStatistics
All checks were successful
Test / Run tests (push) Successful in 2m10s
2025-01-02 17:25:46 +01:00
5e53d864b1 MemberAdminWindow: Add filters for active and non-active members
All checks were successful
Test / Run tests (push) Successful in 2m20s
2025-01-02 14:08:46 +01:00
633b560a67 Tests: Update dependencies 2025-01-02 14:04:50 +01:00
c9e483ba9d Elwig: Update dependencies 2025-01-02 14:04:40 +01:00
c484d27520 Bump version to 0.13.4
All checks were successful
Test / Run tests (push) Successful in 1m54s
Deploy / Build and Deploy (push) Successful in 2m10s
2024-11-25 19:23:20 +01:00
6c7f10cb26 Tests: Update dependencies
All checks were successful
Test / Run tests (push) Successful in 1m55s
2024-11-25 19:19:31 +01:00
4a10e94d71 Elwig: Update dependencies 2024-11-25 19:19:28 +01:00
338f9fe092 AreaComUnderDeliveryData: Also include inactive members with active area commitments
All checks were successful
Test / Run tests (push) Successful in 2m9s
2024-11-25 12:38:04 +01:00
3b97c2243a AreaComUnderDeliveryData: Fix file name 2024-11-25 12:34:43 +01:00
6ba2aa7143 OverUnderDeliveryData: Fix absence of non-deliverers in list
Bug was introduced by commit 9930e6173c772fe433ff69397205872ad30f6a08
and shipped with v0.10.6 (2024-08-30)
2024-11-25 12:17:23 +01:00
a99a23fd08 Tests: Update dependencies
All checks were successful
Test / Run tests (push) Successful in 2m52s
2024-11-17 16:58:09 +01:00
70e01849be Setup: Update dependencies 2024-11-17 16:57:53 +01:00
c3a2f983d5 Elwig: Update dependencies 2024-11-17 16:57:40 +01:00
ebf0c20a90 Bump version to 0.13.3
All checks were successful
Test / Run tests (push) Successful in 2m26s
Deploy / Build and Deploy (push) Successful in 2m21s
2024-11-13 23:35:29 +01:00
2ee0d56dcc Windows: Add MailLogWindow
All checks were successful
Test / Run tests (push) Successful in 1m54s
2024-11-13 18:37:50 +01:00
0a9731af09 MailWindow: Fix small bugs and persist all settings
All checks were successful
Test / Run tests (push) Successful in 1m54s
2024-11-13 18:07:03 +01:00
6718ad4c8d AreaComAdminWindow: Add filters for season
All checks were successful
Test / Run tests (push) Successful in 2m26s
2024-11-10 16:35:44 +01:00
a1d84dd988 MemberAdminWindow: Add more filters for AreaComs 2024-11-10 16:35:03 +01:00
f4fa549130 Windows: Add icons on Buttons and MenuItems
All checks were successful
Test / Run tests (push) Successful in 2m15s
2024-11-10 10:58:44 +01:00
c5453c2fe6 MainWindow: Add 'Flächenbindungen' and 'Liefermenge/Ertrag' button 2024-11-10 10:58:35 +01:00
54deccf021 MainWindow: Add statistics table in Leseabschluss 2024-11-09 09:49:19 +01:00
f5d3a04cb1 Bump version to 0.13.2
All checks were successful
Test / Run tests (push) Successful in 1m50s
Deploy / Build and Deploy (push) Successful in 2m6s
2024-10-13 19:08:20 +02:00
0675c45617 Elwig: Use ExecuteSql/FromSql instead of ExecuteSqlRaw/FromSqlRaw where possible
All checks were successful
Test / Run tests (push) Successful in 1m42s
2024-10-13 18:24:40 +02:00
d1f67dc57d BaseDataWindow: Fix WineAttr/WineCult Id updates in payment variant data
All checks were successful
Test / Run tests (push) Successful in 2m10s
2024-10-13 18:18:31 +02:00
3cbffdbf27 Documents: Add DeliveryDepreciationList
All checks were successful
Test / Run tests (push) Successful in 1m55s
2024-10-13 11:54:18 +02:00
e247925472 Controls: Fix blurry borders when system scaling is enabled
All checks were successful
Test / Run tests (push) Successful in 2m21s
2024-10-12 14:25:24 +02:00
a1b3cff624 [#11] Tests: Add DeliveryServiceTest
All checks were successful
Test / Run tests (push) Successful in 1m56s
2024-10-07 23:43:22 +02:00
65498dd18f App: Fix BranchLocation shortening
All checks were successful
Test / Run tests (push) Successful in 1m46s
2024-10-05 19:31:59 +02:00
27dc4f648f [#11] Tests: Add MemberServiceTest 2024-10-05 19:31:51 +02:00
86f7f693a0 Export/Bki: Format house nr that excel interprets it correctly
All checks were successful
Test / Run tests (push) Successful in 2m10s
2024-10-02 11:08:14 +02:00
8680e51052 Weighing: Fix scale L320 for Baden 2024-10-02 10:45:04 +02:00
1d97f3c422 Bump version to 0.13.1
All checks were successful
Test / Run tests (push) Successful in 1m38s
Deploy / Build and Deploy (push) Successful in 2m12s
2024-09-29 22:25:39 +02:00
b6ae1f5675 Elwig: Update dependencies
All checks were successful
Test / Run tests (push) Successful in 1m37s
2024-09-29 22:24:31 +02:00
c185437b9a DeliveryService: Do not delete own delivery in SplitDeliveryToLsNr()
All checks were successful
Test / Run tests (push) Successful in 2m8s
2024-09-29 22:12:19 +02:00
a2315e84bd Windows: Ask user if they really want to send an email
All checks were successful
Test / Run tests (push) Successful in 1m46s
2024-09-29 09:43:17 +02:00
6ba1973087 MemberAdminWindow: Fix 'Save as PDF' option for DeliveryConfirmation and CreditNote
All checks were successful
Test / Run tests (push) Successful in 2m5s
2024-09-29 09:16:39 +02:00
c62947dacd DeliveryAdminWindow: Add DeliverySplittingDialog
All checks were successful
Test / Run tests (push) Successful in 2m10s
2024-09-28 19:54:50 +02:00
f29a609d3b Bump version to 0.13.0
All checks were successful
Test / Run tests (push) Successful in 1m38s
Deploy / Build and Deploy (push) Successful in 1m58s
2024-09-25 22:56:48 +02:00
8a61747538 DeliveryAdminWindow: Fix extraction error
All checks were successful
Test / Run tests (push) Successful in 1m47s
2024-09-25 22:51:06 +02:00
4a7c95e250 MailWindow: Make more user safe
All checks were successful
Test / Run tests (push) Successful in 1m50s
2024-09-25 22:30:26 +02:00
4fa5b8f6d4 Billing: Include all deliveries when calculating under delivery for area commitments
All checks were successful
Test / Run tests (push) Successful in 1m43s
2024-09-25 15:32:26 +02:00
579ed53487 Windows: Prettify formatting for numbers
All checks were successful
Test / Run tests (push) Successful in 2m5s
2024-09-25 12:19:25 +02:00
1fc736ce16 Bump version to 0.12.0
All checks were successful
Test / Run tests (push) Successful in 1m54s
Deploy / Build and Deploy (push) Successful in 2m9s
2024-09-24 23:10:17 +02:00
ce39797d8b [#56] Windows: Fail gracefully while saving
All checks were successful
Test / Run tests (push) Successful in 2m11s
2024-09-24 20:10:26 +02:00
94a6dd5312 Printing: Replace PDFtoPrinter with PdfiumViewer 2024-09-24 20:10:26 +02:00
b67857ae22 [#56] AppDbContext: Turn off connection pooling 2024-09-24 20:10:26 +02:00
a48ea8e7e2 Printing/Pdf: Small fixes 2024-09-24 20:10:21 +02:00
0f87446906 Bump version to 0.11.4
All checks were successful
Test / Run tests (push) Successful in 1m47s
Deploy / Build and Deploy (push) Successful in 2m7s
2024-09-22 23:33:45 +02:00
e0fcaf1f53 Services: Fix spacing in tool tip grids
All checks were successful
Test / Run tests (push) Successful in 2m3s
2024-09-22 23:22:23 +02:00
26e549caa6 Bump version to 0.11.3
All checks were successful
Test / Run tests (push) Successful in 1m37s
Deploy / Build and Deploy (push) Successful in 2m28s
2024-09-22 21:05:42 +02:00
1d187c25f3 MemberAdminWindow: Revert caching of MembersDeliveries
All checks were successful
Test / Run tests (push) Successful in 1m46s
2024-09-22 21:01:48 +02:00
accbb9df08 Bump version to 0.11.2
All checks were successful
Test / Run tests (push) Successful in 1m36s
Deploy / Build and Deploy (push) Successful in 2m2s
2024-09-22 20:49:14 +02:00
7791d02979 Services: Extract GenerateToolTipData() from GenerateToolTip()
All checks were successful
Test / Run tests (push) Successful in 1m35s
2024-09-22 20:47:06 +02:00
dcec6f03fe DeliveryAdminWindow: Fix 'Collection was modified' error
All checks were successful
Test / Run tests (push) Successful in 2m1s
2024-09-22 12:35:37 +02:00
526e951029 Elwig: Add LogWindow
All checks were successful
Test / Run tests (push) Successful in 1m49s
2024-09-22 12:16:14 +02:00
3cde360aaa DeliveryAncmtAdminWindow: Replace 'Traubenanmeldung' with 'Anmeldung'
All checks were successful
Test / Run tests (push) Successful in 2m4s
2024-09-22 10:29:48 +02:00
66be5a3e2c MemberAdminWindow: Cache MembersDeliveries
All checks were successful
Test / Run tests (push) Successful in 1m46s
2024-09-21 23:29:10 +02:00
5c12dba125 DeliveryService: Fix duplicate LsNr error
All checks were successful
Test / Run tests (push) Successful in 1m49s
2024-09-21 23:04:30 +02:00
165770fa37 DeliveryAncmtList: Add DefaultKg of member 2024-09-21 23:04:30 +02:00
648c406ad2 Services: Make FillInputs synchronous 2024-09-21 23:04:26 +02:00
9fa9d9fbec DeliveryAdminWindow: Minor layout changes 2024-09-21 21:37:57 +02:00
5dc725620b Bump version to 0.11.1
All checks were successful
Test / Run tests (push) Successful in 2m2s
Deploy / Build and Deploy (push) Successful in 2m7s
2024-09-19 12:35:25 +02:00
27d8a5cfb6 DeliveryAncmtAdminWindow: Add more filters and tooltip for weight
All checks were successful
Test / Run tests (push) Successful in 1m47s
2024-09-19 11:56:45 +02:00
642fb3a625 DeliveryService: Small fixes 2024-09-19 11:54:57 +02:00
3f7cd2a6ff DeliveryAncmtAdminWindow: Focus MgNrInput when creating new ancmt
All checks were successful
Test / Run tests (push) Successful in 2m8s
2024-09-19 09:34:44 +02:00
21a1b11d68 AreaCom: Make YearFrom nullable
All checks were successful
Test / Run tests (push) Successful in 1m48s
2024-09-18 18:26:16 +02:00
d8beb03b96 Tests: Update dependencies
All checks were successful
Test / Run tests (push) Successful in 1m53s
2024-09-17 23:18:51 +02:00
eee90c784b Elwig: Update dependencies 2024-09-17 23:18:41 +02:00
74200083ab DeliveryAncmtAdminWindow: Mark cancelled schedules with Strikethrough
All checks were successful
Test / Run tests (push) Successful in 1m38s
2024-09-17 23:08:22 +02:00
871bc299bd Utils: Fix SplitName() for double names
All checks were successful
Test / Run tests (push) Successful in 1m46s
2024-09-17 19:18:43 +02:00
a18b58f438 DeliveryAncmtAdminWindow: Make delivery schedule list bigger
All checks were successful
Test / Run tests (push) Successful in 2m10s
2024-09-17 19:07:59 +02:00
cadb5515ea Bump version to 0.11.0
All checks were successful
Deploy / Build and Deploy (push) Successful in 2m9s
2024-09-16 11:17:23 +02:00
d8a10152b3 DeliveryScheduleAdminWindow: Add Attribute, Cultivation and IsCancelled
All checks were successful
Test / Run tests (push) Successful in 1m37s
2024-09-16 10:10:31 +02:00
f09c43c1bd App: Make HintContextChange() synchronous by using MainDispatcher
All checks were successful
Test / Run tests (push) Successful in 2m37s
2024-09-12 11:40:32 +02:00
5c08f61963 MailWindow: Add feature to address members with ancmts on specific day
All checks were successful
Test / Run tests (push) Successful in 2m28s
2024-09-08 15:55:10 +02:00
7a15050575 Bump version to 0.10.8
All checks were successful
Test / Run tests (push) Successful in 2m9s
Deploy / Build and Deploy (push) Successful in 2m52s
2024-09-05 23:32:23 +02:00
8d9172f91e AppDbContext: Move all calls to App.HintContextChange() outside of any AppDbContext block
All checks were successful
Test / Run tests (push) Successful in 2m44s
2024-09-05 17:21:00 +02:00
7437187630 DeliveryAncmtAdminWindow: Add date column 2024-09-05 15:28:55 +02:00
a38cdaa8af DeliveryAdminWindow: Slightly increase default width
All checks were successful
Test / Run tests (push) Successful in 2m2s
2024-09-04 18:18:19 +02:00
a5638135a3 DeliveryAncmtAdminWindow: Add option to search in all delivery schedules and init mgnr with selected
All checks were successful
Test / Run tests (push) Successful in 2m2s
2024-09-04 18:11:56 +02:00
a04c7d538e MemberDataSheet: Add VAT to 'Buchführend'
All checks were successful
Test / Run tests (push) Successful in 2m2s
2024-09-04 17:32:34 +02:00
26235f8c0a BusinessDocument: Add ':' in aside block
All checks were successful
Test / Run tests (push) Successful in 2m6s
2024-09-04 17:23:22 +02:00
f43d9c020c DeliveryAncmtList: Center 'Anmldg.' column
All checks were successful
Test / Run tests (push) Successful in 2m54s
2024-09-04 17:18:36 +02:00
22514715c1 DeliveryService: Small text fix 2024-09-04 17:16:14 +02:00
5eda25ed14 Bump version to 0.10.7
All checks were successful
Test / Run tests (push) Successful in 2m2s
Deploy / Build and Deploy (push) Successful in 3m42s
2024-09-02 23:42:00 +02:00
543185d48e DeliveryAdminWindow: Add weight filter
All checks were successful
Test / Run tests (push) Successful in 2m58s
2024-09-02 23:28:55 +02:00
141086673f DeliveryAncmtList: Add status to indicate late ancmts 2024-09-02 23:10:37 +02:00
6627ab6d12 App: Try to fix auto context renewal 2
All checks were successful
Test / Run tests (push) Successful in 2m57s
2024-09-01 11:44:38 +02:00
7ef3faa39e Bump version to 0.10.6
All checks were successful
Deploy / Build and Deploy (push) Successful in 3m12s
2024-08-30 22:35:10 +02:00
8c8c0a8c2b App: Try to fix auto context renewal
All checks were successful
Test / Run tests (push) Successful in 2m23s
2024-08-30 22:26:49 +02:00
78a72c641f MemberAdminWindow: Fix checking and unchecking of ContactEmailInput
All checks were successful
Test / Run tests (push) Successful in 2m3s
2024-08-30 21:40:01 +02:00
e18bc58b6c DeliveryAncmtAdminWindow: Fix bug when pressing Enter in weight input
All checks were successful
Test / Run tests (push) Successful in 2m55s
2024-08-30 21:33:58 +02:00
21f68caf4c DeliveryAncmtAdminWindow: Increase window width 2024-08-30 21:33:16 +02:00
2ef10b4bb2 DeliveryAdminWindow: Scroll DeliveryPartList down when adding new delivery parts 2024-08-30 21:31:44 +02:00
5321be46c7 [#33] ChartWindow: Add CheckBox to indicate which graph (gebunden, normal) is currently selected
All checks were successful
Test / Run tests (push) Successful in 2m54s
2024-08-28 11:14:51 +02:00
0f24f9da08 [#33] ChartWindow: Fix scaling bug 2024-08-28 10:20:21 +02:00
8ce8492c74 MainWindow: Use 'Waage' instead of 'Waagen'
All checks were successful
Test / Run tests (push) Successful in 4m42s
2024-08-26 23:00:24 +02:00
ee1315929c AreaComAdminWindow: Fix window title
All checks were successful
Test / Run tests (push) Successful in 2m59s
2024-08-26 22:12:37 +02:00
9930e6173c Dtos: Rewrite SQL queries to be more efficient
All checks were successful
Test / Run tests (push) Successful in 2m28s
2024-08-24 16:37:41 +02:00
b5c1bfb08f Bump version to 0.10.5
All checks were successful
Test / Run tests (push) Successful in 2m4s
Deploy / Build and Deploy (push) Successful in 2m47s
2024-08-24 14:00:49 +02:00
cd2b482b5a Weighing: Add SetDateAndTime()
All checks were successful
Test / Run tests (push) Successful in 2m22s
2024-08-24 13:54:59 +02:00
bec1b165bf MemberAdminWindow: Allow user to keep AreaCom YearTo when transfering
All checks were successful
Test / Run tests (push) Successful in 2m49s
2024-08-24 13:35:14 +02:00
2ae2564647 DeliveryAncmtAdminWinodw: Do not show warning windows
All checks were successful
Test / Run tests (push) Successful in 2m31s
2024-08-23 14:49:05 +02:00
adbe418b7c DeliveryAncmtAdminWindow: Change status bar spacing
All checks were successful
Test / Run tests (push) Successful in 2m51s
2024-08-23 08:53:48 +02:00
94db0723c5 Bump version to 0.10.4
All checks were successful
Test / Run tests (push) Successful in 2m2s
Deploy / Build and Deploy (push) Successful in 2m48s
2024-08-22 13:36:38 +02:00
f54677d429 DeliveryAncmtAdminWindow: Display stats in status bar
All checks were successful
Test / Run tests (push) Successful in 2m27s
2024-08-22 13:32:08 +02:00
49e4b65c27 MemberAdminWindow: Display currently shown member count and business shares
All checks were successful
Test / Run tests (push) Successful in 2m25s
2024-08-22 12:29:41 +02:00
ada5085cae Validator: Add some syntax sugar
All checks were successful
Test / Run tests (push) Successful in 2m51s
2024-08-22 11:04:24 +02:00
85931e62e8 DeliveryAncmtAdminWindow: Show weekday also in ComboBox
All checks were successful
Test / Run tests (push) Successful in 11m7s
2024-08-21 16:57:21 +02:00
39956cbcbd Bump version to 0.10.3
All checks were successful
Test / Run tests (push) Successful in 2m25s
Deploy / Build and Deploy (push) Successful in 3m20s
2024-08-21 00:01:47 +02:00
84d8d0cecb DeliveryScheduleAdminWindow: Fix AncmtToDateInput input bug
All checks were successful
Test / Run tests (push) Successful in 2m3s
2024-08-20 23:39:08 +02:00
fe7f9d675b DeliveryAncmtAdminWindow: Add day of week to delivery schedule list
All checks were successful
Test / Run tests (push) Successful in 3m0s
2024-08-20 23:31:37 +02:00
9d1ee8638c Bump version to 0.10.2
All checks were successful
Test / Run tests (push) Successful in 1m59s
Deploy / Build and Deploy (push) Successful in 2m41s
2024-08-16 21:31:43 +02:00
cd40075702 ElwigData: Allow legacy field family_name to be used and other fixes
All checks were successful
Test / Run tests (push) Successful in 2m50s
2024-08-16 21:24:04 +02:00
204dfe8745 MemberService: Include area commitments when uploading export data 2024-08-16 21:23:32 +02:00
f97dfc3c72 Bump version to 0.10.1
All checks were successful
Test / Run tests (push) Successful in 2m7s
Deploy / Build and Deploy (push) Successful in 2m59s
2024-08-14 12:02:36 +02:00
d3839c288a MemberList: Allow filtering area commitments 2024-08-14 11:57:19 +02:00
2764a0ca21 Dtos: Fix error when given_name is null 2024-08-14 11:57:04 +02:00
48970de652 Billing: Fix error when no deliveries exist in season 2024-08-14 11:56:25 +02:00
367c3ac357 Bump version to 0.10.0
All checks were successful
Test / Run tests (push) Successful in 2m8s
Deploy / Build and Deploy (push) Successful in 2m37s
2024-08-13 15:23:58 +02:00
4d89a17e80 Billing: Allow users to add custom member modifiers before VAT
All checks were successful
Test / Run tests (push) Successful in 2m8s
2024-08-13 14:43:12 +02:00
f52c11b91e PaymentVariantSummary: Subtract member modifiers from sum
All checks were successful
Test / Run tests (push) Successful in 2m20s
2024-08-12 15:44:04 +02:00
f48c6a02cb [#54] Member: Add IsJuridicalPerson
All checks were successful
Test / Run tests (push) Successful in 2m49s
2024-08-12 15:18:34 +02:00
025ff08d84 [#14] Documents: Add DeliveryAncmtList
All checks were successful
Test / Run tests (push) Successful in 2m35s
2024-08-10 15:45:37 +02:00
b091bd0ec3 [#14] Windows: Add DeliveryAncmtWindow
All checks were successful
Test / Run tests (push) Successful in 2m5s
2024-08-09 22:11:47 +02:00
804a17911c [#14] Windows: Add DeliveryScheduleAdminWindow
All checks were successful
Test / Run tests (push) Successful in 2m58s
2024-08-09 17:45:14 +02:00
170cfda37e Windows: Minor cleanups 2024-08-09 17:44:48 +02:00
2d737e2780 [#14] Models: Add DeliveryAncmt 2024-08-09 17:44:21 +02:00
2333077aa5 Billing: Include predecessors in Treuebonus for WGM
All checks were successful
Test / Run tests (push) Successful in 2m29s
2024-08-08 19:09:38 +02:00
9127cd3f03 MemberAdminWindow: Fix area commitment transfer for new members
All checks were successful
Test / Run tests (push) Successful in 2m5s
2024-08-08 17:35:58 +02:00
036a0dc978 App: Use Version class
All checks were successful
Test / Run tests (push) Successful in 2m28s
2024-08-02 20:48:53 +02:00
7749f6ab45 Bump version to 0.9.3
All checks were successful
Test / Run tests (push) Successful in 2m29s
Deploy / Build and Deploy (push) Successful in 2m38s
2024-08-02 12:22:52 +02:00
cf05a0c658 DeliveryJournalData: Add delivery and member branch to excel export
All checks were successful
Test / Run tests (push) Successful in 2m52s
2024-08-02 11:42:27 +02:00
5567d9f25a Bump version to 0.9.2
All checks were successful
Test / Run tests (push) Successful in 2m7s
Deploy / Build and Deploy (push) Successful in 2m50s
2024-08-01 16:06:13 +02:00
4403754ada MemberAdminWindow: Fix error when saving telephone numbers
All checks were successful
Test / Run tests (push) Successful in 2m23s
2024-08-01 15:57:49 +02:00
8db6007264 MainWindow: Fix closing behaviour when other windows are open
All checks were successful
Test / Run tests (push) Successful in 2m24s
2024-08-01 13:49:04 +02:00
944270744a Bump version to 0.9.1
All checks were successful
Test / Run tests (push) Successful in 2m53s
Deploy / Build and Deploy (push) Successful in 2m36s
2024-08-01 09:12:33 +02:00
10ee1d6548 [#3] MemberService: Export area commitments with members 2024-07-30 17:00:29 +02:00
db4de5b5fe [#3] Windows: Add option to export selected member or delivery only
All checks were successful
Test / Run tests (push) Successful in 2m23s
2024-07-30 14:24:53 +02:00
f69d2809f3 MailWindow: Allow users to send emails without documents
All checks were successful
Test / Run tests (push) Successful in 2m14s
2024-07-30 12:57:07 +02:00
1c2e0baa68 [#3] Services: Use .elwig.zip as export extension everywhere 2024-07-30 12:57:04 +02:00
39f93da0ba SysTecITScale: Add error code 20 for negative weight
All checks were successful
Test / Run tests (push) Successful in 2m39s
2024-07-30 00:07:12 +02:00
91717f8efb Services: Add also messages for TaskCanceledException
All checks were successful
Test / Run tests (push) Successful in 2m21s
2024-07-29 23:35:03 +02:00
7786fb421a DeliveryAdminWindow: Fix scale button ordering
All checks were successful
Test / Run tests (push) Successful in 2m54s
2024-07-29 23:12:00 +02:00
12d2aeecaf Bump version to 0.9.0
All checks were successful
Test / Run tests (push) Successful in 2m50s
Deploy / Build and Deploy (push) Successful in 2m40s
2024-07-28 22:13:17 +02:00
1bc0d67d26 [#3] Services: Add 'Check internet connection' message
All checks were successful
Test / Run tests (push) Successful in 2m46s
2024-07-28 09:35:01 +02:00
38315cd928 [#3] Services: Update 'upload successful' message
All checks were successful
Test / Run tests (push) Successful in 2m24s
2024-07-28 00:27:21 +02:00
53d3affefe DeliveryConfirmation: Use checkboxes for 'gerebelt'
All checks were successful
Test / Run tests (push) Successful in 2m2s
2024-07-27 21:55:20 +02:00
4c3f0c40fa DeliveryAdminWindow: Handle members without default KG correctly
All checks were successful
Test / Run tests (push) Successful in 2m44s
2024-07-27 20:46:13 +02:00
662862090e [#3] ElwigData: Fix import error when no deliveries are present
All checks were successful
Test / Run tests (push) Successful in 2m31s
2024-07-27 19:44:22 +02:00
a5164e286f [#3] Services: Use .elwig.zip as export extension 2024-07-27 19:44:22 +02:00
ae1e985656 [#3] MainWindow: Add functionality to import data from files 2024-07-27 19:44:22 +02:00
36c1bd35a7 [#3] MemberAdminWindow: Add Export item in menu 2024-07-27 19:44:22 +02:00
4aa3362029 DeliveryConfirmation: Use 'gerebelt' instead of 'bto./nto.' 2024-07-27 19:44:15 +02:00
cc97004b30 [#3] ElwigData: Fix importing of duplicate data
All checks were successful
Test / Run tests (push) Successful in 2m52s
2024-07-27 16:57:23 +02:00
935b31f6e3 DeliveryAdminWindow: Force user to input gerebelt gewogen input
All checks were successful
Test / Run tests (push) Successful in 2m2s
2024-07-26 19:47:47 +02:00
f09753ccc2 Remove byte order marks
All checks were successful
Test / Run tests (push) Successful in 2m4s
2024-07-26 19:44:41 +02:00
53c7cb2ec0 Workflows: Check for byte order mark 2024-07-26 19:44:37 +02:00
80c3ec1b9c Windows: Use Indeterminate event in ThreeState CheckBoxes
All checks were successful
Test / Run tests (push) Successful in 2m19s
2024-07-26 16:15:51 +02:00
b49c9c65b1 [#3] Elwig: Add user-friendly sync method
All checks were successful
Test / Run tests (push) Successful in 2m57s
2024-07-26 14:58:15 +02:00
b6afb94246 MemberListData: Include contact information like phone numbers and email addresses
All checks were successful
Test / Run tests (push) Successful in 2m25s
2024-07-24 19:28:34 +02:00
8e9f2f4e90 WeighingTests: Add Haugsdorf and Sitzendorf
All checks were successful
Test / Run tests (push) Successful in 2m46s
2024-07-24 16:42:38 +02:00
d741ba92dc Printing/Pdf: Set CreateNoWindow to true
All checks were successful
Test / Run tests (push) Successful in 2m6s
2024-07-23 20:22:59 +02:00
84f772a32f MailWindow: Fix crash when no season is present 2024-07-23 20:22:59 +02:00
fd0ed97305 Weighing: Do not ignore gross and tare weight and show it on DeliveryNote 2024-07-23 20:22:59 +02:00
1141331608 MemberAdminWindow: Add filters for E-Mail, Tel.-Nr., and contact options 2024-07-23 20:22:59 +02:00
f235d5b380 DeliveryAdminWindow: Fix ModifierInput_SelectionChanged event 2024-07-23 20:22:59 +02:00
30116f7848 DeliveryAdminWindow: Improve status bar widths 2024-07-23 20:22:59 +02:00
abe7699a5b [#26] MemberAdminWindow: Rearrange status bar items 2024-07-23 20:22:59 +02:00
bb77a4e79a [#26] AreaComAdminWindow: Overhaul status bar and search filters 2024-07-23 20:22:59 +02:00
46ea0f29ff [#26] MemberAdminWindow: Add tooltip to area commitment status bar 2024-07-23 20:22:59 +02:00
4229fbbef6 [#26] AreaComService: Add GenerateToolTip() 2024-07-23 20:22:59 +02:00
8dde1cb3f4 [#10] ViewModels: Unify FilterMember property 2024-07-23 20:22:59 +02:00
c314321039 Utils: Change CurrentLastSeason to switch in july 2024-07-23 20:22:59 +02:00
cf1e975d8e [#10] MemberAdminWindow: Add tooltip for delivieries 2024-07-23 20:22:59 +02:00
60359935a4 [#10] DeliveryAdminWindow: Move generation of tooltips to DeliveryService 2024-07-23 20:22:59 +02:00
49e988f71a [#10] AreaComAdminWindow: Implement MVVM 2024-07-23 20:22:59 +02:00
2a8de18772 [#10] DeliveryAdminWindow: Implement MVVM 2024-07-23 20:22:59 +02:00
af80e827b7 [#10] MemberAdminWindow: Implement MVVM 2024-07-23 20:22:59 +02:00
9a2fa3ee3d Bump version to 0.8.9
All checks were successful
Deploy / Build and Deploy (push) Successful in 2m37s
2024-07-23 20:22:52 +02:00
ba9e1d7201 Export/Ods: Escape strings in XML 2024-07-23 20:13:04 +02:00
49f03c0a3c CheckComboBox: Fix SelectedItems property
All checks were successful
Test / Run tests (push) Successful in 2m12s
2024-07-23 11:08:42 +02:00
37adf92e80 Bump version to 0.8.8
All checks were successful
Test / Run tests (push) Successful in 2m37s
Deploy / Build and Deploy (push) Successful in 2m33s
2024-07-22 09:48:53 +02:00
4c75dbe4aa GassnerScale: Hot fix record parsing 2024-07-22 09:48:18 +02:00
5c31ad8851 Bump version to 0.8.7
All checks were successful
Deploy / Build and Deploy (push) Successful in 2m47s
2024-07-22 00:19:11 +02:00
3ac9536e76 App: Small changes in auto updater
All checks were successful
Test / Run tests (push) Successful in 2m41s
2024-07-21 15:52:57 +02:00
7246852181 E2ETests: Refactor to use .FindElement(By.Something())
All checks were successful
Test / Run tests (push) Successful in 2m12s
2024-07-19 14:34:12 +02:00
dd48a24c58 Setup: Update dependencies
All checks were successful
Test / Run tests (push) Successful in 2m30s
2024-07-19 13:43:48 +02:00
ffef1fd6e4 Elwig: Update dependencies 2024-07-19 13:43:40 +02:00
01d658f51d Tests: Fix flaky tests when extracting table
All checks were successful
Test / Run tests (push) Successful in 2m10s
2024-07-19 10:49:21 +02:00
5a36e84b1f MemberAdminWindow: Make first email address required when contact via email is enabled
Some checks failed
Test / Run tests (push) Failing after 2m37s
2024-07-19 10:35:14 +02:00
5b2f617a68 Tests: Change IBAN and LfbisNr to be valid
All checks were successful
Test / Run tests (push) Successful in 2m14s
2024-07-19 00:41:49 +02:00
dd5049faae E2ETests: Add DeliveryAdminWindowReceiptTest
All checks were successful
Test / Run tests (push) Successful in 1m56s
2024-07-19 00:16:33 +02:00
ffe0ff5508 WeighingTests: Move all scale handlers into ScaleHandlers.cs 2024-07-18 23:46:05 +02:00
34178105a7 E2ETests: Use ElwigTestDB.sqlite3 instead of default
All checks were successful
Test / Run tests (push) Successful in 2m38s
2024-07-08 13:05:21 +02:00
6b48a1090c E2ETests: Refactor initial class structure
All checks were successful
Test / Run tests (push) Successful in 2m35s
2024-07-07 14:35:54 +02:00
ddd821e478 Tests: Add E2ETests
All checks were successful
Test / Run tests (push) Successful in 2m16s
2024-07-07 01:22:43 +02:00
658a1f4dc1 DeliveryAdminWindow: Only show active modifiers in receipt mode
All checks were successful
Test / Run tests (push) Successful in 1m53s
2024-07-06 18:50:02 +02:00
daf83c4bbc CheckComboBox: Add setter for SelectedItems
All checks were successful
Test / Run tests (push) Successful in 2m6s
2024-07-03 12:25:07 +02:00
26d75ea3cd Controls: Use nameof(Property) instead of string
All checks were successful
Test / Run tests (push) Successful in 1m43s
2024-07-02 11:02:07 +02:00
86937485e4 DocumentTests: Fix Test Date
All checks were successful
Test / Run tests (push) Successful in 1m47s
2024-07-02 01:19:12 +02:00
e9de54415a UpdateDialog: Fix cancellation and buttons
All checks were successful
Test / Run tests (push) Successful in 2m24s
2024-07-01 12:04:13 +02:00
62f63ef63d UpdateDialog: Add Link to Changelog
All checks were successful
Test / Run tests (push) Successful in 2m0s
2024-07-01 11:07:21 +02:00
44656e0022 Workflows: Only test on push on a branch 2024-07-01 10:03:15 +02:00
80df16999f Bump version to 0.8.6
All checks were successful
Deploy / Build and Deploy (push) Successful in 2m29s
Test / Run tests (push) Successful in 1m44s
2024-07-01 09:59:51 +02:00
d317ccc1e0 [#51] ChartWindow: Select 73 Oechsle per default 2024-06-30 22:04:01 +02:00
6195363335 PaymentVariantsWindow: Rearrange button grid
All checks were successful
Test / Run tests (push) Successful in 1m45s
2024-06-28 23:39:49 +02:00
6a92eb76a0 PaymentAdjustmentWindow: Minor layout changes
Some checks failed
Test / Run tests (push) Has been cancelled
2024-06-28 23:38:04 +02:00
f9d6da7bc8 [#49] PaymentVariantSummary: Add section heading
All checks were successful
Test / Run tests (push) Successful in 2m0s
2024-06-28 23:09:07 +02:00
9478f2a1ab [#49] PaymentVariantSummary: Add modifier statistics
All checks were successful
Test / Run tests (push) Successful in 2m18s
2024-06-28 21:07:55 +02:00
157d0b75a2 [#52] BaseDataWindow: Better group penalties in season tab
All checks were successful
Test / Run tests (push) Successful in 2m0s
2024-06-28 17:32:40 +02:00
255bcbe3ad CreditNote: Show business share penalty correctly
All checks were successful
Test / Run tests (push) Successful in 2m3s
2024-06-28 15:43:05 +02:00
bce2eea3ac MemberAdminWindow: Add menu item for generating CreditNotes per member
All checks were successful
Test / Run tests (push) Successful in 2m5s
2024-06-28 13:48:22 +02:00
91c60018f1 [#48] Dtos/CreditNoteData: Include custom member modifiers
All checks were successful
Test / Run tests (push) Successful in 2m28s
2024-06-28 13:05:08 +02:00
5037818997 [#48] Billing: Add custom modifiers for members
All checks were successful
Test / Run tests (push) Successful in 2m21s
2024-06-27 13:40:52 +02:00
5c76b8ec52 PaymentAdjustmentWindow: Improve DataGrid and add status bar
All checks were successful
Test / Run tests (push) Successful in 2m26s
2024-06-26 19:04:43 +02:00
9d9bb099e1 Workflows: Improve deploy and test workflow 2024-06-25 23:09:43 +02:00
f4172235be Changelog: Add v0.7.0, v0.7.1, and v0.7.2
All checks were successful
Test / Run tests (push) Successful in 1m44s
2024-06-25 12:10:11 +02:00
299c65ab1a Changelog: Reformat
All checks were successful
Test / Run tests (push) Successful in 2m29s
2024-06-25 10:43:47 +02:00
ba8034ad75 Changelog: Add v0.6.5, v0.6.6, v0.6.7, and v0.6.8
All checks were successful
Test / Run tests (push) Successful in 2m0s
2024-06-23 21:46:04 +02:00
7c63373a02 Changelog: Add v0.8.0
All checks were successful
Test / Run tests (push) Successful in 2m24s
2024-06-23 21:01:46 +02:00
750ae53428 Test: Fix flaky DocumentTests by using arrays
All checks were successful
Test / Run tests (push) Successful in 1m46s
2024-06-20 22:44:00 +02:00
1121c18dc5 Changelog: Add v0.8.2 and v0.8.1
Some checks failed
Test / Run tests (push) Failing after 2m0s
2024-06-20 11:38:43 +02:00
96a3168d49 Changelog: Add description for v0.8.3
All checks were successful
Test / Run tests (push) Successful in 2m1s
2024-06-19 19:11:54 +02:00
e627f13264 Add Changelog and Readme
All checks were successful
Test / Run tests (push) Successful in 1m47s
2024-06-19 17:27:20 +02:00
7ce8c3cabf MailWindow: Allow user to change document date
All checks were successful
Test / Run tests (push) Successful in 1m55s
2024-06-17 19:28:21 +02:00
763f0197ca CreditNote: Remove calc time and add Variant.Date
All checks were successful
Test / Run tests (push) Successful in 2m1s
2024-06-17 19:01:47 +02:00
43dddf2c07 PaymentAdjustmentWindow: Lock auto ajusting in old seasons 2024-06-17 18:58:39 +02:00
70129695ae Bump version to 0.8.5
All checks were successful
Test / Run tests (push) Successful in 1m41s
Deploy / Build and Deploy (push) Successful in 2m21s
2024-06-17 11:31:26 +02:00
51b9799b56 [#46] PaymentAdjustmentWindow: Persist parameters in ClientParameters
All checks were successful
Test / Run tests (push) Successful in 1m53s
2024-06-17 11:25:35 +02:00
c1903b1f36 [#46] PaymentAdjustmentWindow: Refine DataGrid
All checks were successful
Test / Run tests (push) Successful in 1m55s
2024-06-17 11:03:32 +02:00
66eb177fbf [#46] Billing: Only adjust business shares for active members 2024-06-17 10:51:48 +02:00
87467bbe75 Export/Ebics: Remove blank line for not shown <Ctry/> 2024-06-17 10:47:14 +02:00
abf465f821 OverUnderDeliveryData: Also show inactive members
All checks were successful
Test / Run tests (push) Successful in 1m42s
2024-06-17 10:24:58 +02:00
792c18365e MailWindow: Send DeliveryConfirmations and CreditNotes also to inactive members
All checks were successful
Test / Run tests (push) Successful in 2m16s
2024-06-17 10:17:28 +02:00
5c46a00752 [#46] Windows: Add PaymentAdjustmentWindow
All checks were successful
Test / Run tests (push) Successful in 2m13s
2024-06-17 01:41:32 +02:00
9d9f929843 [#46] MemberHistory: Add Type to PK 2024-06-17 01:19:25 +02:00
b76c5ea874 [#46] CreditNote: Show number of added business shares
All checks were successful
Test / Run tests (push) Successful in 1m57s
2024-06-16 23:54:53 +02:00
86e69e9ff8 [#48] PaymentVariantsWindow: Add checkbox for custom member modifiers
All checks were successful
Test / Run tests (push) Successful in 2m22s
2024-06-16 18:31:02 +02:00
050e4f5b6f ControlUtils: Allow RenewItemsSource for ListBox to reselect all items
All checks were successful
Test / Run tests (push) Successful in 2m21s
2024-06-14 17:02:25 +02:00
01f055ee17 Test: Fix DocumentTests again?
All checks were successful
Test / Run tests (push) Successful in 1m42s
2024-06-14 12:15:05 +02:00
da9df5cbeb Export/Ebics: Warn user if no client IBAN is set
Some checks failed
Test / Run tests (push) Failing after 2m22s
2024-06-14 12:11:49 +02:00
cc0aa6046f Export/Ebics: Also export Ctry in address line mode
All checks were successful
Test / Run tests (push) Successful in 2m25s
2024-06-13 10:00:49 +02:00
5cb7d2cbb0 Export/Ebics: Escape address properly
All checks were successful
Test / Run tests (push) Successful in 1m47s
2024-06-13 01:48:37 +02:00
6c1e50ad02 Bump version to 0.8.4
All checks were successful
Test / Run tests (push) Successful in 1m44s
Deploy / Build and Deploy (push) Successful in 2m23s
2024-06-13 01:45:53 +02:00
46551fb142 Tests: Implement tests for Austrian Ebics
All checks were successful
Test / Run tests (push) Successful in 2m0s
2024-06-13 01:43:23 +02:00
b404839ad1 Tests: Fix DocumentTests? 2024-06-13 01:39:12 +02:00
ab926421b0 Export/Ebics: Implement version customization
Some checks failed
Test / Run tests (push) Failing after 2m22s
2024-06-13 01:19:36 +02:00
bbd8b67afd MainWindow: Unify gaps between buttons
All checks were successful
Test / Run tests (push) Successful in 1m45s
2024-06-12 18:55:50 +02:00
70f8276808 MainWindow: Fix crash on closing, when other window is in editing or creating mode
All checks were successful
Test / Run tests (push) Successful in 2m1s
2024-06-12 17:05:57 +02:00
4483eb6a69 [#37] Controls: Implement CheckComboBox and remove xctk
All checks were successful
Test / Run tests (push) Successful in 2m26s
2024-06-12 16:29:57 +02:00
6fc38b9db0 Bump version to 0.8.3
All checks were successful
Test / Run tests (push) Successful in 1m44s
Deploy / Build and Deploy (push) Successful in 2m27s
2024-06-11 12:43:13 +02:00
5a4ff26f31 [#16] DeleteMemberDialog: Highlight invalid/not-checked inputs
All checks were successful
Test / Run tests (push) Successful in 1m59s
2024-06-11 12:34:07 +02:00
012352c562 DeliveryAdminWindow: Fix ModifierInput source
All checks were successful
Test / Run tests (push) Successful in 1m45s
2024-06-11 11:55:47 +02:00
81f286f504 BaseDataWindow: Set SeasonPenaltyPerBs inputs to readonly when applicable
All checks were successful
Test / Run tests (push) Successful in 2m21s
2024-06-11 11:21:21 +02:00
324a63cf9a DeliveryAdminWindow: Fix modifier bug
All checks were successful
Test / Run tests (push) Successful in 2m2s
2024-06-11 01:22:43 +02:00
ca0497e396 [#44] BaseDataWindow: Fix conversion between season precisions
All checks were successful
Test / Run tests (push) Successful in 1m59s
2024-06-10 23:01:42 +02:00
08f551a394 PaymentVariantSummary: Show net/gross modifier in statistics
All checks were successful
Test / Run tests (push) Successful in 2m18s
2024-06-10 22:48:44 +02:00
3460b9378c [#44] BaseDataWindow: Add button to add/delete seasons
All checks were successful
Test / Run tests (push) Successful in 2m1s
2024-06-10 20:43:38 +02:00
a06921d4ec Windows: Minor UI text changes
All checks were successful
Test / Run tests (push) Successful in 1m57s
2024-06-10 17:38:27 +02:00
f2c2d3270b ChartWindow: Round PriceInput.Text
All checks were successful
Test / Run tests (push) Successful in 1m44s
2024-06-10 16:30:21 +02:00
6927d44b82 AreaComWindow: Add style for UnitTextBox
All checks were successful
Test / Run tests (push) Successful in 1m55s
2024-06-10 16:26:25 +02:00
cc0843f183 [#45] MemberAdminWindow: Add dialog for area commitment cancellation/transfer
Some checks failed
Test / Run tests (push) Failing after 52s
2024-06-10 16:22:28 +02:00
5039c1252a MailWindow: Fix crash on area commitment filter
All checks were successful
Test / Run tests (push) Successful in 2m0s
2024-06-10 12:43:42 +02:00
6e4f3b799d MailWindow: Fix crash when selecting custom members
All checks were successful
Test / Run tests (push) Successful in 1m44s
2024-06-10 11:53:53 +02:00
4c9a151f77 DeliveryConfirmation: Do not print sender by default
All checks were successful
Test / Run tests (push) Successful in 2m2s
2024-06-10 11:49:20 +02:00
03cacf604b Dialogs: Refactor XAML
All checks were successful
Test / Run tests (push) Successful in 2m27s
2024-06-10 10:10:45 +02:00
c12d111c57 [#16] Dialogs: Add DeleteMemberDialog
All checks were successful
Test / Run tests (push) Successful in 2m0s
2024-06-08 18:22:55 +02:00
302870fb58 [#47] Billing: Add penalty per business share
All checks were successful
Test / Run tests (push) Successful in 2m2s
2024-06-08 16:13:01 +02:00
f756220d75 BaseDataWindow: Fix 'Schilling crash'
All checks were successful
Test / Run tests (push) Successful in 2m17s
2024-06-07 09:51:23 +02:00
293c8967be Installer: Update URLs in config.ini
All checks were successful
Test / Run tests (push) Successful in 2m27s
2024-06-07 00:23:34 +02:00
601ac548fe Tests: Update dependencies
All checks were successful
Test / Run tests (push) Successful in 2m7s
2024-06-03 18:30:42 +02:00
4e477c38e0 Elwig: Update dependencies 2024-06-03 18:30:34 +02:00
46fc0db6ba IntegerUpDown: Fix xml spacing
All checks were successful
Test / Run tests (push) Successful in 1m41s
2024-06-03 17:13:37 +02:00
a531e948c1 [#37] IntegerUpDown: Add middle line
All checks were successful
Test / Run tests (push) Successful in 1m42s
2024-06-03 17:07:40 +02:00
cc4ec6c5db [#37] Controls: Implement IntegerUpDown
All checks were successful
Test / Run tests (push) Successful in 2m28s
2024-06-03 16:59:45 +02:00
ff375e3caf curl: Add '--fail' argument to return a meaningful status code
All checks were successful
Test / Run tests (push) Successful in 1m54s
2024-05-15 00:24:42 +02:00
5b952c4eb1 Workflows: Update upload URL
All checks were successful
Test / Run tests (push) Successful in 1m37s
Deploy / Build and Deploy (push) Successful in 2m26s
2024-05-14 23:00:29 +02:00
48c441b787 Bump version to 0.8.2
Some checks failed
Test / Run tests (push) Successful in 1m43s
Deploy / Build and Deploy (push) Failing after 2m17s
2024-05-14 22:51:36 +02:00
be246d6f06 Tests: Fix old url in fetch-resources.bat 2024-05-14 22:51:00 +02:00
2b10e52ab0 Installer: Fix url for PDFtoPrinter once again
Some checks failed
Test / Run tests (push) Failing after 1m36s
Deploy / Build and Deploy (push) Has been cancelled
2024-05-14 22:45:26 +02:00
e3fd705f52 App: Show scale errors always in debug mode
All checks were successful
Test / Run tests (push) Successful in 2m12s
2024-05-13 22:34:19 +02:00
81e18ac553 Config: Add 'required' option to scales
All checks were successful
Test / Run tests (push) Successful in 4m3s
2024-05-13 11:33:27 +02:00
f95f0f0ef3 BaseDataWindow: Fix crash when CurrentLastSeason does not exist
All checks were successful
Test / Run tests (push) Successful in 2m8s
2024-05-12 22:34:53 +02:00
8eba40a8c1 Bump version to 0.8.1
All checks were successful
Test / Run tests (push) Successful in 2m15s
Deploy / Build and Deploy (push) Successful in 2m16s
2024-05-12 22:01:31 +02:00
69aa75a50a PaymentVariantSummary: Use '–' instead of '~'
All checks were successful
Test / Run tests (push) Successful in 1m56s
2024-05-09 23:42:08 +02:00
13340c0979 Tests: Remove unused code 2024-05-09 19:40:59 +02:00
4bd378e048 [#32] PaymentVariantSummary: Add summary header 2024-05-09 19:12:27 +02:00
602c237fa0 MemberDataSheet: Fix spacing 2024-05-09 12:07:16 +02:00
5e6d0ebf17 [#32] PaymentVariantsWindow: Add export options for Summary 2024-05-09 11:15:32 +02:00
b064643010 [#32] Tests: Add PaymentVariantSummaryTest 2024-05-08 23:24:05 +02:00
3526234432 [#32] Dtos/PaymentVariantSummaryData: Allow Data to be exported as Ods 2024-05-08 16:48:22 +02:00
b03f81d4f2 PaymentVariantsWindow: Add Menu and StatusBar 2024-05-08 11:53:41 +02:00
f123bb44c5 PaymentVariantWindow: Fix sum calculation and crash on export 2024-05-07 12:53:25 +02:00
30536819e7 [#32] Documents: Add PaymentVariantSummary 2024-05-07 12:32:53 +02:00
384f7c9ec0 Migrate Installer and Setup to Wix 5 2024-05-03 21:27:27 +02:00
d102a1cb7a Add german localization to Wix Setup 2024-05-03 18:56:44 +02:00
6906584ef0 App: Fix errors on startup 2024-05-03 21:22:20 +02:00
c0c0b4a26a Models: Move IAddress from Helpers to Models 2024-05-03 14:57:31 +02:00
0ce8e488f9 ChartWindow: Replace deprecated property names 2024-05-03 10:31:12 +02:00
eb4562dceb MemberAdminWindow: Save season instead of bool for cancelling or transfering area commitments 2024-05-03 10:24:01 +02:00
35e5a1dfff Revert wix extensions to v4.0.5
Some checks failed
Deploy / Build and Deploy (push) Has been cancelled
2024-05-01 12:35:35 +02:00
179c8bd4f7 Bump version to 0.8.0 2024-05-01 12:08:26 +02:00
fd17d294b9 Update dependencies 2024-05-01 10:19:57 +02:00
2a4e8d69d0 MainWindow: Add WeightBreakdownButton 2024-04-30 23:34:31 +02:00
8bf8362480 App: Replace DataTemplate/ControlTemplate with TemplateSelector 2024-04-30 13:02:45 +02:00
c4d68d11bc Controls: Rewrite UnitTextBox as extension of TextBox instead of UserControl 2024-04-28 20:07:09 +02:00
21fe5bc094 Update dependencies 2024-04-18 16:13:09 +02:00
e7bfc69842 [#3] Elwig: Add feature to sync deliveries 2024-04-15 15:22:04 +02:00
f53371ab19 Helpers: Collapse extensions into one single file 2024-04-15 13:04:49 +02:00
5f8688f0cd Small fixes 2024-04-15 11:36:55 +02:00
fa00eaaefc Tests: Remove Console.WriteLine 2024-04-13 18:00:54 +02:00
443e111594 Weighing: Add Gassner scale and update tests 2024-04-13 18:00:17 +02:00
c6905bbb42 Elwig: Update dependencies 2024-04-06 17:37:41 +02:00
c360e6b6a7 MainWindow: Add feedback for users when App.CheckForUpdates finds no updates 2024-04-06 17:35:30 +02:00
9062d55b20 BaseDataWindow: Fix Parameter in FillInputs() 2024-03-31 20:43:53 +02:00
a9f38a3ccb Windows: Remove SeasonFinishWindow and TestWindow 2024-03-31 17:11:10 +02:00
cbc0d0ebff Tests: Rename class 2024-03-30 15:21:15 +01:00
27b5d653e6 WineQualityStatistics: Add and fix grouping by KMW 2024-03-30 15:21:02 +01:00
869f652afc WineQualityStatistics: Add KMW mode 2024-03-30 13:12:54 +01:00
80e91ad776 Tests: Add CreditNoteTest 2024-03-30 12:42:22 +01:00
bce709efe4 Tests: Remove Console.WriteLine()s 2024-03-30 11:28:28 +01:00
12eb53cb44 Tests: Add DeliveryConfirmationTest 2024-03-30 11:16:51 +01:00
657910ff48 Tests: Add WineQualityStatisticsTest 2024-03-30 11:09:39 +01:00
5c3cf41d3d Tests: Add DeliveryJournalTest 2024-03-30 10:59:42 +01:00
b8851fb241 Tests: Rename LetterheadTest 2024-03-30 09:57:45 +01:00
66898714bb Tests: Add more DocumentsTests 2024-03-30 09:51:48 +01:00
1047bc6e8f BaseDataWindow: Fix data renewal 2024-03-30 08:49:11 +01:00
1419c834ac BaseDataWindow: Fix season modifier crash 2024-03-30 08:39:57 +01:00
eddea88e77 Documents: Use border thickness of 0.5pt 2024-03-28 20:34:59 +01:00
7274d793c4 Bump version to 0.7.2
Some checks failed
Deploy / Build and Deploy (push) Has been cancelled
2024-03-28 17:57:59 +01:00
79d9e5d242 MemberList: Fix member DTO name refactoring bug 2024-03-28 17:18:46 +01:00
b2f52072f8 Windows: Add feature to save pdf in menu 2024-03-28 16:54:51 +01:00
9aa6cba1ff Documents: Add variable for border thickness 2024-03-28 15:56:46 +01:00
d501cfaf72 WineQualityStatistics: Small changes 2024-03-28 15:45:58 +01:00
c2b6486ede Helpers: Add enum ExportMode 2024-03-28 13:41:05 +01:00
82ea5920f2 [#30] WineQualityStatistics: Add number of deliveries 2024-03-28 13:18:32 +01:00
d011c69812 DeliveryJournalData: Fix modifier ordering 2024-03-28 12:48:23 +01:00
26a9902a13 [#30] Documents: Add WineQualityStatistics 2024-03-28 12:27:48 +01:00
57662534f3 Dtos: Unify member names in DataTables 2024-03-27 16:41:08 +01:00
d87f3ce6a6 DeliveryAdminWindow: Include more data when refreshing list 2024-03-27 16:24:29 +01:00
7f21b7b231 [#13][#31] DeliveryAdminWindow: Rework menu and add DeliveryJournal export 2024-03-27 16:10:28 +01:00
cac0959fe7 Models: Add IDelivery for DeliveryParts 2024-03-27 16:04:47 +01:00
a04f887b4d DeliveryNote: Add Name property 2024-03-27 15:40:14 +01:00
5c42ef8104 MemberAdminWindow: Restructure some doc.Show()/.Print() calls 2024-03-27 11:03:19 +01:00
cf2ec3bdc4 DeliveryAdminWindow: Improve filters even more 2024-03-26 17:32:05 +01:00
b31b5f6164 DeliveryAdminWindow: Show modifiers in DataGrid 2024-03-26 17:18:33 +01:00
85f48f1d2a Windows: Add Strg+F tooltip to SearchInput 2024-03-26 16:58:36 +01:00
e3cb20366c [#12] DeliveryAdminWindow: Improve filters 2024-03-26 16:55:40 +01:00
56ac79b4dd DeliveryJournal: Small changes in header 2024-03-26 13:10:09 +01:00
175d006d5b MemberAdminWindow: Add try/catch block in delete 2024-03-26 13:09:34 +01:00
f4ef75ac40 [#36] MemberAdminWindow: Add MemberList 2024-03-26 12:53:31 +01:00
5795c5e8ba Ods: Add support for bool types 2024-03-25 17:19:24 +01:00
04351a906f DeliveryJournal: Add Name Property 2024-03-25 16:58:02 +01:00
3f9c4cb1f6 Utils: Add ActiveAreaCommitments() 2024-03-25 16:22:58 +01:00
c6e83ffff4 Ods: Add support for DateOnly, TimeOnly and DateTime 2024-03-25 14:03:21 +01:00
dd408ca40e [#31] AdministrationWindow: Add shortcuts 2024-03-24 21:53:18 +01:00
555ce228d4 [#31] ContextWindow: Add shortcuts for Ctrl+R and F5 2024-03-24 20:28:19 +01:00
9d80c5913f DataTable: Add Subtitle 2024-03-24 13:50:20 +01:00
e435e5da8d [#31] MemberAdminWindow: Rework menu and add more features 2024-03-23 21:50:29 +01:00
7b48385992 Utils: Add SendEmail() 2024-03-23 21:49:29 +01:00
48f0ddd232 ClientParameters: Add ORDERING_MEMBERLIST 2024-03-23 21:49:17 +01:00
c9bb075910 App: Do not use FileSystemWatcher any more 2024-03-23 13:49:24 +01:00
ee1f4081f4 BaseDataWindow: Add transaction for saving 2024-03-21 11:37:00 +01:00
b6e37c0c67 [#43] BaseDataWindow: Fix editing 2024-03-21 11:29:35 +01:00
87da56b7a9 App: Improve auto update behaviour 2024-03-21 10:23:44 +01:00
afc143e1e4 ControlUtils: Fix ScrollIntoView() 2024-03-19 22:32:23 +01:00
545033daf5 [#41] MemberAdminWindow: Add feature to cancel area commitments on member non-active 2024-03-19 18:52:57 +01:00
1b822a88f3 [#41] MemberAdminWindow: Add feature to transfer area commitments from predecessor 2024-03-19 18:27:19 +01:00
a9bad4dd3f DeliveryAdminWindow: Fix editing of delivery 2024-03-19 15:36:51 +01:00
1806b02039 [#43] App: Use FileSystemWatcher to renew contexts on demand 2024-03-19 13:17:06 +01:00
98688168b8 [#43] DeliveryAdminWindow: Do not use Context from WintextWindow any more 2024-03-19 11:48:26 +01:00
2f3524db9d ControlUtils: Cleanup SelectItem() method and use accordingly 2024-03-18 17:55:27 +01:00
51e345f1fd AdministrationWindow: Use GetHashCode() to compare default/original values 2024-03-18 16:10:31 +01:00
729d2fd76c [#43] BaseDataWindow: Do not use Context from ContextWindow any more 2024-03-18 10:28:35 +01:00
5715c41a2e SeasonFinishWindow: Remove async from OnRenewContext() to avoid warnings 2024-03-16 13:44:08 +01:00
9353581a56 ActionCommand: Small fixes to avoid warnings 2024-03-16 13:42:08 +01:00
a72803f749 WineQualLevel: Implement GetHashCode() to avoid warning 2024-03-16 13:41:31 +01:00
9d1ce4138c BillingTest: Remove warnings 2024-03-16 13:36:40 +01:00
4afd2d8242 AppDbContext: Cleanup UpdateDeliveryPartModifiers() 2024-03-16 12:47:31 +01:00
b7d33e6d89 PaymentVariantsWindow: Use async for UpdateSums() 2024-03-15 17:25:42 +01:00
711bab5d33 [#43] PaymentVariantWindow: Do not use Context from ContextWindow any more 2024-03-15 17:20:07 +01:00
d9f9ab2391 Models: Use nameof() in InverseProperty 2024-03-15 16:41:40 +01:00
ebb196b094 [#43] OriginHierarchyWindow: Do not use Context from ContextWindow any more 2024-03-15 16:35:19 +01:00
298e423de8 [#43] Billing: Do only use AppDbContext short-lived 2024-03-15 15:05:15 +01:00
e2e46bc52a [#43] SeasonFinishWindow: Do not use Context from ContextWindow any more 2024-03-15 14:26:21 +01:00
a8e3eb6c1c [#43] MailWindow: Do not use Context from ContextWindow any more 2024-03-11 11:09:53 +01:00
4e2c087260 [#43] ChartWindow: Do not use Context from ContextWindow any more 2024-03-11 10:32:54 +01:00
0c63e315bb [#43] AdministrationWindow: Do not use Context from ContextWindow any more 2024-03-11 10:14:39 +01:00
f3cdac8a61 [#43] AreaComAdminWindow: Do not use Context from ContextWindow any more 2024-03-10 21:31:59 +01:00
acc159ed9c [#43] MemberAdminWindow: Context cleanup 2024-03-10 20:39:36 +01:00
1eba3d9d20 [#43] MemberAdminWindow: Do not use Context from ContextWindow any more 2024-03-10 13:05:15 +01:00
239b8a9091 QueryWindow: Minor improvements 2024-03-12 18:23:38 +01:00
279 changed files with 22650 additions and 4857 deletions

View File

@ -41,6 +41,7 @@ jobs:
uses: akkuman/gitea-release-action@v1 uses: akkuman/gitea-release-action@v1
with: with:
name: Elwig ${{ env.APP_VERSION }} name: Elwig ${{ env.APP_VERSION }}
body: "**[Changelog](src/branch/main/CHANGELOG.md#v${{ env.APP_VERSION }})**"
files: |- files: |-
Setup/bin/x64/Release/Elwig-${{ env.APP_VERSION }}.exe Setup/bin/x64/Release/Elwig-${{ env.APP_VERSION }}.exe
- name: Upload to website - name: Upload to website
@ -48,7 +49,7 @@ jobs:
run: | run: |
$content = [System.IO.File]::ReadAllBytes("Setup/bin/x64/Release/Elwig-${{ env.APP_VERSION }}.exe") $content = [System.IO.File]::ReadAllBytes("Setup/bin/x64/Release/Elwig-${{ env.APP_VERSION }}.exe")
Invoke-WebRequest ` Invoke-WebRequest `
-Uri "https://www.necronda.net/elwig/files/Elwig-${{ env.APP_VERSION }}.exe" ` -Uri "https://elwig.at/files/Elwig-${{ env.APP_VERSION }}.exe" `
-Method PUT ` -Method PUT `
-Body $content ` -Body $content `
-Headers @{ Authorization = "${{ secrets.API_AUTHORIZATION }}" } ` -Headers @{ Authorization = "${{ secrets.API_AUTHORIZATION }}" } `

View File

@ -2,6 +2,7 @@ name: Test
on: on:
push: push:
branches: ["**"] branches: ["**"]
paths: ["Elwig/**", "Tests/**", "Installer/Files/*.exe", ".gitea/workflows/test.yaml"]
jobs: jobs:
test: test:
name: Run tests name: Run tests
@ -9,6 +10,19 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Check for Byte order marks
shell: powershell
run: |
$pattern = [char]::ConvertFromUtf32(0xFEFF)
$files = git grep -IEl "^$pattern"
if ( $lastexitcode -ne 1 ) {
echo "Files with BOM found:"
echo $files
exit 1
} else {
echo "No files with BOM found"
exit 0
}
- name: Setup MSBuild - name: Setup MSBuild
uses: microsoft/setup-msbuild@v1.1 uses: microsoft/setup-msbuild@v1.1
- name: Setup NuGet - name: Setup NuGet
@ -26,4 +40,4 @@ jobs:
shell: powershell shell: powershell
run: | run: |
$env:PATH = "$(pwd)\Installer\Files;" + $env:PATH $env:PATH = "$(pwd)\Installer\Files;" + $env:PATH
$(& dotnet test Tests; $a=$lastexitcode) | findstr x*; exit $a $(& dotnet test Tests --filter "FullyQualifiedName!~E2ETests"; $a=$lastexitcode) | findstr x*; exit $a

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ bin/
Tests/Resources/Sql/Create.sql Tests/Resources/Sql/Create.sql
*.exe *.exe
!WinziPrint.exe !WinziPrint.exe
*.sqlite3

982
CHANGELOG.md Normal file
View File

@ -0,0 +1,982 @@
Changelog
=========
[v0.13.9][v0.13.9] (2025-05-05) {#v0.13.9}
------------------------------------------
### Sonstiges {#v0.13.9-misc}
* Abhängigkeiten aktualisiert. (bf0db37872, 4af2fa256e, d1c07ee92a, 41c5288fc5)
* Automatisches Aktualisieren der Synchroisations-URL (`https://elwig.at/clients/` -> `https://sync.elwig.at/`). (e50e7337e6)
[v0.13.9]: https://git.necronda.net/winzer/elwig/releases/tag/v0.13.9
[v0.13.8][v0.13.8] (2025-02-21) {#v0.13.8}
------------------------------------------
### Behobene Fehler {#v0.13.8-bugfixes}
* Details im Lieferungen-Fenster (`DeliveryAdminWindow`) für die WG Weinland richtiggestellt (Lesweagen, Verhalten bei _Gerebelt gewogen_). (138dae715e, aa98909c0a)
### Sonstiges {#v0.13.8-misc}
* Abhängigkeiten aktualisiert. (fe0a7dab2a, 775bb08e95)
[v0.13.8]: https://git.necronda.net/winzer/elwig/releases/tag/v0.13.8
[v0.13.7][v0.13.7] (2025-01-21) {#v0.13.7}
------------------------------------------
### Behobene Fehler {#v0.13.7-bugfixes}
* In seltenen Fällen konnten im Auszahlungsvariante-Fenster (`ChartWindow`) manche (Sorten-/Attribut-/Bewirtschaftungsart-)Zuordnungen zu Kurven nicht richtig gespeichert werden. (0b8a1b321f)
* Beim Öffnen des Ausgangs-Protokoll-Fensters (`MailLogWindow`) kam es zu einem Absturz. (5d017cc8ea)
* Im Auszahlungsvarianten-Fenster (`PaymentVariantsWindow`) war es nicht möglich die Überweisungsdaten zu exportieren, sofern mindestens eine Traubengutschrift einen negativen Betrag aufwies.
Jetzt wird der Benutzer nur gewarnt und es ist möglich alle anderen Gutschriften zu exportieren. (6d88c5645c, c7a2f2241d)
### Sonstiges {#v0.13.7-misc}
* Im Auszahlungsvarianten-Fenster (`PaymentVariantsWindow`) ist es nun möglich das Datum einer Auszahlungsvariante zu ändern. (bd4ebb8c35)
[v0.13.7]: https://git.necronda.net/winzer/elwig/releases/tag/v0.13.7
[v0.13.6][v0.13.6] (2025-01-14) {#v0.13.6}
------------------------------------------
### Behobene Fehler {#v0.13.6-bugfixes}
* In seltenen Fällen konnten im Auszahlungsvariante-Fenster (`ChartWindow`) manche (Sorten-/Attribut-/Bewirtschaftungsart-)Zuordnungen zu Kurven nicht richtig gespeichert werden.
Berechnungen basierend auf diesen (evtl. falschen) Zuordnungen wurden immer richtig ausgeführt, eine nachträgliche Überprüfung ist daher möglich. (20e3e2a76b)
### Sonstiges {#v0.13.6-misc}
* Abhängigkeiten aktualisiert. (95ccb2627c, 80fec4473a)
[v0.13.6]: https://git.necronda.net/winzer/elwig/releases/tag/v0.13.6
[v0.13.5][v0.13.5] (2025-01-02) {#v0.13.5}
------------------------------------------
### Neue Funktionen {#v0.13.5-features}
* Im Mitglieder-Fenster (`MemberAdminWindow`) Filter `aktiv` und `!aktiv` hinzugefügt. (5e53d864b1)
* Im Lieferungen Fenster (`DeliveryAdminWindow`) Menüpunkt _Statistik_ mitsamt _Qualitätsstatistik_ und _Lieferstatistik pro Ort_ hinzugefügt. (c24b1ca2b9)
### Sonstiges {#v0.13.5-misc}
* Abhängigkeiten aktualisiert. (c9e483ba9d, 633b560a67)
[v0.13.5]: https://git.necronda.net/winzer/elwig/releases/tag/v0.13.5
[v0.13.4][v0.13.4] (2024-11-25) {#v0.13.4}
------------------------------------------
### Behobene Fehler {#v0.13.4-bugfixes}
* Bei _Unter-/Überlieferungen lt. gez. GA_ waren seit [v0.10.6](#v0.10.6) (2023-08-30) Nicht-Lieferanten nicht aufgeführt. (6ba2aa7143)
* Bei _Unterlieferungen laut Flächenbindungen_ wurden nur Mitglieder berücksichtigt, die zum Zeitpunkt des Exports aktiv waren. (3b97c2243a, 338f9fe092)
### Sonstiges {#v0.13.4-misc}
* Abhängigkeiten aktualisiert. (c3a2f983d5, 70e01849be, a99a23fd08, 4a10e94d71, 6c7f10cb26)
[v0.13.4]: https://git.necronda.net/winzer/elwig/releases/tag/v0.13.4
[v0.13.3][v0.13.3] (2024-11-13) {#v0.13.3}
------------------------------------------
### Neue Funktionen {#v0.13.3-features}
* Im Haupt-Fenster (`MainWindow`) unter _Leseabschluss_ eine Statistik-Tabelle (Gebunden/Ungeb., Mitglieder/Gewicht/Fläche) hinzugefügt. (54deccf021)
* Im Haupt-Fenster (`MainWindow`) unter _Leseabschluss_ zwei Exportmöglichkeiten (_Flächenbindungen_, _Liefermenge/Ertrag_) hinzugefügt. (c5453c2fe6)
* Ausgangs-Protokoll-Fenster (`MailLogWindow`) zum Ansehen aller ausgehenden E-Mails oder ausgedruckten Rundschreiben hinzugefügt (_Rundschreiben_ -> _Hilfe_). (2ee0d56dcc)
### Behobene Fehler {#v0.13.3-bugfixes}
* Im Rundschreiben-Fenster (`MailWindow`) kleinere Fehler behoben und alle Einstellungen werden nun gespeichert. (0a9731af09)
### Sonstiges {#v0.13.3-misc}
* In allen Fenstern an passenden Stellen Symbole/Icons hinzugefügt. (f4fa549130)
* Im Mitglieder-Fenster (`MemberAdminWindow`) Filter `Flächenbindung` (Mitglieder mit irgendeiner aktiven Flächenbindung) hinzugefügt. (a1d84dd988)
* Im Flächenbindungen-Fenster (`AreaComAdminWindow`) Filter für explizite Saisons hinzugefügt. (6718ad4c8d)
[v0.13.3]: https://git.necronda.net/winzer/elwig/releases/tag/v0.13.3
[v0.13.2][v0.13.2] (2024-10-13) {#v0.13.2}
------------------------------------------
### Neue Funktionen {#v0.13.2-features}
* Im Lieferungen-Fenster den Menüpunkt _Abwertungsliste_ (`DeliveryDepreciationList`) hinzugefügt. (3cbffdbf27)
### Behobene Fehler {#v0.13.2-bugfixes}
* Fehler im Waagenprotokoll `Avery-Async` (L320) behoben. (8680e51052)
* Hausnummern in der BKI Traubentransportscheinliste werden in Excel richtig angezeigt. (86f7f693a0)
* Beim Ändern der Identifikatoren von Attributen/Bewirtschaftungsarten wurden diese in Auszahlungsvarianten nicht aktualisiert. (d1f67dc57d)
### Sonstiges {#v0.13.2-misc}
* Weitere automatisierte Tests hinzugefügt. ([#11][i11])
* Namenszusätze bei Gemeinden (z.B. an, bei, im, am) genauer angegeben. (65498dd18f)
* Bei einigen Eingabefeldern waren die Ränder unscharf. (e247925472)
* Wo leicht möglich wird `ExecuteSql`/`FromSql` statt `ExecuteSqlRaw`/`FromSqlRaw` verwendet. (0675c45617)
[v0.13.2]: https://git.necronda.net/winzer/elwig/releases/tag/v0.13.2
[i11]: https://git.necronda.net/winzer/elwig/issues/11
[v0.13.1][v0.13.1] (2024-09-29) {#v0.13.1}
------------------------------------------
### Neue Funktionen {#v0.13.1-features}
* Das Extrahieren/Abwerten/Aufteilen von (Teil-)Lieferungen wurde grundlegend überarbeitet und funktioniert ab jetzt in einem einzigen, übersichtlicheren Dialog. (c62947dacd, c185437b9a)
### Behobene Fehler {#v0.13.1-bugfixes}
* Im Mitglieder-Fenster (`MemberAdminWinodw`) wurden bei `Anlieferungsbestätigung -> speichern (PDF)` und `Traubengutschrift -> speichern (PDF)` E-Mails verschickt, anstatt ein PDF gespeichert. (6ba1973087, a2315e84bd)
### Sonstiges {#v0.13.1-misc}
* Abhängigkeiten aktualisiert. (b6ae1f5675)
[v0.13.1]: https://git.necronda.net/winzer/elwig/releases/tag/v0.13.1
[v0.13.0][v0.13.0] (2024-09-25) {#v0.13.0}
------------------------------------------
> [!NOTE]
> Ab dieser Version verhält sich die Berechnung der Unterlieferungen bei Flächenbindungen anders.
### Behobene Fehler {#v0.13.0-bugfixes}
* Im Lieferungen-Fenster (`DeliveryAdminWindow`) war das Extrahieren in eine neue Lieferung seit ca. 6 Monaten (98688168b8) nicht mehr möglich. (8a61747538)
### Sonstiges {#v0.13.0-misc}
* Es werden alle Lieferungen (inkl. `WEI`, `RSW`, `LDW`) zur Berechnung der Unterlieferung bei Flächenbindungen herangezogen. (4fa5b8f6d4)
* In diversen Fenstern die Formatierung von Zahlen verbessert. (579ed53487)
* Das Rundschreiben-Fenster (`MailWindow`) ist nun benutzerfreundlicher/-sicherer. (4a7c95e250)
[v0.13.0]: https://git.necronda.net/winzer/elwig/releases/tag/v0.13.0
[v0.12.0][v0.12.0] (2024-09-24) {#v0.12.0}
------------------------------------------
### Behobene Fehler {#v0.12.0-bugfixes}
* Das Drucken von Dokumenten ist wieder möglich. (94a6dd5312, a48ea8e7e2)
### Sonstiges {#v0.12.0-misc}
* Versuch 1: `Database is locked` Fehler beheben. ([#56][i56])
[v0.12.0]: https://git.necronda.net/winzer/elwig/releases/tag/v0.12.0
[i56]: https://git.necronda.net/winzer/elwig/issues/56
[v0.11.4][v0.11.4] (2024-09-22) {#v0.11.4}
------------------------------------------
> [!WARNING]
> Aufgrund eines Fehlers ist in dieser Version das Drucken von Dokumenten nicht möglich!
>
> Es wird empfohlen die nächste Version zu verwenden.
### Behobene Fehler {#v0.11.4-bugfixes}
* In den Tooltips für Gewicht und Flächenbindungen wurden die Abstände wieder hergestellt. (e0fcaf1f53)
[v0.11.4]: https://git.necronda.net/winzer/elwig/releases/tag/v0.11.4
[v0.11.3][v0.11.3] (2024-09-22) {#v0.11.3}
------------------------------------------
> [!WARNING]
> Aufgrund eines Fehlers ist in dieser Version das Drucken von Dokumenten nicht möglich!
>
> Es wird empfohlen Version 0.12.0 zu verwenden.
### Behobene Fehler {#v0.11.3-bugfixes}
* Im Mitglieder-Fenster (`MemberAdminWindow`) wurde das Berechnen der Mitgliederdaten pro Jahr und Mitglied rückgängig gemacht. (1d187c25f3)
[v0.11.3]: https://git.necronda.net/winzer/elwig/releases/tag/v0.11.3
[v0.11.2][v0.11.2] (2024-09-22) {#v0.11.2}
------------------------------------------
> [!WARNING]
> Aufgrund eines Fehlers ist in dieser Version das Drucken von Dokumenten nicht möglich!
>
> Es wird empfohlen Version 0.12.0 zu verwenden.
### Neue Funktionen {#v0.11.2-features}
* In der Anmeldeliste (`DeliveryAncmtList`) wird die Stamm-KG des Mitgliedes angegeben. (165770fa37)
* Im Haupt-Fenster (`MainWindow`) wurde im Menüpunkt _Hilfe_ ein Fehler-Protokoll-Fenster (`LogWindow`) hinzugefügt. (526e951029)
### Behobene Fehler {#v0.11.2-bugfixes}
* Falls das Übernahme-Fenster (`DeliveryAdminWindow`) geöffnet war und man währenddessen die aktuellste Lieferung gelöscht hat kam es zum Fehler beim Nummerieren des Lieferscheines. (5c12dba125)
* In Seltenen Fällen konnte es vorkommen, dass sich Elwig aufgrund eines internen Fehlers im Übernahme-Fenster (`DeliveryAdminWindow`) geschlossen hat. (dcec6f03fe)
### Sonstiges {#v0.11.2-misc}
* Im Lieferungen-Fenster (`DeliveryAdminWindow`) wurde das Layout der Teillieferungen-Liste angepasst. (9fa9d9fbec)
* Alle Aufrufe von `FillInputs()` in `Services` sind nun synchron. (648c406ad2, 7791d02979)
* Im Mitglieder-Fenster (`MemberAdminWindow`) werden die Lieferinformationen pro Jahr und Mitglied im Voraus berechnet. (66be5a3e2c)
* Im Anmeldungen-Fenster (`DeliveryAncmtAdminWindow`) heißt es nun _Anmeldungen_, nicht mehr _Traubenanmeldungen_. (3cde360aaa)
[v0.11.2]: https://git.necronda.net/winzer/elwig/releases/tag/v0.11.2
[v0.11.1][v0.11.1] (2024-09-19) {#v0.11.1}
------------------------------------------
### Neue Funktionen {#v0.11.1-features}
* Im Anmeldungen-Fenster (`DeliveryAncmtAdminWindow`) mehr Filter hinzugefügt; das Gewicht wird (wie im Lieferungen-Fenster) aufgeschlüsselt. (27d8a5cfb6)
### Behobene Fehler {#v0.11.1-bugfixes}
* Beim Export der BKI-Liste werden Namen von Rechnungsadressen _immer_ richtig getrennt. (871bc299bd)
* Flächenbindungen ohne Startsaison werden ohne Fehler gehandhabt. (21a1b11d68)
### Sonstiges {#v0.11.1-misc}
* Im Anmeldungen-Fenster (`DeliveryAncmtAdminWindow`)...
* die Liste der Lesepläne vergrößert. (a18b58f438)
* sind abgesagte Lesepläne durchgestrichen. (74200083ab)
* wird beim Erstellen das Feld für die MgNr direkt fokussiert. (3f7cd2a6ff)
* Abhängigkeiten aktualisiert. (eee90c784b, d8beb03b96)
* Kleinigkeiten in `DeliveryService`. (642fb3a625)
[v0.11.1]: https://git.necronda.net/winzer/elwig/releases/tag/v0.11.1
[v0.11.0][v0.11.0] (2024-09-16) {#v0.11.0}
------------------------------------------
### Neue Funktionen {#v0.11.0-features}
* Im Rundschreiben-Fenster (`MailWindow`) können jetzt auch Funktionäre, Bio-Betriebe oder angemeldete Mitglieder für einen Leseplan adressiert werden. (5c08f61963)
* Im Leseplan-Fenster (`DeliveryScheduleAdminWindow`) können nun auch Attribut, Bewirtschaftungsart, und ob der Leseplan abgesagt ist angegeben werden. (d8a10152b3)
### Sonstiges {#v0.11.0-misc}
* `App.HintContextChange()` ist synchron und fügt Arbeitsaufträge dem Dispatcher hinzu. (f09c43c1bd)
[v0.11.0]: https://git.necronda.net/winzer/elwig/releases/tag/v0.11.0
[v0.10.8][v0.10.8] (2024-09-05) {#v0.10.8}
------------------------------------------
### Neue Funktionen {#v0.10.8-features}
* Im Anmeldungen-Fenster (`DeliveryAncmtAdminWindow`) gibt es eine neue Spalte `Datum` und es können alle Lesepläne nach z.B. einem Mitglied durchsucht werden. (a5638135a3, 7437187630)
### Behobene Fehler {#v0.10.8-bugfixes}
* Versuch 3: Fehler bei automatischer Daten-Erneuerung bei längerer Benutzung. (8d9172f91e)
### Sonstiges {#v0.10.8-misc}
* Im Lieferjournal (`DeliveryJournal`) ist die Sortierung der Filter für das Gewicht korrigiert. (22514715c1)
* In der Anmeldeliste (`DeliveryAncmtList`) wurde die Spalte `Anmldg.` zentriert. (f43d9c020c)
* In Geschäftsdokumenten (`BusinessDocument`) wurden im Informationsblock `:` hinzugefügt. (26235f8c0a)
* Im Mitgliedsstammdatenblat (`MemberDataSheet`) wird der USt.-Steuersatz zusätzlich zu _Buchführend: ja/nein_ angezeigt. (a04c7d538e)
[v0.10.8]: https://git.necronda.net/winzer/elwig/releases/tag/v0.10.8
[v0.10.7][v0.10.7] (2024-09-02) {#v0.10.7}
------------------------------------------
### Neue Funktionen {#v0.10.7-features}
* Im Anmeldungen-Fenster (`DeliveryAncmtAdminWindow`) wird der Anmeldezeitpunkt angezeigt; in der Anmeldeliste als `ok` oder `verspät.`. (141086673f)
* Im Lieferungen-Fenster (`DeliveryAdminWindow`) kann nach dem Gesamtgewicht einer Lieferung gefiltert werden (z.B. `<500kg`, `>8000kg`). (543185d48e)
### Behobene Fehler {#v0.10.7-bugfixes}
* Versuch 2: Fehler bei automatischer Daten-Erneuerung bei längerer Benutzung. (6627ab6d12)
[v0.10.7]: https://git.necronda.net/winzer/elwig/releases/tag/v0.10.7
[v0.10.6][v0.10.6] (2024-08-30) {#v0.10.6}
------------------------------------------
### Behobene Fehler {#v0.10.6-bugfixes}
* Der Titel des Flächenbindungen-Fensters (`AreaComAdminWindow`) ist jetzt _Flächenbindungen_, nicht mehr _Lieferungen_. (ee1315929c)
* Im Auszahlungsvariante-Fenster (`ChartWindow`) einen Skalierungs-Fehler behoben. ([#33][i33])
* Versuch: Fehler bei automatischer Daten-Erneuerung bei längerer Benutzung. (8c8c0a8c2b)
### Sonstiges {#v0.10.6-misc}
* SQL-Queries für Auszahlung-Anpassen-Fenster (`PaymentAdjustmentWindow`) und Über-/Unterlieferungen effizienter umgeschrieben. (9930e6173c)
* Im Haupt-Fenster (`MainWindow`) den Menüpunkt _Waagen_ zu _Waage_ geändert. (8ce8492c74)
* Im Übernahme-Fenster (`DeliveryAdminWindow`) wird in der Teil-Lieferungen-Liste immer die letzte Teil-Lieferung angezeigt. (2ef10b4bb2)
* Breite des Traubenanmeldungen-Fensters (`DeliveryAncmtAdminWindow`) leicht erhöht und Fehler beim Enter-Drücken im _Gewicht_ Eingabefeld. (21f68caf4c, e18bc58b6c)
* Im Mitglieder-Fenster (`MemberAdminWindow`) wird das ändern der Kontaktart E-Mail wieder farblich hervorgehoben. (78a72c641f)
[v0.10.6]: https://git.necronda.net/winzer/elwig/releases/tag/v0.10.6
[i33]: https://git.necronda.net/winzer/elwig/issues/33
[v0.10.5][v0.10.5] (2024-08-24) {#v0.10.5}
------------------------------------------
### Neue Funktionen {#v0.10.5-features}
* Im Mitglieder-Fenster (`MemberAdminWindow`) kann der Benutzer beim Übertragen von Flächenbindungen entscheiden, ob der Beginn der Laufzeit übernommen werden soll, oder nicht. (bec1b165bf)
* Im Haupt-Fenster (`MainWindow`) ist es nun möglich für Waagen vom Typ `SysTec-IT` Datum und Uhrzeit festzulegen. (cd2b482b5a)
### Sonstiges {#v0.10.5-misc}
* Abstände in der Statusleiste im Anmeldungen-Fenster (`DeliveryAncmtAdminWindow`). (adbe418b7c)
* Im Anmeldungen-Fenster (`DeliveryAncmtAdminWindow`) scheinen keine Warnungen mehr auf, wenn ein nicht-optionales Feld nicht ausgefüllt wurde. (2ae2564647)
[v0.10.5]: https://git.necronda.net/winzer/elwig/releases/tag/v0.10.5
[v0.10.4][v0.10.4] (2024-08-22) {#v0.10.4}
------------------------------------------
### Sonstiges {#v0.10.4-misc}
* Im Anmeldungen-Fenster (`DeliveryAncmtAdminWindow`) wird der Wochentag auch beim Bearbeiten angezeigt. (85931e62e8)
* Im Mitglieder-Fenster (`MemberAdminWindow`) werden Anzahl der Mitglieder und Geschäftsanteile nicht mehr von allen aktiven angezeigt, sonder von den momentan gefilterten. (49e4b65c27)
* Statusleiste im Anmeldungen-Fenster (`DeliveryAncmtAdminWindow`). (f54677d429)
[v0.10.4]: https://git.necronda.net/winzer/elwig/releases/tag/v0.10.4
[v0.10.3][v0.10.3] (2024-08-21) {#v0.10.3}
------------------------------------------
### Behobene Fehler {#v0.10.3-bugfixes}
* Datum für _Anmeldungen bis_ im Leseplan-Fenster (`DeliveryScheduleAdminWindow`) ab jetzt änderbar. (84d8d0cecb)
### Sonstiges {#v0.10.3-misc}
* Wochentag bei Leseplan-Liste im Anmeldungen-Fenster (`DeliveryAncmtAdminWindow`) anzeigen. (fe7f9d675b)
[v0.10.3]: https://git.necronda.net/winzer/elwig/releases/tag/v0.10.3
[v0.10.2][v0.10.2] (2024-08-16) {#v0.10.2}
------------------------------------------
### Behobene Fehler {#v0.10.2-bugfixes}
* Beim Hochladen der Mitgliederdaten sind Flächenbindungen nicht mitexportiert worden. (204dfe8745)
* Beim Importieren von älteren Exportdaten kam es zu einem Fehler. (cd40075702)
[v0.10.2]: https://git.necronda.net/winzer/elwig/releases/tag/v0.10.2
[v0.10.1][v0.10.1] (2024-08-14) {#v0.10.1}
------------------------------------------
### Neue Funktionen {#v0.10.1-features}
* In der Mitglieder-Liste (PDF und Excel) werden ggf. gefilterte Flächenbindungen angegeben. (d3839c288a)
### Behobene Fehler {#v0.10.1-bugfixes}
* Fehler beim Berechnen...
* falls in Saison keine Lieferungen vorhanden sind. (48970de652)
* falls der Vorname eines Mitgliedes nicht gesetzt ist. (2764a0ca21)
[v0.10.1]: https://git.necronda.net/winzer/elwig/releases/tag/v0.10.1
[v0.10.0][v0.10.0] (2024-08-13) {#v0.10.0}
------------------------------------------
> [!NOTE]
> Mitglieder können ab dieser Version als juristische Person markiert werden.
> Es wird empfohlen, dies bei entsprechenden Mitglieder zu überprüfen und einzusetzen (z.B. Bezirksbauernkammer, Lagerhaus).
### Neue Funktionen {#v0.10.0-features}
* Traubenanmeldungen und Leseplanung sind ab jetzt verfügbar. ([#14][i14])
* Mitglieder können nun auch als juristische Person markiert werden. ([#54][i54])
* Im Auszahlungsvarianten-Fenster kann unter _Anpassen_ (`PaymentAdjustmentWindow`) nun auch ein Absoluter oder Relativer Zu-/Abschlag pro Mitglied festgelegt werden, der **vor der MwSt.** angewendet wird. (4d89a17e80)
### Behobene Fehler {#v0.10.0-bugfixes}
* Beim Erstellen eines neuen Mitgliedes war es nicht möglich die Flächenbindungen des Vorgängers zu übernehmen. (9127cd3f03)
### Sonstiges {#v0.10.0-misc}
* Für den Treuebonus der WG Matzen werden nun auch Lieferungen der Vorgänger miteinbezogen. (2333077aa5)
* In den Variantendaten einer Auszahlungsvariante (`PaymentVariantSummary`) werden Zu-/Abschläge pro Mitglied explizit angegeben, somit ist die Zwischensumme korrekt. (f52c11b91e)
[v0.10.0]: https://git.necronda.net/winzer/elwig/releases/tag/v0.10.0
[i14]: https://git.necronda.net/winzer/elwig/issues/14
[i54]: https://git.necronda.net/winzer/elwig/issues/54
[v0.9.3][v0.9.3] (2024-08-02) {#v0.9.3}
---------------------------------------
### Sonstiges {#v0.9.3-misc}
* Das Lieferjournal als Excel-Liste beinhaltet nun auch Liefer-Zweigstelle und Stamm-Zweigstelle des Liefernaten. (cf05a0c658)
[v0.9.3]: https://git.necronda.net/winzer/elwig/releases/tag/v0.9.3
[v0.9.2][v0.9.2] (2024-08-01) {#v0.9.2}
---------------------------------------
### Behobene Fehler {#v0.9.2-bugfixes}
* Verhalten beim Schließen des Haupt-Fensters (`MainWindow`) nicht immer richtig. (8db6007264)
* Im Mitglieder-Fenster (`MemberAdminWindow`) führt beim Erstellen eines Mitglied das Eingeben einer Tel.-Nr. zu einem Fehler. (4403754ada)
[v0.9.2]: https://git.necronda.net/winzer/elwig/releases/tag/v0.9.2
[v0.9.1][v0.9.1] (2024-08-01) {#v0.9.1}
---------------------------------------
### Neue Funktionen {#v0.9.1-features}
* Im Rundschreiben-Fenster (`MailWindow`) können E-Mails ohne Anhänge verschickt werden. (f69d2809f3)
* Im Mitglieder-Fenster (`MemberAdminWindow`) und Lieferungen-Fenster (`DeliveryAdminWindow`) kann das/die momentan ausgewählte Mitglied/Lieferung alleine exportiert werden. (db4de5b5fe)
* Exporte für Mitglieder enthalten nun auch deren Flächenbindungen. (10ee1d6548)
### Behobene Fehler {#v0.9.1-bugfixes}
* Sortierung der Wiegen-Knöpfe im Übernahme-Fenster (`DeliveryAdminWindow`). (7786fb421a)
### Sonstiges {#v0.9.1-misc}
* Deutsche Fehlermeldung beim Hochladen/Herunterladen im Haupt-Fenster (`MainWindow`). (91717f8efb)
* Waagen-Fehler _Brutto negativ_ implementiert. (39f93da0ba)
[v0.9.1]: https://git.necronda.net/winzer/elwig/releases/tag/v0.9.1
[v0.9.0][v0.9.0] (2024-07-28) {#v0.9.0}
---------------------------------------
### Neue Funktionen {#v0.9.0-features}
* Flächenbindungen werden als Tabelle in der Status-Leiste im Mitglieder-Fenster (`MemberAdminWindow`) angezeigt. ([#26][i26])
* Filter für E-Mail-Adressen, Tel.-Nr. und Kontaktarten im Mitglieder-Fenster (`MemberAdminWindow`). (1141331608)
* Auf Lieferscheinen (`DeliveryNote`) werden nun _Brutto, Tara, Netto_ werte abgedruckt. (fd0ed97305, 4aa3362029, 53d3affefe)
* Beim Exportieren der Mitgliederliste werden auch Tel.-Nr. und E-Mail-Adressen exportiert. (b6afb94246)
* Benutzerfreundliches Synchronisieren zwischen Standorten. ([#3][i3])
* Übernehmer _müssen_ explizit entscheiden, ob _gerebelt gewogen_ oder nicht. (935b31f6e3)
### Behobene Fehler {#v0.9.0-bugfixes}
* Zu-/Abschläge im Lieferungen-Fenster (`DeliveryAdminWindow`). (f235d5b380)
* Falls keine Saison existierte führte das zu einem Absturz im Rundschreiben-Fenster (`MailWindow`). (84f772a32f)
* Beim Drucken _sollte_ kein Kommandozeilen-Fenster mehr sichtbar sein. (d741ba92dc)
* Wenn bei einem Mitglied keine Stamm-KG hinterlegt war, kam es zu inkonsistentem Verhalten im Übernahme-Fenster (`DeliveryAdminWinodw`). (4c3f0c40fa)
### Sonstiges {#v0.9.0-misc}
* Model-View-ViewModel (MVVM) implementiert! ([#10][i10])
* Die nächste Saison "beginnt" bereits im Juli (statt erst im August) (`CurrentLastSeason`). (c314321039)
* Automatisierte Tests für Waagen (Sitzendorf, Haugsdorf). (8e9f2f4e90)
* Überprüfung auf Byte-Order-Marks in allen Quellcode-Dateien. (53c7cb2ec0, f09753ccc2)
[v0.9.0]: https://git.necronda.net/winzer/elwig/releases/tag/v0.9.0
[i3]: https://git.necronda.net/winzer/elwig/issues/3
[i10]: https://git.necronda.net/winzer/elwig/issues/10
[i26]: https://git.necronda.net/winzer/elwig/issues/26
[v0.8.9][v0.8.9] (2024-07-23) {#v0.8.9}
---------------------------------------
### Behobene Fehler {#v0.8.9-bugfixes}
* Absturz im Rundschreiben-Fenster (`MailWindow`). Fehler bei `CheckComboBox`. (49f03c0a3c)
* Excel-Exporte können nun auch Sonderzeichen (wie `&`) enthalten. (ba9e1d7201)
[v0.8.9]: https://git.necronda.net/winzer/elwig/releases/tag/v0.8.9
[v0.8.8][v0.8.8] (2024-07-22) {#v0.8.8}
---------------------------------------
### Behobene Fehler {#v0.8.8-bugfixes}
* Schnittstelle für Gassner-Waagen nicht funktionsfähig. (4c75dbe4aa)
[v0.8.8]: https://git.necronda.net/winzer/elwig/releases/tag/v0.8.8
[v0.8.7][v0.8.7] (2024-07-22) {#v0.8.7}
---------------------------------------
### Neue Funktionen {#v0.8.7-features}
* Im Mitglieder-Fenster (`MemberAdminWindow`) muss eine E-Mail-Adresse angegeben werden, wenn E-Mail als Kontaktart angegeben wird. (5a36e84b1f)
### Sonstiges {#v0.8.7-misc}
* Automatisierte Ende-zu-Ende-Tests (`E2ETests`) hinzugefügt. (ddd821e478, 6b48a1090c, dd5049faae, 5b2f617a68, 7246852181)
* Automatisierte Tests überarbeitet. (44656e0022, 86937485e4, 34178105a7, ffe0ff5508, 01d658f51d)
* Update-Dialog (`UpdateDialog`) überarbeitet. (62f63ef63d, e9de54415a)
* Eingebefelder vom Typ _Mehrfachauswahl mit Dropdown_ (`CheckComboBox`) etwas verbessert. (26d75ea3cd, daf83c4bbc)
* Im Übernahme-Fenster (`DeliveryAdminWindow`) werden nur noch aktive Mitglieder angezeigt. (658a1f4dc1)
* Abhängigkeiten aktualisiert. (ffef1fd6e4, dd48a24c58)
* Auto-Update-Funktion überarbeitet. (3ac9536e76)
[v0.8.7]: https://git.necronda.net/winzer/elwig/releases/tag/v0.8.7
[v0.8.6][v0.8.6] (2024-07-01) {#v0.8.6}
---------------------------------------
### Neue Funktionen {#v0.8.6-features}
* Es ist nun möglich benutzerdefinierte Zu-/Abschläge pro Mitglied anzugeben. ([#48][i48], 255bcbe3ad, 6a92eb76a0)
* Im Mitglieder-Fenster (`MemberAdminWindow`) Menü-Eintrag zum Ansehen von Traubengutschriften (`CreditNote`) hinzugefügt. (bce2eea3ac)
### Sonstiges {#v0.8.6-misc}
* Im Rundschreiben-Fenster (`MailWindow`) kann das Datum geändert werden. (7ce8c3cabf)
* Im Auszahlung-Anpassen-Fenster (`PaymentAdjustmentWindow`):
* kann nur noch die aktuellste Saison angepasst werden. (43dddf2c07)
* gibt es jetzt eine Statusleiste. (5c76b8ec52)
* Auf Traubengutschriften (`CreditNote`) wird die Berechnungszeit nicht mehr angeführt, dafür jedoch das Datum der Auszahlungsvariante. (763f0197ca)
* Im Stammdaten-Fenster (`BaseDataWindow`) sind Strafen bei Unterlieferung lt. GA besser gruppiert. ([#52][i52])
* Statistiken zu Zu-/Abschlägen im Übersichtsdokument für Auszahlungsvarianten (`PaymentVariantSummary`). ([#49][i49])
* Knöpfe (und Pfeile) im Auszahlungsvarianten-Fenster (`PaymentVariantsWindow`) anders angeordnet. (6195363335)
* Im Auszahlungsvariante-Fenster (`ChartWindow`) sind 73 °Oe standardmäßig ausgewählt. ([#51][i51])
[v0.8.6]: https://git.necronda.net/winzer/elwig/releases/tag/v0.8.6
[i48]: https://git.necronda.net/winzer/elwig/issues/48
[i49]: https://git.necronda.net/winzer/elwig/issues/49
[i51]: https://git.necronda.net/winzer/elwig/issues/51
[i52]: https://git.necronda.net/winzer/elwig/issues/52
[v0.8.5][v0.8.5] (2024-06-17) {#v0.8.5}
---------------------------------------
### Neue Funktionen {#v0.8.5-features}
* Fenster zum Anpassen der Auszahlung einer Saison (`PaymentAdjustmentWindow`) hinzugefügt. ([#46][i46])
### Behobene Fehler {#v0.8.5-bugfixes}
* Inaktive Mitglieder, die in der letzten Saison geliefert haben, bisher in _Über-/Unterlieferungen_ nicht aufgeschienen. (abf465f821)
* Inaktive Mitglieder, die in der letzten Saison geliefert haben, haben keine entsprechenden Rundschreiben bekommen. (792c18365e)
* Falls unter `Stammdaten > Mandant` kein IBAN gesetzt war führte das zu einem Fehler beim Exportieren der Überweisungsdaten. (da9df5cbeb)
* Falls sich Daten in der Datenbank im Hintergrund geändert haben wurden Daten für Eingebefelder vom Typ _Mehrfachauswahl mit Dropdown_ (`CheckComboBox`) falsch neu geladen. (050e4f5b6f)
### Sonstiges {#v0.8.5-misc}
* Kleine Änderungen im EBICS-/XML-Export. (5cb7d2cbb0, cc0aa6046f, 87467bbe75)
[v0.8.5]: https://git.necronda.net/winzer/elwig/releases/tag/v0.8.5
[i46]: https://git.necronda.net/winzer/elwig/issues/46
[v0.8.4][v0.8.4] (2024-06-13) {#v0.8.4}
---------------------------------------
### Neue Funktionen {#v0.8.4-features}
* EBICS-Überweisung-Version für EBICS-/XML-Exporte der Überweisungsdaten nun unter `Stammdaten > Parameter > Daten-Export` konfigurierbar. (ab926421b0)
### Behobene Fehler {#v0.8.4-bugfixes}
* Falls beim Schließen des Haupt-Fensters (`MainWindow`) ein anderes Fenster im Bearbeiten-/Erstellen-Modus war führte das zu einem Absturz. (70f8276808)
### Sonstiges {#v0.8.4-misc}
* Eingabetyp _Mehrfachauswahl mit Dropdown_ (`CheckComboBox`) überarbeitet. ([#37][i37], 4483eb6a69)
* Abhängigkeit `Extended.Wpf.Toolkit` endgültig entfernt. ([#37][i37], 4483eb6a69)
* Automatisierte Tests für österreich-spezifischen EBICS-Standard hinzugefügt. (46551fb142)
[v0.8.4]: https://git.necronda.net/winzer/elwig/releases/tag/v0.8.4
[v0.8.3][v0.8.3] (2024-06-11) {#v0.8.3}
---------------------------------------
### Neue Funktionen {#v0.8.3-features}
* Strafe für Unterlieferung lt. GA kann abhängig von GAs angegeben werden. ([#47][i47])
* Es ist nun möglich Mitglieder zu löschen. (c12d111c57, 5a4ff26f31)
* Beim Übertragen der Flächenbindungen an einen Nachfolger (bzw. beim Inaktiv-Setzen) kann nun die gewünschte Saison angegeben werden. ([#45][i45])
* Es ist nun möglich Saisons anzulegen und zu löschen. ([#44][i44])
### Behobene Fehler {#v0.8.3-bugfixes}
* Beim Auswählen einer Saison mit einer anderen Währung unter `Stammdaten > Saisons` kam es zu einem Absturz. (f756220d75)
* Abstürze im Rundschreiben-Fenster (`MailWindow`) beim Auswählen von einzelnen Mitgliedern oder nach Flächenbindung. (6e4f3b799d, 5039c1252a)
* Es nun wieder möglich Zu-/Abschläge bei Lieferungen hinzuzufügen oder zu entfernen. (324a63cf9a, 012352c562)
### Sonstiges {#v0.8.3-misc}
* Eingabetyp _Ganzzahl_ (`IntegerUpDown`) überarbeitet. ([#37](i37), cc4ec6c5db, a531e948c1)
* Beim Erstellen der Installationsdatei wird der Rückgabewert überprüft (`curl --fail`). (ff375e3caf)
* URL in vordefinierter `config.ini`-Datei auf `https://elwig.at/` ausgebessert. (293c8967be)
* Abhängigkeiten aktualisiert. (4e477c38e0, 601ac548fe)
* Absender bei Anlieferungsbestätigung (`DeliveryConfirmation`) wird nicht mehr immer abgedruckt. (4c9a151f77)
* In der Zusammenfassung der Auszahlungsvariantendaten (`PaymentVariantSummary`) werden Rebel Zu-/Abschläge angeführt. (08f551a394)
[v0.8.3]: https://git.necronda.net/winzer/elwig/releases/tag/v0.8.3
[i37]: https://git.necronda.net/winzer/elwig/issues/37
[i44]: https://git.necronda.net/winzer/elwig/issues/44
[i45]: https://git.necronda.net/winzer/elwig/issues/45
[i47]: https://git.necronda.net/winzer/elwig/issues/47
[v0.8.2][v0.8.2] (2024-05-14) {#v0.8.2}
---------------------------------------
### Neue Funktionen {#v0.8.2-features}
* In der Konfigurationsdatei (`config.ini`) gibt es die Möglichkeit bei Waagen `required = false` hinzuzufügen.
So werden Fehlermeldungen der Waagen beim Starten dem Benutzer nicht angezeigt (außer `debug = true` ist in `[general]` gesetzt). (81e18ac553, e3fd705f52)
### Behobene Fehler {#v0.8.2-bugfixes}
* Falls die Saison für das aktuelle Jahr nicht angelegt war führte das zu einem Absturz im Stammdaten-Fenster (`BaseDataWindow`). (f95f0f0ef3)
* Das Drucken von Dokumenten ist wieder möglich. (2b10e52ab0)
### Sonstiges {#v0.8.2-misc}
* Umstieg von `https://www.necronda.net/elwig/` auf `https://elwig.at/`. (be246d6f06, 5b952c4eb1)
[v0.8.2]: https://git.necronda.net/winzer/elwig/releases/tag/v0.8.2
[v0.8.1][v0.8.1] (2024-05-12) {#v0.8.1}
---------------------------------------
> [!WARNING]
> Aufgrund eines Fehlers beim Erstellen der Installationsdatei ist in dieser Version das Drucken von Dokumenten nicht möglich!
>
> Es wird empfohlen die nächste Version zu verwenden.
### Neue Funktionen {#v0.8.1-features}
* Übersichtsdokument für Auszahlungsvarianten (`PaymentVariantSummary`). ([#32][i32], 69aa75a50a)
### Behobene Fehle {#v0.8.1-bugfixes}
* Falls in einer neuen Version die Datenbank aktualisiert werden musste, konnte es vorkommen, dass es beim Start zu einem Absturz kam. (6906584ef0)
* Beim Exportieren der Überweisungsdaten kam es zu einem Absturz. (f123bb44c5)
### Sonstiges {#v0.8.1-misc}
* Das Installationsprogramm ist nun auf deutsch verfügbar. (d102a1cb7a)
* Menüleiste und Statusleiste im Fenster für Auszahlungsvarianten (`PaymentVaiantWindow`) hinzugefügt. (b03f81d4f2)
[v0.8.1]: https://git.necronda.net/winzer/elwig/releases/tag/v0.8.1
[i32]: https://git.necronda.net/winzer/elwig/issues/32
[v0.8.0][v0.8.0] (2024-05-01) {#v0.8.0}
---------------------------------------
### Neue Funktionen {#v0.8.0-features}
* Die Qualitätsstatistik (`WineQualityStatistic`) kann in °KMW angezeigt werden. (869f652afc, 27b5d653e6)
* Waagen-Schnittstelle vom Typ `Gassner` implementiert. (443e111594)
* Teile zum Synchronisierung der Datenbank zwischen Zweigstellen implementiert. ([#3][i3])
* Sorten-/Qualitätsaufschlüsselung im Hauptfenster. (2a4e8d69d0)
### Behobene Fehler {#v0.8.0-bugfixes}
* Beim Anzeigen von Zu-/Abschlägen einer Saison im Stammdaten-Fenster (`BaseDataWindow`) kam es zu einem Absturz. (1419c834ac)
* Das Laden von einigen Daten im Stammdaten-Fenster (`BaseDataWindow`) war nicht immer konsistent. (1047bc6e8f)
* Die Einstellungen in `Stammdaten -> Parameter` haben nicht konsistent funktioniert. (9062d55b20)
### Sonstiges {#v0.8.0-misc}
* Rahmenstärke in Dokumenten wider auf 0.5pt gesetzt. (eddea88e77)
* Mehr automatisierte Tests für Dokumente hinzugefügt. (66898714bb, b8851fb241, 5c3cf41d3d, 657910ff48, 12eb53cb44, 80e91ad776)
* `SeasonFinishWindow` und `TestWindow` entfernt. (a9f38a3ccb)
* Abhängigkeiten aktualisiert. (c6905bbb42, 21fe5bc094, fd17d294b9, 35e5a1dfff)
* Beim Überprüfen auf Updates bekommt der Nutzer nun auch eine Rückmeldung, wenn es keine Updates gibt. (c360e6b6a7)
[v0.8.0]: https://git.necronda.net/winzer/elwig/releases/tag/v0.8.0
[i3]: https://git.necronda.net/winzer/elwig/issues/3
[v0.7.2][v0.7.2] (2024-03-28) {#v0.7.2}
---------------------------------------
### Neue Funktionen {#v0.7.2-features}
* Flächenbindungen können vom Vorgänger übernommen bzw. beim Inaktiv-Setzen storniert werden. ([#41][i41])
* Für diverse Funktionen wurden Tastenkürzel hinzugefügt (in den Menüleisten oder beim Drüberfahren mit der Maus genauer ersichtlich; z.B. Speichern/Neu/Löschen/Zurücksetzen/Abbrechen/Bearbeiten). ([#31][i31])
* Die Mitgliederliste ist nun auch als PDF/Ausdruck und Excel-Liste verfügbar. ([#36][i36])
* Im Lieferungen-Fenster (`DeliveryAdminWindow`):
* kann nach mehr Dingen gefiltert werden (Handwiegung, Handlese, gebunden/ungebunden, brutto/netto). ([#12][i12], cf2ec3bdc4)
* werden verwendete Zu-/Abschläge zusätzlich in der Tabelle angezeigt. (b31b5f6164)
* können gefilterte Lieferungen nun auch als Excel-Liste exportiert werden. ([#13][i13])
* gibt es ab jetzt ein Übersichtsdokument/Qualitätsstatistik für gefilterte Lieferungen (`WineQualityStatistic`). ([#30][i30], d011c69812, d501cfaf72)
* Einige Optionen zum PDF-Speichern/-Ausdrucken überarbeitet. (b2f52072f8)
### Behobene Fehler {#v0.7.2-bugfixes}
* Falls beim Starten des Programms die Datenbank-Version "zu neu" war, wurde nur ein Fehler angezeigt und kein Auto-Update versucht. (87da56b7a9)
* Falls ein Element einer Liste ausgewählt wurde, das nicht in der Liste existierte führte dies zu einem Absturz. (afc143e1e4)
### Sonstiges {#v0.7.2-misc}
* Das Gesamte Programm ist nun schneller und wird nach längerer Benutzung nicht mehr langsamer (`AppDbContext` Lifecycle). ([#43][i43])
* Excel-Exports überarbeitet (Datumswerte, Wahr-/Falsch-Werte). (9d80c5913f, c6e83ffff4, 5795c5e8ba)
* Rahmenstärke in Dokumenten wider auf 0.05pt gesetzt. (9aa6cba1ff)
[v0.7.2]: https://git.necronda.net/winzer/elwig/releases/tag/v0.7.2
[i12]: https://git.necronda.net/winzer/elwig/issues/12
[i13]: https://git.necronda.net/winzer/elwig/issues/13
[i30]: https://git.necronda.net/winzer/elwig/issues/30
[i31]: https://git.necronda.net/winzer/elwig/issues/31
[i36]: https://git.necronda.net/winzer/elwig/issues/36
[i41]: https://git.necronda.net/winzer/elwig/issues/41
[i43]: https://git.necronda.net/winzer/elwig/issues/43
[v0.7.1][v0.7.1] (2024-03-11) {#v0.7.1}
---------------------------------------
### Neue Funktionen {#v0.7.1-features}
* Für Auszahlungsvarianten kann ein Rebel-Zu-/Abschlag festgelegt werden. ([#40][i40])
* Im Rundschreiben-Fenster (`MailWindow`) gibt es ab jetzt die Möglichkeit den Ort anzupassen. (34ebc8fa34, a5df03aa2c)
### Behobene Fehler {#v0.7.1-bugfixes}
* Falls im Rundschreiben-Fenster (`MailWindow`) der Ausdruck nach PLZ und Ort sortiert werden sollte, wurden Rechnungsadressen nicht beachtet. (746d0f10de)
### Sonstiges {#v0.7.1-misc}
* Im Mitglieder-Fenster (`MemberAdminWindow`) wurde die Möglichkeit zum Verfassen von Rundschreiben entfernt. (61c8d1ee97)
* Das Erzeugen von PDF-Dokumenten wurde verbessert. (c70772b47d, 6a5507060a, c0f4a484ab)
[v0.7.1]: https://git.necronda.net/winzer/elwig/releases/tag/v0.7.1
[i40]: https://git.necronda.net/winzer/elwig/issues/40
[v0.7.0][v0.7.0] (2024-03-06) {#v0.7.0}
---------------------------------------
> [!NOTE]
> Das Attribut "Bio" wird ab dieser Version automatisch entfernt und als Bewirtschaftungsart "Bio" verwendet.
> Sämtliche Lieferungen und Auszahlungsvarianten werden automatisch aktualisiert.
### Neue Funktionen {#v0.7.0-features}
* Das Attribut "Bio" wird nun als Bewirtschaftungsart "Bio" verwendet. ([#34][i34], 25a0722f96, 8031654e86, 7181d744fc, cc72a8365e)
* Im Übernahme-Fenster (`DeliveryAdminWindow`) wurde eine Abklingzeit von 1 Sekunde zum Wiegen-Knopf hinzugefügt. (efe91192bc)
* Im Übernahme-/Lieferungen-Fenster (`DeliveryAdminWindow`) und im Mitglieder-Fenster (`MemberAdminWindow`) wurde ein Knopf hinzugefügt,
mit dem man zum entsprechenden Mitglied (bzw. Vorgänger) springen kann. (665e16d78f)
* Ein Rundschreiben-Fenster (`MailWindow`) wurde hinzugefügt. ([#15][i15], cc5396711d, 060acc56c3, a275385b5c, 37e10136f4, 376af72700)
* Eine automatische Update Funktion wurde hinzugefügt. Diese überprüft regelmäßig, ob eine neuere Version von Elwig verfügbar ist. ([#8][i8], a5a6915db1, 271e085fdf)
### Behobene Fehler {#v0.7.0-bugfixes}
* Falls in einer Auszahlungsvariante ein unbekanntes Attribut verwendet wurde, kam es zu einem Absturz. ([#39][i39])
* Falls für ein Mitglied kein IBAN hinterlegt war, kam es beim Exportieren der Traubengutschriften zu einem Absturz. (958fbaae50)
* Waagen-Schnittstelle für Badner Waage war Fehlerhaft. (092c5788a4)
* Falls Flächenbindungen von Mitgliedern keine Bewirtschaftungsart gesetzt haben sollten, kam es beim Erstellen des Stammdatenblattes (`MemberDataSheet`) zum Absturz. (3324a9a238)
* Telefonnummern und E-Mail-Adressen können wieder beliebig hinzugefügt und gelöscht werden. (e9f6f22bc8, 3a0f2e9556)
### Sonstiges {#v0.7.0-misc}
* Beim Schließen des Hauptfensters wird der Nutzer gefragt, ob er alle anderen Fenster auch schließen möchte. (96c9890b90)
* Im Lieferungen-Fenster (`DeliveryAdminWindow`) und auf Lieferscheinen (`DeliveryNote`) wird neben _(nicht) gerebelt_ nun auch _brutto/netto_ angegeben. (049927f90c)
* Falls man die Lieferungen eines einzelnen Mitglieds ansieht, werden nun standardmäßig nur die Lieferungen der aktuellen Saison angezeigt. (5a6317fcdb)
* Im Stammdaten-Fenster (`BaseDataWindow`) unter Attributen findet sich nun ein kurzer Erklärtext. (e6cab7993f)
* Die Installationsdatei erkennt nun die Elwig-Version wieder richtig. ([#35][i35])
* Am Mitglieds-Stammdatenblatt (`MemberDataSheet`) werden die Flächenbindungen ab jetzt immer frühestens auf der zweiten Seite abgedruckt. (4673877d36)
* Beim automatischen Aktualisieren der Datenbank werden Fremdschlüssel validiert. (d897e44f3b, 3b94875a7f, 614e0010fd, 92c3ed991b)
* Nachnamen von Mitgliedern in Großschreibung werden ab jetzt mit `ẞ` statt mit `ß` geschrieben. (ccb83911b1)
* Dokumente können nun parallel erstellt werden. ([#19][i19], 9139557cc4, ac4026571e, e9d0eec3bd, 77cf47e154, 5a488369be)
* Die Popup-Fenster sind modernisiert worden. ([#16][i16])
* Grunstücksnummern bei Flächenbindungen werden bei der Eingabe nicht mehr validiert. (ea6621ee57)
* Abhängigkeiten aktualisiert. (d944aabc06)
[v0.7.0]: https://git.necronda.net/winzer/elwig/releases/tag/v0.7.0
[i8]: https://git.necronda.net/winzer/elwig/issues/8
[i15]: https://git.necronda.net/winzer/elwig/issues/15
[i16]: https://git.necronda.net/winzer/elwig/issues/16
[i19]: https://git.necronda.net/winzer/elwig/issues/19
[i34]: https://git.necronda.net/winzer/elwig/issues/34
[i35]: https://git.necronda.net/winzer/elwig/issues/35
[i39]: https://git.necronda.net/winzer/elwig/issues/39
[v0.6.8][v0.6.8] (2024-02-22) {#v0.6.8}
---------------------------------------
### Neue Funktionen {#v0.6.8-features}
* Waagen-Schnittstelle vom Typ `Schember-Async` implementiert. (10b78dfb72, c0ff852f5e, ae7fdef2ea)
[v0.6.8]: https://git.necronda.net/winzer/elwig/releases/tag/v0.6.8
[v0.6.7][v0.6.7] (2024-02-21) {#v0.6.7}
---------------------------------------
### Sonstiges {#v0.6.7-misc}
* WG Weinland und WG Baden nun registierte Mandanten. (583d5b4e3e)
* Im Mitglieds-Stammdatenblatt (`MemberDataSheet`) werden Flächenbindungen nun immer für das momentane Jahr angezeigt (nicht die neueste angelegte Saison). (8732141e6b)
* Umstrukturierung der internen Waagen-Schnittstellen-Implementation. (3f2b5b684c, 7ff069d068, 99ca12b276)
[v0.6.7]: https://git.necronda.net/winzer/elwig/releases/tag/v0.6.7
[v0.6.6][v0.6.6] (2024-02-18) {#v0.6.6}
---------------------------------------
### Sonstiges {#v0.6.6-misc}
* Automatisierte Tests für Waagen-Schnittstellen hinzugefügt. (f13fb3aaf0, f4eb6456be, 7f4cfdc1b5)
* Im Übernahme-Fenster (`DeliveryAdminWindow`) wird die Saison ab jetzt ausschließlich über die Jahreszahl bestimmt. (3c0fea30f5)
[v0.6.6]: https://git.necronda.net/winzer/elwig/releases/tag/v0.6.6
[v0.6.5][v0.6.5] (2024-02-15) {#v0.6.5}
---------------------------------------
### Neue Funktionen {#v0.6.5-features}
* Die Installationsdatei kann ab jetzt automatisch erstellt werden. ([#6][i6], 6d53e35399)
### Behobene Fehler {#v0.6.5-bugfixes}
* Im Mitglieds-Stammdatenblatt (`MemberDataSheet`) fehlte bei Rechnungsadressen der Abstand zwischen PLZ und Ort. (68f1a2c091)
### Sonstiges {#v0.6.5-misc}
* Berechnung der Aufteilung für Bio-Attribute aktualisiert. (0591d91f49, 42eb68d431)
* Kleine Änderungen im EBICS-/XML-Export. (befe6a753b, 825bd6f304)
* Automatisierte Tests für EBICS-/XML-Export hinzugefügt. (6fdd72e28b, c07a6b450c, 4daa6deb26)
* Automatisierte Tests für Dokumente hinzugefügt. (912206f52d, 805f782c83, 1b9064a97c)
* Anpassungen für PLZ und KG-Nr. in der Datenbank. (11be424c38)
* Im Mitglieder-Fenster (`MemberAdminWindow`) dürfen Freitextsuchen nun 2 (statt 3) Zeichen lang sein. (59cd69ddaf)
[v0.6.5]: https://git.necronda.net/winzer/elwig/releases/tag/v0.6.5
[i6]: https://git.necronda.net/winzer/elwig/issues/6

View File

@ -3,11 +3,10 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctrl="clr-namespace:Elwig.Controls" xmlns:ctrl="clr-namespace:Elwig.Controls"
StartupUri="Windows\MainWindow.xaml"
Exit="Application_Exit"> Exit="Application_Exit">
<Application.Resources> <Application.Resources>
<ctrl:BoolToStringConverter x:Key="BoolToStarConverter" FalseValue="" TrueValue="*"/> <ctrl:BoolToStringConverter x:Key="BoolToStarConverter" FalseValue="" TrueValue="*"/>
<ctrl:WidthToPaddingConverter x:Key="WidthToPaddingConverter"/> <ctrl:WidthToMarginConverter x:Key="WidthToMarginConverter"/>
<DataTemplate x:Key="PostalDestTemplate"> <DataTemplate x:Key="PostalDestTemplate">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
@ -29,25 +28,17 @@
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>
<ControlTemplate x:Key="WineVarietyTemplateSimple"> <DataTemplate x:Key="WineVarietyTemplateCollapsed">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"/> <TextBlock Text="{Binding Name}"/>
</StackPanel> </StackPanel>
</ControlTemplate> </DataTemplate>
<ControlTemplate x:Key="WineVarietyTemplateExtended"> <DataTemplate x:Key="WineVarietyTemplateExpanded">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding SortId}" MinWidth="36" Margin="0,0,10,0"/> <TextBlock Text="{Binding SortId}" MinWidth="36" Margin="0,0,10,0"/>
<TextBlock Text="{Binding Name}"/> <TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding CommentFormat}" FontSize="10" VerticalAlignment="Bottom" Margin="0,0,0,2"/> <TextBlock Text="{Binding CommentFormat}" FontSize="10" VerticalAlignment="Bottom" Margin="0,0,0,2"/>
</StackPanel> </StackPanel>
</ControlTemplate>
<DataTemplate x:Key="WineVarietyTemplate">
<Control x:Name="Control" Focusable="False" Template="{StaticResource WineVarietyTemplateExtended}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}">
<Setter TargetName="Control" Property="Template" Value="{StaticResource WineVarietyTemplateSimple}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="ModifierTemplate"> <DataTemplate x:Key="ModifierTemplate">
@ -63,25 +54,17 @@
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>
<ControlTemplate x:Key="WineQualityLevelTemplateSimple"> <DataTemplate x:Key="WineQualityLevelTemplateCollapsed">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"/> <TextBlock Text="{Binding Name}"/>
</StackPanel> </StackPanel>
</ControlTemplate> </DataTemplate>
<ControlTemplate x:Key="WineQualityLevelTemplateExtended"> <DataTemplate x:Key="WineQualityLevelTemplateExpanded">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding IsPredicate, Converter={StaticResource BoolToStarConverter}}" MinWidth="6"/> <TextBlock Text="{Binding IsPredicate, Converter={StaticResource BoolToStarConverter}}" MinWidth="6"/>
<TextBlock Text="{Binding Name}" MinWidth="100" Margin="0,0,10,0"/> <TextBlock Text="{Binding Name}" MinWidth="100" Margin="0,0,10,0"/>
<TextBlock Text="{Binding MinKmwStr}"/> <TextBlock Text="{Binding MinKmwStr}"/>
</StackPanel> </StackPanel>
</ControlTemplate>
<DataTemplate x:Key="WineQualityLevelTemplate">
<Control x:Name="Control" Focusable="False" Template="{StaticResource WineQualityLevelTemplateExtended}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}">
<Setter TargetName="Control" Property="Template" Value="{StaticResource WineQualityLevelTemplateSimple}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="WineOriginTemplate"> <DataTemplate x:Key="WineOriginTemplate">
@ -90,24 +73,16 @@
<TextBlock Text="{Binding Name}"/> <TextBlock Text="{Binding Name}"/>
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>
<ControlTemplate x:Key="WineOriginTemplateSimple"> <DataTemplate x:Key="WineOriginTemplateCollapsed">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"/> <TextBlock Text="{Binding Name}"/>
</StackPanel> </StackPanel>
</ControlTemplate> </DataTemplate>
<ControlTemplate x:Key="WineOriginTemplateExtended"> <DataTemplate x:Key="WineOriginTemplateExpanded">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding HkIdLevel}" MinWidth="70" Margin="0,0,10,0"/> <TextBlock Text="{Binding HkIdLevel}" MinWidth="70" Margin="0,0,10,0"/>
<TextBlock Text="{Binding Name}"/> <TextBlock Text="{Binding Name}"/>
</StackPanel> </StackPanel>
</ControlTemplate>
<DataTemplate x:Key="WineOriginComboTemplate">
<Control x:Name="Control" Focusable="False" Template="{StaticResource WineOriginTemplateExtended}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}">
<Setter TargetName="Control" Property="Template" Value="{StaticResource WineOriginTemplateSimple}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="GemTemplate"> <DataTemplate x:Key="GemTemplate">

View File

@ -7,9 +7,6 @@ using Elwig.Helpers;
using Elwig.Helpers.Weighing; using Elwig.Helpers.Weighing;
using System.Collections.Generic; using System.Collections.Generic;
using System.Windows.Threading; using System.Windows.Threading;
using System.Globalization;
using System.Threading;
using System.Windows.Markup;
using System.Reflection; using System.Reflection;
using Elwig.Helpers.Printing; using Elwig.Helpers.Printing;
using Elwig.Windows; using Elwig.Windows;
@ -17,6 +14,7 @@ using Elwig.Dialogs;
using System.Threading.Tasks; using System.Threading.Tasks;
using Elwig.Helpers.Billing; using Elwig.Helpers.Billing;
using Elwig.Models.Entities; using Elwig.Models.Entities;
using System.Text;
namespace Elwig { namespace Elwig {
public partial class App : Application { public partial class App : Application {
@ -28,22 +26,13 @@ namespace Elwig {
private readonly DispatcherTimer _autoUpdateTimer = new() { Interval = TimeSpan.FromHours(1) }; private readonly DispatcherTimer _autoUpdateTimer = new() { Interval = TimeSpan.FromHours(1) };
public static readonly string DataPath = @"C:\ProgramData\Elwig\"; public static readonly string DataPath = @"C:\ProgramData\Elwig\";
public static readonly string MailsPath = Path.Combine(DataPath, "mails");
public static readonly string ConfigPath = Path.Combine(DataPath, "config.ini");
public static readonly string ExePath = @"C:\Program Files\Elwig\"; public static readonly string ExePath = @"C:\Program Files\Elwig\";
public static readonly string TempPath = Path.Combine(Path.GetTempPath(), "Elwig"); public static readonly string TempPath = Path.Combine(Path.GetTempPath(), "Elwig");
public static readonly Config Config = new(DataPath + "config.ini");
public static int VersionMajor { get; private set; } public static Config Config { get; private set; } = new(ConfigPath);
public static int VersionMinor { get; private set; } public static Version Version { get; private set; } = new();
public static int VersionPatch { get; private set; }
public static string Version {
get => $"{VersionMajor}.{VersionMinor}.{VersionPatch}";
private set {
var p = value.Split(".").Select(p => int.Parse(p.Trim())).ToArray();
VersionMajor = p.ElementAtOrDefault(0);
VersionMinor = p.ElementAtOrDefault(1);
VersionPatch = p.ElementAtOrDefault(2);
}
}
public static int BranchNum { get; private set; } public static int BranchNum { get; private set; }
public static string ZwstId { get; private set; } public static string ZwstId { get; private set; }
@ -60,45 +49,55 @@ namespace Elwig {
public static ClientParameters Client { get; set; } public static ClientParameters Client { get; set; }
public static Dispatcher MainDispatcher { get; private set; } public static Dispatcher MainDispatcher { get; private set; }
private DateTime LastChanged;
private static DateTime CurrentLastWrite => File.GetLastWriteTime(Config.DatabaseFile);
private readonly DispatcherTimer ContextTimer = new() { Interval = TimeSpan.FromSeconds(2) };
public App() : base() { public App() : base() {
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Directory.CreateDirectory(TempPath); Directory.CreateDirectory(TempPath);
Directory.CreateDirectory(DataPath); Directory.CreateDirectory(DataPath);
Directory.CreateDirectory(MailsPath);
MainDispatcher = Dispatcher; MainDispatcher = Dispatcher;
Scales = []; Scales = [];
CurrentApp = this; CurrentApp = this;
OverrideCulture(); Utils.OverrideCulture();
var args = Environment.GetCommandLineArgs();
if (args.Length >= 2) {
Config = new(Path.GetFullPath(args[1]));
}
ContextTimer.Tick += (object? sender, EventArgs evt) => {
var ch = CurrentLastWrite;
if (ch > LastChanged) {
LastChanged = ch;
OnContextChanged();
}
};
} }
private static void OverrideCulture() { private static void OnContextChanged() {
var locale = new CultureInfo("de-AT", false); MainDispatcher.BeginInvoke(HintContextChange);
locale.NumberFormat.CurrencyGroupSeparator = Utils.GroupSeparator;
locale.NumberFormat.NumberGroupSeparator = Utils.GroupSeparator;
locale.NumberFormat.PercentGroupSeparator = Utils.GroupSeparator;
CultureInfo.CurrentCulture = locale;
CultureInfo.CurrentUICulture = locale;
Thread.CurrentThread.CurrentCulture = locale;
Thread.CurrentThread.CurrentUICulture = locale;
CultureInfo.DefaultThreadCurrentCulture = locale;
CultureInfo.DefaultThreadCurrentUICulture = locale;
FrameworkElement.LanguageProperty.OverrideMetadata(
typeof(FrameworkElement),
new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.Name))
);
} }
protected override async void OnStartup(StartupEventArgs evt) { protected override async void OnStartup(StartupEventArgs evt) {
Version = typeof(App).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion.Split('+')[0] ?? "0.0.0"; Version = new Version(typeof(App).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion.Split('+')[0] ?? "0.0.0");
try { try {
await AppDbUpdater.CheckDb(); await AppDbUpdater.CheckDb();
} catch (Exception e) { } catch (Exception e) {
if (Config.UpdateUrl != null && Utils.HasInternetConnectivity()) {
await CheckForUpdates();
}
MessageBox.Show($"Invalid Database:\n\n{e.Message}", "Invalid Database", MessageBoxButton.OK, MessageBoxImage.Error); MessageBox.Show($"Invalid Database:\n\n{e.Message}", "Invalid Database", MessageBoxButton.OK, MessageBoxImage.Error);
Shutdown(); Shutdown();
return; return;
} }
LastChanged = CurrentLastWrite;
ContextTimer.Start();
Dictionary<string, (string, string, int?, string?, string?, string?, string?, string?)> branches = []; Dictionary<string, (string, string, int?, string?, string?, string?, string?, string?)> branches = [];
using (var ctx = new AppDbContext()) { using (var ctx = new AppDbContext()) {
branches = ctx.Branches.ToDictionary(b => b.Name.ToLower(), b => (b.ZwstId, b.Name, b.PostalDest?.AtPlz?.Plz, b.PostalDest?.AtPlz?.Ort.Name, b.Address, b.PhoneNr, b.FaxNr, b.MobileNr)); branches = ctx.Branches.ToDictionary(b => b.Name.ToLower(), b => (b.ZwstId, b.Name, b.PostalDest?.AtPlz?.Plz, b.PostalDest?.AtPlz?.Ort.Name, b.Address, b.PhoneNr, b.FaxNr, b.MobileNr));
@ -138,7 +137,9 @@ namespace Elwig {
list.Add(Scale.FromConfig(s)); list.Add(Scale.FromConfig(s));
} catch (Exception e) { } catch (Exception e) {
list.Add(new InvalidScale(s.Id)); list.Add(new InvalidScale(s.Id));
MessageBox.Show($"Unable to create scale {s.Id}:\n\n{e.Message}", "Scale Error", MessageBoxButton.OK, MessageBoxImage.Error); if (s.Required)
MessageBox.Show($"Unable to create scale {s.Id}:\n\n{e.Message}", "Scale Error",
MessageBoxButton.OK, MessageBoxImage.Error);
} }
} }
Scales = list; Scales = list;
@ -158,9 +159,15 @@ namespace Elwig {
} }
base.OnStartup(evt); base.OnStartup(evt);
var window = new MainWindow();
window.Show();
} }
private async void Application_Exit(object sender, ExitEventArgs evt) { private async void Application_Exit(object sender, ExitEventArgs evt) {
foreach (var s in EventScales) {
s.Dispose();
}
await Pdf.Cleanup(); await Pdf.Cleanup();
} }
@ -172,17 +179,27 @@ namespace Elwig {
ZwstId = entry.Item1; ZwstId = entry.Item1;
BranchName = entry.Item2; BranchName = entry.Item2;
BranchPlz = entry.Item3; BranchPlz = entry.Item3;
BranchLocation = entry.Item4?.Split(" im ")[0].Split(" an ")[0].Split(" bei ")[0]; // FIXME BranchLocation = entry.Item4?
.Split(" in ")[0]
.Split(" im ")[0]
.Split(" an ")[0]
.Split(" am ")[0]
.Split(" bei ")[0]
.Split(" beim ")[0];
BranchAddress = entry.Item5; BranchAddress = entry.Item5;
BranchPhoneNr = entry.Item6; BranchPhoneNr = entry.Item6;
BranchFaxNr = entry.Item7; BranchFaxNr = entry.Item7;
BranchMobileNr = entry.Item8; BranchMobileNr = entry.Item8;
} }
public static async Task HintContextChange() { public static void HintContextChange() {
if (CurrentApp == null) return;
var ch = CurrentLastWrite;
if (ch > CurrentApp.LastChanged)
CurrentApp.LastChanged = ch;
foreach (Window w in CurrentApp.Windows) { foreach (Window w in CurrentApp.Windows) {
if (w is not ContextWindow c) continue; if (w is not ContextWindow c) continue;
await c.HintContextChange(); MainDispatcher.BeginInvoke(c.HintContextChange);
} }
} }
@ -191,14 +208,14 @@ namespace Elwig {
if (w is UpdateDialog) return; if (w is UpdateDialog) return;
} }
if (Utils.HasInternetConnectivity()) { if (Utils.HasInternetConnectivity()) {
Utils.RunBackground("Auto Updater", CheckForUpdates); Utils.RunBackground("Auto Updater", async () => await CheckForUpdates());
} }
} }
public static async Task CheckForUpdates() { public static async Task CheckForUpdates(bool showAlert = false) {
if (Config.UpdateUrl == null) return; if (Config.UpdateUrl == null) return;
var latest = await Utils.GetLatestInstallerUrl(Config.UpdateUrl); var latest = await Utils.GetLatestInstallerUrl(Config.UpdateUrl);
if (latest != null && new Version(latest.Value.Version) > new Version(Version)) { if (latest != null && new Version(latest.Value.Version) > Version) {
await MainDispatcher.BeginInvoke(() => { await MainDispatcher.BeginInvoke(() => {
var d = new UpdateDialog(latest.Value.Version, latest.Value.Url, latest.Value.Size); var d = new UpdateDialog(latest.Value.Version, latest.Value.Url, latest.Value.Size);
if (d.ShowDialog() == true) { if (d.ShowDialog() == true) {
@ -206,6 +223,14 @@ namespace Elwig {
Current.Shutdown(); Current.Shutdown();
} }
}); });
} else if (showAlert) {
if (latest == null) {
MessageBox.Show("Informationen konnten nicht abgerufen werden!", "Nach Updates suchen",
MessageBoxButton.OK, MessageBoxImage.Error);
} else {
MessageBox.Show($"Elwig ist auf dem aktuellsten Stand! (Version: {latest.Value.Version})", "Nach Updates suchen",
MessageBoxButton.OK, MessageBoxImage.Information);
}
} }
} }
@ -224,15 +249,15 @@ namespace Elwig {
} }
public static DeliveryAdminWindow FocusReceipt() { public static DeliveryAdminWindow FocusReceipt() {
return FocusWindow<DeliveryAdminWindow>(() => new(true), w => w.IsReceipt); return FocusWindow<DeliveryAdminWindow>(() => new(true), w => w.ViewModel.IsReceipt);
} }
public static DeliveryAdminWindow FocusMemberDeliveries(int mgnr) { public static DeliveryAdminWindow FocusMemberDeliveries(int mgnr) {
return FocusWindow<DeliveryAdminWindow>(() => new(mgnr), w => w.MgNr == mgnr); return FocusWindow<DeliveryAdminWindow>(() => new(mgnr), w => w.ViewModel.FilterMember?.MgNr == mgnr);
} }
public static AreaComAdminWindow FocusMemberAreaComs(int mgnr) { public static AreaComAdminWindow FocusMemberAreaComs(int mgnr) {
return FocusWindow<AreaComAdminWindow>(() => new(mgnr), w => w.MgNr == mgnr); return FocusWindow<AreaComAdminWindow>(() => new(mgnr), w => w.ViewModel.FilterMember.MgNr == mgnr);
} }
public static BaseDataWindow FocusBaseData() { public static BaseDataWindow FocusBaseData() {
@ -248,14 +273,10 @@ namespace Elwig {
public static BaseDataWindow FocusBaseDataSeason(int year) { public static BaseDataWindow FocusBaseDataSeason(int year) {
var w = FocusBaseData(); var w = FocusBaseData();
w.Seasons.Focus(); w.Seasons.Focus();
ControlUtils.SelectListBoxItem(w.SeasonList, s => (s as Season)?.Year, year); ControlUtils.SelectItemWithPk(w.SeasonList, year);
return w; return w;
} }
public static SeasonFinishWindow FocusSeasonFinish() {
return FocusWindow<SeasonFinishWindow>(() => new());
}
public static OriginHierarchyWindow FocusOriginHierarchy() { public static OriginHierarchyWindow FocusOriginHierarchy() {
return FocusWindow<OriginHierarchyWindow>(() => new()); return FocusWindow<OriginHierarchyWindow>(() => new());
} }
@ -266,10 +287,22 @@ namespace Elwig {
return w; return w;
} }
public static DeliveryAncmtAdminWindow FocusDeliveryAncmt() {
return FocusWindow<DeliveryAncmtAdminWindow>(() => new());
}
public static DeliveryScheduleAdminWindow FocusDeliverySchedule() {
return FocusWindow<DeliveryScheduleAdminWindow>(() => new());
}
public static PaymentVariantsWindow FocusPaymentVariants(int year) { public static PaymentVariantsWindow FocusPaymentVariants(int year) {
return FocusWindow<PaymentVariantsWindow>(() => new(year), w => w.Year == year); return FocusWindow<PaymentVariantsWindow>(() => new(year), w => w.Year == year);
} }
public static PaymentAdjustmentWindow FocusPaymentAdjustment(int year) {
return FocusWindow<PaymentAdjustmentWindow>(() => new(year), w => w.Year == year);
}
public static ChartWindow FocusChartWindow(int year, int avnr) { public static ChartWindow FocusChartWindow(int year, int avnr) {
return FocusWindow<ChartWindow>(() => new(year, avnr), w => w.Year == year && w.AvNr == avnr); return FocusWindow<ChartWindow>(() => new(year, avnr), w => w.Year == year && w.AvNr == avnr);
} }

View File

@ -0,0 +1,167 @@
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace Elwig.Controls {
public class CheckComboBox : ListBox {
public new static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(nameof(SelectedItems), typeof(IList), typeof(CheckComboBox), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemsChangedCallback));
public new IList SelectedItems {
get => (IList)GetValue(SelectedItemsProperty);
set => SetValue(SelectedItemsProperty, value);
}
public static readonly DependencyProperty DelimiterProperty = DependencyProperty.Register(nameof(Delimiter), typeof(string), typeof(CheckComboBox), new FrameworkPropertyMetadata(", "));
public string Delimiter {
get => (string)GetValue(DelimiterProperty);
set => SetValue(DelimiterProperty, value);
}
public static readonly DependencyProperty ListDisplayMemberPathProperty = DependencyProperty.Register(nameof(ListDisplayMemberPath), typeof(string), typeof(CheckComboBox), new FrameworkPropertyMetadata(null));
public string ListDisplayMemberPath {
get => (string)GetValue(ListDisplayMemberPathProperty);
set => SetValue(ListDisplayMemberPathProperty, value);
}
public static readonly DependencyProperty AllItemsSelectedContentProperty = DependencyProperty.Register(nameof(AllItemsSelectedContent), typeof(string), typeof(CheckComboBox), new FrameworkPropertyMetadata("All"));
public string AllItemsSelectedContent {
get => (string)GetValue(AllItemsSelectedContentProperty);
set => SetValue(AllItemsSelectedContentProperty, value);
}
public static readonly DependencyProperty IsSelectAllActiveProperty = DependencyProperty.Register(nameof(IsSelectAllActive), typeof(bool), typeof(CheckComboBox), new FrameworkPropertyMetadata(false));
public bool IsSelectAllActive {
get => (bool)GetValue(IsSelectAllActiveProperty);
set => SetValue(IsSelectAllActiveProperty, value);
}
public static readonly DependencyProperty SelectAllContentProperty = DependencyProperty.Register(nameof(SelectAllContent), typeof(string), typeof(CheckComboBox), new FrameworkPropertyMetadata("All"));
public string SelectAllContent {
get => (string)GetValue(SelectAllContentProperty);
set => SetValue(SelectAllContentProperty, value);
}
public static readonly DependencyProperty AllItemsSelectedProperty = DependencyProperty.Register(nameof(AllItemsSelected), typeof(bool?), typeof(CheckComboBox), new FrameworkPropertyMetadata(false));
public bool? AllItemsSelected {
get => (bool?)GetValue(AllItemsSelectedProperty);
set => SetValue(AllItemsSelectedProperty, value);
}
public static readonly DependencyProperty IsDropDownOpenProperty = DependencyProperty.Register(nameof(IsDropDownOpen), typeof(bool), typeof(CheckComboBox), new FrameworkPropertyMetadata(false));
public bool IsDropDownOpen {
get => (bool)GetValue(IsDropDownOpenProperty);
set => SetValue(IsDropDownOpenProperty, value);
}
public static readonly DependencyProperty MaxDropDownHeightProperty = DependencyProperty.Register(nameof(MaxDropDownHeight), typeof(double), typeof(CheckComboBox), new FrameworkPropertyMetadata(ComboBox.MaxDropDownHeightProperty.DefaultMetadata.DefaultValue));
public double MaxDropDownHeight {
get => (double)GetValue(MaxDropDownHeightProperty);
set => SetValue(MaxDropDownHeightProperty, value);
}
public new static readonly RoutedEvent SelectionChangedEvent = EventManager.RegisterRoutedEvent(nameof(SelectionChanged), RoutingStrategy.Bubble, typeof(SelectionChangedEventHandler), typeof(CheckComboBox));
public new event SelectionChangedEventHandler SelectionChanged {
add => AddHandler(SelectionChangedEvent, value);
remove => RemoveHandler(SelectionChangedEvent, value);
}
static CheckComboBox() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(CheckComboBox), new FrameworkPropertyMetadata(typeof(CheckComboBox)));
}
private bool _viewHandled;
private bool _modelHandled;
private TextBlock _textBox;
public CheckComboBox() {
SelectionMode = SelectionMode.Multiple;
SelectedItems = new ObservableCollection<object>();
}
public override void OnApplyTemplate() {
_textBox = (GetTemplateChild("TextBox") as TextBlock)!;
var button = GetTemplateChild("Button") as Button;
button!.Click += Button_MouseDown;
var item = GetTemplateChild("SelectAllItem") as ListBoxItem;
item!.PreviewMouseDown += SelectAllItem_MouseDown;
if (SelectedItems is INotifyCollectionChanged collection) {
collection.CollectionChanged += (s, e) => { SelectItems(); };
}
IsEnabledChanged += OnIsEnabledChanged;
base.SelectionChanged += OnSelectionChanged;
base.OnApplyTemplate();
}
private static void OnSelectedItemsChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs e) {
if (sender is CheckComboBox ccb)
ccb.OnSelectedItemsChanged();
}
private void OnSelectedItemsChanged() {
if (SelectedItems is INotifyCollectionChanged collection) {
collection.CollectionChanged += (s, e) => { SelectItems(); };
}
SelectItems();
}
private void Button_MouseDown(object sender, RoutedEventArgs evt) {
IsDropDownOpen = !IsDropDownOpen;
}
private void SelectAllItem_MouseDown(object sender, RoutedEventArgs evt) {
if (AllItemsSelected == false) {
SelectAll();
} else {
UnselectAll();
}
evt.Handled = true;
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs evt) {
SelectItemsReverse();
var dmp = !string.IsNullOrEmpty(ListDisplayMemberPath) ? ListDisplayMemberPath : !string.IsNullOrEmpty(DisplayMemberPath) ? DisplayMemberPath : null;
if (SelectedItems.Count == ItemsSource.Cast<object>().Count() && AllItemsSelectedContent != null) {
_textBox.Text = AllItemsSelectedContent;
AllItemsSelected = true;
} else if (SelectedItems.Count == 0) {
_textBox.Text = "";
AllItemsSelected = false;
} else {
_textBox.Text = string.Join(Delimiter,
dmp == null ? SelectedItems.Cast<object>() :
SelectedItems.Cast<object>()
.Select(i => i.GetType().GetProperty(dmp)?.GetValue(i))
);
AllItemsSelected = null;
}
RaiseEvent(new SelectionChangedEventArgs(SelectionChangedEvent, evt.RemovedItems, evt.AddedItems));
}
private void OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs evt) {
if (!IsEnabled) IsDropDownOpen = false;
}
private void SelectItems() {
if (_viewHandled || _modelHandled)
return;
_viewHandled = true;
base.SelectedItems.Clear();
foreach (var item in SelectedItems)
base.SelectedItems.Add(item);
_viewHandled = false;
}
private void SelectItemsReverse() {
if (_modelHandled || _viewHandled)
return;
_modelHandled = true;
SelectedItems.Clear();
foreach (var item in base.SelectedItems)
SelectedItems.Add(item);
_modelHandled = false;
}
}
}

View File

@ -0,0 +1,118 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctrl="clr-namespace:Elwig.Controls">
<ctrl:VisibilityConverter x:Key="VisibilityConverter"/>
<Style TargetType="ctrl:CheckComboBox" BasedOn="{StaticResource {x:Type ListBox}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ctrl:CheckComboBox">
<Grid Style="{x:Null}">
<Button x:Name="Button" ClickMode="Press" BorderThickness="1"
BorderBrush="{TemplateBinding BorderBrush}"
IsEnabled="{Binding IsEnabled, RelativeSource={RelativeSource TemplatedParent}}">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="White"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}" SnapsToDevicePixels="True"
BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1">
<ContentPresenter HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Button.Style>
<Path x:Name="IconDropdown" Data="M 0,0 L 3,3 L 6,0" Stroke="#FF606060" StrokeThickness="1" Margin="0,0,5,0"
HorizontalAlignment="Right" VerticalAlignment="Center"/>
</Button>
<TextBlock x:Name="TextBox" Style="{x:Null}" Margin="6,0,18,0" IsHitTestVisible="False"
HorizontalAlignment="Stretch" VerticalAlignment="Center"/>
<Popup x:Name="Popup" Placement="Bottom" Focusable="True"
IsOpen="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
PopupAnimation="Slide" AllowsTransparency="True">
<Popup.Style>
<Style TargetType="{x:Type Popup}">
<Setter Property="StaysOpen" Value="False"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, ElementName=Button}" Value="True">
<Setter Property="StaysOpen" Value="True"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsMouseOver, ElementName=Border}" Value="True">
<Setter Property="StaysOpen" Value="True"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Popup.Style>
<Border x:Name="Border" Style="{x:Null}" BorderThickness="1" BorderBrush="Gray" Background="White" SnapsToDevicePixels="True"
MinWidth="{TemplateBinding ActualWidth}"
MaxHeight="{TemplateBinding MaxDropDownHeight}">
<DockPanel>
<ListBoxItem x:Name="SelectAllItem" Padding="2,1,2,1" DockPanel.Dock="Top"
Visibility="{TemplateBinding IsSelectAllActive, Converter={StaticResource VisibilityConverter}}">
<StackPanel Orientation="Horizontal">
<CheckBox VerticalAlignment="Center" Margin="0,0,5,0" IsThreeState="True"
IsChecked="{Binding AllItemsSelected, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"/>
<TextBlock Text="{TemplateBinding SelectAllContent}" VerticalAlignment="Center" Margin="0" SnapsToDevicePixels="True"/>
</StackPanel>
</ListBoxItem>
<ScrollViewer Style="{x:Null}">
<StackPanel Style="{x:Null}" IsItemsHost="True" SnapsToDevicePixels="True"
KeyboardNavigation.DirectionalNavigation="Contained"/>
</ScrollViewer>
</DockPanel>
</Border>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="IconDropdown" Property="Stroke" Value="#FFA0A0A0"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="IconDropdown" Property="Stroke" Value="Black"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Padding="2,1,2,1">
<StackPanel Orientation="Horizontal">
<CheckBox VerticalAlignment="Center" Margin="0,0,5,0"
IsChecked="{Binding IsSelected, Mode=TwoWay, RelativeSource={RelativeSource AncestorType=ListBoxItem}}"/>
<ContentPresenter VerticalAlignment="Center" Margin="0" SnapsToDevicePixels="True"/>
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="BorderThickness" Value="1"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="BorderBrush" Value="#FF70C0E7"/>
<Setter Property="Background" Value="#FFE5F3FB"/>
</Trigger>
</Style.Triggers>
</Style>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="Gray"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="BorderBrush" Value="#FF7EB4EA"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>

View File

@ -0,0 +1,106 @@
using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
namespace Elwig.Controls {
public class IntegerUpDown : TextBox {
public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register(nameof(Minimum), typeof(int?), typeof(IntegerUpDown), new FrameworkPropertyMetadata(null));
public int? Minimum {
get => (int?)GetValue(MinimumProperty);
set => SetValue(MinimumProperty, value);
}
public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register(nameof(Maximum), typeof(int?), typeof(IntegerUpDown), new FrameworkPropertyMetadata(null));
public int? Maximum {
get => (int?)GetValue(MaximumProperty);
set => SetValue(MaximumProperty, value);
}
public int? Value {
get => int.TryParse(Text, out var res) ? res : null;
set => Text = $"{value}";
}
static IntegerUpDown() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(IntegerUpDown), new FrameworkPropertyMetadata(typeof(IntegerUpDown)));
}
public IntegerUpDown() {
TextChanged += IntegerUpDown_TextChanged;
LostFocus += IntegerUpDown_LostFocus;
KeyUp += IntegerUpDown_KeyUp;
}
public override void OnApplyTemplate() {
var incButton = GetTemplateChild("IncrementButton") as RepeatButton;
var decButton = GetTemplateChild("DecrementButton") as RepeatButton;
incButton!.Click += IncrementButton_Click;
decButton!.Click += DecrementButton_Click;
base.OnApplyTemplate();
}
private void IntegerUpDown_TextChanged(object sender, TextChangedEventArgs evt) {
var idx = CaretIndex;
Text = new string(Text.Where(char.IsAsciiDigit).Take(4).ToArray());
CaretIndex = idx;
evt.Handled = !(Value >= Minimum && Value <= Maximum);
if (idx >= 4) {
if (Value < Minimum) {
Value = Minimum;
} else if (Value > Maximum) {
Value = Maximum;
}
CaretIndex = 4;
}
}
private void IntegerUpDown_LostFocus(object sender, RoutedEventArgs evt) {
if (Value < Minimum) {
Value = Minimum;
} else if (Value > Maximum) {
Value = Maximum;
}
}
private void IncrementButton_Click(object sender, RoutedEventArgs evt) {
Value = Math.Min((Value ?? 0) + 1, Maximum ?? int.MaxValue);
}
private void DecrementButton_Click(object sender, RoutedEventArgs evt) {
Value = Math.Max((Value ?? 0) - 1, Minimum ?? int.MinValue);
}
private void IntegerUpDown_KeyUp(object sender, KeyEventArgs evt) {
switch (evt.Key) {
case Key.Up:
case Key.Add:
case Key.OemPlus:
Value = Math.Min((Value ?? 0) + 1, Maximum ?? int.MaxValue);
evt.Handled = true;
CaretIndex = 4;
break;
case Key.Down:
case Key.Subtract:
case Key.OemMinus:
Value = Math.Max((Value ?? 0) - 1, Minimum ?? int.MinValue);
evt.Handled = true;
CaretIndex = 4;
break;
case Key.PageUp:
Value = Math.Min((Value ?? 0) + 10, Maximum ?? int.MaxValue);
evt.Handled = true;
CaretIndex = 4;
break;
case Key.PageDown:
Value = Math.Max((Value ?? 0) - 10, Minimum ?? int.MinValue);
evt.Handled = true;
CaretIndex = 4;
break;
}
}
}
}

View File

@ -0,0 +1,53 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctrl="clr-namespace:Elwig.Controls">
<Style TargetType="ctrl:IntegerUpDown" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ctrl:IntegerUpDown">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="18"/>
</Grid.ColumnDefinitions>
<Border x:Name="Border" BorderThickness="1,1,0,1" Grid.RowSpan="2"
BorderBrush="{Binding Path=BorderBrush, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}">
<ScrollViewer x:Name="PART_ContentHost" VerticalAlignment="Center"/>
</Border>
<RepeatButton x:Name="IncrementButton" Padding="0" Height="Auto" Width="Auto" BorderThickness="1,1,1,1"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="0" Grid.Column="1">
<Path x:Name="IconIncrement" Data="M 0,4 L 4,0 L 8,4 Z" Fill="#FF444444"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</RepeatButton>
<RepeatButton x:Name="DecrementButton" Padding="0" Height="Auto" Width="Auto" BorderThickness="1,0,1,1"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="1" Grid.Column="1">
<Path x:Name="IconDecrement" Data="M 0,0 L 4,4 L 8,0 Z" Fill="#FF444444"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</RepeatButton>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Border" Property="BorderBrush" Value="LightGray"/>
<Setter TargetName="IconIncrement" Property="Fill" Value="#FF888888"/>
<Setter TargetName="IconDecrement" Property="Fill" Value="#FF888888"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="TextAlignment" Value="Right"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="UseLayoutRounding" Value="True"/>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="Gray"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>

View File

@ -0,0 +1,41 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Elwig.Controls {
public class UnitConverter : DependencyObject, IValueConverter {
public static readonly DependencyProperty UnitProperty = DependencyProperty.Register(nameof(Unit), typeof(string), typeof(UnitConverter), new FrameworkPropertyMetadata(null));
public string Unit {
get => (string)GetValue(UnitProperty);
set => SetValue(UnitProperty, value);
}
public static readonly DependencyProperty PrecisionProperty = DependencyProperty.Register(nameof(Precision), typeof(byte), typeof(UnitConverter), new FrameworkPropertyMetadata((byte)0));
public byte Precision {
get => (byte)GetValue(PrecisionProperty);
set => SetValue(PrecisionProperty, value);
}
public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture) {
if (value == null) {
return null;
}
var fmt = $"{{0:N{Precision}}}";
var unit = $"{(Unit != null ? " " : "")}{Unit}";
if (value is int i) {
return $"{string.Format(fmt, i)}{unit}";
} else if (value is decimal d) {
return $"{string.Format(fmt, d)}{unit}";
}
return Binding.DoNothing;
}
public object? ConvertBack(object? value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,17 @@
using System.Windows;
using System.Windows.Controls;
namespace Elwig.Controls {
public class UnitTextBox : TextBox {
public static readonly DependencyProperty UnitProperty = DependencyProperty.Register(nameof(Unit), typeof(string), typeof(UnitTextBox), new FrameworkPropertyMetadata(""));
public string Unit {
get => (string)GetValue(UnitProperty);
set => SetValue(UnitProperty, value);
}
static UnitTextBox() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(UnitTextBox), new FrameworkPropertyMetadata(typeof(UnitTextBox)));
}
}
}

View File

@ -1,11 +1,42 @@
<UserControl x:Class="Elwig.Controls.UnitTextBox" <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:ctrl="clr-namespace:Elwig.Controls">
<Grid VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Height="25"> <Style TargetType="ctrl:UnitTextBox" BasedOn="{StaticResource {x:Type TextBox}}">
<TextBox x:Name="TextBox" TextAlignment="Right" FontSize="14" VerticalAlignment="Stretch" <Setter Property="Template">
Padding="{Binding ElementName=UnitBlock, Path=ActualWidth, Converter={StaticResource WidthToPaddingConverter}}" <Setter.Value>
Text="{Binding Path=Text}" TextChanged="TextBox_TextChanged"/> <ControlTemplate TargetType="ctrl:UnitTextBox">
<TextBlock x:Name="UnitBlock" Text="{Binding Path=Unit}" Margin="0,0,4,4" FontSize="10" <Border x:Name="Border"
HorizontalAlignment="Right" VerticalAlignment="Bottom"/> BorderThickness="{Binding Path=BorderThickness, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
</Grid> BorderBrush="{Binding Path=BorderBrush, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}">
</UserControl> <Grid Background="{Binding Path=Background, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}">
<ScrollViewer x:Name="PART_ContentHost" VerticalAlignment="Bottom">
<ScrollViewer.Margin>
<Binding ElementName="UnitBlock" Path="ActualWidth">
<Binding.Converter>
<ctrl:WidthToMarginConverter/>
</Binding.Converter>
</Binding>
</ScrollViewer.Margin>
</ScrollViewer>
<TextBlock x:Name="UnitBlock" Text="{Binding Path=Unit, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
FontSize="10" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="3"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Border" Property="BorderBrush" Value="LightGray"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="TextAlignment" Value="Right"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="UseLayoutRounding" Value="True"/>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="Gray"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>

View File

@ -1,37 +0,0 @@
using System.Windows;
using System.Windows.Controls;
namespace Elwig.Controls {
public partial class UnitTextBox : UserControl {
public event TextChangedEventHandler? TextChanged;
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(UnitTextBox));
public string Text {
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value ?? "");
}
public static readonly DependencyProperty UnitProperty = DependencyProperty.Register("Unit", typeof(string), typeof(UnitTextBox));
public string Unit {
get => (string)GetValue(UnitProperty);
set => SetValue(UnitProperty, value ?? "");
}
public UnitTextBox() {
Text = "";
Unit = "";
InitializeComponent();
DataContext = this;
}
private void TextBox_TextChanged(object sender, TextChangedEventArgs evt) {
Text = TextBox.Text;
TextChanged?.Invoke(sender, evt);
}
public new void Focus() {
TextBox.Focus();
}
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Windows;
using System.Windows.Data;
using System.Globalization;
namespace Elwig.Controls {
public class VisibilityConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return (bool)value ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return (Visibility)value == Visibility.Visible;
}
}
}

View File

@ -1,16 +1,16 @@
using System; using System;
using System.Windows; using System.Windows;
using System.Windows.Data; using System.Windows.Data;
using System.Globalization; using System.Globalization;
namespace Elwig.Controls { namespace Elwig.Controls {
public class WidthToPaddingConverter : IValueConverter { public class WidthToMarginConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return new Thickness(2, 2, 4 + (double)value, 2); return new Thickness(0, 0, 2 + (double)value, 0);
} }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return ((Thickness)value).Right - 4; return ((Thickness)value).Right - 2;
} }
} }
} }

View File

@ -0,0 +1,15 @@
using System.Windows.Controls;
using System.Windows;
namespace Elwig.Controls {
public class WineOriginTemplateSelector : DataTemplateSelector {
public override DataTemplate SelectTemplate(object item, DependencyObject container) {
ContentPresenter presenter = (ContentPresenter)container;
if (presenter.TemplatedParent is ComboBox) {
return (DataTemplate)presenter.FindResource("WineOriginTemplateCollapsed");
} else {
return (DataTemplate)presenter.FindResource("WineOriginTemplateExpanded");
}
}
}
}

View File

@ -0,0 +1,15 @@
using System.Windows.Controls;
using System.Windows;
namespace Elwig.Controls {
public class WineQualityLevelTemplateSelector : DataTemplateSelector {
public override DataTemplate SelectTemplate(object item, DependencyObject container) {
ContentPresenter presenter = (ContentPresenter)container;
if (presenter.TemplatedParent is ComboBox) {
return (DataTemplate)presenter.FindResource("WineQualityLevelTemplateCollapsed");
} else {
return (DataTemplate)presenter.FindResource("WineQualityLevelTemplateExpanded");
}
}
}
}

View File

@ -0,0 +1,15 @@
using System.Windows.Controls;
using System.Windows;
namespace Elwig.Controls {
public class WineVarietyTemplateSelector : DataTemplateSelector {
public override DataTemplate SelectTemplate(object item, DependencyObject container) {
ContentPresenter presenter = (ContentPresenter)container;
if (presenter.TemplatedParent is ComboBox) {
return (DataTemplate)presenter.FindResource("WineVarietyTemplateCollapsed");
} else {
return (DataTemplate)presenter.FindResource("WineVarietyTemplateExpanded");
}
}
}
}

View File

@ -1,59 +0,0 @@
<Window x:Class="Elwig.Dialogs.AbwertenDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Dialogs"
mc:Ignorable="d"
ResizeMode="NoResize"
ShowInTaskbar="False"
Topmost="True"
WindowStartupLocation="CenterOwner"
FocusManager.FocusedElement="{Binding ElementName=WeightInput}"
Title="Teillieferung abwerten" Height="190" Width="400">
<Window.Resources>
<Style TargetType="Label">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="Padding" Value="2,4,2,4"/>
<Setter Property="Height" Value="25"/>
</Style>
<Style TargetType="TextBox">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="2"/>
<Setter Property="Height" Value="25"/>
<Setter Property="TextWrapping" Value="NoWrap"/>
</Style>
<Style TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="25"/>
</Style>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="70"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Margin="10,10,10,10" Grid.ColumnSpan="2" TextWrapping="Wrap" TextAlignment="Center">
Welche Menge der Teillieferung <Run x:Name="TextLsNr" FontWeight="Bold" Text="20201010A000/1"/><LineBreak/>
von <Run x:Name="TextMember" FontWeight="Bold" Text="Max Mustermann"/><LineBreak/>
mit <Run x:Name="TextWeight" FontWeight="Bold" Text="1&#x202f;000&#x202f;kg"/> soll abgewertet werden?
</TextBlock>
<Label Content="Gewicht:" Margin="10,70,10,10"/>
<Grid Grid.Column="1" Width="70" Height="25" Margin="0,70,10,10" HorizontalAlignment="Left" VerticalAlignment="Top">
<TextBox x:Name="WeightInput" TextAlignment="Right" Padding="2,2,17,2"
TextChanged="WeightInput_TextChanged"/>
<Label Content="kg" Margin="0,4,3,0" HorizontalAlignment="Right" FontSize="10" Padding="2,4,2,4"/>
</Grid>
<Button x:Name="ConfirmButton" Content="Bestätigen" Margin="10,10,115,10" Grid.Column="1" IsEnabled="False" IsDefault="True"
Click="ConfirmButton_Click"/>
<Button x:Name="CancelButton" Content="Abbrechen" Margin="10,10,10,10" Grid.Column="1" IsCancel="True"/>
</Grid>
</Window>

View File

@ -1,33 +0,0 @@
using Elwig.Helpers;
using System.Windows;
using System.Windows.Controls;
namespace Elwig.Dialogs {
public partial class AbwertenDialog : Window {
public int Weight;
public AbwertenDialog(string lsnr, string name, int weight) {
Weight = weight;
InitializeComponent();
TextLsNr.Text = lsnr;
TextMember.Text = name;
TextWeight.Text = $"{weight:N0}{Utils.UnitSeparator}kg";
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
DialogResult = true;
Weight = int.Parse(WeightInput.Text);
Close();
}
private void UpdateButtons() {
ConfirmButton.IsEnabled = int.TryParse(WeightInput.Text, out var w) && w > 0 && w <= Weight;
}
private void WeightInput_TextChanged(object sender, TextChangedEventArgs evt) {
Validator.CheckInteger(WeightInput, true, 5);
UpdateButtons();
}
}
}

View File

@ -0,0 +1,78 @@
<Window x:Class="Elwig.Dialogs.AreaComDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Dialogs"
xmlns:ctrl="clr-namespace:Elwig.Controls"
ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
WindowStartupLocation="CenterOwner"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="Flächenbindungen übertragen" Height="260" Width="450">
<Window.Resources>
<Style TargetType="Label">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="Padding" Value="2,4,2,4"/>
<Setter Property="Height" Value="25"/>
</Style>
<Style TargetType="TextBox">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="2"/>
<Setter Property="Height" Value="25"/>
<Setter Property="TextWrapping" Value="NoWrap"/>
</Style>
<Style TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="25"/>
</Style>
</Window.Resources>
<Grid>
<TextBlock x:Name="QuestionBlock1" TextAlignment="Center" Margin="0,10,0,0"
HorizontalAlignment="Center" VerticalAlignment="Top">
Sollen die aktiven Flächenbindungen des angegebenen Vorgängers<LineBreak/>
übernommen werden? (<Run Text="{Binding AreaComNum}"/> FB, <Run Text="{Binding Area}"/> m²)
</TextBlock>
<TextBlock x:Name="QuestionBlock2" TextAlignment="Center" Margin="0,10,0,0" Visibility="Hidden"
HorizontalAlignment="Center" VerticalAlignment="Top">
Sollen die aktiven Flächenbindungen gekündigt werden? (<Run Text="{Binding AreaComNum}"/> FB, <Run Text="{Binding Area}"/> m²)
</TextBlock>
<Label x:Name="SeasonLabel" Content="Saison:" Margin="0,50,100,0"
HorizontalAlignment="Center" VerticalAlignment="Top"/>
<ctrl:IntegerUpDown x:Name="SeasonInput" Width="56" Height="25" Margin="0,50,0,0" FontSize="14"
Minimum="1900" Maximum="9999"
HorizontalAlignment="Center" VerticalAlignment="Top"
TextChanged="SeasonInput_TextChanged"/>
<CheckBox x:Name="CopyYearToInput" Content="Beginn der Laufzeit von Vorgänger übernehmen" Margin="0,80,0,0"
HorizontalAlignment="Center" VerticalAlignment="Top"
Checked="CopyYearToInput_Changed" Unchecked="CopyYearToInput_Changed"
IsChecked="{Binding MaintainYearTo}"/>
<TextBlock x:Name="DescBlock1" Margin="0,105,0,0" TextAlignment="Center"
HorizontalAlignment="Center" VerticalAlignment="Top">
Die Flächenbindungen beim <Bold>Vorgänger</Bold> sind bis inkl. Saison <Bold><Run x:Name="CancelSeason1"/></Bold> gültig,<LineBreak/>
und werden beim <Bold>Nachfolger</Bold> ab <Run x:Name="DescBlock1Season" Text="inkl. Saison "/><Bold><Run x:Name="TransferSeason"/></Bold> übernommen.
</TextBlock>
<TextBlock x:Name="DescBlock2" Margin="0,70,0,0" TextAlignment="Center" Visibility="Hidden"
HorizontalAlignment="Center" VerticalAlignment="Top">
Die Flächenbindungen sind bis inklusive Saison <Bold><Run x:Name="CancelSeason2"/></Bold> gültig.
</TextBlock>
<TextBlock x:Name="InfoBlock" Margin="0,0,0,75" TextAlignment="Center"
HorizontalAlignment="Center" VerticalAlignment="Bottom">
Falls die Flächenbindungen später an ein neues Mitglied<LineBreak/>
übertragen werden sollen bitte <Italic>Nein</Italic> auswählen!
</TextBlock>
<TextBlock Text="Die Änderungen werden erst beim Speichern übernommen!"
HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,50"/>
<Button x:Name="ConfirmButton" Content="Ja" Margin="10,10,115,10" Grid.Column="1"
Click="ConfirmButton_Click"/>
<Button x:Name="CancelButton" Content="Nein" Margin="10,10,10,10" Grid.Column="1" IsCancel="True"/>
</Grid>
</Window>

View File

@ -0,0 +1,60 @@
using Elwig.Helpers;
using System.Windows;
using System.Windows.Controls;
namespace Elwig.Dialogs {
public partial class AreaComDialog : Window {
public int CancelSeason { get; set; }
public int SuccessorSeason => CancelSeason + 1;
public bool MaintainYearTo { get; set; }
public string AreaComNum { get; set; }
public string Area { get; set; }
public AreaComDialog(string name, int areaComNum, int area) {
CancelSeason = Utils.FollowingSeason - 1;
AreaComNum = $"{areaComNum:N0}";
Area = $"{area:N0}";
InitializeComponent();
SeasonInput.Text = $"{CancelSeason}";
Title = $"Aktive Flächenbindungen kündigen - {name}";
QuestionBlock1.Visibility = Visibility.Hidden;
QuestionBlock2.Visibility = Visibility.Visible;
DescBlock1.Visibility = Visibility.Hidden;
DescBlock2.Visibility = Visibility.Visible;
CopyYearToInput.Visibility = Visibility.Hidden;
Height = 240;
SeasonInput.Margin = new(0, 40, 0, 0);
SeasonLabel.Margin = new(0, 40, 100, 0);
}
public AreaComDialog(string name, string successorName, int areaComNum, int area) {
CancelSeason = Utils.FollowingSeason - 1;
AreaComNum = $"{areaComNum:N0}";
Area = $"{area:N0}";
InitializeComponent();
SeasonInput.Text = $"{CancelSeason}";
Title = $"Aktive Flächenbindungen übertragen - {name} - {successorName}";
InfoBlock.Visibility = Visibility.Hidden;
}
private void SeasonInput_TextChanged(object sender, TextChangedEventArgs evt) {
CancelSeason = (int)SeasonInput.Value!;
CancelSeason1.Text = $"{CancelSeason}";
CancelSeason2.Text = $"{CancelSeason}";
TransferSeason.Text = MaintainYearTo ? "" : $"{SuccessorSeason}";
DescBlock1Season.Text = MaintainYearTo ? "dem originalen Beginn der FB" : "inkl. Saison ";
}
private void CopyYearToInput_Changed(object sender, RoutedEventArgs evt) {
TransferSeason.Text = MaintainYearTo ? "" : $"{SuccessorSeason}";
DescBlock1Season.Text = MaintainYearTo ? "dem originalen Beginn der FB" : "inkl. Saison ";
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
DialogResult = true;
Close();
}
}
}

View File

@ -0,0 +1,64 @@
<Window x:Class="Elwig.Dialogs.DeleteMemberDialog"
AutomationProperties.AutomationId="DeleteMemberDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Dialogs"
ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
WindowStartupLocation="CenterOwner"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="Mitglied löschen" Height="280" Width="400">
<Window.Resources>
<Style TargetType="Label">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="Padding" Value="2,4,2,4"/>
<Setter Property="Height" Value="25"/>
</Style>
<Style TargetType="TextBox">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="2"/>
<Setter Property="Height" Value="25"/>
<Setter Property="TextWrapping" Value="NoWrap"/>
</Style>
<Style TargetType="CheckBox">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Top"/>
</Style>
<Style TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="25"/>
</Style>
</Window.Resources>
<Grid>
<TextBlock TextAlignment="Center" Margin="10,10,10,10" VerticalAlignment="Top">
Bei Bestätigung wird das Mitglied samt zugehöriger Daten<LineBreak/>
<Bold>unwiderruflich gelöscht!</Bold> Wenn möglich sollte stattdessen<LineBreak/>
der Status des Mitglieds auf <Italic>Inaktiv</Italic> gesetzt werden!
</TextBlock>
<Label Content="Name u. MgNr. wiederholen:" Margin="10,60,10,10" HorizontalAlignment="Center"/>
<TextBox x:Name="NameInput" Margin="10,85,10,10"
TextChanged="NameInput_TextChanged"/>
<Label Content="Beim Löschen müssen folgende Daten auch gelöscht werden:" Margin="10,120,10,10"/>
<CheckBox x:Name="AreaComInput" Content="Flächenbindungen" Margin="40,145,0,0"
Checked="CheckBox_Changed" Unchecked="CheckBox_Changed"
IsChecked="{Binding DeleteAreaComs}"/>
<CheckBox x:Name="DeliveryInput" Content="Lieferungen" Margin="40,165,0,0"
Checked="CheckBox_Changed" Unchecked="CheckBox_Changed"
IsChecked="{Binding DeleteDeliveries}"/>
<CheckBox x:Name="PaymentInput" Content="Auszahlungsdaten" Margin="40,185,0,0"
Checked="CheckBox_Changed" Unchecked="CheckBox_Changed"
IsChecked="{Binding DeletePaymentData}"/>
<Button x:Name="ConfirmButton" Content="Bestätigen" Margin="10,10,115,10" IsEnabled="False"
Click="ConfirmButton_Click"/>
<Button x:Name="CancelButton" Content="Abbrechen" Margin="10,10,10,10" IsCancel="True"/>
</Grid>
</Window>

View File

@ -0,0 +1,66 @@
using Elwig.Helpers;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace Elwig.Dialogs {
public partial class DeleteMemberDialog : Window {
protected string[] NameParts;
public bool DeleteAreaComs { get; set; }
public bool DeleteDeliveries { get; set; }
public bool DeletePaymentData { get; set; }
public DeleteMemberDialog(int mgnr, string name, int numAreaComs, int numDeliveries, int numCredits) {
NameParts = name.ToLower().Split(' ').Where(p => p.Length > 0).Append($"{mgnr}").ToArray();
InitializeComponent();
Title += " - " + name;
AreaComInput.IsEnabled = numAreaComs != 0;
AreaComInput.Content += $" ({numAreaComs:N0})";
DeliveryInput.IsEnabled = numDeliveries != 0;
DeliveryInput.Content += $" ({numDeliveries:N0})";
PaymentInput.IsEnabled = numCredits != 0;
PaymentInput.Content += $" ({numCredits:N0})";
}
private void NameInput_TextChanged(object sender, TextChangedEventArgs evt) {
Update();
}
private void CheckBox_Changed(object sender, RoutedEventArgs evt) {
Update();
}
private static void UpdateCheckBox(CheckBox cb) {
if (cb.IsEnabled && cb.IsChecked != true) {
ControlUtils.SetInputInvalid(cb);
} else {
ControlUtils.ClearInputState(cb);
}
}
private void Update() {
var t = NameInput.Text.ToLower().Split(' ');
var nameValid = NameParts.All(t.Contains);
UpdateCheckBox(AreaComInput);
UpdateCheckBox(DeliveryInput);
UpdateCheckBox(PaymentInput);
if (!nameValid) {
ControlUtils.SetInputInvalid(NameInput);
} else {
ControlUtils.ClearInputState(NameInput);
}
ConfirmButton.IsEnabled =
(!AreaComInput.IsEnabled || DeleteAreaComs) &&
(!DeliveryInput.IsEnabled || DeleteDeliveries) &&
(!PaymentInput.IsEnabled || DeletePaymentData) &&
nameValid;
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
DialogResult = true;
Close();
}
}
}

View File

@ -1,60 +0,0 @@
<Window x:Class="Elwig.Dialogs.DeliveryExtractionDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Dialogs"
mc:Ignorable="d"
ResizeMode="NoResize"
ShowInTaskbar="False"
Topmost="True"
WindowStartupLocation="CenterOwner"
FocusManager.FocusedElement="{Binding ElementName=WeightInput}"
Title="Teillieferung extrahieren" Height="210" Width="380">
<Window.Resources>
<Style TargetType="Label">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="Padding" Value="2,4,2,4"/>
<Setter Property="Height" Value="25"/>
</Style>
<Style TargetType="TextBox">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="2"/>
<Setter Property="Height" Value="25"/>
<Setter Property="TextWrapping" Value="NoWrap"/>
</Style>
<Style TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="25"/>
</Style>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Margin="10,10,0,10" TextWrapping="Wrap">
Was soll mit der Teillieferung <Run x:Name="TextLsNr" FontWeight="Bold" Text="20201010A000/1"/><LineBreak/>
von <Run x:Name="TextMember" FontWeight="Bold" Text="Max Mustermann"/> geschehen?
</TextBlock>
<RadioButton x:Name="NewDeliveryButton" Content="Neue Lieferung erstellen"
Margin="10,80,0,10" HorizontalAlignment="Left" VerticalAlignment="Top"
Checked="Selection_Changed" Unchecked="Selection_Changed"/>
<RadioButton x:Name="AddToDeliveryButton" Content="Zu Lieferung hinzufügen"
Margin="10,100,0,10" HorizontalAlignment="Left" VerticalAlignment="Top"
Checked="Selection_Changed" Unchecked="Selection_Changed"/>
<ListBox x:Name="DeliveryList" Grid.Column="1" Margin="10,10,10,45"
SelectionChanged="DeliveryList_SelectionChanged"/>
<Button x:Name="ConfirmButton" Content="Bestätigen" Margin="10,10,115,10" Grid.ColumnSpan="2" IsEnabled="False" IsDefault="True"
Click="ConfirmButton_Click"/>
<Button x:Name="CancelButton" Content="Abbrechen" Margin="10,10,10,10" Grid.ColumnSpan="2" IsCancel="True"/>
</Grid>
</Window>

View File

@ -1,37 +0,0 @@
using System.Collections.Generic;
using System.Windows;
namespace Elwig.Dialogs {
public partial class DeliveryExtractionDialog : Window {
public string? AddTo;
public DeliveryExtractionDialog(string lsnr, string name, bool single, IEnumerable<string> lsnrs) {
InitializeComponent();
TextLsNr.Text = lsnr;
TextMember.Text = name;
NewDeliveryButton.IsEnabled = !single;
DeliveryList.IsEnabled = false;
DeliveryList.ItemsSource = lsnrs;
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
DialogResult = true;
AddTo = NewDeliveryButton.IsChecked == true ? "new" : DeliveryList.SelectedItem as string;
Close();
}
private void UpdateButtons() {
ConfirmButton.IsEnabled = NewDeliveryButton.IsChecked == true || (AddToDeliveryButton.IsChecked == true && DeliveryList.SelectedItem != null);
DeliveryList.IsEnabled = AddToDeliveryButton.IsChecked == true;
}
private void Selection_Changed(object sender, RoutedEventArgs evt) {
UpdateButtons();
}
private void DeliveryList_SelectionChanged(object sender, RoutedEventArgs evt) {
UpdateButtons();
}
}
}

View File

@ -0,0 +1,104 @@
<local:ContextWindow
x:Class="Elwig.Dialogs.DeliverySplittingDialog"
AutomationProperties.AutomationId="DeliverySplittingDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Elwig.Windows"
xmlns:ctrl="clr-namespace:Elwig.Controls"
ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
WindowStartupLocation="CenterOwner"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="Lieferung abwerten oder aufteilen" Height="400" Width="600">
<Window.Resources>
<Style TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="25"/>
</Style>
</Window.Resources>
<Grid>
<RadioButton x:Name="DepreciateModeInput" GroupName="ModeInput" Content="Abwerten" Margin="15,10,10,10" IsChecked="True"
VerticalAlignment="Top" HorizontalAlignment="Left"
Checked="ModeInput_Changed"/>
<RadioButton x:Name="MemberModeInput" GroupName="ModeInput" Content="Auf Mitglied übertragen" Margin="15,30,10,10"
VerticalAlignment="Top" HorizontalAlignment="Left"
Checked="ModeInput_Changed"/>
<RadioButton x:Name="DeliveryModeInput" GroupName="ModeInput" Content="Zu anderer Lieferung hinzufügen" Margin="15,50,10,10"
VerticalAlignment="Top" HorizontalAlignment="Left"
Checked="ModeInput_Changed"/>
<TextBox x:Name="MgNrInput" FontSize="14" Padding="2" Visibility="Hidden"
Width="48" Margin="220,10,0,0" Height="25" TextAlignment="Right"
TextChanged="MgNrInput_TextChanged"
VerticalAlignment="Top" HorizontalAlignment="Left"/>
<ComboBox x:Name="MemberInput" FontSize="14" Visibility="Hidden"
Margin="273,10,40,10" IsEditable="True" Height="25"
ItemTemplate="{StaticResource MemberAdminNameTemplate}" TextSearch.TextPath="AdministrativeName"
SelectionChanged="MemberInput_SelectionChanged"
VerticalAlignment="Top" HorizontalAlignment="Stretch"/>
<Button x:Name="MemberReferenceButton" Height="25" Width="25" FontFamily="Segoe MDL2 Assets" Content="&#xEE35;" Padding="0"
Margin="10,10,10,10" VerticalAlignment="Top" HorizontalAlignment="Right" ToolTip="Zu Mitglied springen" FontSize="14" Visibility="Hidden"
Click="MemberReferenceButton_Click"/>
<ComboBox x:Name="DeliveryInput" FontSize="14" Visibility="Hidden"
Margin="220,10,10,10" Height="25"
TextSearch.TextPath="LsNr"
SelectionChanged="DeliveryInput_SelectionChanged"
VerticalAlignment="Top" HorizontalAlignment="Stretch">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding LsNr}" Width="100"/>
<TextBlock Text="{Binding Weight, StringFormat='{}{0:N0} kg'}" Width="80" TextAlignment="Right" Margin="0,0,10,0"/>
<TextBlock Text="{Binding Member.AdministrativeName}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock x:Name="InfoBlock" Margin="230,45,10,10" FontSize="14" TextAlignment="Right"
VerticalAlignment="Top" HorizontalAlignment="Stretch"
Text="Insgesamt 0 kg / 0 kg ausgewählt."/>
<ListBox x:Name="DeliveryPartList" Margin="10,75,10,40" ItemsSource="{Binding DeliveryParts, Mode=TwoWay}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Focusable" Value="False"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Part.DPNr}" Width="20" TextAlignment="Right"
VerticalAlignment="Center" Margin="0,0,5,0" FontSize="14"/>
<TextBlock Text="{Binding Part.SortId}" Width="40" TextAlignment="Center"
VerticalAlignment="Center" Margin="0,0,0,0" FontSize="14"/>
<TextBlock Text="{Binding Part.Kmw, StringFormat='{}{0:N1}°'}" Width="40" TextAlignment="Right" Padding="0,0,10,0"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding Part.QualId}" Width="30"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding Part.Weight, StringFormat='{}{0:N0} kg'}" Width="70" TextAlignment="Right"
VerticalAlignment="Center" Margin="0,0,10,0" FontSize="14"/>
<TextBlock Text="{Binding Part.Attribute.Name}" Width="60"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding Part.Cultivation.Name}" Width="50"
VerticalAlignment="Center"/>
<CheckBox IsChecked="{Binding SplitCompletely, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Content="Vollständig" Tag="{Binding Part.DPNr}"
VerticalAlignment="Center" Margin="20,0,5,0"
Checked="SplitCompletelyInput_Changed" Unchecked="SplitCompletelyInput_Changed"/>
<ctrl:UnitTextBox Text="{Binding SplitWeightString, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Unit="kg" Width="70" Height="25" IsEnabled="{Binding SplitWeightEnabled}" Tag="{Binding Part.DPNr}"
VerticalAlignment="Center" Margin="5,0,5,0" FontSize="14" Padding="2" Background="White"
TextChanged="SplitWeightInput_TextChanged"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button x:Name="ConfirmButton" Content="Bestätigen" Margin="10,10,115,10" IsEnabled="False"
Click="ConfirmButton_Click"/>
<Button x:Name="CancelButton" Content="Abbrechen" Margin="10,10,10,10" IsCancel="True"/>
</Grid>
</local:ContextWindow>

View File

@ -0,0 +1,163 @@
using Elwig.Helpers;
using Elwig.Models.Entities;
using Elwig.Windows;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace Elwig.Dialogs {
public partial class DeliverySplittingDialog : ContextWindow {
public class Row {
public DeliveryPart Part { get; set; }
public bool SplitCompletely { get; set; }
public string? SplitWeightString { get; set; }
public int? SplitWeight {
get => int.TryParse(SplitWeightString, out var v) ? v : null;
set => SplitWeightString = $"{value}";
}
public bool SplitWeightEnabled { get; set; }
}
private readonly Delivery _delivery;
public int? MgNr { get; set; }
public string? LsNr { get; set; }
public int[]? Weights => DeliveryParts.Select(r => r.SplitCompletely ? r.Part.Weight : r.SplitWeight ?? 0).ToArray();
public ObservableCollection<Row> DeliveryParts { get; set; }
public DeliverySplittingDialog(Delivery d) {
_delivery = d;
DeliveryParts = new(d.Parts.Select(p => new Row {
Part = p,
SplitCompletely = false,
SplitWeight = null,
SplitWeightEnabled = true,
}).ToList());
InitializeComponent();
}
protected override async Task OnRenewContext(AppDbContext ctx) {
ControlUtils.RenewItemsSource(MemberInput, await ctx.Members
.Where(m => m.IsActive)
.OrderBy(m => m.Name)
.ThenBy(m => m.GivenName)
.ToListAsync());
ControlUtils.RenewItemsSource(DeliveryInput, await ctx.Deliveries
.Where(d => d.DateString == $"{_delivery.Date:yyyy-MM-dd}" && d.ZwstId == _delivery.ZwstId)
.OrderBy(d => d.LsNr)
.Include(d => d.Member)
.Include(d => d.Parts)
.ToListAsync());
if (DeliveryInput.SelectedItem == null)
ControlUtils.SelectItem(DeliveryInput, _delivery);
CheckValidity();
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
if (DepreciateModeInput.IsChecked == true) {
MgNr = null;
LsNr = null;
} else if (MemberModeInput.IsChecked == true) {
MgNr = ((Member)MemberInput.SelectedItem).MgNr;
LsNr = null;
} else if (DeliveryModeInput.IsChecked == true) {
MgNr = null;
LsNr = ((Delivery)DeliveryInput.SelectedItem).LsNr;
}
DialogResult = true;
Close();
}
private void ModeInput_Changed(object sender, RoutedEventArgs evt) {
if (!IsLoaded) return;
CheckValidity();
if (DepreciateModeInput.IsChecked == true) {
MgNrInput.Visibility = Visibility.Hidden;
MemberInput.Visibility = Visibility.Hidden;
MemberReferenceButton.Visibility = Visibility.Hidden;
DeliveryInput.Visibility = Visibility.Hidden;
} else if (MemberModeInput.IsChecked == true) {
MgNrInput.Visibility = Visibility.Visible;
MemberInput.Visibility = Visibility.Visible;
MemberReferenceButton.Visibility = Visibility.Visible;
DeliveryInput.Visibility = Visibility.Hidden;
} else if (DeliveryModeInput.IsChecked == true) {
MgNrInput.Visibility = Visibility.Hidden;
MemberInput.Visibility = Visibility.Hidden;
MemberReferenceButton.Visibility = Visibility.Hidden;
DeliveryInput.Visibility = Visibility.Visible;
}
}
private void CheckValidity() {
var weight = DeliveryParts.Sum(r => r.SplitCompletely ? r.Part.Weight : r.SplitWeight ?? 0);
var total = DeliveryParts.Sum(r => r.Part.Weight);
InfoBlock.Text = $"Insgesamt {weight:N0} kg / {total:N0} kg ausgewählt.";
ConfirmButton.IsEnabled = DeliveryParts.Any(r => r.SplitCompletely || r.SplitWeight > 0) && (
DepreciateModeInput.IsChecked == true ||
(MemberModeInput.IsChecked == true && MemberInput.SelectedItem != null && !DeliveryParts.All(r => r.SplitCompletely)) ||
(DeliveryModeInput.IsChecked == true && DeliveryInput.SelectedItem != null));
}
private void MgNrInput_TextChanged(object sender, TextChangedEventArgs evt) {
var res = Validator.CheckMgNr((TextBox)sender, true);
var text = MgNrInput.Text;
var caret = MgNrInput.CaretIndex;
ControlUtils.SelectItemWithPk(MemberInput, res.IsValid ? int.Parse(MgNrInput.Text) : null);
MgNrInput.Text = text;
MgNrInput.CaretIndex = caret;
}
private void MemberInput_SelectionChanged(object sender, SelectionChangedEventArgs evt) {
var m = MemberInput.SelectedItem as Member;
MgNrInput.Text = m?.MgNr.ToString();
CheckValidity();
}
private void MemberReferenceButton_Click(object sender, RoutedEventArgs evt) {
if (MemberInput.SelectedItem is not Member m) return;
App.FocusMember(m.MgNr);
}
private void DeliveryInput_SelectionChanged(object sender, SelectionChangedEventArgs evt) {
CheckValidity();
}
private void SplitCompletelyInput_Changed(object sender, RoutedEventArgs evt) {
var checkbox = (CheckBox)sender;
var dpnr = int.Parse(checkbox.Tag.ToString()!);
var row = DeliveryParts.First(d => d.Part.DPNr == dpnr);
if (checkbox.IsChecked == true) {
row.SplitWeightEnabled = false;
row.SplitWeight = row.Part.Weight;
} else if (checkbox.IsChecked == false) {
row.SplitWeightEnabled = true;
row.SplitWeight = null;
}
CollectionViewSource.GetDefaultView(DeliveryParts).Refresh();
CheckValidity();
}
private void SplitWeightInput_TextChanged(object sender, TextChangedEventArgs evt) {
var textbox = (TextBox)sender;
Validator.CheckInteger(textbox, false, 6);
var dpnr = int.Parse(textbox.Tag.ToString()!);
var row = DeliveryParts.First(d => d.Part.DPNr == dpnr);
var w = int.TryParse(textbox.Text, out var v) ? v : (int?)null;
if (w >= row.Part.Weight) {
row.SplitCompletely = true;
row.SplitWeightEnabled = false;
row.SplitWeight = row.Part.Weight;
CollectionViewSource.GetDefaultView(DeliveryParts).Refresh();
}
CheckValidity();
}
}
}

View File

@ -4,10 +4,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Dialogs" xmlns:local="clr-namespace:Elwig.Dialogs"
mc:Ignorable="d" ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
ResizeMode="NoResize"
ShowInTaskbar="False"
Topmost="True"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
FocusManager.FocusedElement="{Binding ElementName=PriceInput}" FocusManager.FocusedElement="{Binding ElementName=PriceInput}"
Title="Linear wachsen" Height="140" Width="270"> Title="Linear wachsen" Height="140" Width="270">

View File

@ -4,10 +4,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Dialogs" xmlns:local="clr-namespace:Elwig.Dialogs"
mc:Ignorable="d" ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
ResizeMode="NoResize"
ShowInTaskbar="False"
Topmost="True"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
FocusManager.FocusedElement="{Binding ElementName=WeightInput}" FocusManager.FocusedElement="{Binding ElementName=WeightInput}"
Title="Handwiegung" Height="170" Width="400"> Title="Handwiegung" Height="170" Width="400">

View File

@ -0,0 +1,69 @@
<Window x:Class="Elwig.Dialogs.NewSeasonDialog"
AutomationProperties.AutomationId="NewSeasonDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Dialogs"
ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
WindowStartupLocation="CenterOwner"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="Saison anlegen" Height="180" Width="400">
<Window.Resources>
<Style TargetType="Label">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="Padding" Value="2,4,2,4"/>
<Setter Property="Height" Value="25"/>
</Style>
<Style TargetType="ComboBox">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Height" Value="25"/>
</Style>
<Style TargetType="CheckBox">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Top"/>
</Style>
<Style TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="25"/>
</Style>
</Window.Resources>
<Grid>
<Label Content="Währung:" Margin="10,10,10,10"/>
<ComboBox x:Name="CurrencyInput" Width="150" Margin="130,10,10,10"
ItemsSource="{Binding Currencies}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Code}" Width="30"/>
<TextBlock Text="- "/>
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Label Content="Nachkommastellen:" Margin="10,40,10,10"/>
<ComboBox x:Name="PrecisionInput" Width="50" Margin="130,40,10,10">
<ComboBoxItem>2</ComboBoxItem>
<ComboBoxItem>3</ComboBoxItem>
<ComboBoxItem IsSelected="True">4</ComboBoxItem>
<ComboBoxItem>5</ComboBoxItem>
<ComboBoxItem>6</ComboBoxItem>
<ComboBoxItem>7</ComboBoxItem>
<ComboBoxItem>8</ComboBoxItem>
</ComboBox>
<CheckBox x:Name="CopyModifiersInput" Content="Zu-/Abschläge der letzten Saison übernehmen"
Margin="15,75,10,10" IsChecked="{Binding CopyModifiers}"/>
<Button x:Name="ConfirmButton" Content="Bestätigen" Margin="10,10,115,10" Grid.Column="1" IsDefault="True"
Click="ConfirmButton_Click"/>
<Button x:Name="CancelButton" Content="Abbrechen" Margin="10,10,10,10" Grid.Column="1" IsCancel="True"/>
</Grid>
</Window>

View File

@ -0,0 +1,30 @@
using Elwig.Helpers;
using Elwig.Models.Entities;
using System.Collections.Generic;
using System.Windows;
namespace Elwig.Dialogs {
public partial class NewSeasonDialog : Window {
public IEnumerable<Currency> Currencies { get; set; }
public int Year { get; set; }
public string CurrencyCode => (CurrencyInput.SelectedItem as Currency)!.Code;
public byte Precision => (byte)(PrecisionInput.SelectedIndex + 2);
public bool CopyModifiers { get; set; }
public NewSeasonDialog(Season? s, IEnumerable<Currency> currencies) {
Currencies = currencies;
CopyModifiers = s != null;
InitializeComponent();
CopyModifiersInput.IsEnabled = s != null;
ControlUtils.SelectItemWithPk(CurrencyInput, s?.CurrencyCode ?? "EUR");
PrecisionInput.SelectedIndex = (s?.Precision ?? 4) - 2;
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
DialogResult = true;
Close();
}
}
}

View File

@ -1,18 +1,17 @@
<Window x:Class="Elwig.Dialogs.UpdateDialog" <Window x:Class="Elwig.Dialogs.UpdateDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
ResizeMode="NoResize"
ShowInTaskbar="False"
Topmost="True"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
Title="Neues Update verfügbar - Elwig" Height="180" Width="400"> Title="Neues Update verfügbar - Elwig" Height="190" Width="400"
Closed="OnClosed">
<Grid> <Grid>
<TextBlock x:Name="Description" FontSize="14" Margin="0,0,0,30" <TextBlock x:Name="Description" FontSize="14" Margin="0,0,0,40"
HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center"> HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center">
Version <Run x:Name="VersionText" FontWeight="Bold">0.0.0</Run> von Elwig ist verfügbar!<LineBreak/> Version <Run x:Name="VersionText" FontWeight="Bold">0.0.0</Run> von Elwig ist verfügbar!
(<Hyperlink NavigateUri="https://elwig.at/changelog" RequestNavigate="Hyperlink_RequestNavigate">Änderungen</Hyperlink>)<LineBreak/>
Soll das Update heruntergeladen und<LineBreak/> Soll das Update heruntergeladen und<LineBreak/>
installiert werden? (ca. <Run x:Name="SizeText">100</Run> MB)<LineBreak/> installiert werden? (ca. <Run x:Name="SizeText">100</Run> MB)<LineBreak/>
<Run FontWeight="Bold">Achtung</Run>: Elwig wird dabei geschlossen! <Run FontWeight="Bold">Achtung</Run>: Elwig wird dabei geschlossen!
@ -22,12 +21,12 @@
HorizontalAlignment="Center" VerticalAlignment="Center" HorizontalAlignment="Center" VerticalAlignment="Center"
Height="27" Width="300" SnapsToDevicePixels="True"/> Height="27" Width="300" SnapsToDevicePixels="True"/>
<Button x:Name="InstallButton" Content="Installieren" Margin="10,10,115,10" <Button x:Name="InstallButton" Content="Installieren" Margin="10,10,115,20"
FontSize="14" HorizontalAlignment="Right" VerticalAlignment="Bottom" FontSize="14" HorizontalAlignment="Center" VerticalAlignment="Bottom"
Width="100" Height="27" Width="100" Height="27"
Click="InstallButton_Click"/> Click="InstallButton_Click"/>
<Button x:Name="CancelButton" Content="Abbrechen" Margin="10,10,10,10" IsCancel="True" IsDefault="True" <Button x:Name="CancelButton" Content="Abbrechen" Margin="115,10,10,20" IsCancel="True"
FontSize="14" HorizontalAlignment="Right" VerticalAlignment="Bottom" FontSize="14" HorizontalAlignment="Center" VerticalAlignment="Bottom"
Width="100" Height="27"/> Width="100" Height="27"/>
</Grid> </Grid>
</Window> </Window>

View File

@ -1,10 +1,12 @@
using Elwig.Helpers; using Elwig.Helpers;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Navigation;
namespace Elwig.Dialogs { namespace Elwig.Dialogs {
public partial class UpdateDialog : Window { public partial class UpdateDialog : Window {
@ -12,36 +14,54 @@ namespace Elwig.Dialogs {
public string Version { get; private set; } public string Version { get; private set; }
public string Url { get; private set; } public string Url { get; private set; }
private readonly CancellationTokenSource Cancellation;
public UpdateDialog(string version, string url, long size) { public UpdateDialog(string version, string url, long size) {
Version = version; Version = version;
Url = url; Url = url;
Cancellation = new();
InitializeComponent(); InitializeComponent();
VersionText.Text = version; VersionText.Text = version;
SizeText.Text = $"{size / 1024 / 1024}"; SizeText.Text = $"{size / 1024 / 1024}";
} }
private void OnClosed(object sender, EventArgs evt) {
Cancellation.Cancel();
}
private async void InstallButton_Click(object sender, RoutedEventArgs evt) { private async void InstallButton_Click(object sender, RoutedEventArgs evt) {
Description.Visibility = Visibility.Hidden; Description.Visibility = Visibility.Hidden;
ProgressBar.Visibility = Visibility.Visible; ProgressBar.Visibility = Visibility.Visible;
InstallButton.IsEnabled = false; InstallButton.IsEnabled = false;
await Install(); await Install();
DialogResult = true;
Close(); Close();
} }
public async Task Install() { public async Task Install() {
var fileName = Path.Combine(App.TempPath, $"Elwig-{Version}.exe"); var fileName = Path.Combine(App.TempPath, $"Elwig-{Version}.exe");
{ try {
using var stream = new FileStream(fileName, FileMode.Create); using var stream = new FileStream(fileName, FileMode.Create);
using var client = new HttpClient() { using var client = new HttpClient() {
Timeout = TimeSpan.FromSeconds(5), Timeout = TimeSpan.FromSeconds(5),
}; };
await client.DownloadAsync(Url, stream, new Progress<double>(p => { await client.DownloadAsync(Url, stream, new Progress<double>(p => {
ProgressBar.Value = p * 100.0; ProgressBar.Value = p * 100.0;
})); }), Cancellation.Token);
} catch (OperationCanceledException) {
File.Delete(fileName);
return;
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
} }
Process.Start(fileName); Process.Start(fileName);
DialogResult = true;
}
private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e) {
Process.Start(new ProcessStartInfo {
FileName = e.Uri.ToString(),
UseShellExecute = true,
});
} }
} }
} }

View File

@ -1,4 +1,5 @@
using Elwig.Helpers; using Elwig.Helpers;
using Elwig.Models;
using Elwig.Models.Entities; using Elwig.Models.Entities;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
@ -21,9 +22,9 @@ namespace Elwig.Documents {
var uid = (m.UstIdNr ?? "-") + (m.IsBuchführend ? "" : " <i>(pauschaliert)</i>"); var uid = (m.UstIdNr ?? "-") + (m.IsBuchführend ? "" : " <i>(pauschaliert)</i>");
Aside = $"<table><colgroup><col span='1' style='width: 22.5mm;'/><col span='1' style='width: 42.5mm;'/></colgroup>" + Aside = $"<table><colgroup><col span='1' style='width: 22.5mm;'/><col span='1' style='width: 42.5mm;'/></colgroup>" +
$"<thead><tr><th colspan='2'>Mitglied</th></tr></thead><tbody>" + $"<thead><tr><th colspan='2'>Mitglied</th></tr></thead><tbody>" +
$"<tr><th>Mitglieds-Nr.</th><td>{m.MgNr}</td></tr>" + $"<tr><th>Mitglieds-Nr.:</th><td>{m.MgNr}</td></tr>" +
$"<tr><th>Betriebs-Nr.</th><td>{m.LfbisNr}</td></tr>" + $"<tr><th>Betriebs-Nr.:</th><td>{m.LfbisNr}</td></tr>" +
$"<tr><th>UID</th><td>{uid}</td></tr>" + $"<tr><th>UID:</th><td>{uid}</td></tr>" +
$"</tbody></table>"; $"</tbody></table>";
} }
@ -31,7 +32,7 @@ namespace Elwig.Documents {
get { get {
IAddress addr = (Member.BillingAddress != null && UseBillingAddress) ? Member.BillingAddress : Member; IAddress addr = (Member.BillingAddress != null && UseBillingAddress) ? Member.BillingAddress : Member;
var plz = addr.PostalDest.AtPlz; var plz = addr.PostalDest.AtPlz;
return (addr is BillingAddr ? $"{addr.Name}\n" : "") + $"{Member.AdministrativeName}\n{addr.Address}\n{plz?.Plz} {plz?.Ort.Name.Split(",")[0]}\n{addr.PostalDest.Country.Name}"; return string.Join("\n", ((string?[])[Member.BillingAddress?.FullName, Member.AdministrativeName, Member.ForTheAttentionOf, addr.Address, $"{plz?.Plz} {plz?.Ort.Name.Split(",")[0]}", addr.PostalDest.Country.Name]).Where(s => !string.IsNullOrWhiteSpace(s)));
} }
} }
@ -80,11 +81,11 @@ namespace Elwig.Documents {
} }
private static string FormatRow( private static string FormatRow(
int obligation, int right, int delivery, int? payment = null, int? area = null, int obligation, int right, int delivery, int? totalDelivery = null, int? payment = null, int? area = null,
bool isGa = false, bool showPayment = false, bool showArea = false bool isGa = false, bool showPayment = false, bool showArea = false
) { ) {
totalDelivery ??= delivery;
payment ??= delivery; payment ??= delivery;
var baseline = showPayment ? payment : delivery;
if (showArea) { if (showArea) {
return $"<td>{(area == null ? "" : $"{area:N0}")}</td>" + return $"<td>{(area == null ? "" : $"{area:N0}")}</td>" +
@ -94,15 +95,15 @@ namespace Elwig.Documents {
return $"<td>{(obligation == 0 ? "-" : $"{obligation:N0}")}</td>" + return $"<td>{(obligation == 0 ? "-" : $"{obligation:N0}")}</td>" +
$"<td>{(right == 0 ? "-" : $"{right:N0}")}</td>" + $"<td>{(right == 0 ? "-" : $"{right:N0}")}</td>" +
$"<td>{(baseline < obligation ? $"<b>{obligation - baseline:N0}</b>" : "-")}</td>" + $"<td>{(totalDelivery < obligation ? $"<b>{obligation - totalDelivery:N0}</b>" : "-")}</td>" +
$"<td>{(baseline >= obligation && delivery <= right ? $"{right - delivery:N0}" : "-")}</td>" + $"<td>{(delivery <= right ? $"{right - delivery:N0}" : "-")}</td>" +
$"<td>{(obligation == 0 && right == 0 ? "-" : (delivery > right ? ((isGa ? "<b>" : "") + $"{delivery - right:N0}" + (isGa ? "</b>" : "")) : "-"))}</td>" + $"<td>{(obligation == 0 && right == 0 ? "-" : (delivery > right ? ((isGa ? "<b>" : "") + $"{delivery - right:N0}" + (isGa ? "</b>" : "")) : "-"))}</td>" +
(showPayment ? $"<td>{(isGa ? "" : obligation == 0 && right == 0 ? "-" : $"{payment:N0}")}</td>" : "") + (showPayment ? $"<td>{(isGa ? "" : obligation == 0 && right == 0 ? "-" : $"{payment:N0}")}</td>" : "") +
$"<td>{delivery:N0}</td>"; $"<td>{totalDelivery:N0}</td>";
} }
private static string FormatRow(MemberBucket bucket, bool isGa = false, bool showPayment = false, bool showArea = false) { private static string FormatRow(MemberBucket bucket, bool isGa = false, bool showPayment = false, bool showArea = false) {
return FormatRow(bucket.Obligation, bucket.Right, bucket.Delivery, bucket.Payment, bucket.Area, isGa, showPayment, showArea); return FormatRow(bucket.Obligation, bucket.Right, bucket.Delivery, bucket.DeliveryTotal, bucket.Payment, bucket.Area, isGa, showPayment, showArea);
} }
public string PrintBucketTable( public string PrintBucketTable(
@ -156,7 +157,7 @@ namespace Elwig.Documents {
tbl += $"<tr><th>Gesamtlieferung lt. gez. GA</th>{FormatRow( tbl += $"<tr><th>Gesamtlieferung lt. gez. GA</th>{FormatRow(
Member.BusinessShares * season.MinKgPerBusinessShare, Member.BusinessShares * season.MinKgPerBusinessShare,
Member.BusinessShares * season.MaxKgPerBusinessShare, Member.BusinessShares * season.MaxKgPerBusinessShare,
Member.Deliveries.Where(d => d.Year == season.Year).Sum(d => d.Weight), season.Deliveries.Where(d => d.MgNr == Member.MgNr).Sum(d => d.Weight),
isGa: true, showPayment: includePayment, showArea: !includeDelivery isGa: true, showPayment: includePayment, showArea: !includeDelivery
)}</tr>"; )}</tr>";
if (fbs.Any()) { if (fbs.Any()) {

View File

@ -55,13 +55,13 @@ aside {
aside table { aside table {
border-collapse: collapse; border-collapse: collapse;
border: 0.5pt solid #808080; border: var(--border-thickness) solid #808080;
width: 65mm; width: 65mm;
margin-right: 10mm; margin-right: 10mm;
} }
aside table thead:not(:first-child) tr { aside table thead:not(:first-child) tr {
border-top: 0.5pt solid #808080; border-top: var(--border-thickness) solid #808080;
} }
aside table thead tr { aside table thead tr {
@ -143,7 +143,7 @@ main h1 {
.main-wrapper .signatures > * { .main-wrapper .signatures > * {
width: 50mm; width: 50mm;
border-top: 0.5pt solid black; border-top: var(--border-thickness) solid black;
padding-top: 1mm; padding-top: 1mm;
text-align: center; text-align: center;
font-size: 10pt; font-size: 10pt;

View File

@ -1,6 +1,7 @@
using Elwig.Helpers; using Elwig.Helpers;
using Elwig.Models.Dtos; using Elwig.Models.Dtos;
using Elwig.Models.Entities; using Elwig.Models.Entities;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -18,7 +19,9 @@ namespace Elwig.Documents {
public string MemberModifier; public string MemberModifier;
public IEnumerable<(string Name, int Kg, decimal Amount)>? MemberUnderDeliveries; public IEnumerable<(string Name, int Kg, decimal Amount)>? MemberUnderDeliveries;
public decimal MemberTotalUnderDelivery; public decimal MemberTotalUnderDelivery;
public decimal MemberAutoBusinessShares; public int MemberAutoBusinessShares;
public decimal MemberAutoBusinessSharesAmount;
public PaymentCustom? CustomPayment;
public CreditNote( public CreditNote(
AppDbContext ctx, AppDbContext ctx,
@ -27,26 +30,33 @@ namespace Elwig.Documents {
bool considerContractPenalties, bool considerContractPenalties,
bool considerTotalPenalty, bool considerTotalPenalty,
bool considerAutoBusinessShares, bool considerAutoBusinessShares,
bool considerCustomModifiers,
Dictionary<string, UnderDelivery>? underDeliveries = null Dictionary<string, UnderDelivery>? underDeliveries = null
) : ) :
base($"{Name} {(p.Credit != null ? $"Nr. {p.Credit.Year}/{p.Credit.TgNr:000}" : p.Member.Name)} {p.Variant.Name}", p.Member) { base($"{Name} {(p.Credit != null ? $"Nr. {p.Credit.Year}/{p.Credit.TgNr:000}" : p.Member.FullName)} {p.Variant.Name}", p.Member) {
UseBillingAddress = true; UseBillingAddress = true;
ShowDateAndLocation = true; ShowDateAndLocation = true;
Data = data; Data = data;
Payment = p; Payment = p;
Credit = p.Credit; Credit = p.Credit;
var season = p.Variant.Season; var season = p.Variant.Season;
if (considerCustomModifiers) {
CustomPayment = ctx.CustomPayments.Find(p.Year, p.MgNr);
}
var mod = App.Client.IsMatzen ? ctx.Modifiers.Where(m => m.Year == season.Year && m.Name.StartsWith("Treue")).FirstOrDefault() : null; var mod = App.Client.IsMatzen ? ctx.Modifiers.Where(m => m.Year == season.Year && m.Name.StartsWith("Treue")).FirstOrDefault() : null;
if (mod != null) { if (CustomPayment?.ModComment != null) {
MemberModifier = CustomPayment.ModComment;
} else if (mod != null) {
MemberModifier = $"{mod.Name} ({mod.ValueStr})"; MemberModifier = $"{mod.Name} ({mod.ValueStr})";
} else { } else {
MemberModifier = "Sonstige Zu-/Abschläge"; MemberModifier = "Sonstige Zu-/Abschläge";
} }
Aside = Aside.Replace("</table>", "") + Aside = Aside.Replace("</table>", "") +
$"<thead><tr><th colspan='2'>Gutschrift</th></tr></thead><tbody>" + $"<thead><tr><th colspan='2'>Gutschrift</th></tr></thead><tbody>" +
$"<tr><th>TG-Nr.</th><td>{(p.Credit != null ? $"{p.Credit.Year}/{p.Credit.TgNr:000}" : "-")}</td></tr>" + $"<tr><th>TG-Nr.:</th><td>{(p.Credit != null ? $"{p.Credit.Year}/{p.Credit.TgNr:000}" : "-")}</td></tr>" +
$"<tr><th>Überw. am</th><td>{p.Variant.TransferDate:dd.MM.yyyy}</td></tr>" + $"<tr><th>Datum:</th><td>{p.Variant.Date:dd.MM.yyyy}</td></tr>" +
$"<tr><th>Datum/Zeit</th><td>{p.Credit?.ModifiedTimestamp:dd.MM.yyyy} / {p.Credit?.ModifiedTimestamp:HH:mm}</td></tr>" + $"<tr><th>Überw. am:</th><td>{p.Variant.TransferDate:dd.MM.yyyy}</td></tr>" +
$"</tbody></table>"; $"</tbody></table>";
Text = App.Client.TextCreditNote; Text = App.Client.TextCreditNote;
DocumentId = $"Tr.-Gutschr. " + (p.Credit != null ? $"{p.Credit.Year}/{p.Credit.TgNr:000}" : p.MgNr); DocumentId = $"Tr.-Gutschr. " + (p.Credit != null ? $"{p.Credit.Year}/{p.Credit.TgNr:000}" : p.MgNr);
@ -56,9 +66,9 @@ namespace Elwig.Documents {
if (considerTotalPenalty) { if (considerTotalPenalty) {
var total = data.Rows.SelectMany(r => r.Buckets).Sum(b => b.Value); var total = data.Rows.SelectMany(r => r.Buckets).Sum(b => b.Value);
var totalUnderDelivery = total - p.Member.BusinessShares * season.MinKgPerBusinessShare; var totalUnderDelivery = total - p.Member.BusinessShares * season.MinKgPerBusinessShare;
MemberTotalUnderDelivery = totalUnderDelivery < 0 ? totalUnderDelivery * (season.PenaltyPerKg ?? 0) - (season.PenaltyAmount ?? 0) : 0; MemberTotalUnderDelivery = totalUnderDelivery < 0 ? totalUnderDelivery * (season.PenaltyPerKg ?? 0) - (season.PenaltyAmount ?? 0) - (season.PenaltyPerBsAmount * Math.Floor(-(decimal)totalUnderDelivery / season.MinKgPerBusinessShare) ?? 0) : 0;
if (total == 0) if (total == 0)
MemberTotalUnderDelivery -= (season.PenaltyNone ?? 0); MemberTotalUnderDelivery -= (season.PenaltyNone ?? 0) + (season.PenaltyPerBsNone * p.Member.BusinessShares ?? 0);
} }
if (considerAutoBusinessShares) { if (considerAutoBusinessShares) {
var fromDate = $"{season.Year}-01-01"; var fromDate = $"{season.Year}-01-01";
@ -66,7 +76,8 @@ namespace Elwig.Documents {
MemberAutoBusinessShares = ctx.MemberHistory MemberAutoBusinessShares = ctx.MemberHistory
.Where(h => h.MgNr == p.Member.MgNr && h.Type == "auto") .Where(h => h.MgNr == p.Member.MgNr && h.Type == "auto")
.Where(h => h.DateString.CompareTo(fromDate) >= 0 && h.DateString.CompareTo(toDate) <= 0) .Where(h => h.DateString.CompareTo(fromDate) >= 0 && h.DateString.CompareTo(toDate) <= 0)
.Sum(h => h.BusinessShares) * (-season.BusinessShareValue ?? 0); .Sum(h => h.BusinessShares);
MemberAutoBusinessSharesAmount = MemberAutoBusinessShares * (-season.BusinessShareValue ?? 0);
} }
if (considerContractPenalties) { if (considerContractPenalties) {
var varieties = ctx.WineVarieties.ToDictionary(v => v.SortId, v => v); var varieties = ctx.WineVarieties.ToDictionary(v => v.SortId, v => v);

View File

@ -9,8 +9,8 @@
<table class="credit"> <table class="credit">
<colgroup> <colgroup>
<col style="width: 25mm;"/> <col style="width: 25mm;"/>
<col style="width: 5mm;"/> <col style="width: 6mm;"/>
<col style="width: 22mm;"/> <col style="width: 21mm;"/>
<col style="width: 15mm;"/> <col style="width: 15mm;"/>
<col style="width: 10mm;"/> <col style="width: 10mm;"/>
<col style="width: 10mm;"/> <col style="width: 10mm;"/>
@ -51,7 +51,7 @@
<tr class="@(i == 0 ? "first" : "") @(rows == i + 1 ? "last" : "")"> <tr class="@(i == 0 ? "first" : "") @(rows == i + 1 ? "last" : "")">
@if (i == 0) { @if (i == 0) {
<td rowspan="@rows">@p.LsNr</td> <td rowspan="@rows">@p.LsNr</td>
<td rowspan="@rows">@p.DPNr</td> <td rowspan="@rows" class="center narrow">@p.DPNr</td>
<td class="small">@p.Variety</td> <td class="small">@p.Variety</td>
<td class="small"> <td class="small">
@p.Attribute@(p.Attribute != null && p.Cultivation != null ? " / " : "")@p.Cultivation @p.Attribute@(p.Attribute != null && p.Cultivation != null ? " / " : "")@p.Cultivation
@ -153,9 +153,13 @@
@Raw(FormatRow("Unterlieferung (GA)", Model.MemberTotalUnderDelivery, add: true)); @Raw(FormatRow("Unterlieferung (GA)", Model.MemberTotalUnderDelivery, add: true));
penalty += Model.MemberTotalUnderDelivery; penalty += Model.MemberTotalUnderDelivery;
} }
@if (Model.MemberAutoBusinessShares != 0) { @if (Model.MemberAutoBusinessSharesAmount != 0) {
@Raw(FormatRow("Autom. Nachz. von GA", Model.MemberAutoBusinessShares, add: true)); @Raw(FormatRow($"Autom. Nachz. von GA ({Model.MemberAutoBusinessShares})", Model.MemberAutoBusinessSharesAmount, add: true));
penalty += Model.MemberAutoBusinessShares; penalty += Model.MemberAutoBusinessSharesAmount;
}
@if (Model.CustomPayment?.Amount != null) {
@Raw(FormatRow(Model.CustomPayment.Comment ?? ((Model.CustomPayment.Amount.Value) < 0 ? "Weitere Abzüge" : "Weitere Zuschläge"), Model.CustomPayment.Amount.Value, add: true));
penalty += Model.CustomPayment.Amount.Value;
} }
@if (Model.Credit == null) { @if (Model.Credit == null) {
@ -163,7 +167,7 @@
} else { } else {
var diff = Model.Credit.Modifiers - penalty; var diff = Model.Credit.Modifiers - penalty;
if (diff != 0) { if (diff != 0) {
@Raw(FormatRow(diff < 0 ? "Weitere Abzüge" : "Weitere Zuschläge", diff, add: true)) @Raw(FormatRow(diff < 0 ? "Sonstige Abzüge" : "Sonstige Zuschläge", diff, add: true))
} }
if (Model.Credit.PrevModifiers != null && Model.Credit.PrevModifiers != 0) { if (Model.Credit.PrevModifiers != null && Model.Credit.PrevModifiers != 0) {
@Raw(FormatRow("Bereits berücksichtigte Abzüge", -Model.Credit.PrevModifiers, add: true)) @Raw(FormatRow("Bereits berücksichtigte Abzüge", -Model.Credit.PrevModifiers, add: true))

View File

@ -0,0 +1,21 @@
using Elwig.Models.Dtos;
using System.Collections.Generic;
namespace Elwig.Documents {
public class DeliveryAncmtList : Document {
public new static string Name => "Anmeldeliste";
public string Filter;
public IEnumerable<DeliveryAncmtListRow> Announcements;
public DeliveryAncmtList(string filter, IEnumerable<DeliveryAncmtListRow> announcements) : base($"{Name} {filter}") {
Filter = filter;
Announcements = announcements;
}
public DeliveryAncmtList(string filter, DeliveryAncmtListData data) :
this(filter, data.Rows) {
}
}
}

View File

@ -0,0 +1,52 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.DeliveryAncmtList>
@model Elwig.Documents.DeliveryAncmtList
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\DeliveryAncmtList.css" />
<main>
<h1>Anmeldeliste</h1>
<h2>@Model.Filter</h2>
<table class="announcement-list">
<colgroup>
<col style="width: 15mm;"/>
<col style="width: 12mm;"/>
<col style="width: 50mm;"/>
<col style="width: 25mm;"/>
<col style="width: 38mm;"/>
<col style="width: 11mm;"/>
<col style="width: 14mm;"/>
</colgroup>
<thead>
<tr>
<th rowspan="2">Datum</th>
<th rowspan="2">MgNr.</th>
<th rowspan="2" style="text-align: left;">Mitglied</th>
<th rowspan="2" style="text-align: left;">Ort</th>
<th rowspan="2" style="text-align: left;">Sorte</th>
<th rowspan="2">Anmldg.</th>
<th>Gewicht</th>
</tr>
<tr>
<th class="unit">[kg]</th>
</tr>
</thead>
<tbody>
@foreach (var a in Model.Announcements) {
<tr>
<td class="small">@($"{a.Date:dd.MM.yyyy}")</td>
<td class="number">@a.MgNr</td>
<td>@a.AdministrativeName</td>
<td class="small">@a.DefaultKg</td>
<td>@a.Variety</td>
<td class="small center">@(a.Status ?? "-")</td>
<td class="number">@($"{a.Weight:N0}")</td>
</tr>
}
<tr class="sum bold">
<td colspan="2">Gesamt:</td>
<td colspan="3">Anmeldungen: @($"{Model.Announcements.Count():N0}")</td>
<td colspan="2" class="number">@($"{Model.Announcements.Sum(a => a.Weight):N0}")</td>
</tr>
</tbody>
</table>
</main>

View File

@ -0,0 +1,13 @@
h1 {
text-align: center;
font-size: 24pt;
margin-top: 10mm;
margin-bottom: 2mm;
}
h2 {
text-align: center;
font-size: 14pt;
margin-top: 2mm;
}

View File

@ -20,7 +20,6 @@ namespace Elwig.Documents {
Season = ctx.Seasons.Find(year) ?? throw new ArgumentException("invalid season"); Season = ctx.Seasons.Find(year) ?? throw new ArgumentException("invalid season");
ShowDateAndLocation = true; ShowDateAndLocation = true;
UseBillingAddress = true; UseBillingAddress = true;
IncludeSender = true;
DocumentId = $"Anl.-Best. {Season.Year}/{m.MgNr}"; DocumentId = $"Anl.-Best. {Season.Year}/{m.MgNr}";
Data = data; Data = data;
MemberBuckets = ctx.GetMemberBuckets(Season.Year, m.MgNr).GetAwaiter().GetResult(); MemberBuckets = ctx.GetMemberBuckets(Season.Year, m.MgNr).GetAwaiter().GetResult();

View File

@ -9,8 +9,8 @@
<table class="delivery-confirmation"> <table class="delivery-confirmation">
<colgroup> <colgroup>
<col style="width: 25mm;"/> <col style="width: 25mm;"/>
<col style="width: 5mm;"/> <col style="width: 6mm;"/>
<col style="width: 24mm;"/> <col style="width: 23mm;"/>
<col style="width: 16mm;"/> <col style="width: 16mm;"/>
<col style="width: 17mm;"/> <col style="width: 17mm;"/>
<col style="width: 10mm;"/> <col style="width: 10mm;"/>
@ -33,9 +33,9 @@
<th>Gewicht</th> <th>Gewicht</th>
<th rowspan="3" style="padding: 0;"> <th rowspan="3" style="padding: 0;">
<svg width="10" height="40" xmlns="http://www.w3.org/2000/svg"> <svg width="10" height="40" xmlns="http://www.w3.org/2000/svg">
<text x="-40" y="5" transform="rotate(270)" font-size="8pt" font-style="italic" font-family="Times New Roman" <text x="-40" y="4" transform="rotate(270)" font-size="8pt" font-style="italic" font-family="Times New Roman"
style="text-anchor: start; alignment-baseline: middle;"> style="text-anchor: start; alignment-baseline: middle;">
bto./nto. gerebelt
</text> </text>
</svg> </svg>
</th> </th>
@ -60,7 +60,7 @@
<tr class="@(first ? "first" : "") @(p.Variety != lastVariety && lastVariety != "" ? "new": "") @(rows > i + 1 ? "last" : "")"> <tr class="@(first ? "first" : "") @(p.Variety != lastVariety && lastVariety != "" ? "new": "") @(rows > i + 1 ? "last" : "")">
@if (first) { @if (first) {
<td rowspan="@rows">@p.LsNr</td> <td rowspan="@rows">@p.LsNr</td>
<td rowspan="@rows">@p.DPNr</td> <td rowspan="@rows" class="center narrow">@p.DPNr</td>
<td class="small">@p.Variety</td> <td class="small">@p.Variety</td>
<td class="small">@p.Attribute@(p.Attribute != null && p.Cultivation != null ? " / " : "")@p.Cultivation</td> <td class="small">@p.Attribute@(p.Attribute != null && p.Cultivation != null ? " / " : "")@p.Cultivation</td>
<td class="small">@p.QualityLevel</td> <td class="small">@p.QualityLevel</td>
@ -81,7 +81,7 @@
} }
@if (i == p.Buckets.Length - 1) { @if (i == p.Buckets.Length - 1) {
<td class="number">@($"{p.Weight:N0}")</td> <td class="number">@($"{p.Weight:N0}")</td>
<td class="small">@(p.IsNetWeight ? "n" : "b")</td> <td style="font-size: 7pt;">@(p.IsNetWeight ? "\u2611" : "\u2610")</td>
} else { } else {
<td></td> <td></td>
<td></td> <td></td>

View File

@ -0,0 +1,22 @@
using Elwig.Models.Dtos;
using System.Collections.Generic;
namespace Elwig.Documents {
public class DeliveryDepreciationList : Document {
public new static string Name => "Abwertungsliste";
public string Filter;
public IEnumerable<DeliveryJournalRow> Deliveries;
public DeliveryDepreciationList(string filter, IEnumerable<DeliveryJournalRow> deliveries) :
base($"{Name} {filter}") {
Filter = filter;
Deliveries = deliveries;
}
public DeliveryDepreciationList(string filter, DeliveryJournalData data) :
this(filter, data.Rows) {
}
}
}

View File

@ -0,0 +1,104 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.DeliveryDepreciationList>
@model Elwig.Documents.DeliveryDepreciationList
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\DeliveryDepreciationList.css" />
<main>
<h1>Abwertungsliste</h1>
<h2>@Model.Filter</h2>
<table class="journal">
<colgroup>
<col style="width: 25mm;"/>
<col style="width: 6mm;"/>
<col style="width: 20mm;"/>
<col style="width: 15mm;"/>
<col style="width: 35mm;"/>
<col style="width: 30mm;"/>
<col style="width: 10mm;"/>
<col style="width: 10mm;"/>
<col style="width: 14mm;"/>
</colgroup>
<thead>
<tr>
<th rowspan="2" style="text-align: left;">Lieferschein-Nr.</th>
<th rowspan="2" class="narrow">Pos.</th>
<th rowspan="2">Datum</th>
<th rowspan="2">Zeit</th>
<th rowspan="2" style="text-align: left;">Sorte</th>
<th rowspan="2" style="text-align: left;">Attr./Bewirt.</th>
<th colspan="2">Gradation</th>
<th>Gewicht</th>
</tr>
<tr>
<th class="unit">[°Oe]</th>
<th class="unit narrow">[°KMW]</th>
<th class="unit">[kg]</th>
</tr>
</thead>
<tbody>
@{
int? lastMember = null;
}
@foreach (var p in Model.Deliveries) {
if (lastMember != p.MgNr) {
<tr class="subheading @(lastMember != null ? "new" : "")">
@{
var memberDeliveries = Model.Deliveries.Where(d => d.MgNr == p.MgNr).ToList();
var memberKmw = Elwig.Helpers.Utils.AggregateDeliveryPartsKmw(memberDeliveries);
var memberOe = Elwig.Helpers.Utils.KmwToOe(memberKmw);
}
<th colspan="5">
@($"{p.MgNr}, {p.AdministrativeName}")
</th>
<td>Teil-Lfrg.: <span style="float: right;">@($"{memberDeliveries.Count():N0}")</span></td>
<td class="center">@($"{memberOe:N0}")</td>
<td class="center">@($"{memberKmw:N1}")</td>
<td class="number">@($"{memberDeliveries.Sum(p => p.Weight):N0}")</td>
</tr>
}
<tr>
<td>@p.LsNr</td>
<td class="center narrow">@p.Pos</td>
<td>@($"{p.Date:dd.MM.yyyy}")</td>
<td>@($"{p.Time:HH:mm}")</td>
<td>@p.Variety</td>
<td>@p.Attribute@(p.Attribute != null && p.Cultivation != null ? " / " : "")@p.Cultivation</td>
<td class="center">@($"{p.Oe:N0}")</td>
<td class="center">@($"{p.Kmw:N1}")</td>
<td class="number">@($"{p.Weight:N0}")</td>
</tr>
lastMember = p.MgNr;
}
@{
var branches = Model.Deliveries.Select(d => d.DeliveryBranch).Distinct().Order().ToArray();
if (branches.Length > 1) {
foreach (var b in branches) {
<tr class="@(branches[0] == b ? "sum" : "") bold">
@{
var branchDeliveries = Model.Deliveries.Where(d => d.DeliveryBranch == b).ToList();
var branchKmw = Elwig.Helpers.Utils.AggregateDeliveryPartsKmw(branchDeliveries);
var branchOe = Elwig.Helpers.Utils.KmwToOe(branchKmw);
}
<td colspan="2">@b:</td>
<td colspan="4">(Teil-)Lieferungen: @($"{branchDeliveries.DistinctBy(p => p.LsNr).Count():N0}") (@($"{branchDeliveries.Count():N0}"))</td>
<td class="center">@($"{branchOe:N0}")</td>
<td class="center">@($"{branchKmw:N1}")</td>
<td class="number">@($"{branchDeliveries.Sum(p => p.Weight):N0}")</td>
</tr>
}
}
}
<tr class="sum bold">
@{
var kmw = Elwig.Helpers.Utils.AggregateDeliveryPartsKmw(Model.Deliveries);
var oe = Elwig.Helpers.Utils.KmwToOe(kmw);
}
<td colspan="2">Gesamt:</td>
<td colspan="4">(Teil-)Lieferungen: @($"{Model.Deliveries.DistinctBy(p => p.LsNr).Count():N0}") (@($"{Model.Deliveries.Count():N0}"))</td>
<td class="center">@($"{oe:N0}")</td>
<td class="center">@($"{kmw:N1}")</td>
<td class="number">@($"{Model.Deliveries.Sum(p => p.Weight):N0}")</td>
</tr>
</tbody>
</table>
</main>

View File

@ -0,0 +1,13 @@
h1 {
text-align: center;
font-size: 24pt;
margin-top: 10mm;
margin-bottom: 2mm;
}
h2 {
text-align: center;
font-size: 14pt;
margin-top: 2mm;
}

View File

@ -1,33 +1,22 @@
using Elwig.Helpers; using Elwig.Models.Dtos;
using Elwig.Models.Entities;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Elwig.Documents { namespace Elwig.Documents {
public class DeliveryJournal : Document { public class DeliveryJournal : Document {
public string Filter; public new static string Name => "Lieferjournal";
public IEnumerable<DeliveryPart> Deliveries;
public DeliveryJournal(string filter, IEnumerable<DeliveryPart> deliveries) : base($"Lieferjournal {filter}") { public string Filter;
public IEnumerable<DeliveryJournalRow> Deliveries;
public DeliveryJournal(string filter, IEnumerable<DeliveryJournalRow> deliveries) :
base($"{Name} {filter}") {
Filter = filter; Filter = filter;
Deliveries = deliveries; Deliveries = deliveries;
} }
public DeliveryJournal(string filter, IQueryable<DeliveryPart> deliveries) : public DeliveryJournal(string filter, DeliveryJournalData data) :
this(filter, deliveries this(filter, data.Rows) {
.Include(p => p.Delivery).ThenInclude(d => d.Member) }
.Include(p => p.Variety)
.ToList()) { }
public DeliveryJournal(AppDbContext ctx, DateOnly date) :
this(date.ToString("dd.MM.yyyy"), ctx.DeliveryParts
.Where(p => p.Delivery.DateString == date.ToString("yyy-MM-dd"))
.OrderBy(p => p.Delivery.DateString)
.ThenBy(p => p.Delivery.TimeString)
.ThenBy(p => p.Delivery.LsNr)
.ThenBy(p => p.DPNr)) { }
} }
} }

View File

@ -9,10 +9,10 @@
<table class="journal"> <table class="journal">
<colgroup> <colgroup>
<col style="width: 25mm;"/> <col style="width: 25mm;"/>
<col style="width: 5mm;"/> <col style="width: 6mm;"/>
<col style="width: 15mm;"/> <col style="width: 15mm;"/>
<col style="width: 8mm;"/> <col style="width: 8mm;"/>
<col style="width: 12mm;"/> <col style="width: 11mm;"/>
<col style="width: 38mm;"/> <col style="width: 38mm;"/>
<col style="width: 28mm;"/> <col style="width: 28mm;"/>
<col style="width: 10mm;"/> <col style="width: 10mm;"/>
@ -25,7 +25,8 @@
<th rowspan="2" class="narrow">Pos.</th> <th rowspan="2" class="narrow">Pos.</th>
<th rowspan="2">Datum</th> <th rowspan="2">Datum</th>
<th rowspan="2">Zeit</th> <th rowspan="2">Zeit</th>
<th colspan="2" rowspan="2" style="text-align: left;">Mitglied</th> <th rowspan="2">MgNr.</th>
<th rowspan="2" style="text-align: left;">Mitglied</th>
<th rowspan="2" style="text-align: left;">Sorte</th> <th rowspan="2" style="text-align: left;">Sorte</th>
<th colspan="2">Gradation</th> <th colspan="2">Gradation</th>
<th>Gewicht</th> <th>Gewicht</th>
@ -39,25 +40,44 @@
<tbody> <tbody>
@foreach (var p in Model.Deliveries) { @foreach (var p in Model.Deliveries) {
<tr> <tr>
<td>@p.Delivery.LsNr</td> <td>@p.LsNr</td>
<td>@p.DPNr</td> <td class="center narrow">@p.Pos</td>
<td class="small">@($"{p.Delivery.Date:dd.MM.yyyy}")</td> <td class="small">@($"{p.Date:dd.MM.yyyy}")</td>
<td class="small">@($"{p.Delivery.Time:HH:mm}")</td> <td class="small">@($"{p.Time:HH:mm}")</td>
<td class="number">@p.Delivery.Member.MgNr</td> <td class="number">@p.MgNr</td>
<td class="small">@p.Delivery.Member.AdministrativeName</td> <td class="small">@p.AdministrativeName</td>
<td class="small">@p.Variety.Name</td> <td class="small">@p.Variety</td>
<td class="center">@($"{p.Oe:N0}")</td> <td class="center">@($"{p.Oe:N0}")</td>
<td class="center">@($"{p.Kmw:N1}")</td> <td class="center">@($"{p.Kmw:N1}")</td>
<td class="number">@($"{p.Weight:N0}")</td> <td class="number">@($"{p.Weight:N0}")</td>
</tr> </tr>
} }
@{
var branches = Model.Deliveries.Select(d => d.DeliveryBranch).Distinct().Order().ToArray();
if (branches.Length > 1) {
foreach (var b in branches) {
<tr class="@(branches[0] == b ? "sum" : "") bold">
@{
var branchDeliveries = Model.Deliveries.Where(d => d.DeliveryBranch == b).ToList();
var branchKmw = Elwig.Helpers.Utils.AggregateDeliveryPartsKmw(branchDeliveries);
var branchOe = Elwig.Helpers.Utils.KmwToOe(branchKmw);
}
<td colspan="2">@b:</td>
<td colspan="5">(Teil-)Lieferungen: @($"{branchDeliveries.DistinctBy(p => p.LsNr).Count():N0}") (@($"{branchDeliveries.Count():N0}"))</td>
<td class="center">@($"{branchOe:N0}")</td>
<td class="center">@($"{branchKmw:N1}")</td>
<td class="number">@($"{branchDeliveries.Sum(p => p.Weight):N0}")</td>
</tr>
}
}
}
<tr class="sum bold"> <tr class="sum bold">
@{ @{
var kmw = Elwig.Helpers.Utils.AggregateDeliveryPartsKmw(Model.Deliveries); var kmw = Elwig.Helpers.Utils.AggregateDeliveryPartsKmw(Model.Deliveries);
var oe = Elwig.Helpers.Utils.KmwToOe(kmw); var oe = Elwig.Helpers.Utils.KmwToOe(kmw);
} }
<td colspan="2">Gesamt:</td> <td colspan="2">Gesamt:</td>
<td colspan="5">(Teil-)Lieferungen: @($"{Model.Deliveries.DistinctBy(p => p.Delivery).Count():N0}") (@($"{Model.Deliveries.Count():N0}"))</td> <td colspan="5">(Teil-)Lieferungen: @($"{Model.Deliveries.DistinctBy(p => p.LsNr).Count():N0}") (@($"{Model.Deliveries.Count():N0}"))</td>
<td class="center">@($"{oe:N0}")</td> <td class="center">@($"{oe:N0}")</td>
<td class="center">@($"{kmw:N1}")</td> <td class="center">@($"{kmw:N1}")</td>
<td class="number">@($"{Model.Deliveries.Sum(p => p.Weight):N0}")</td> <td class="number">@($"{Model.Deliveries.Sum(p => p.Weight):N0}")</td>

View File

@ -2,6 +2,7 @@
h1 { h1 {
text-align: center; text-align: center;
font-size: 24pt; font-size: 24pt;
margin-top: 10mm;
margin-bottom: 2mm; margin-bottom: 2mm;
} }

View File

@ -5,6 +5,8 @@ using System.Collections.Generic;
namespace Elwig.Documents { namespace Elwig.Documents {
public class DeliveryNote : BusinessDocument { public class DeliveryNote : BusinessDocument {
public new static string Name => "Traubenübernahmeschein";
public Delivery Delivery; public Delivery Delivery;
public string? Text; public string? Text;
public Dictionary<string, MemberBucket> MemberBuckets; public Dictionary<string, MemberBucket> MemberBuckets;
@ -15,15 +17,15 @@ namespace Elwig.Documents {
// 3 - full // 3 - full
public int DisplayStats = App.Client.ModeDeliveryNoteStats; public int DisplayStats = App.Client.ModeDeliveryNoteStats;
public DeliveryNote(Delivery d, AppDbContext? ctx = null) : base($"Traubenübernahmeschein Nr. {d.LsNr}", d.Member) { public DeliveryNote(Delivery d, AppDbContext? ctx = null) : base($"{Name} Nr. {d.LsNr}", d.Member) {
UseBillingAddress = true; UseBillingAddress = true;
ShowDateAndLocation = true; ShowDateAndLocation = true;
Delivery = d; Delivery = d;
Aside = Aside.Replace("</table>", "") + Aside = Aside.Replace("</table>", "") +
$"<thead><tr><th colspan='2'>Lieferung</th></tr></thead><tbody>" + $"<thead><tr><th colspan='2'>Lieferung</th></tr></thead><tbody>" +
$"<tr><th>LS-Nr.</th><td>{d.LsNr}</td></tr>" + $"<tr><th>LS-Nr.:</th><td>{d.LsNr}</td></tr>" +
$"<tr><th>Datum/Zeit</th><td>{d.Date:dd.MM.yyyy} / {d.Time:HH:mm}</td></tr>" + $"<tr><th>Datum/Zeit:</th><td>{d.Date:dd.MM.yyyy} / {d.Time:HH:mm}</td></tr>" +
$"<tr><th>Zweigstelle</th><td>{d.Branch.Name}</td></tr>" + $"<tr><th>Zweigstelle:</th><td>{d.Branch.Name}</td></tr>" +
$"</tbody></table>"; $"</tbody></table>";
Text = App.Client.TextDeliveryNote; Text = App.Client.TextDeliveryNote;
DocumentId = d.LsNr; DocumentId = d.LsNr;

View File

@ -60,8 +60,18 @@
} }
} }
<tr><td></td><td colspan="5"> <tr><td></td><td colspan="5">
@Raw(part.IsManualWeighing ? "<i>Handwiegung</i>" : $"<i>Waage:</i> {part.ScaleId ?? "?"}, <i>ID:</i> {part.WeighingId ?? "?"}") @if (part.IsManualWeighing) {
(@(part.IsNetWeight ? "netto/gerebelt gewogen" : "brutto/nicht gerebelt gewogen"))@Raw(part.WeighingReason != null ? $", <i>Begründung:</i>" : "") @part.WeighingReason <i>Handwiegung @(part.IsNetWeight ? " (gerebelt gewogen)" : " (nicht gerebelt gewogen)")</i>@Raw(part.WeighingReason != null ? ", <i>Begründung:</i> " : "") @part.WeighingReason
} else {
var info = part.WeighingInfo;
<i>Waage:</i> @(part.ScaleId ?? "?")@(", ") <i>ID:</i> @(info.Id ?? "?")
@(info.Date != null || info.Time != null ? " " : "")@(info.Time != null ? $"{info.Time:HH:mm}" : "")@(info.Date != null ? $", {info.Date:dd.MM.yyyy}" : "")
@if (info.Gross != null && info.Tare != null && info.Net != null) {
<br/><i>Brutto:</i> @($"{info.Gross:N0} kg")@(" ") <i>Tara:</i> @($"{info.Tare:N0} kg")@(" ") <i>Netto:</i> @($"{info.Net:N0} kg")@(" ")@Raw(part.IsNetWeight ? "<i>gerebelt gewogen</i>" : "<i>nicht gerebelt gewogen</i>")
} else {
@Raw($" <i>({(part.IsNetWeight ? "gerebelt gewogen" : "nicht gerebelt gewogen")})</i>")
}
}
</td></tr> </td></tr>
@if (part.Comment != null) { @if (part.Comment != null) {
<tr><td></td><td colspan="5"><i>Anmerkung:</i> @part.Comment</td></tr> <tr><td></td><td colspan="5"><i>Anmerkung:</i> @part.Comment</td></tr>

View File

@ -4,7 +4,7 @@
width: 10mm; width: 10mm;
position: fixed; position: fixed;
left: -25mm; left: -25mm;
border-top: 0.5pt solid black; border-top: var(--border-thickness) solid black;
} }
.m1.r, .m2.r, .m3.r { .m1.r, .m2.r, .m3.r {
left: initial; left: initial;

View File

@ -11,7 +11,7 @@ main table.tiny {
} }
main table.border { main table.border {
border: 0.5pt solid black; border: var(--border-thickness) solid black;
} }
main table tr { main table tr {
@ -113,7 +113,28 @@ main table tr.sectionheading th {
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
font-size: 10pt; font-size: 10pt;
border-top: 0.5pt solid black; border-top: var(--border-thickness) solid black;
}
main table tr.header {
border: var(--border-thickness) solid black;
background-color: #E0E0E0;
}
main table tr.header th {
font-weight: bold;
font-style: normal;
font-size: 16pt;
padding: 1mm 2mm;
}
main table tr.spacing td,
main table tr.spacing th {
height: 5mm;
}
main table tr.spacing ~ tr.header {
break-before: avoid;
} }
main table.number thead tr:first-child th:first-child { main table.number thead tr:first-child th:first-child {
@ -128,7 +149,7 @@ main table tr.sum,
main table td.sum, main table td.sum,
main table tr.new, main table tr.new,
main table tr.border { main table tr.border {
border-top: 0.5pt solid black; border-top: var(--border-thickness) solid black;
} }
main table th.unit { main table th.unit {
@ -144,13 +165,14 @@ main table th.narrow {
padding-right: 0; padding-right: 0;
} }
main table .lborder {border-left: 0.5pt solid black;} main table .tborder {border-top: var(--border-thickness) solid black;}
main table .rborder {border-right: 0.5pt solid black;} main table .lborder {border-left: var(--border-thickness) solid black;}
main table .rborder {border-right: var(--border-thickness) solid black;}
main table .fleft { main table .fleft {
float: left; float: left;
} }
main tbody.sum tr:last-child { main tbody.sum tr:last-child {
border-bottom: 0.5pt solid black; border-bottom: var(--border-thickness) solid black;
} }

View File

@ -30,7 +30,7 @@ namespace Elwig.Documents {
public string Author; public string Author;
public string Header; public string Header;
public string Footer; public string Footer;
public DateTime Date; public DateOnly Date;
public Document(string title) { public Document(string title) {
var c = App.Client; var c = App.Client;
@ -47,7 +47,7 @@ namespace Elwig.Documents {
.Item("Betriebs-Nr.", c.LfbisNr).Item("UID", c.UstIdNr).NextLine() .Item("Betriebs-Nr.", c.LfbisNr).Item("UID", c.UstIdNr).NextLine()
.Item("BIC", c.Bic).Item("IBAN", c.Iban) .Item("BIC", c.Bic).Item("IBAN", c.Iban)
.ToString(); .ToString();
Date = DateTime.Today; Date = DateOnly.FromDateTime(Utils.Today);
} }
~Document() { ~Document() {
@ -78,12 +78,22 @@ namespace Elwig.Documents {
name = "CreditNote"; name = "CreditNote";
} else if (this is DeliveryJournal) { } else if (this is DeliveryJournal) {
name = "DeliveryJournal"; name = "DeliveryJournal";
} else if (this is DeliveryDepreciationList) {
name = "DeliveryDepreciationList";
} else if (this is Letterhead) { } else if (this is Letterhead) {
name = "Letterhead"; name = "Letterhead";
} else if (this is DeliveryConfirmation) { } else if (this is DeliveryConfirmation) {
name = "DeliveryConfirmation"; name = "DeliveryConfirmation";
} else if (this is MemberDataSheet) { } else if (this is MemberDataSheet) {
name = "MemberDataSheet"; name = "MemberDataSheet";
} else if (this is MemberList) {
name = "MemberList";
} else if (this is WineQualityStatistics) {
name = "WineQualityStatistics";
} else if (this is PaymentVariantSummary) {
name = "PaymentVariantSummary";
} else if (this is DeliveryAncmtList) {
name = "DeliveryAncmtList";
} else { } else {
throw new InvalidOperationException("Invalid document object"); throw new InvalidOperationException("Invalid document object");
} }
@ -95,6 +105,8 @@ namespace Elwig.Documents {
} }
public async Task Generate(IProgress<double>? progress = null) { public async Task Generate(IProgress<double>? progress = null) {
if (_pdfFile != null)
return;
progress?.Report(0.0); progress?.Report(0.0);
if (this is PdfDocument) { if (this is PdfDocument) {
// nothing to do // nothing to do
@ -143,12 +155,12 @@ namespace Elwig.Documents {
public async Task Print(int copies = 1) { public async Task Print(int copies = 1) {
if (PdfPath == null) throw new InvalidOperationException("Pdf file has not been generated yet"); if (PdfPath == null) throw new InvalidOperationException("Pdf file has not been generated yet");
await Pdf.Print(PdfPath, copies); await Pdf.Print(PdfPath, copies, DoublePaged);
} }
public void Show() { public void Show() {
if (_pdfFile == null) throw new InvalidOperationException("Pdf file has not been generated yet"); if (_pdfFile == null) throw new InvalidOperationException("Pdf file has not been generated yet");
Pdf.Show(_pdfFile.NewReference(), Title + (this is BusinessDocument b ? $" - {b.Member.Name}" : "")); Pdf.Show(_pdfFile.NewReference(), Title + (this is BusinessDocument b ? $" - {b.Member.FullName}" : ""));
} }
public MimePart AsEmailAttachment(string filename) { public MimePart AsEmailAttachment(string filename) {

View File

@ -2,6 +2,7 @@
:root { :root {
font-family: "Times New Roman", serif; font-family: "Times New Roman", serif;
line-height: 1; line-height: 1;
--border-thickness: 0.5pt;
} }
* { * {
@ -58,7 +59,7 @@ header .type {
position: running(page-footer); position: running(page-footer);
width: 165mm; width: 165mm;
/* for some reason the position without the following statement changes on the second page */ /* for some reason the position without the following statement changes on the second page */
border: 0.5pt solid #00000000; border: var(--border-thickness) solid #00000000;
} }
.footer-wrapper.left { .footer-wrapper.left {
@ -95,7 +96,7 @@ header .type {
footer { footer {
font-size: 10pt; font-size: 10pt;
border-top: 0.5pt solid black; border-top: var(--border-thickness) solid black;
height: 25mm; height: 25mm;
padding-top: 1mm; padding-top: 1mm;
text-align: center; text-align: center;
@ -107,6 +108,6 @@ footer {
hr { hr {
border: none; border: none;
border-top: 0.5pt solid black; border-top: var(--border-thickness) solid black;
margin: 5mm 0; margin: 5mm 0;
} }

View File

@ -2,7 +2,7 @@ using Elwig.Models.Entities;
namespace Elwig.Documents { namespace Elwig.Documents {
public class Letterhead : BusinessDocument { public class Letterhead : BusinessDocument {
public Letterhead(Member m) : base($"Briefkopf {m.Name}", m, true) { public Letterhead(Member m) : base($"Briefkopf {m.FullName}", m, true) {
Aside = ""; Aside = "";
} }
} }

View File

@ -1,4 +1,4 @@
using Elwig.Helpers; using Elwig.Helpers;
using Elwig.Models.Entities; using Elwig.Models.Entities;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -11,11 +11,13 @@ namespace Elwig.Documents {
public Season Season; public Season Season;
public Dictionary<string, MemberBucket> MemberBuckets; public Dictionary<string, MemberBucket> MemberBuckets;
public IEnumerable<AreaCom> ActiveAreaCommitments;
public MemberDataSheet(Member m, AppDbContext ctx) : base($"{Name} {m.AdministrativeName}", m) { public MemberDataSheet(Member m, AppDbContext ctx) : base($"{Name} {m.AdministrativeName}", m) {
DocumentId = $"{Name} {m.MgNr}"; DocumentId = $"{Name} {m.MgNr}";
Season = ctx.Seasons.ToList().MaxBy(s => s.Year) ?? throw new ArgumentException("invalid season"); Season = ctx.Seasons.ToList().MaxBy(s => s.Year) ?? throw new ArgumentException("invalid season");
MemberBuckets = ctx.GetMemberBuckets(Utils.CurrentYear, m.MgNr).GetAwaiter().GetResult(); MemberBuckets = ctx.GetMemberBuckets(Utils.CurrentYear, m.MgNr).GetAwaiter().GetResult();
ActiveAreaCommitments = m.ActiveAreaCommitments(ctx);
} }
} }
} }

View File

@ -1,10 +1,8 @@
@using RazorLight @using RazorLight
@using Elwig.Helpers @using Elwig.Helpers
@inherits TemplatePage<Elwig.Documents.MemberDataSheet> @inherits TemplatePage<Elwig.Documents.MemberDataSheet>
@model Elwig.Documents.MemberDataSheet @model Elwig.Documents.MemberDataSheet
@{ @{ Layout = "BusinessDocument"; }
Layout = "BusinessDocument";
}
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\MemberDataSheet.css" /> <link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\MemberDataSheet.css" />
<main> <main>
<h1>@Model.Title</h1> <h1>@Model.Title</h1>
@ -20,21 +18,31 @@
<tbody> <tbody>
<tr class="sectionheading"><th colspan="6">Persönliche Daten</th></tr> <tr class="sectionheading"><th colspan="6">Persönliche Daten</th></tr>
<tr> <tr>
<th class="small">Titel (vorangestellt)</th> @if (Model.Member.IsJuridicalPerson) {
<th class="small">Vorname</th> <th colspan="3" class="small">Name</th>
<th colspan="3" class="small">Nachname</th> <th colspan="3" class="small">Zu Handen</th>
<th class="small">Titel (nachgestellt)</th> } else {
<th class="small">Titel (vorangestellt)</th>
<th class="small">Vorname</th>
<th colspan="3" class="small">Nachname</th>
<th class="small">Titel (nachgestellt)</th>
}
</tr> </tr>
<tr> <tr>
<td class="large">@Model.Member.Prefix</td> @if (Model.Member.IsJuridicalPerson) {
<td class="large">@Model.Member.GivenName @Model.Member.MiddleName</td> <td colspan="3" class="large">@Model.Member.Name</td>
<td class="large" colspan="3">@Model.Member.FamilyName</td> <td colspan="3" class="large">@Model.Member.ForTheAttentionOf</td>
<td class="large">@Model.Member.Suffix</td> } else {
<td class="large">@Model.Member.Prefix</td>
<td class="large">@Model.Member.GivenName @Model.Member.MiddleName</td>
<td class="large" colspan="3">@Model.Member.Name</td>
<td class="large">@Model.Member.Suffix</td>
}
</tr> </tr>
<tr> <tr>
<th>Mitglieds-Nr.:</th> <th>Mitglieds-Nr.:</th>
<td>@Model.Member.MgNr</td> <td>@Model.Member.MgNr</td>
<th colspan="2">Geburtsjahr/-tag:</th> <th colspan="2">@(Model.Member.IsJuridicalPerson ? "Gründungsjahr/-tag" : "Geburtsjahr/-tag"):</th>
<td colspan="2">@(string.Join('.', Model.Member.Birthday?.Split('-')?.Reverse() ?? Array.Empty<string>()))</td> <td colspan="2">@(string.Join('.', Model.Member.Birthday?.Split('-')?.Reverse() ?? Array.Empty<string>()))</td>
</tr> </tr>
<tr> <tr>
@ -52,7 +60,7 @@
<tr class="sectionheading"><th colspan="6">Rechnungsadresse (optional)</th></tr> <tr class="sectionheading"><th colspan="6">Rechnungsadresse (optional)</th></tr>
<tr> <tr>
<th>Name:</th> <th>Name:</th>
<td colspan="5">@Model.Member.BillingAddress?.Name</td> <td colspan="5">@Model.Member.BillingAddress?.FullName</td>
</tr> </tr>
<tr> <tr>
<th>Adresse:</th> <th>Adresse:</th>
@ -107,7 +115,7 @@
<th>Stammgemeinde:</th> <th>Stammgemeinde:</th>
<td>@Model.Member.DefaultKg?.Name</td> <td>@Model.Member.DefaultKg?.Name</td>
<th colspan="2">Buchführend:</th> <th colspan="2">Buchführend:</th>
<td colspan="2">@(Model.Member.IsBuchführend ? "Ja" : "Nein")</td> <td colspan="2">@(Model.Member.IsBuchführend ? "Ja" : "Nein") <span class="small">(@((Model.Member.IsBuchführend ? Model.Season.VatNormal : Model.Season.VatFlatrate) * 100)% USt.)</span></td>
</tr> </tr>
<tr> <tr>
<th colspan="2" class="small">(Katastralgemeinde mit dem größten Anteil an Weinbauflächen)</th> <th colspan="2" class="small">(Katastralgemeinde mit dem größten Anteil an Weinbauflächen)</th>
@ -149,7 +157,7 @@
@Raw(Model.PrintBucketTable(Model.Season, Model.MemberBuckets, includeDelivery: false)) @Raw(Model.PrintBucketTable(Model.Season, Model.MemberBuckets, includeDelivery: false))
@{ @{
var areaComs = Model.Member.ActiveAreaCommitments.GroupBy(a => a.AreaComType).Select(group => new { var areaComs = Model.ActiveAreaCommitments.GroupBy(a => a.AreaComType).Select(group => new {
AreaComType = group.Key, AreaComType = group.Key,
AreaComs = group.OrderBy(c => c.Kg.AtKg.Name), AreaComs = group.OrderBy(c => c.Kg.AtKg.Name),
Size = group.Sum(c => c.Area) Size = group.Sum(c => c.Area)
@ -198,14 +206,14 @@
<td class="text">@areaCom.GstNr.Replace(",", ", ").Replace("-", "")</td> <td class="text">@areaCom.GstNr.Replace(",", ", ").Replace("-", "")</td>
<td class="number">@($"{areaCom.Area:N0}")</td> <td class="number">@($"{areaCom.Area:N0}")</td>
<td class="center">@areaCom.WineCult?.Name</td> <td class="center">@areaCom.WineCult?.Name</td>
<td class="center">@(areaCom.YearTo == null ? $"ab {areaCom.YearFrom}" : $"{areaCom.YearFrom}{areaCom.YearTo}")</td> <td class="center">@(areaCom.YearTo == null ? (areaCom.YearFrom == null ? "unbefristet" : $"ab {areaCom.YearFrom}") : (areaCom.YearFrom == null ? $"bis {areaCom.YearTo}" : $"{areaCom.YearFrom}{areaCom.YearTo}"))</td>
</tr> </tr>
lastContract = contractType.AreaComType.DisplayName; lastContract = contractType.AreaComType.DisplayName;
} }
} }
<tr class="sum bold"> <tr class="sum bold">
<td colspan="3">Gesamt:</td> <td colspan="3">Gesamt:</td>
<td class="number">@($"{Model.Member.ActiveAreaCommitments.Select(a => a.Area).Sum():N0}")</td> <td class="number">@($"{Model.ActiveAreaCommitments.Sum(a => a.Area):N0}")</td>
<td colspan="2"></td> <td colspan="2"></td>
</tr> </tr>
</tbody> </tbody>

View File

@ -1,4 +1,4 @@

h2 { h2 {
margin-bottom: 0.5em !important; margin-bottom: 0.5em !important;
} }

View File

@ -0,0 +1,30 @@
using Elwig.Models.Dtos;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Documents {
public class MemberList : Document {
public new static string Name => "Mitgliederliste";
public string Filter;
public IEnumerable<MemberListRow> Members;
public string[] AreaComFilters;
public bool FilterAreaComs => AreaComFilters.Length > 0;
public MemberList(string filter, IEnumerable<MemberListRow> members) : base(Name) {
Filter = filter;
Members = members;
AreaComFilters = [..members
.SelectMany(m => m.AreaCommitmentsFiltered)
.Select(c => c.VtrgId)
.Distinct()
.Order()];
}
public MemberList(string filter, MemberListData data) :
this(filter, data.Rows) {
}
}
}

View File

@ -0,0 +1,109 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.MemberList>
@model Elwig.Documents.MemberList
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\MemberList.css"/>
<main>
<h1>Mitgliederliste</h1>
<h2>@Model.Filter</h2>
<table class="members">
<colgroup>
<col style="width: 8mm;"/>
@if (Model.AreaComFilters.Length > 1) {
<col style="width: 38mm;"/>
} else {
<col style="width: 42mm;"/>
}
@if (Model.AreaComFilters.Length > 1) {
<col style="width: 36mm;"/>
} else {
<col style="width: 40mm;"/>
}
<col style="width: 8mm;"/>
@if (Model.AreaComFilters.Length > 1) {
<col style="width: 18mm;"/>
} else {
<col style="width: 20mm;"/>
}
<col style="width: 12mm;"/>
<col style="width: 5mm;"/>
@if (Model.AreaComFilters.Length > 1) {
<col style="width: 16mm;"/>
} else {
<col style="width: 18mm;"/>
}
<col style="width: 12mm;"/>
@if (Model.AreaComFilters.Length > 1) {
<col style="width: 12mm;"/>
}
</colgroup>
<thead>
<tr>
@{
var headerSpan = Model.FilterAreaComs ? 3 : 2;
}
<th rowspan="@headerSpan">Nr.</th>
<th rowspan="@headerSpan" style="text-align: left;">Name</th>
<th rowspan="@headerSpan" style="text-align: left;">Adresse</th>
<th rowspan="@headerSpan">PLZ</th>
<th rowspan="@headerSpan" style="text-align: left;">Ort</th>
<th rowspan="@headerSpan">Betr.-Nr.</th>
<th rowspan="@headerSpan">GA</th>
<th rowspan="@headerSpan" style="text-align: left;">Stamm-KG</th>
<th colspan="@(Model.FilterAreaComs ? Model.AreaComFilters.Length : 1)">Geb. Fl.</th>
</tr>
@if (Model.FilterAreaComs) {
<tr>
@foreach (var vtrgId in Model.AreaComFilters) {
<th>@vtrgId</th>
}
</tr>
}
<tr>
@for (int i = 0; i < Math.Max(Model.AreaComFilters.Length, 1); i++) {
<th class="unit">[m²]</th>
}
</tr>
</thead>
<tbody class="small">
@{
string? lastBranch = Model.Members.Select(m => m.Branch).Distinct().Count() == 1 ? null : "";
}
@foreach (var m in Model.Members) {
if (lastBranch != null && m.Branch != lastBranch) {
<tr class="spacing"><td colspan="@(8 + Math.Max(Model.AreaComFilters.Length, 1))"></td></tr>
<tr class="header">
<th colspan="@(8 + Math.Max(Model.AreaComFilters.Length, 1))">@m.Branch</th>
</tr>
lastBranch = m.Branch;
}
<tr>
<td class="number" rowspan="@(m.BillingName != null ? 2 : 1)">@m.MgNr</td>
<td>@m.AdminName1 @m.Name2</td>
<td>@m.Address</td>
<td>@m.Plz</td>
<td class="tiny">@m.Locality</td>
<td>@m.LfbisNr</td>
<td class="number">@m.BusinessShares</td>
<td class="tiny">@m.DefaultKg</td>
@if (Model.AreaComFilters.Length > 0) {
foreach (var v in Model.AreaComFilters) {
<td class="number">@($"{m.AreaCommitmentsFiltered.FirstOrDefault(c => c.VtrgId == v).Area:N0}")</td>
}
} else {
<td class="number">@($"{m.AreaCommitment:N0}")</td>
}
</tr>
if (m.BillingName != null) {
<tr>
<td>@m.BillingName</td>
<td>@m.BillingAddress</td>
<td>@m.BillingPlz</td>
<td class="tiny">@m.BillingLocality</td>
<td colspan="4"></td>
</tr>
}
}
</tbody>
</table>
</main>

View File

@ -0,0 +1,13 @@
h1 {
text-align: center;
font-size: 24pt;
margin-top: 10mm;
margin-bottom: 2mm;
}
h2 {
text-align: center;
font-size: 14pt;
margin-top: 2mm;
}

View File

@ -0,0 +1,36 @@
using Elwig.Helpers;
using Elwig.Helpers.Billing;
using Elwig.Models.Dtos;
using Elwig.Models.Entities;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Documents {
public class PaymentVariantSummary : Document {
public new static string Name => "Auszahlungsvariante";
public PaymentVariantSummaryData Data;
public PaymentVar Variant;
public BillingData BillingData;
public string CurrencySymbol;
public int MemberNum;
public int DeliveryNum;
public int DeliveryPartNum;
public List<ModifierStat> ModifierStat;
public Dictionary<string, Modifier> Modifiers;
public PaymentVariantSummary(PaymentVar v, PaymentVariantSummaryData data) :
base($"{Name} {v.Year} - {v.Name}") {
Variant = v;
BillingData = BillingData.FromJson(v.Data);
Data = data;
CurrencySymbol = v.Season.Currency.Symbol ?? v.Season.Currency.Code;
MemberNum = v.Credits.Count;
DeliveryNum = v.DeliveryPartPayments.DistinctBy(p => p.DeliveryPart.Delivery).Count();
DeliveryPartNum = v.DeliveryPartPayments.Count;
ModifierStat = AppDbContext.GetModifierStats(v.Year, v.AvNr).GetAwaiter().GetResult();
Modifiers = v.Season.Modifiers.ToDictionary(m => m.ModId);
}
}
}

View File

@ -0,0 +1,279 @@
@using RazorLight
@using Elwig.Helpers
@inherits TemplatePage<Elwig.Documents.PaymentVariantSummary>
@model Elwig.Documents.PaymentVariantSummary
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\PaymentVariantSummary.css" />
<main>
<h1>Auszahlungsvariante Lese @Model.Variant.Year</h1>
<h2>@Model.Variant.Name</h2>
<table class="payment-variant border">
<colgroup>
<col style="width: 20.0mm;"/>
<col style="width: 30.0mm;"/>
<col style="width: 4.5mm;"/>
<col style="width: 28.0mm;"/>
<col style="width: 47.5mm;"/>
<col style="width: 15.0mm;"/>
<col style="width: 20.0mm;"/>
</colgroup>
@{
//var sum1 = Model.Variant.DeliveryPartPayments.Sum(p => p.NetAmount);
//var sum2 = Model.Variant.Credits.Sum(p => p.); //Model.Variant.MemberPayments.Sum(p => p.Amount);
var deliveryModifiers = Model.Variant.DeliveryPartPayments.Sum(p => p.Amount - p.NetAmount);
var memberModifiers = Model.Variant.Credits.Sum(c => c.Payment.Amount - c.Payment.NetAmount);
var sum2 = Model.Variant.Credits.Sum(p => p.NetAmount);
var sum1 = sum2 - deliveryModifiers - memberModifiers;
var payed = -Model.Variant.Credits.Sum(p => p.PrevNetAmount ?? 0m);
var netSum = Model.Variant.Credits.Sum(p => p.NetAmount) - Model.Variant.Credits.Sum(p => p.PrevNetAmount ?? 0m);
var vat = Model.Variant.Credits.Sum(p => p.VatAmount);
var grossSum = Model.Variant.Credits.Sum(p => p.GrossAmount);
var totalMods = Model.Variant.Credits.Sum(p => p.Modifiers ?? 0m);
var considered = -Model.Variant.Credits.Sum(p => p.PrevModifiers ?? 0m);
var totalSum = Model.Variant.Credits.Sum(p => p.Amount);
}
<tbody>
<tr class="sectionheading">
<th colspan="4">Allgemein</th>
<th colspan="3" class="lborder">Berücksichtigt</th>
</tr>
<tr>
<th>Name:</th>
<td colspan="3">@Model.Variant.Name</td>
<th colspan="2" class="lborder">Zu-/Abschläge bei Lieferungen:</th>
<td class="center">@(Model.BillingData.ConsiderDelieryModifiers ? "Ja" : "Nein")</td>
</tr>
<tr>
<th>Beschr.:</th>
<td colspan="3">@Model.Variant.Comment</td>
<th colspan="2" class="lborder">Pönalen bei Unterlieferungen (FB):</th>
<td class="center">@(Model.BillingData.ConsiderContractPenalties ? "Ja" : "Nein")</td>
</tr>
<tr>
<th style="overflow: visible;">Rebel-Zuschl.:</th>
<td colspan="3" class="center">
@($"{Utils.GetSign(Model.BillingData.NetWeightModifier)}{Math.Abs(Model.BillingData.NetWeightModifier) * 100:N2}") % /
@($"{Utils.GetSign(Model.BillingData.GrossWeightModifier)}{Math.Abs(Model.BillingData.GrossWeightModifier) * 100:N2}") %
</td>
<th colspan="2" class="lborder">Strafen bei Unterlieferungen (GA):</th>
<td class="center">@(Model.BillingData.ConsiderTotalPenalty ? "Ja" : "Nein")</td>
</tr>
<tr>
<th style="overflow: visible;">Datum/Überw.:</th>
<td colspan="3" class="center">
@($"{Model.Variant.Date:dd.MM.yyyy}") /
@($"{Model.Variant.TransferDate:dd.MM.yyyy}")
</td>
<th colspan="2" class="lborder">Automatische Nachzeichnung der GA:</th>
<td class="center">@(Model.BillingData.ConsiderAutoBusinessShares ? "Ja" : "Nein")</td>
</tr>
<tr>
<th>Berechnung:</th>
<td colspan="3" class="center">@($"{Model.Variant.CalcTime:dd.MM.yyyy, HH:mm:ss}")</td>
<th colspan="2" class="lborder">Benutzerdef. Zu-/Abschläge pro Mitglied:</th>
<td class="center">@(Model.BillingData.ConsiderCustomModifiers ? "Ja" : "Nein")</td>
</tr>
<tr class="sectionheading">
<th colspan="4">Beträge</th>
<th colspan="3" class="lborder">Statistik</th>
</tr>
<tr>
<th colspan="2">Zwischensumme:</th>
<td></td>
<td class="number"><span class="fleft">@Model.CurrencySymbol</span>@($"{sum1:N2}")</td>
<th class="lborder">Lieferanten:</th>
<td colspan="2" class="number">@($"{Model.MemberNum:N0}")</td>
</tr>
<tr>
<th colspan="2">Zu-/Abschläge (Mitglieder):</th>
<td class="number">@Utils.GetSign(memberModifiers)</td>
<td class="number"><span class="fleft">@Model.CurrencySymbol</span>@($"{Math.Abs(memberModifiers):N2}")</td>
<th class="lborder">Lieferungen:</th>
<td colspan="2" class="number">@($"{Model.DeliveryNum:N0}")</td>
</tr>
<tr>
<th colspan="2">Zu-/Abschläge (Lieferungen):</th>
<td class="number">@Utils.GetSign(deliveryModifiers)</td>
<td class="number"><span class="fleft">@Model.CurrencySymbol</span>@($"{Math.Abs(deliveryModifiers):N2}")</td>
<th class="lborder">Teillieferungen:</th>
<td colspan="2" class="number">@($"{Model.DeliveryPartNum:N0}")</td>
</tr>
<tr>
<th colspan="2">Gesamtsumme:</th>
<td class="number tborder"></td>
<td class="number tborder"><span class="fleft">@Model.CurrencySymbol</span>@($"{sum2:N2}")</td>
<th class="lborder"></th>
<td colspan="2"></td>
</tr>
<tr>
<th colspan="2">Bisher ausgezahlt:</th>
<td class="number">@Utils.GetSign(payed)</td>
<td class="number"><span class="fleft">@Model.CurrencySymbol</span>@($"{Math.Abs(payed):N2}")</td>
@{
var weiRows = Model.Data.Rows.Where(r => r.QualityLevel == "Wein");
var minWei = weiRows.Min(r => r.Ungeb.Price);
var maxWei = weiRows.Max(r => r.Ungeb.Price);
}
<th class="lborder tborder">Preis (abgewertet):</th>
<td colspan="2" class="center tborder">@(minWei != maxWei ? $"{minWei:N4}{maxWei:N4}" : $"{minWei:N4}") @Model.CurrencySymbol/kg</td>
</tr>
<tr>
<th colspan="2">Nettosumme:</th>
<td class="number tborder"></td>
<td class="number tborder"><span class="fleft">@Model.CurrencySymbol</span>@($"{netSum:N2}")</td>
@{
var quwRows = Model.Data.Rows.Where(r => r.QualityLevel != "Wein");
var minPrice = quwRows.Min(r => r.Ungeb.Price);
var maxPrice = quwRows.Max(r => r.Ungeb.Price);
}
<th class="lborder">Preis (ungeb., nicht abgew.):</th>
<td colspan="2" class="center">@(minPrice != maxPrice ? $"{minPrice:N4}{maxPrice:N4}" : $"{minPrice:N4}") @Model.CurrencySymbol/kg</td>
</tr>
<tr>
<th colspan="2">Mehrwertsteuer:</th>
<td class="number">@Utils.GetSign(vat)</td>
<td class="number"><span class="fleft">@Model.CurrencySymbol</span>@($"{Math.Abs(vat):N2}")</td>
@{
var gebRows = Model.Data.Rows
.Where(r => r.Geb.Price != null && r.Ungeb.Price != null)
.Select(r => r.Geb.Price - r.Ungeb.Price);
var minGeb = gebRows.Min();
var maxGeb = gebRows.Max();
}
<th class="lborder">Gebunden-Zuschlag:</th>
<td colspan="2" class="center">
@(minGeb != maxGeb ? $"{minGeb:N4}{maxGeb:N4} {Model.CurrencySymbol}/kg" : minGeb == 0 ? "-" : $"{minGeb:N4} {Model.CurrencySymbol}/kg")
</td>
</tr>
<tr>
<th colspan="2">Bruttosumme:</th>
<td class="number tborder"></td>
<td class="number tborder"><span class="fleft">@Model.CurrencySymbol</span>@($"{grossSum:N2}")</td>
<th class="lborder"></th>
<td colspan="2"></td>
</tr>
<tr>
<th colspan="2">Abzüge (Strafen/Pönalen, GA, ...):</th>
<td class="number">@Utils.GetSign(totalMods)</td>
<td class="number"><span class="fleft">@Model.CurrencySymbol</span>@($"{Math.Abs(totalMods):N2}")</td>
<th class="lborder tborder">Menge (ungebunden):</th>
<td colspan="2" class="number tborder">@($"{Model.Data.Rows.Sum(r => r.Ungeb.Weight):N0}") kg</td>
</tr>
<tr>
<th colspan="2">Bereits berücksichtigte Abzüge:</th>
<td class="number">@Utils.GetSign(considered)</td>
<td class="number"><span class="fleft">@Model.CurrencySymbol</span>@($"{Math.Abs(considered):N2}")</td>
<th class="lborder">Menge (gebunden):</th>
<td colspan="2" class="number">@($"{Model.Data.Rows.Sum(r => r.Geb.Weight):N0}") kg</td>
</tr>
<tr>
<th colspan="2">Auszahlungsbetrag:</th>
<td class="number tborder"></td>
<td class="number tborder"><span class="fleft">@Model.CurrencySymbol</span>@($"{totalSum:N2}")</td>
<th class="lborder">Gesamtmenge:</th>
<td colspan="2" class="number tborder">@($"{Model.Data.Rows.Sum(r => r.Ungeb.Weight + r.Geb.Weight):N0}") kg</td>
</tr>
</tbody>
</table>
<table class="payment-variant border">
<colgroup>
<col style="width: 35mm;"/>
<col style="width: 30mm;"/>
<col style="width: 25mm;"/>
<col style="width: 25mm;"/>
<col style="width: 25mm;"/>
<col style="width: 25mm;"/>
</colgroup>
<thead>
<tr class="sectionheading">
<th colspan="6">Statistik Zu-/Abschläge</th>
</tr>
<tr>
<th rowspan="2">Name</th>
<th rowspan="2">Zu-/Abschlag</th>
<th>Lieferungen</th>
<th>Minimum</th>
<th>Maximum</th>
<th>Betrag</th>
</tr>
<tr>
<th>[#]</th>
<th>[@Model.CurrencySymbol]</th>
<th>[@Model.CurrencySymbol]</th>
<th>[@Model.CurrencySymbol]</th>
</tr>
</thead>
<tbody>
@foreach (var m in Model.ModifierStat) {
var mod = Model.Modifiers[m.ModId];
<tr>
<th>@mod.Name</th>
<td class="number">@mod.ValueStr</td>
<td class="number">@($"{m.Count:N0}")</td>
<td class="number">@($"{m.Min:N2}")</td>
<td class="number">@($"{m.Max:N2}")</td>
<td class="number">@($"{m.Sum:N2}")</td>
</tr>
}
</tbody>
</table>
<table class="payment-variant-data">
<colgroup>
<col style="width: 30mm;"/>
<col style="width: 20mm;"/>
<col style="width: 25mm;"/>
<col style="width: 20mm;"/>
<col style="width: 25mm;"/>
<col style="width: 20mm;"/>
<col style="width: 25mm;"/>
</colgroup>
<thead>
<tr>
<th rowspan="2" style="text-align: left;">Qualitätsstufe</th>
<th>Gradation</th>
<th colspan="2">ungebunden</th>
<th colspan="2">gebunden</th>
<th>Gesamt</th>
</tr>
<tr>
<th>[@(true ? "°Oe" : "°KMW")]</th>
<th>[kg]</th>
<th>[@(Model.CurrencySymbol)/kg]</th>
<th>[kg]</th>
<th>[@(Model.CurrencySymbol)/kg]</th>
<th>[@(Model.CurrencySymbol)]</th>
</tr>
</thead>
<tbody>
@{
string? lastHdr = null;
}
@foreach (var row in Model.Data.Rows) {
var hdr = $"{row.Variety}{(row.Attribute != null ? " / " : "")}{row.Attribute}{(row.Cultivation != null ? " / " : "")}{row.Cultivation}";
if (lastHdr != hdr) {
var rows = Model.Data.Rows
.Where(r => r.Variety == row.Variety && r.Attribute == row.Attribute && r.Cultivation == row.Cultivation)
.ToList();
<tr class="subheading @(lastHdr != null ? "new" : "")">
<th colspan="2">@hdr</th>
<td class="number">@($"{rows.Sum(r => r.Ungeb.Weight):N0}")</td>
<td></td>
<td class="number">@($"{rows.Sum(r => r.Geb.Weight):N0}")</td>
<td></td>
<td class="number">@($"{rows.Sum(r => r.Amount):N2}")</td>
</tr>
}
<tr>
<td>@(row.QualityLevel)</td>
<td class="center">@($"{row.Oe:N0}")</td>
<td class="number">@(row.Ungeb.Weight != 0 ? $"{row.Ungeb.Weight:N0}" : "-")</td>
<td class="number">@(row.Ungeb.Price != null ? $"{row.Ungeb.Price:N4}" : "-")</td>
<td class="number">@(row.Geb.Weight != 0 ? $"{row.Geb.Weight:N0}" : "-")</td>
<td class="number">@(row.Geb.Price != null ? $"{row.Geb.Price:N4}" : "-")</td>
<td class="number">@($"{row.Amount:N2}")</td>
</tr>
lastHdr = hdr;
}
</tbody>
</table>
</main>

View File

@ -0,0 +1,21 @@
h1 {
text-align: center;
font-size: 24pt;
margin-top: 10mm;
margin-bottom: 2mm;
}
h2 {
text-align: center;
font-size: 14pt;
margin-top: 2mm;
}
table.payment-variant {
margin-top: 10mm;
}
table.payment-variant-data {
break-before: page;
}

View File

@ -0,0 +1,27 @@
using Elwig.Models.Dtos;
using System.Collections.Generic;
namespace Elwig.Documents {
public class WineQualityStatistics : Document {
public new static string Name => "Qualitätsstatistik";
public readonly string[][] QualIds = [["WEI"], ["RSW", "LDW"], ["QUW"], ["KAB"]];
public readonly Dictionary<string, string> QualityLevels = new() {
["WEI"] = "Wein",
["RSW"] = "Rebsortenwein",
["LDW"] = "Landwein",
["QUW"] = "Qualitätswein",
["KAB"] = "Kabinett",
};
public string Filter;
public WineQualityStatisticsData Data;
public bool UseOe => Data.UseOe;
public WineQualityStatistics(string filter, WineQualityStatisticsData data) : base($"{Name} {filter}") {
Filter = filter;
Data = data;
}
}
}

View File

@ -0,0 +1,81 @@
@using RazorLight
@using Elwig.Helpers
@inherits TemplatePage<Elwig.Documents.WineQualityStatistics>
@model Elwig.Documents.WineQualityStatistics
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\WineQualityStatistics.css"/>
<main>
<h1>Qualitätsstatistik</h1>
<h2>@Model.Filter</h2>
@foreach (var sec in Model.Data.Sections) {
<table>
<colgroup>
<col style="width: 25%;"/>
<col style="width: 25%;"/>
<col style="width: 25%;"/>
<col style="width: 25%;"/>
</colgroup>
<thead>
<tr>
<th colspan="4" class="header @(sec.Type == "R" ? "red" : sec.Type == "W" ? "green" : "")">
<h3>@sec.Name</h3>
</th>
</tr>
</thead>
<tbody>
<tr>
@foreach (var qualIds in Model.QualIds) {
<td class="container">
<div class="row">
<span class="units">[@(Model.UseOe ? "°Oe" : "°KMW")]</span>
<span class="units">[#]</span>
<span class="units">[kg]</span>
</div>
@foreach (var qualId in qualIds) {
<h4>@(Model.QualityLevels.GetValueOrDefault(qualId, qualId))</h4>
@foreach (var (grad, avgKmw, num, weight) in sec.Data.GetValueOrDefault(qualId, Array.Empty<(double, double, int, int)>())) {
<div class="row">
<span class="gradation">@(Model.UseOe ? $"{grad:N0}" : $"{grad:N1}")</span>
<span class="number">@($"{num:N0}")</span>
<span class="number">@($"{weight:N0}")</span>
</div>
}
}
</td>
}
</tr>
<tr>
@foreach (var qualIds in Model.QualIds) {
var quals = qualIds.Select(q => sec.Data.GetValueOrDefault(q, Array.Empty<(double Grad, double AvgKmw, int Num, int Weight)>()));
var weight = quals.Sum(q => q.Sum(kv => kv.Weight));
var num = quals.Sum(q => q.Sum(kv => kv.Num));
var kmw = quals.Sum(q => q.Sum(kv => kv.AvgKmw * kv.Weight)) / weight;
<td class="container bold">
<div class="row">
<span class="gradation">@(weight == 0 ? "-" : Model.UseOe ? $"{Utils.KmwToOe(kmw):N0}" : $"{kmw:N1}")</span>
<span class="number">@($"{num:N0}")</span>
<span class="number">@($"{weight:N0}")</span>
</div>
</td>
}
</tr>
</tbody>
<tfoot>
<tr>
@{
var totalWeight = sec.Data.Values.Sum(q => q.Sum(kv => kv.Weight));
var totalNum = sec.Data.Values.Sum(q => q.Sum(kv => kv.Num));
var totalKmw = sec.Data.Values.Sum(q => q.Sum(kv => kv.AvgKmw * kv.Weight)) / totalWeight;
}
<td colspan="4" class="container bold footer @(sec.Type == "R" ? "red" : sec.Type == "W" ? "green" : "")">
<div class="row" style="width: 24%; margin-left: 76%;">
<span class="gradation">@(totalWeight == 0 ? "-" : Model.UseOe ? $"{Utils.KmwToOe(totalKmw):N0}" : $"{totalKmw:N1}")</span>
<span class="number">@($"{totalNum:N0}")</span>
<span class="number">@($"{totalWeight:N0}")</span>
</div>
</td>
</tr>
</tfoot>
</table>
}
</main>

View File

@ -0,0 +1,97 @@
h1 {
text-align: center;
font-size: 24pt;
margin-top: 10mm;
margin-bottom: 2mm;
}
h2 {
text-align: center;
font-size: 14pt;
margin-top: 2mm;
}
h3 {
font-weight: bold;
font-style: normal;
font-size: 14pt;
margin: 0;
text-align: left;
}
h4 {
font-weight: bold;
font-style: italic;
font-size: 10pt;
margin: 0;
text-align: center;
margin: 2mm 0 2mm 0;
}
.row:first-child { margin-top: 0.5mm; }
.row:last-child { margin-bottom: 0.5mm; }
.bold {
font-weight: bold;
}
table {
margin-top: 10mm;
break-inside: avoid;
}
table th,
table td {
border: var(--border-thickness) solid black;
vertical-align: top !important;
}
table .header {
padding: 1mm 2mm;
}
table .header,
table .footer {
background-color: #E0E0E0;
}
table .header.red,
table .footer.red {
background-color: #FFC0C0;
}
table .header.green,
table .footer.green {
background-color: #C0FFC0;
}
.row {
display: flex;
width: 100%;
font-size: 10pt;
}
.row span {
flex: 10mm 1 1;
display: block;
padding: 0 1mm;
}
.row .units {
text-align: center;
font-size: 8pt;
font-style: italic;
padding: 1mm;
}
.gradation {
text-align: center;
}
.number {
text-align: right;
}
.row span:first-child { flex-basis: 7.5mm; }
.row span:last-child { flex-basis: 17.5mm; }

View File

@ -7,7 +7,7 @@
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<PreserveCompilationContext>true</PreserveCompilationContext> <PreserveCompilationContext>true</PreserveCompilationContext>
<ApplicationIcon>Resources\Images\Elwig.ico</ApplicationIcon> <ApplicationIcon>Resources\Images\Elwig.ico</ApplicationIcon>
<Version>0.7.1</Version> <Version>0.13.9</Version>
<SatelliteResourceLanguages>de-AT</SatelliteResourceLanguages> <SatelliteResourceLanguages>de-AT</SatelliteResourceLanguages>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
@ -25,19 +25,21 @@
</Target> </Target>
<ItemGroup> <ItemGroup>
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.6.0" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="LinqKit" Version="1.2.5" /> <PackageReference Include="LinqKit" Version="1.3.8" />
<PackageReference Include="MailKit" Version="4.4.0" /> <PackageReference Include="MailKit" Version="4.12.0" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="6.0.27" /> <PackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="6.0.36" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="9.0.4" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2365.46" /> <PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3179.45" />
<PackageReference Include="NJsonSchema" Version="11.0.0" /> <PackageReference Include="NJsonSchema" Version="11.3.2" />
<PackageReference Include="PdfiumViewer" Version="2.13.0" />
<PackageReference Include="PdfiumViewer.Native.x86_64.no_v8-no_xfa" Version="2018.4.8.256" />
<PackageReference Include="RazorLight" Version="2.3.1" /> <PackageReference Include="RazorLight" Version="2.3.1" />
<PackageReference Include="ScottPlot.WPF" Version="5.0.21" /> <PackageReference Include="ScottPlot.WPF" Version="5.0.55" />
<PackageReference Include="System.IO.Ports" Version="8.0.0" /> <PackageReference Include="System.IO.Ports" Version="9.0.4" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.4" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,21 +1,21 @@
using System; using System;
using System.Windows.Input; using System.Windows.Input;
namespace Elwig.Helpers { namespace Elwig.Helpers {
public class ActionCommand : ICommand { public class ActionCommand : ICommand {
public event EventHandler CanExecuteChanged; public event EventHandler? CanExecuteChanged;
private readonly Action Action; private readonly Action Action;
public ActionCommand(Action action) { public ActionCommand(Action action) {
Action = action; Action = action;
} }
public void Execute(object parameter) { public void Execute(object? parameter) {
Action(); Action();
} }
public bool CanExecute(object parameter) { public bool CanExecute(object? parameter) {
return true; return true;
} }
} }

View File

@ -17,8 +17,9 @@ namespace Elwig.Helpers {
public record struct AreaComBucket(int Area, int Obligation, int Right); public record struct AreaComBucket(int Area, int Obligation, int Right);
public record struct UnderDelivery(int Weight, int Diff); public record struct UnderDelivery(int Weight, int Diff);
public record struct MemberBucket(string Name, int Area, int Obligation, int Right, int Delivery, int DeliveryStrict, int Payment); public record struct MemberBucket(string Name, int Area, int Obligation, int Right, int Delivery, int DeliveryStrict, int DeliveryTotal, int Payment);
public record struct MemberStat(string Variety, string Discr, int Weight); public record struct MemberStat(string Variety, string Discr, int Weight);
public record struct ModifierStat(string ModId, string Name, int Count, decimal? Min, decimal? Max, decimal Sum);
public class AppDbContext : DbContext { public class AppDbContext : DbContext {
@ -46,22 +47,32 @@ namespace Elwig.Helpers {
public DbSet<Member> Members { get; private set; } public DbSet<Member> Members { get; private set; }
public DbSet<BillingAddr> BillingAddresses { get; private set; } public DbSet<BillingAddr> BillingAddresses { get; private set; }
public DbSet<MemberTelNr> MemberTelephoneNrs { get; private set; } public DbSet<MemberTelNr> MemberTelephoneNrs { get; private set; }
public DbSet<MemberEmailAddr> MemberEmailAddrs { get; private set; }
public DbSet<MemberHistory> MemberHistory { get; private set; } public DbSet<MemberHistory> MemberHistory { get; private set; }
public DbSet<AreaCom> AreaCommitments { get; private set; } public DbSet<AreaCom> AreaCommitments { get; private set; }
public DbSet<Season> Seasons { get; private set; } public DbSet<Season> Seasons { get; private set; }
public DbSet<DeliverySchedule> DeliverySchedules { get; private set; }
public DbSet<DeliveryScheduleWineVar> DeliveryScheduleWineVarieties { get; private set; }
public DbSet<DeliveryAncmt> DeliveryAnnouncements { get; private set; }
public DbSet<Modifier> Modifiers { get; private set; } public DbSet<Modifier> Modifiers { get; private set; }
public DbSet<Delivery> Deliveries { get; private set; } public DbSet<Delivery> Deliveries { get; private set; }
public DbSet<DeliveryPart> DeliveryParts { get; private set; } public DbSet<DeliveryPart> DeliveryParts { get; private set; }
public DbSet<DeliveryPartModifier> DeliveryPartModifiers { get; private set; } public DbSet<DeliveryPartModifier> DeliveryPartModifiers { get; private set; }
public DbSet<PaymentVar> PaymentVariants { get; private set; } public DbSet<PaymentVar> PaymentVariants { get; private set; }
public DbSet<PaymentMember> MemberPayments { get; private set; } public DbSet<PaymentMember> MemberPayments { get; private set; }
public DbSet<PaymentDeliveryPart> PaymentDeliveryParts { get; private set; }
public DbSet<PaymentCustom> CustomPayments { get; private set; }
public DbSet<Credit> Credits { get; private set; } public DbSet<Credit> Credits { get; private set; }
public DbSet<DeliveryPartBucket> DeliveryPartBuckets { get; private set; }
public DbSet<OverUnderDeliveryRow> OverUnderDeliveryRows { get; private set; } public DbSet<OverUnderDeliveryRow> OverUnderDeliveryRows { get; private set; }
public DbSet<AreaComUnderDeliveryRowSingle> AreaComUnderDeliveryRows { get; private set; } public DbSet<AreaComUnderDeliveryRowSingle> AreaComUnderDeliveryRows { get; private set; }
public DbSet<MemberDeliveryPerVariantRowSingle> MemberDeliveryPerVariantRows { get; private set; } public DbSet<MemberDeliveryPerVariantRowSingle> MemberDeliveryPerVariantRows { get; private set; }
public DbSet<MemberAreaComsRowSingle> MemberAreaComsRows { get; private set; }
public DbSet<CreditNoteDeliveryRowSingle> CreditNoteDeliveryRows { get; private set; } public DbSet<CreditNoteDeliveryRowSingle> CreditNoteDeliveryRows { get; private set; }
public DbSet<CreditNoteRowSingle> CreditNoteRows { get; private set; } public DbSet<CreditNoteRowSingle> CreditNoteRows { get; private set; }
public DbSet<WeightBreakdownRow> WeightBreakDownRows { get; private set; }
public DbSet<PaymentVariantSummaryRow> PaymentVariantSummaryRows { get; private set; }
private readonly StreamWriter? LogFile = null; private readonly StreamWriter? LogFile = null;
public static DateTime LastWriteTime => File.GetLastWriteTime(App.Config.DatabaseFile); public static DateTime LastWriteTime => File.GetLastWriteTime(App.Config.DatabaseFile);
@ -69,7 +80,7 @@ namespace Elwig.Helpers {
public bool HasBackendChanged => SavedLastWriteTime != LastWriteTime; public bool HasBackendChanged => SavedLastWriteTime != LastWriteTime;
public static string? ConnectionStringOverride { get; set; } = null; public static string? ConnectionStringOverride { get; set; } = null;
public static string ConnectionString => ConnectionStringOverride ?? $"Data Source=\"{App.Config.DatabaseFile}\"; Mode=ReadWrite; Foreign Keys=True; Cache=Default"; public static string ConnectionString => ConnectionStringOverride ?? $"Data Source=\"{App.Config.DatabaseFile}\"; Mode=ReadWrite; Foreign Keys=True; Cache=Default; Pooling=False";
private readonly Dictionary<int, Dictionary<int, Dictionary<string, AreaComBucket>>> _memberAreaCommitmentBuckets = []; private readonly Dictionary<int, Dictionary<int, Dictionary<string, AreaComBucket>>> _memberAreaCommitmentBuckets = [];
private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberDeliveryBuckets = []; private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberDeliveryBuckets = [];
@ -92,15 +103,15 @@ namespace Elwig.Helpers {
SavedChanges += OnSavedChanges; SavedChanges += OnSavedChanges;
} }
public static SqliteConnection Connect() { public static SqliteConnection Connect(string? connectionString = null) {
var cnx = new SqliteConnection(ConnectionString); var cnx = new SqliteConnection(connectionString ?? ConnectionString);
cnx.CreateFunction<string, string?, bool?>("REGEXP", (pattern, value) => value == null ? null : Regex.Match(value, pattern).Success, true); cnx.CreateFunction<string, string?, bool?>("REGEXP", (pattern, value) => value == null ? null : Regex.Match(value, pattern).Success, true);
cnx.Open(); cnx.Open();
return cnx; return cnx;
} }
public static async Task<SqliteConnection> ConnectAsync() { public static async Task<SqliteConnection> ConnectAsync(string? connectionString = null) {
var cnx = new SqliteConnection(ConnectionString); var cnx = new SqliteConnection(connectionString ?? ConnectionString);
cnx.CreateFunction<string, string?, bool?>("REGEXP", (pattern, value) => value == null ? null : Regex.Match(value, pattern).Success, true); cnx.CreateFunction<string, string?, bool?>("REGEXP", (pattern, value) => value == null ? null : Regex.Match(value, pattern).Success, true);
await cnx.OpenAsync(); await cnx.OpenAsync();
return cnx; return cnx;
@ -194,10 +205,10 @@ namespace Elwig.Helpers {
return c + 1; return c + 1;
} }
public async Task<int> NextLNr(DateOnly date) { public async Task<int> NextLNr(DateOnly date, string zwstid) {
var dateStr = date.ToString("yyyy-MM-dd"); var dateStr = date.ToString("yyyy-MM-dd");
int c = 0; int c = 0;
(await Deliveries.Where(d => d.DateString == dateStr).Select(d => d.LNr).ToListAsync()) (await Deliveries.Where(d => d.DateString == dateStr && d.ZwstId == zwstid).Select(d => d.LNr).ToListAsync())
.ForEach(a => { if (a <= c + 100) c = a; }); .ForEach(a => { if (a <= c + 100) c = a; });
return c + 1; return c + 1;
} }
@ -230,35 +241,68 @@ namespace Elwig.Helpers {
return c + 1; return c + 1;
} }
public async Task<int> NextDsNr(int year) {
int c = 0;
(await DeliverySchedules.Where(s => s.Year == year).Select(s => s.DsNr).ToListAsync())
.ForEach(a => { if (a <= c + 100) c = a; });
return c + 1;
}
public async Task<WineQualLevel> GetWineQualityLevel(double kmw) { public async Task<WineQualLevel> GetWineQualityLevel(double kmw) {
return await WineQualityLevels return await WineQualityLevels
.Where(q => !q.IsPredicate && (q.MinKmw == null || q.MinKmw <= kmw)) .Where(q => !q.IsPredicate && (q.MinKmw == null || q.MinKmw <= kmw))
.OrderBy(q => q.MinKmw) .OrderBy(q => q.MinKmw)
.LastOrDefaultAsync(); .LastAsync();
} }
public async Task UpdateDeliveryPartModifiers(DeliveryPart part, IEnumerable<Modifier> modifiers) { public void UpdateDeliveryPartModifiers(DeliveryPart part, IEnumerable<Modifier> oldModifiers, IEnumerable<Modifier> newModifiers) {
foreach (var m in Modifiers.Where(m => m.Year == part.Year)) { foreach (var m in Modifiers.Where(m => m.Year == part.Year)) {
var mod = part.PartModifiers.Where(pa => pa.ModId == m.ModId).FirstOrDefault(); var mod = new DeliveryPartModifier {
if (modifiers.Contains(m)) { Year = part.Year,
DeliveryPartModifier dpm = mod ?? this.CreateProxy<DeliveryPartModifier>(); DId = part.DId,
dpm.Year = part.Year; DPNr = part.DPNr,
dpm.DId = part.DId; ModId = m.ModId,
dpm.DPNr = part.DPNr; };
dpm.ModId = m.ModId; var old = oldModifiers.Where(pa => pa.ModId == m.ModId).FirstOrDefault();
if (mod == null) { if (newModifiers.Any(md => md.ModId == m.ModId)) {
await AddAsync(dpm); if (old == null) {
Add(mod);
} else { } else {
Update(dpm); Update(mod);
} }
} else { } else {
if (mod != null) { if (old != null) {
Remove(mod); Remove(mod);
} }
} }
} }
} }
public void UpdateDeliveryScheduleWineVarieties(DeliverySchedule schedule, IEnumerable<(WineVar, int)> oldVarieties, IEnumerable<(WineVar, int)> newVarieties) {
foreach (var v in WineVarieties) {
var e = new DeliveryScheduleWineVar {
Year = schedule.Year,
DsNr = schedule.DsNr,
SortId = v.SortId,
Priority = 1,
};
var o = oldVarieties.Where(x => x.Item1.SortId == e.SortId).Select(x => x.Item2).FirstOrDefault(-1);
var n = newVarieties.Where(x => x.Item1.SortId == e.SortId).Select(x => x.Item2).FirstOrDefault(-1);
if (n != -1) {
e.Priority = n;
if (o == -1) {
Add(e);
} else {
Update(e);
}
} else {
if (o != -1) {
Remove(e);
}
}
}
}
private async Task FetchMemberAreaCommitmentBuckets(int year, SqliteConnection? cnx = null) { private async Task FetchMemberAreaCommitmentBuckets(int year, SqliteConnection? cnx = null) {
var ownCnx = cnx == null; var ownCnx = cnx == null;
cnx ??= await ConnectAsync(); cnx ??= await ConnectAsync();
@ -400,6 +444,7 @@ namespace Elwig.Helpers {
rightsAndObligations.GetValueOrDefault(id).Right, rightsAndObligations.GetValueOrDefault(id).Right,
deliveryBuckets.GetValueOrDefault(id), deliveryBuckets.GetValueOrDefault(id),
deliveryBucketsStrict.GetValueOrDefault(id), deliveryBucketsStrict.GetValueOrDefault(id),
deliveryBuckets.GetValueOrDefault(id) + deliveryBuckets.GetValueOrDefault(id + "_"),
paymentBuckets.GetValueOrDefault(id) paymentBuckets.GetValueOrDefault(id)
); );
} }
@ -431,5 +476,34 @@ namespace Elwig.Helpers {
if (ownCnx) await cnx.DisposeAsync(); if (ownCnx) await cnx.DisposeAsync();
return list; return list;
} }
public static async Task<List<ModifierStat>> GetModifierStats(int year, int avnr, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var list = new List<ModifierStat>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"""
SELECT m.modid, m.name, m.count, m.min, m.max, m.sum, s.precision
FROM v_stat_modifier m
JOIN season s ON s.year = m.year
WHERE m.year = {year} AND m.avnr = {avnr}
""";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
var prec = (byte)reader.GetInt16(6);
long? min = reader.IsDBNull(3) ? null : reader.GetInt64(3);
long? max = reader.IsDBNull(4) ? null : reader.GetInt64(4);
var sum = reader.GetInt64(5);
if (min != null && max != null && Math.Abs((long)min) > Math.Abs((long)max))
(min, max) = (max, min);
list.Add(new(reader.GetString(0), reader.GetString(1), reader.GetInt32(2),
min == null ? null : Utils.DecFromDb((long)min, prec),
max == null ? null : Utils.DecFromDb((long)max, prec),
Utils.DecFromDb(sum, prec)));
}
}
if (ownCnx) await cnx.DisposeAsync();
return list;
}
} }
} }

View File

@ -9,15 +9,15 @@ namespace Elwig.Helpers {
public static class AppDbUpdater { public static class AppDbUpdater {
// Don't forget to update value in Tests/fetch-resources.bat! // Don't forget to update value in Tests/fetch-resources.bat!
public static readonly int RequiredSchemaVersion = 18; public static readonly int RequiredSchemaVersion = 31;
private static int VersionOffset = 0; private static int VersionOffset = 0;
public static async Task<string> CheckDb() { public static async Task<Version> CheckDb() {
using var cnx = AppDbContext.Connect(); using var cnx = AppDbContext.Connect();
var applId = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA application_id") ?? 0; var applId = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA application_id") ?? 0;
if (applId != 0x454C5747) throw new Exception("Invalid application_id of database"); if (applId != 0x454C5747) throw new Exception($"Invalid application_id in database (0x{applId:X08})");
var schemaVers = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA schema_version") ?? 0; var schemaVers = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA schema_version") ?? 0;
VersionOffset = (int)(schemaVers % 100); VersionOffset = (int)(schemaVers % 100);
@ -28,18 +28,14 @@ namespace Elwig.Helpers {
await UpdateDbSchema(cnx, (int)(schemaVers / 100), RequiredSchemaVersion); await UpdateDbSchema(cnx, (int)(schemaVers / 100), RequiredSchemaVersion);
var userVers = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA user_version") ?? 0; var userVers = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA user_version") ?? 0;
var major = userVers >> 24; var v = new Version((int)(userVers >> 24), (int)((userVers >> 16) & 0xFF), (int)(userVers & 0xFFFF));
var minor = (userVers >> 16) & 0xFF;
var patch = userVers & 0xFFFF;
if (App.VersionMajor > major || if (App.Version > v) {
(App.VersionMajor == major && App.VersionMinor > minor) || long vers = (App.Version.Major << 24) | (App.Version.Minor << 16) | App.Version.Build;
(App.VersionMajor == major && App.VersionMinor == minor && App.VersionPatch > patch)) {
long vers = (App.VersionMajor << 24) | (App.VersionMinor << 16) | App.VersionPatch;
await AppDbContext.ExecuteBatch(cnx, $"PRAGMA user_version = {vers}"); await AppDbContext.ExecuteBatch(cnx, $"PRAGMA user_version = {vers}");
} }
return $"{major}.{minor}.{patch}"; return v;
} }
private static async Task UpdateDbSchema(SqliteConnection cnx, int fromVersion, int toVersion) { private static async Task UpdateDbSchema(SqliteConnection cnx, int fromVersion, int toVersion) {

View File

@ -1,7 +1,9 @@
using Elwig.Models.Entities; using Elwig.Models.Entities;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -9,7 +11,6 @@ namespace Elwig.Helpers.Billing {
public class Billing { public class Billing {
protected readonly int Year; protected readonly int Year;
protected readonly AppDbContext Context;
protected readonly Season Season; protected readonly Season Season;
protected readonly Dictionary<string, string> Attributes; protected readonly Dictionary<string, string> Attributes;
protected readonly Dictionary<string, (decimal?, decimal?)> Modifiers; protected readonly Dictionary<string, (decimal?, decimal?)> Modifiers;
@ -17,11 +18,11 @@ namespace Elwig.Helpers.Billing {
public Billing(int year) { public Billing(int year) {
Year = year; Year = year;
Context = new AppDbContext(); using var ctx = new AppDbContext();
Season = Context.Seasons.Find(Year)!; Season = ctx.Seasons.Find(Year)!;
Attributes = Context.WineAttributes.ToDictionary(a => a.AttrId, a => a.Name); Attributes = ctx.WineAttributes.ToDictionary(a => a.AttrId, a => a.Name);
Modifiers = Context.Modifiers.Where(m => m.Year == Year).ToDictionary(m => m.ModId, m => (m.Abs, m.Rel)); Modifiers = ctx.Modifiers.Where(m => m.Year == Year).Include(m => m.Season).ToDictionary(m => m.ModId, m => (m.Abs, m.Rel));
AreaComTypes = Context.AreaCommitmentTypes.ToDictionary(v => v.VtrgId, v => (v.SortId, v.AttrId, v.Discriminator, v.MinKgPerHa, v.PenaltyAmount)); AreaComTypes = ctx.AreaCommitmentTypes.ToDictionary(v => v.VtrgId, v => (v.SortId, v.AttrId, v.Discriminator, v.MinKgPerHa, v.PenaltyAmount));
} }
public async Task FinishSeason() { public async Task FinishSeason() {
@ -33,15 +34,45 @@ namespace Elwig.Helpers.Billing {
"""); """);
} }
public async Task AutoAdjustBusinessShare() { public async Task AutoAdjustBusinessShares(DateOnly date, int allowanceKg = 0, double allowanceBs = 0, int allowanceKgPerBs = 0, double allowanceRel = 0, int addMinBs = 1) {
if (addMinBs < 1) addMinBs = 1;
using var cnx = await AppDbContext.ConnectAsync(); using var cnx = await AppDbContext.ConnectAsync();
await AppDbContext.ExecuteBatch(cnx, $""" await AppDbContext.ExecuteBatch(cnx, $"""
INSERT INTO member_history (mgnr, date, business_shares, type) UPDATE member
SELECT u.mgnr, '{Utils.Today:yyyy-MM-dd}', u.diff / s.max_kg_per_bs AS bs, 'auto' SET business_shares = member.business_shares - h.business_shares
FROM member_history h
WHERE h.date = '{Year}-11-30' AND h.type = 'auto' AND h.mgnr = member.mgnr AND member.active;
INSERT INTO member_history (mgnr, date, type, business_shares)
SELECT u.mgnr,
'{date:yyyy-MM-dd}',
'auto',
CEIL((u.diff - {allowanceKg}.0 - {allowanceKgPerBs}.0 * u.business_shares) / s.max_kg_per_bs
- {allowanceBs.ToString(CultureInfo.InvariantCulture)}
- {allowanceRel.ToString(CultureInfo.InvariantCulture)} * u.business_shares) AS bs
FROM v_total_under_delivery u FROM v_total_under_delivery u
JOIN season s ON s.year = u.year JOIN season s ON s.year = u.year
WHERE s.year = {Year} AND bs > 0 JOIN member m ON m.mgnr = u.mgnr
ON CONFLICT DO NOTHING WHERE s.year = {Year} AND bs >= {addMinBs} AND m.active
ON CONFLICT DO UPDATE
SET business_shares = excluded.business_shares;
UPDATE member
SET business_shares = member.business_shares + h.business_shares
FROM member_history h
WHERE h.date = '{Year}-11-30' AND h.type = 'auto' AND h.mgnr = member.mgnr;
""");
}
public async Task UnAdjustBusinessShares() {
using var cnx = await AppDbContext.ConnectAsync();
await AppDbContext.ExecuteBatch(cnx, $"""
UPDATE member
SET business_shares = member.business_shares - h.business_shares
FROM member_history h
WHERE h.date = '{Year}-11-30' AND h.type = 'auto' AND h.mgnr = member.mgnr AND member.active;
DELETE FROM member_history WHERE date = '{Year}-11-30' AND type = 'auto';
"""); """);
} }
@ -51,14 +82,15 @@ namespace Elwig.Helpers.Billing {
bool? avoidUnderDeliveries = null, bool? avoidUnderDeliveries = null,
SqliteConnection? cnx = null SqliteConnection? cnx = null
) { ) {
using var ctx = new AppDbContext();
var honorGebunden = honorGebundenField ?? Season.Billing_HonorGebunden; var honorGebunden = honorGebundenField ?? Season.Billing_HonorGebunden;
var allowAttrsIntoLower = allowAttributesIntoLower ?? Season.Billing_AllowAttrsIntoLower; var allowAttrsIntoLower = allowAttributesIntoLower ?? Season.Billing_AllowAttrsIntoLower;
var avoidUnderDlvrs = avoidUnderDeliveries ?? Season.Billing_AvoidUnderDeliveries; var avoidUnderDlvrs = avoidUnderDeliveries ?? Season.Billing_AvoidUnderDeliveries;
var attrVals = Context.WineAttributes.ToDictionary(a => a.AttrId, a => (a.IsStrict, a.FillLower)); var attrVals = ctx.WineAttributes.ToDictionary(a => a.AttrId, a => (a.IsStrict, a.FillLower));
var attrForced = attrVals.Where(a => a.Value.IsStrict && a.Value.FillLower == 0).Select(a => a.Key).ToArray(); var attrForced = attrVals.Where(a => a.Value.IsStrict && a.Value.FillLower == 0).Select(a => a.Key).ToArray();
var ownCnx = cnx == null; var ownCnx = cnx == null;
cnx ??= await AppDbContext.ConnectAsync(); cnx ??= await AppDbContext.ConnectAsync();
await Context.GetMemberAreaCommitmentBuckets(Year, 0, cnx); await ctx.GetMemberAreaCommitmentBuckets(Year, 0, cnx);
var inserts = new List<(int, int, int, string, int)>(); var inserts = new List<(int, int, int, string, int)>();
var deliveries = new List<(int, int, int, string, int, double, string, string?, string[], bool?)>(); var deliveries = new List<(int, int, int, string, int, double, string, string?, string[], bool?)>();
@ -87,7 +119,7 @@ namespace Elwig.Helpers.Billing {
Dictionary<string, int> used = []; Dictionary<string, int> used = [];
foreach (var (mgnr, did, dpnr, sortid, weight, kmw, qualid, attrid, modifiers, gebunden) in deliveries) { foreach (var (mgnr, did, dpnr, sortid, weight, kmw, qualid, attrid, modifiers, gebunden) in deliveries) {
if (lastMgNr != mgnr) { if (lastMgNr != mgnr) {
rightsAndObligations = await Context.GetMemberAreaCommitmentBuckets(Year, mgnr); rightsAndObligations = await ctx.GetMemberAreaCommitmentBuckets(Year, mgnr);
used = []; used = [];
} }
if ((honorGebunden && gebunden == false) || if ((honorGebunden && gebunden == false) ||
@ -125,13 +157,15 @@ namespace Elwig.Helpers.Billing {
lastMgNr = mgnr; lastMgNr = mgnr;
} }
await AppDbContext.ExecuteBatch(cnx, $""" await AppDbContext.ExecuteBatch(cnx, $"UPDATE delivery_part_bucket SET value = 0 WHERE year = {Year}");
UPDATE delivery_part_bucket SET value = 0 WHERE year = {Year}; if (inserts.Count > 0) {
INSERT INTO delivery_part_bucket (year, did, dpnr, bktnr, discr, value) await AppDbContext.ExecuteBatch(cnx, $"""
VALUES {string.Join(",\n ", inserts.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '{i.Item4}', {i.Item5})"))} INSERT INTO delivery_part_bucket (year, did, dpnr, bktnr, discr, value)
ON CONFLICT DO UPDATE VALUES {string.Join(",\n ", inserts.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '{i.Item4}', {i.Item5})"))}
SET discr = excluded.discr, value = value + excluded.value; ON CONFLICT DO UPDATE
"""); SET discr = excluded.discr, value = value + excluded.value;
""");
}
if (!avoidUnderDlvrs) { if (!avoidUnderDlvrs) {
if (ownCnx) await cnx.DisposeAsync(); if (ownCnx) await cnx.DisposeAsync();

View File

@ -1,4 +1,4 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using NJsonSchema; using NJsonSchema;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -41,6 +41,10 @@ namespace Elwig.Helpers.Billing {
get => GetConsider("consider_auto_business_shares"); get => GetConsider("consider_auto_business_shares");
set => SetConsider(value, "consider_auto_business_shares"); set => SetConsider(value, "consider_auto_business_shares");
} }
public bool ConsiderCustomModifiers {
get => GetConsider("consider_custom_modifiers");
set => SetConsider(value, "consider_custom_modifiers");
}
public double NetWeightModifier { public double NetWeightModifier {
get => GetWeightModifier("net_weight_modifier", "Rebelzuschlag"); get => GetWeightModifier("net_weight_modifier", "Rebelzuschlag");
@ -295,22 +299,30 @@ namespace Elwig.Helpers.Billing {
return (rev1, rev2); return (rev1, rev2);
} }
protected static void CollapsePaymentData(JsonObject data, IEnumerable<RawVaribute> vaributes, bool useDefault = true) { protected static void CollapsePaymentData(JsonObject data, JsonObject originalData, IEnumerable<RawVaribute> vaributes, bool useDefault = true) {
var (rev1, rev2) = GetReverseKeys(data); var (rev1, rev2) = GetReverseKeys(data);
if (!data.ContainsKey("default")) { if (!data.ContainsKey("default")) {
foreach (var (v, ks) in rev1) { foreach (var (v, ks) in rev1) {
if ((ks.Count >= vaributes.Count() * 0.5 && useDefault) || ks.Count == vaributes.Count()) { if ((ks.Count > vaributes.Count() * 0.5 && useDefault) || ks.Count == vaributes.Count()) {
foreach (var k in ks) data.Remove(k); foreach (var k in ks) {
if (!(originalData[$"{k[..2]}/"]?.AsValue().TryGetValue<string>(out var o) ?? false) || o == v)
if (!(originalData[k.Split('-')[0]]?.AsValue().TryGetValue<string>(out var o2) ?? false) || o2 == v)
data.Remove(k);
}
data["default"] = v; data["default"] = v;
CollapsePaymentData(data, vaributes, useDefault); CollapsePaymentData(data, originalData, vaributes, useDefault);
return; return;
} }
} }
foreach (var (v, ks) in rev2) { foreach (var (v, ks) in rev2) {
if ((ks.Count >= vaributes.Count() * 0.5 && useDefault) || ks.Count == vaributes.Count()) { if ((ks.Count > vaributes.Count() * 0.5 && useDefault) || ks.Count == vaributes.Count()) {
foreach (var k in ks) data.Remove(k); foreach (var k in ks) {
if (!(originalData[$"{k[..2]}/"]?.AsValue().TryGetValue<decimal>(out var o) ?? false) || o == v)
if (!(originalData[k.Split('-')[0]]?.AsValue().TryGetValue<decimal>(out var o2) ?? false) || o2 == v)
data.Remove(k);
}
data["default"] = v; data["default"] = v;
CollapsePaymentData(data, vaributes, useDefault); CollapsePaymentData(data, originalData, vaributes, useDefault);
return; return;
} }
} }
@ -326,16 +338,26 @@ namespace Elwig.Helpers.Billing {
var len = vaributes.Count(e => $"{e.AttrId}{(e.CultId != null && e.CultId != "" ? "-" : "")}{e.CultId}" == idx); var len = vaributes.Count(e => $"{e.AttrId}{(e.CultId != null && e.CultId != "" ? "-" : "")}{e.CultId}" == idx);
foreach (var (v, ks) in rev1) { foreach (var (v, ks) in rev1) {
var myKs = ks.Where(k => k.EndsWith($"/{idx}")).ToList(); var myKs = ks.Where(k => k.EndsWith($"/{idx}")).ToList();
if (myKs.Count > 1 && ((myKs.Count >= len * 0.5 && useDefault) || myKs.Count == len)) { if (myKs.Count > 1 && ((myKs.Count > len * 0.5 && useDefault) || myKs.Count == len)) {
foreach (var k in myKs) data.Remove(k); foreach (var k in myKs) data.Remove(k);
data[(idx.StartsWith('-') && !useDefault ? "" : "/") + idx] = v; var discr = (idx.StartsWith('-') && !useDefault ? "" : "/") + idx;
data[discr] = v;
foreach (var (k, o) in originalData) {
if (o!.AsValue().TryGetValue<string>(out var o2) && o2 != v && k.Contains(discr))
data[k] = o2;
}
} }
} }
foreach (var (v, ks) in rev2) { foreach (var (v, ks) in rev2) {
var myKs = ks.Where(k => k.EndsWith($"/{idx}")).ToList(); var myKs = ks.Where(k => k.EndsWith($"/{idx}")).ToList();
if (myKs.Count > 1 && ((myKs.Count >= len * 0.5 && useDefault) || myKs.Count == len)) { if (myKs.Count > 1 && ((myKs.Count > len * 0.5 && useDefault) || myKs.Count == len)) {
foreach (var k in myKs) data.Remove(k); foreach (var k in myKs) data.Remove(k);
data[(idx.StartsWith('-') && !useDefault ? "" : "/") + idx] = v; var discr = (idx.StartsWith('-') && !useDefault ? "" : "/") + idx;
data[discr] = v;
foreach (var (k, o) in originalData) {
if (o!.AsValue().TryGetValue<decimal>(out var o2) && o2 != v && k.Contains(discr))
data[k] = o2;
}
} }
} }
} }
@ -351,13 +373,22 @@ namespace Elwig.Helpers.Billing {
} else if (k.Contains("/-")) { } else if (k.Contains("/-")) {
data.Remove(k, out var val); data.Remove(k, out var val);
data.Add(k.Replace("/-", "-"), val); data.Add(k.Replace("/-", "-"), val);
if (k[0] == '/' || k.Contains('-')) {
foreach (var (k2, o) in originalData) {
if (!data.ContainsKey(k2) && k2.Contains('-') && k2.Contains("-" + k.Split('-')[1]) && !k2.Contains("/-")
&& (!k2.Contains('/') || k2.Length <= 4 || !data.ContainsKey(k2[2..])))
{
data[k2] = o?.DeepClone();
}
}
}
} }
} }
(rev1, rev2) = GetReverseKeys(data, false); (rev1, rev2) = GetReverseKeys(data, false);
var keyVaributes = data var keyVaributes = data
.Select(e => e.Key.Split('-')[0]) .Select(e => e.Key)
.Where(e => e.Length > 0 && e != "default") .Where(e => e.Length > 0 && !e.Contains('-') && e != "default")
.Distinct() .Distinct()
.ToList(); .ToList();
foreach (var idx in keyVaributes) { foreach (var idx in keyVaributes) {
@ -415,8 +446,8 @@ namespace Elwig.Helpers.Billing {
} }
} }
CollapsePaymentData(payment, vaributes ?? payment.Select(e => new RawVaribute(e.Key)).ToList(), useDefaultPayment); CollapsePaymentData(payment, payment.DeepClone().AsObject(), vaributes ?? payment.Select(e => new RawVaribute(e.Key)).ToList(), useDefaultPayment);
CollapsePaymentData(qualityWei, vaributes ?? qualityWei.Select(e => new RawVaribute(e.Key)).ToList(), useDefaultQuality); CollapsePaymentData(qualityWei, qualityWei.DeepClone().AsObject(), vaributes ?? qualityWei.Select(e => new RawVaribute(e.Key)).ToList(), useDefaultQuality);
var data = new JsonObject { var data = new JsonObject {
["mode"] = "elwig", ["mode"] = "elwig",

View File

@ -1,5 +1,6 @@
using Elwig.Models.Entities; using Elwig.Models.Entities;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -14,8 +15,9 @@ namespace Elwig.Helpers.Billing {
public BillingVariant(int year, int avnr) : base(year) { public BillingVariant(int year, int avnr) : base(year) {
AvNr = avnr; AvNr = avnr;
PaymentVariant = Context.PaymentVariants.Find(Year, AvNr) ?? throw new ArgumentException("PaymentVar not found"); using var ctx = new AppDbContext();
Data = PaymentBillingData.FromJson(PaymentVariant.Data, Utils.GetVaributes(Context, Year, onlyDelivered: false)); PaymentVariant = ctx.PaymentVariants.Include(v => v.Season).Where(v => v.Year == Year && v.AvNr == AvNr).Single() ?? throw new ArgumentException("PaymentVar not found");
Data = PaymentBillingData.FromJson(PaymentVariant.Data, Utils.GetVaributes(ctx, Year, onlyDelivered: false));
} }
public async Task Calculate(bool? honorGebunden = null, bool? allowAttrsIntoLower = null, bool? avoidUnderDeliveries = null) { public async Task Calculate(bool? honorGebunden = null, bool? allowAttrsIntoLower = null, bool? avoidUnderDeliveries = null) {
@ -27,6 +29,8 @@ namespace Elwig.Helpers.Billing {
await CalculatePrices(cnx); await CalculatePrices(cnx);
if (Data.ConsiderDelieryModifiers) { if (Data.ConsiderDelieryModifiers) {
await CalculateDeliveryModifiers(cnx); await CalculateDeliveryModifiers(cnx);
}
if (Data.ConsiderCustomModifiers) {
await CalculateMemberModifiers(cnx); await CalculateMemberModifiers(cnx);
} }
await tx.CommitAsync(); await tx.CommitAsync();
@ -42,13 +46,14 @@ namespace Elwig.Helpers.Billing {
m.mgnr, m.mgnr,
v.avnr, v.avnr,
ROUND(p.amount / POW(10, s.precision - 2)) AS net_amount, ROUND(p.amount / POW(10, s.precision - 2)) AS net_amount,
ROUND(lp.amount / POW(10, s.precision - 2)) AS prev_amount, IIF(lc.amount >= 0, ROUND(lp.amount / POW(10, s.precision - 2)), 0) AS prev_net_amount,
IIF(m.buchführend, s.vat_normal, s.vat_flatrate) AS vat, IIF(m.buchführend, s.vat_normal, s.vat_flatrate) AS vat,
ROUND(IIF({Data.ConsiderContractPenalties}, COALESCE(u.total_penalty, 0), 0) / POW(10, 4 - 2)) +
ROUND(IIF({Data.ConsiderTotalPenalty}, COALESCE(b.total_penalty, 0), 0) / POW(10, s.precision - 2)) + ROUND(IIF({Data.ConsiderTotalPenalty}, COALESCE(b.total_penalty, 0), 0) / POW(10, s.precision - 2)) +
ROUND(IIF({Data.ConsiderAutoBusinessShares}, -COALESCE(a.total_amount, 0), 0) / POW(10, s.precision - 2)) ROUND(IIF({Data.ConsiderContractPenalties}, COALESCE(u.total_penalty, 0), 0) / POW(10, 4 - 2)) +
ROUND(IIF({Data.ConsiderAutoBusinessShares}, -COALESCE(a.total_amount, 0), 0) / POW(10, s.precision - 2)) +
IIF({Data.ConsiderCustomModifiers}, COALESCE(x.amount, 0), 0)
AS modifiers, AS modifiers,
lc.modifiers AS prev_modifiers IIF(lc.amount >= 0, lc.modifiers, 0) AS prev_modifiers
FROM season s FROM season s
JOIN payment_variant v ON v.year = s.year JOIN payment_variant v ON v.year = s.year
LEFT JOIN payment_variant l ON l.year = s.year LEFT JOIN payment_variant l ON l.year = s.year
@ -63,13 +68,15 @@ namespace Elwig.Helpers.Billing {
LEFT JOIN payment_member lp ON (lp.year, lp.avnr, lp.mgnr) = (l.year, l.avnr, m.mgnr) LEFT JOIN payment_member lp ON (lp.year, lp.avnr, lp.mgnr) = (l.year, l.avnr, m.mgnr)
LEFT JOIN payment_member p ON (p.year, p.avnr, p.mgnr) = (v.year, v.avnr, m.mgnr) LEFT JOIN payment_member p ON (p.year, p.avnr, p.mgnr) = (v.year, v.avnr, m.mgnr)
LEFT JOIN credit lc ON (lc.year, lc.avnr, lc.mgnr) = (l.year, l.avnr, m.mgnr) LEFT JOIN credit lc ON (lc.year, lc.avnr, lc.mgnr) = (l.year, l.avnr, m.mgnr)
LEFT JOIN v_penalty_area_commitments u ON (u.year, u.mgnr) = (s.year, m.mgnr)
LEFT JOIN v_penalty_business_shares b ON (b.year, b.mgnr) = (s.year, m.mgnr) LEFT JOIN v_penalty_business_shares b ON (b.year, b.mgnr) = (s.year, m.mgnr)
LEFT JOIN v_penalty_area_commitments u ON (u.year, u.mgnr) = (s.year, m.mgnr)
LEFT JOIN v_auto_business_shares a ON (a.year, a.mgnr) = (s.year, m.mgnr) LEFT JOIN v_auto_business_shares a ON (a.year, a.mgnr) = (s.year, m.mgnr)
LEFT JOIN payment_custom x ON (x.year, x.mgnr) = (s.year, m.mgnr)
WHERE s.year = {Year} AND v.avnr = {AvNr}; WHERE s.year = {Year} AND v.avnr = {AvNr};
UPDATE payment_variant SET test_variant = FALSE WHERE (year, avnr) = ({Year}, {AvNr});
"""); """);
await AppDbContext.ExecuteBatch(cnx, $"""
UPDATE payment_variant SET test_variant = FALSE WHERE (year, avnr) = ({Year}, {AvNr});
""");
} }
public async Task Revert() { public async Task Revert() {
@ -99,18 +106,21 @@ namespace Elwig.Helpers.Billing {
if (App.Client.IsMatzen) { if (App.Client.IsMatzen) {
var lastYears = 3; var lastYears = 3;
var multiplier = 0.50; var multiplier = 0.50;
var includePredecessor = true;
var modName = "Treue%"; var modName = "Treue%";
await AppDbContext.ExecuteBatch(cnx, $""" await AppDbContext.ExecuteBatch(cnx, $"""
INSERT INTO payment_member (year, avnr, mgnr, net_amount, mod_abs, mod_rel) INSERT INTO payment_member (year, avnr, mgnr, net_amount, mod_abs, mod_rel)
SELECT c.year, {AvNr}, s.mgnr, 0, SELECT c.year, {AvNr}, s.mgnr, 0,
ROUND(s.sum * COALESCE(m.abs, 0)), ROUND(s.sum * COALESCE(m.abs, 0)),
COALESCE(m.rel, 0) COALESCE(m.rel, 0)
FROM (SELECT {Year} AS year, mgnr, FROM (SELECT {Year} AS year, m.mgnr,
ROUND(AVG(sum) * {multiplier}) AS baseline, ROUND(AVG(COALESCE(a.sum, b.sum)) * {multiplier}) AS baseline,
COUNT(*) = {lastYears} AND MIN(sum) > 0 AS allowed COUNT(*) = {lastYears} AND MIN(COALESCE(a.sum, b.sum)) > 0 AS allowed
FROM v_stat_member FROM member m
WHERE year > {Year} - {lastYears} LEFT JOIN v_stat_member a ON a.mgnr = m.mgnr
GROUP BY mgnr FULL OUTER JOIN v_stat_member b ON b.mgnr = m.predecessor_mgnr AND b.year = a.year AND {(includePredecessor ? "TRUE" : "FALSE")}
WHERE a.year > {Year} - {lastYears}
GROUP BY m.mgnr
HAVING allowed) c HAVING allowed) c
JOIN v_stat_member s ON (s.year, s.mgnr) = (c.year, c.mgnr) JOIN v_stat_member s ON (s.year, s.mgnr) = (c.year, c.mgnr)
LEFT JOIN modifier m ON m.year = c.year AND m.name LIKE '{modName}' LEFT JOIN modifier m ON m.year = c.year AND m.name LIKE '{modName}'
@ -120,6 +130,16 @@ namespace Elwig.Helpers.Billing {
mod_rel = mod_rel + excluded.mod_rel mod_rel = mod_rel + excluded.mod_rel
"""); """);
} }
await AppDbContext.ExecuteBatch(cnx, $"""
INSERT INTO payment_member (year, avnr, mgnr, net_amount, mod_abs, mod_rel)
SELECT x.year, {AvNr}, x.mgnr, 0, COALESCE(x.mod_abs * POW(10, s.precision - 2), 0), COALESCE(x.mod_rel, 0)
FROM payment_custom x
JOIN season s ON s.year = x.year
WHERE x.year = {Year}
ON CONFLICT DO UPDATE
SET mod_abs = mod_abs + excluded.mod_abs,
mod_rel = mod_rel + excluded.mod_rel
""");
} }
protected async Task CalculatePrices(SqliteConnection cnx) { protected async Task CalculatePrices(SqliteConnection cnx) {

View File

@ -1,4 +1,4 @@
using Elwig.Models.Entities; using Elwig.Models.Entities;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;

View File

@ -1,4 +1,5 @@
using Elwig.Models.Entities; using Elwig.Models.Entities;
using Microsoft.EntityFrameworkCore;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -45,6 +46,7 @@ namespace Elwig.Helpers.Billing {
} }
} }
[PrimaryKey("Listing")]
public class Varibute : IComparable<Varibute> { public class Varibute : IComparable<Varibute> {
public WineVar? Variety { get; } public WineVar? Variety { get; }

View File

@ -1,6 +1,7 @@
using Elwig.Models.Entities; using Elwig.Models.Entities;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -22,6 +23,10 @@ namespace Elwig.Helpers {
public bool HasNetWeighing(Branch? b) => HasNetWeighing(b?.ZwstId); public bool HasNetWeighing(Branch? b) => HasNetWeighing(b?.ZwstId);
public bool HasNetWeighing() => HasNetWeighing(App.ZwstId); public bool HasNetWeighing() => HasNetWeighing(App.ZwstId);
public bool HasBoxWeighing(string? zwstId) => IsWinzerkeller && (zwstId != "W");
public bool HasBoxWeighing(Branch? b) => HasBoxWeighing(b?.ZwstId);
public bool HasBoxWeighing() => HasBoxWeighing(App.ZwstId);
public string NameToken; public string NameToken;
public string NameShort; public string NameShort;
public string Name; public string Name;
@ -57,6 +62,8 @@ namespace Elwig.Helpers {
public string? Website; public string? Website;
public int ModeDeliveryNoteStats; public int ModeDeliveryNoteStats;
public int ModeWineQualityStatistics;
public int OrderingMemberList;
public string? TextDeliveryNote; public string? TextDeliveryNote;
public string? TextDeliveryConfirmation; public string? TextDeliveryConfirmation;
@ -64,6 +71,17 @@ namespace Elwig.Helpers {
public string? TextEmailSubject; public string? TextEmailSubject;
public string? TextEmailBody; public string? TextEmailBody;
public bool MailIncludeNonDeliverers;
public bool MailDoublePaged;
public int MailSendPostal;
public int MailSendEmail;
public int MailOrdering;
public int ExportEbicsVersion;
public int ExportEbicsAddress;
public (int? AllowanceKg, double? AllowanceBs, int? AllowanceKgPerBs, double? AllowancePercent, int? MinBs) AutoAdjustBs;
public ClientParameters(AppDbContext ctx) : this(ctx.ClientParameters.ToDictionary(e => e.Param, e => e.Value)) { } public ClientParameters(AppDbContext ctx) : this(ctx.ClientParameters.ToDictionary(e => e.Param, e => e.Value)) { }
public ClientParameters(Dictionary<string, string?> parameters) { public ClientParameters(Dictionary<string, string?> parameters) {
@ -102,6 +120,18 @@ namespace Elwig.Helpers {
case "SHORT": ModeDeliveryNoteStats = 2; break; case "SHORT": ModeDeliveryNoteStats = 2; break;
case "FULL": ModeDeliveryNoteStats = 3; break; case "FULL": ModeDeliveryNoteStats = 3; break;
} }
switch (parameters.GetValueOrDefault("MODE_WINEQUALITYSTATISTICS", "OE")?.ToUpper()) {
case "OE": ModeWineQualityStatistics = 0; break;
case "KMW/1": ModeWineQualityStatistics = 1; break;
case "KMW/2": ModeWineQualityStatistics = 2; break;
case "KMW/5": ModeWineQualityStatistics = 3; break;
case "KMW/10": ModeWineQualityStatistics = 4; break;
}
switch (parameters.GetValueOrDefault("ORDERING_MEMBERLIST", "MGNR")?.ToUpper()) {
case "MGNR": OrderingMemberList = 0; break;
case "NAME": OrderingMemberList = 1; break;
case "KG": OrderingMemberList = 2; break;
}
Sender2 = parameters.GetValueOrDefault("DOCUMENT_SENDER") ?? ""; Sender2 = parameters.GetValueOrDefault("DOCUMENT_SENDER") ?? "";
TextDeliveryNote = parameters.GetValueOrDefault("TEXT_DELIVERYNOTE"); TextDeliveryNote = parameters.GetValueOrDefault("TEXT_DELIVERYNOTE");
@ -114,6 +144,47 @@ namespace Elwig.Helpers {
if (TextEmailSubject == "") TextEmailSubject = null; if (TextEmailSubject == "") TextEmailSubject = null;
TextEmailBody = parameters.GetValueOrDefault("TEXT_EMAIL_BODY"); TextEmailBody = parameters.GetValueOrDefault("TEXT_EMAIL_BODY");
if (TextEmailBody == "") TextEmailBody = null; if (TextEmailBody == "") TextEmailBody = null;
MailIncludeNonDeliverers = (parameters.GetValueOrDefault("MAIL_INCLUDE_NON_DELIVERERS")?.ToUpper()) switch {
"1" or "TRUE" or "YES" or "JA" => true,
_ => false,
};
MailDoublePaged = (parameters.GetValueOrDefault("MAIL_DOUBLE_PAGED")?.ToUpper()) switch {
"1" or "TRUE" or "YES" or "JA" => true,
_ => false,
};
switch (parameters.GetValueOrDefault("MAIL_SEND_POSTAL", "WISH")?.ToUpper()) {
case "ALL": MailSendPostal = 3; break;
case "WISH": MailSendPostal = 2; break;
case "NO_EMAIL": MailSendPostal = 1; break;
case "NONE": MailSendPostal = 0; break;
}
switch (parameters.GetValueOrDefault("MAIL_SEND_EMAIL", "WISH")?.ToUpper()) {
case "ALL": MailSendEmail = 2; break;
case "WISH": MailSendEmail = 1; break;
case "NONE": MailSendEmail = 0; break;
}
switch (parameters.GetValueOrDefault("MAIL_ORDERING", "MGNR")?.ToUpper()) {
case "MGNR": MailOrdering = 0; break;
case "NAME": MailOrdering = 1; break;
case "PLZ": MailOrdering = 2; break;
}
ExportEbicsVersion = int.TryParse(parameters.GetValueOrDefault("EXPORT_EBICS_VERSION"), out var v) ? v : 9;
switch (parameters.GetValueOrDefault("EXPORT_EBICS_ADDRESS", "FULL")?.ToUpper()) {
case "OMIT": ExportEbicsAddress = 0; break;
case "LINES": ExportEbicsAddress = 1; break;
case "FULL": ExportEbicsAddress = 2; break;
}
var autoAdjust = (parameters.GetValueOrDefault("AUTOADJUST_BUSINESSSHARES") ?? "").Split(';');
AutoAdjustBs = autoAdjust.Length == 5 ? (
int.TryParse(autoAdjust[0], out var v1) ? v1 : null,
double.TryParse(autoAdjust[1], out var v2) ? v2 : null,
int.TryParse(autoAdjust[2], out var v3) ? v3 : null,
double.TryParse(autoAdjust[3], out var v4) ? v4 : null,
int.TryParse(autoAdjust[4], out var v5) ? v5 : null
) : (null, null, null, null, null);
} catch { } catch {
throw new KeyNotFoundException(); throw new KeyNotFoundException();
} }
@ -127,6 +198,48 @@ namespace Elwig.Helpers {
case 2: deliveryNoteStats = "SHORT"; break; case 2: deliveryNoteStats = "SHORT"; break;
case 3: deliveryNoteStats = "FULL"; break; case 3: deliveryNoteStats = "FULL"; break;
} }
string modeWineQualityStatistics = "OE";
switch (ModeWineQualityStatistics) {
case 0: modeWineQualityStatistics = "OE"; break;
case 1: modeWineQualityStatistics = "KMW/1"; break;
case 2: modeWineQualityStatistics = "KMW/2"; break;
case 3: modeWineQualityStatistics = "KMW/5"; break;
case 4: modeWineQualityStatistics = "KMW/10"; break;
}
string orderingMemberList = "MGNR";
switch (OrderingMemberList) {
case 0: orderingMemberList = "MGNR"; break;
case 1: orderingMemberList = "NAME"; break;
case 2: orderingMemberList = "KG"; break;
}
string mailSendPostal = "MGNR";
switch (MailOrdering) {
case 0: mailSendPostal = "NONE"; break;
case 1: mailSendPostal = "NO_EMAIL"; break;
case 2: mailSendPostal = "WISH"; break;
case 3: mailSendPostal = "ALL"; break;
}
string mailSendEmail = "MGNR";
switch (MailOrdering) {
case 0: mailSendEmail = "NONE"; break;
case 1: mailSendEmail = "WISH"; break;
case 2: mailSendEmail = "ALL"; break;
}
string mailOrdering = "MGNR";
switch (MailOrdering) {
case 0: mailOrdering = "MGNR"; break;
case 1: mailOrdering = "NAME"; break;
case 2: mailOrdering = "PLZ"; break;
}
string exportEbicsAddress = "FULL";
switch (ExportEbicsAddress) {
case 0: exportEbicsAddress = "OMIT"; break;
case 1: exportEbicsAddress = "LINES"; break;
case 2: exportEbicsAddress = "FULL"; break;
}
string autoAdjust = $"{AutoAdjustBs.AllowanceKg};{AutoAdjustBs.AllowanceBs?.ToString(CultureInfo.InvariantCulture)};" +
$"{AutoAdjustBs.AllowanceKgPerBs};{AutoAdjustBs.AllowancePercent?.ToString(CultureInfo.InvariantCulture)};" +
$"{AutoAdjustBs.MinBs}";
return [ return [
("CLIENT_NAME_TOKEN", NameToken), ("CLIENT_NAME_TOKEN", NameToken),
("CLIENT_NAME_SHORT", NameShort), ("CLIENT_NAME_SHORT", NameShort),
@ -145,12 +258,22 @@ namespace Elwig.Helpers {
("CLIENT_BIC", Bic), ("CLIENT_BIC", Bic),
("CLIENT_IBAN", Iban), ("CLIENT_IBAN", Iban),
("MODE_DELIVERYNOTE_STATS", deliveryNoteStats), ("MODE_DELIVERYNOTE_STATS", deliveryNoteStats),
("MODE_WINEQUALITYSTATISTICS", modeWineQualityStatistics),
("ORDERING_MEMBERLIST", orderingMemberList),
("DOCUMENT_SENDER", Sender2), ("DOCUMENT_SENDER", Sender2),
("TEXT_DELIVERYNOTE", TextDeliveryNote), ("TEXT_DELIVERYNOTE", TextDeliveryNote),
("TEXT_DELIVERYCONFIRMATION", TextDeliveryConfirmation), ("TEXT_DELIVERYCONFIRMATION", TextDeliveryConfirmation),
("TEXT_CREDITNOTE", TextCreditNote), ("TEXT_CREDITNOTE", TextCreditNote),
("TEXT_EMAIL_SUBJECT", TextEmailSubject), ("TEXT_EMAIL_SUBJECT", TextEmailSubject),
("TEXT_EMAIL_BODY", TextEmailBody) ("TEXT_EMAIL_BODY", TextEmailBody),
("MAIL_INCLUDE_NON_DELIVERERS", MailIncludeNonDeliverers ? "YES" : "NO"),
("MAIL_DOUBLE_PAGED", MailDoublePaged ? "YES" : "NO"),
("MAIL_SEND_POSTAL", mailSendPostal),
("MAIL_SEND_EMAIL", mailSendEmail),
("MAIL_ORDERING", mailOrdering),
("EXPORT_EBICS_VERSION", ExportEbicsVersion.ToString()),
("EXPORT_EBICS_ADDRESS", exportEbicsAddress),
("AUTOADJUST_BUSINESSSHARES", autoAdjust),
]; ];
} }
@ -171,7 +294,6 @@ namespace Elwig.Helpers {
} }
await cmd.ExecuteNonQueryAsync(); await cmd.ExecuteNonQueryAsync();
await App.HintContextChange();
} }
} }
} }

View File

@ -13,10 +13,11 @@ namespace Elwig.Helpers {
public string? Empty; public string? Empty;
public string? Filling; public string? Filling;
public string? Limit; public string? Limit;
public bool Required;
public string? Log; public string? Log;
public string? _Log; public string? _Log;
public ScaleConfig(string id, string? type, string? model, string? cnx, string? empty, string? filling, string? limit, string? log) { public ScaleConfig(string id, string? type, string? model, string? cnx, string? empty, string? filling, string? limit, bool? required, string? log) {
Id = id; Id = id;
Type = type; Type = type;
Model = model; Model = model;
@ -24,6 +25,7 @@ namespace Elwig.Helpers {
Empty = empty; Empty = empty;
Filling = filling; Filling = filling;
Limit = limit; Limit = limit;
Required = required ?? true;
_Log = log; _Log = log;
Log = log != null ? Path.Combine(App.DataPath, log) : null; Log = log != null ? Path.Combine(App.DataPath, log) : null;
} }
@ -41,6 +43,9 @@ namespace Elwig.Helpers {
public string? Branch = null; public string? Branch = null;
public string? UpdateUrl = null; public string? UpdateUrl = null;
public bool UpdateAuto = false; public bool UpdateAuto = false;
public string? SyncUrl = null;
public string SyncUsername = "";
public string SyncPassword = "";
public string? SmtpHost = null; public string? SmtpHost = null;
public int? SmtpPort = null; public int? SmtpPort = null;
@ -64,13 +69,16 @@ namespace Elwig.Helpers {
public void Read() { public void Read() {
var config = new ConfigurationBuilder().AddIniFile(FileName).Build(); var config = new ConfigurationBuilder().AddIniFile(FileName).Build();
DatabaseFile = Path.Combine(App.DataPath, config["database:file"] ?? "database.sqlite3"); DatabaseFile = Path.Combine(Path.GetDirectoryName(FileName) ?? App.DataPath, config["database:file"] ?? "database.sqlite3");
var log = config["database:log"]; var log = config["database:log"];
DatabaseLog = log != null ? Path.Combine(App.DataPath, log) : null; DatabaseLog = log != null ? Path.Combine(Path.GetDirectoryName(FileName) ?? App.DataPath, log) : null;
Branch = config["general:branch"]; Branch = config["general:branch"];
Debug = TrueValues.Contains(config["general:debug"]?.ToLower()); Debug = TrueValues.Contains(config["general:debug"]?.ToLower());
UpdateUrl = config["update:url"]; UpdateUrl = config["update:url"];
UpdateAuto = TrueValues.Contains(config["update:auto"]?.ToLower()); UpdateAuto = TrueValues.Contains(config["update:auto"]?.ToLower());
SyncUrl = config["sync:url"];
SyncUsername = config["sync:username"] ?? "";
SyncPassword = config["sync:password"] ?? "";
SmtpHost = config["smtp:host"]; SmtpHost = config["smtp:host"];
SmtpPort = config["smtp:port"]?.All(char.IsAsciiDigit) == true && config["smtp:port"]?.Length > 0 ? int.Parse(config["smtp:port"]!) : null; SmtpPort = config["smtp:port"]?.All(char.IsAsciiDigit) == true && config["smtp:port"]?.Length > 0 ? int.Parse(config["smtp:port"]!) : null;
@ -79,13 +87,15 @@ namespace Elwig.Helpers {
SmtpPassword = config["smtp:password"]; SmtpPassword = config["smtp:password"];
SmtpFrom = config["smtp:from"]; SmtpFrom = config["smtp:from"];
var scales = config.AsEnumerable().Where(i => i.Key.StartsWith("scale.")).GroupBy(i => i.Key.Split(':')[0][6..]).Select(i => i.Key); var scales = config.AsEnumerable().Where(i => i.Key.StartsWith("scale.")).GroupBy(i => i.Key.Split(':')[0][6..]).Select(i => i.Key).Order();
ScaleList.Clear(); ScaleList.Clear();
Scales = ScaleList; Scales = ScaleList;
foreach (var s in scales) { foreach (var s in scales) {
ScaleList.Add(new( ScaleList.Add(new(
s, config[$"scale.{s}:type"], config[$"scale.{s}:model"], config[$"scale.{s}:connection"], s, config[$"scale.{s}:type"], config[$"scale.{s}:model"], config[$"scale.{s}:connection"],
config[$"scale.{s}:empty"], config[$"scale.{s}:filling"], config[$"scale.{s}:limit"], config[$"scale.{s}:log"] config[$"scale.{s}:empty"], config[$"scale.{s}:filling"], config[$"scale.{s}:limit"],
config[$"scale.{s}:required"] != null ? TrueValues.Contains(config[$"scale.{s}:required"]?.ToLower()) : null,
config[$"scale.{s}:log"]
)); ));
} }
} }

View File

@ -5,17 +5,14 @@ using System.Linq;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Controls.Primitives; using System.Windows.Controls.Primitives;
using System.Windows.Threading;
using Brush = System.Windows.Media.Brush; using Brush = System.Windows.Media.Brush;
using Brushes = System.Windows.Media.Brushes; using Brushes = System.Windows.Media.Brushes;
namespace Elwig.Helpers { namespace Elwig.Helpers {
public class ControlUtils { public class ControlUtils {
public enum RenewSourceDefault { public enum RenewSourceDefault { None, IfOnly, First }
None,
IfOnly,
First
}
private static void SetControlBorderBrush(Control input, Brush brush) { private static void SetControlBorderBrush(Control input, Brush brush) {
if (input is ComboBox cb) { if (input is ComboBox cb) {
@ -84,13 +81,13 @@ namespace Elwig.Helpers {
return null; return null;
} }
public static void RenewItemsSource(Selector selector, IEnumerable? source, Func<object?, object?> getId, SelectionChangedEventHandler? handler = null, RenewSourceDefault def = RenewSourceDefault.None) { public static void RenewItemsSource(Selector selector, IEnumerable? source, SelectionChangedEventHandler? handler = null, RenewSourceDefault def = RenewSourceDefault.None) {
if (selector.ItemsSource == source) if (selector.ItemsSource == source)
return; return;
var selectedId = getId(selector.SelectedItem); var selectedId = Utils.GetEntityIdentifier(selector.SelectedItem);
object? selItem = null; object? selItem = null;
if (selectedId != null && source != null) if (selectedId != 0 && source != null)
selItem = source.Cast<object>().FirstOrDefault(i => selectedId.Equals(getId(i))); selItem = source.Cast<object>().FirstOrDefault(i => selectedId.Equals(Utils.GetEntityIdentifier(i)));
if (source != null && selItem == null) { if (source != null && selItem == null) {
if ((def == RenewSourceDefault.IfOnly && source.Cast<object>().Count() == 1) || def == RenewSourceDefault.First) { if ((def == RenewSourceDefault.IfOnly && source.Cast<object>().Count() == 1) || def == RenewSourceDefault.First) {
selItem = source.Cast<object>().First(); selItem = source.Cast<object>().First();
@ -102,28 +99,16 @@ namespace Elwig.Helpers {
selector.SelectedItem = selItem; selector.SelectedItem = selItem;
} }
public static void RenewItemsSource(Xceed.Wpf.Toolkit.Primitives.Selector selector, IEnumerable? source, Func<object?, object?> getId) { public static void RenewItemsSource(DataGrid dataGrid, IEnumerable? source, SelectionChangedEventHandler? handler = null, RenewSourceDefault def = RenewSourceDefault.None, bool keepSort = true) {
if (selector.ItemsSource == source)
return;
var selectedIds = selector.SelectedItems.Cast<object>().Select(i => getId(i)).ToList();
selector.ItemsSource = source;
if (source != null) {
selector.SelectedItems.Clear();
foreach (var i in source.Cast<object>().Where(i => selectedIds.Contains(getId(i))))
selector.SelectedItems.Add(i);
}
}
public static void RenewItemsSource(DataGrid dataGrid, IEnumerable? source, Func<object?, object?> getId, SelectionChangedEventHandler? handler = null, RenewSourceDefault def = RenewSourceDefault.None, bool keepSort = true) {
if (dataGrid.ItemsSource == source) if (dataGrid.ItemsSource == source)
return; return;
var column = dataGrid.CurrentCell.Column; var column = dataGrid.CurrentCell.Column;
var sortColumns = dataGrid.Columns.Select(c => c.SortDirection).ToList(); var sortColumns = dataGrid.Columns.Select(c => c.SortDirection).ToList();
var sort = dataGrid.Items.SortDescriptions.ToList(); var sort = dataGrid.Items.SortDescriptions.ToList();
var selectedId = getId(dataGrid.SelectedItem); var selectedId = Utils.GetEntityIdentifier(dataGrid.SelectedItem);
object? selItem = null; object? selItem = null;
if (selectedId != null && source != null) if (selectedId != 0 && source != null)
selItem = source.Cast<object>().FirstOrDefault(i => selectedId.Equals(getId(i))); selItem = source.Cast<object>().FirstOrDefault(i => selectedId.Equals(Utils.GetEntityIdentifier(i)));
if (source != null && selItem == null) { if (source != null && selItem == null) {
if ((def == RenewSourceDefault.IfOnly && source.Cast<object>().Count() == 1) || def == RenewSourceDefault.First) { if ((def == RenewSourceDefault.IfOnly && source.Cast<object>().Count() == 1) || def == RenewSourceDefault.First) {
selItem = source.Cast<object>().First(); selItem = source.Cast<object>().First();
@ -143,92 +128,128 @@ namespace Elwig.Helpers {
dataGrid.CurrentCell = new(dataGrid.SelectedItem, column); dataGrid.CurrentCell = new(dataGrid.SelectedItem, column);
} }
public static void RenewItemsSource(ListBox listBox, IEnumerable? source, Func<object?, object?> getId, SelectionChangedEventHandler? handler = null, RenewSourceDefault def = RenewSourceDefault.None) { public static void RenewItemsSource(ListBox listBox, IEnumerable? source, SelectionChangedEventHandler? handler = null, RenewSourceDefault def = RenewSourceDefault.None) {
if (listBox.ItemsSource == source) if (listBox.ItemsSource == source)
return; return;
var selectedId = getId(listBox.SelectedItem); if (listBox.SelectionMode == SelectionMode.Single) {
object? selItem = null; var selectedId = Utils.GetEntityIdentifier(listBox.SelectedItem);
if (selectedId != null && source != null) object? selItem = null;
selItem = source.Cast<object>().FirstOrDefault(i => selectedId.Equals(getId(i))); if (selectedId != 0 && source != null)
if (source != null && selItem == null) { selItem = source.Cast<object>().FirstOrDefault(i => selectedId.Equals(Utils.GetEntityIdentifier(i)));
if ((def == RenewSourceDefault.IfOnly && source.Cast<object>().Count() == 1) || def == RenewSourceDefault.First) { if (source != null && selItem == null) {
selItem = source.Cast<object>().FirstOrDefault(); if ((def == RenewSourceDefault.IfOnly && source.Cast<object>().Count() == 1) || def == RenewSourceDefault.First) {
selItem = source.Cast<object>().FirstOrDefault();
}
} }
if (handler != null && selItem != null) listBox.SelectionChanged -= handler;
listBox.ItemsSource = source;
if (handler != null && selItem != null) listBox.SelectionChanged += handler;
listBox.SelectedItem = selItem;
} else {
var selectedIds = listBox.SelectedItems.Cast<object>().Select(Utils.GetEntityIdentifier).ToList();
if (handler != null && selectedIds != null) listBox.SelectionChanged -= handler;
listBox.ItemsSource = source;
if (source != null && selectedIds != null) {
listBox.SelectedItems.Clear();
foreach (var i in source.Cast<object>().Where(i => selectedIds.Contains(Utils.GetEntityIdentifier(i))))
listBox.SelectedItems.Add(i);
}
if (handler != null && selectedIds != null) listBox.SelectionChanged += handler;
} }
if (handler != null && selItem != null) listBox.SelectionChanged -= handler;
listBox.ItemsSource = source;
if (handler != null && selItem != null) listBox.SelectionChanged += handler;
listBox.SelectedItem = selItem;
} }
public static object? GetItemFromSource(IEnumerable source, Func<object?, object?> getId, object? id) { public static object? GetItemFromSource(IEnumerable source, int? hash) {
if (source == null) if (source == null)
return null; return null;
var items = source.Cast<object>(); var items = source.Cast<object>();
var item = items.Where(i => getId(i)?.Equals(id) ?? false).FirstOrDefault(); var item = items.Where(i => Utils.GetEntityIdentifier(i) == hash).FirstOrDefault();
if (item == null && items.Any(i => i is NullItem)) if (item == null && items.Any(i => i is NullItem))
return items.Where(i => i is NullItem).First(); return items.Where(i => i is NullItem).First();
return item; return item;
} }
public static object? GetItemFromSource(IEnumerable source, object? item, Func<object?, object?> getId) { public static T? GetItemFromSource<T>(IEnumerable source, T? item) {
return GetItemFromSource(source, getId, getId(item)); return (T?)GetItemFromSource(source, Utils.GetEntityIdentifier(item));
} }
public static void SelectComboBoxItem(ComboBox cb, Func<object?, object?> getId, object? id) { public static object? GetItemFromSourceWithPk(IEnumerable source, params object?[] primaryKey) {
cb.SelectedItem = GetItemFromSource(cb.ItemsSource, getId, id); return GetItemFromSource(source, (int?)Utils.GetEntityIdetifierForPk(primaryKey));
} }
public static void SelectComboBoxItem(ComboBox cb, object? item, Func<object?, object?> getId) { public static void SelectItemWithHash(Selector input, int? hash) {
SelectComboBoxItem(cb, getId, getId(item)); if (hash == null) {
input.SelectedItem = null;
} else {
input.SelectedItem = GetItemFromSource(input.ItemsSource, hash);
}
if (input is ListBox lb && lb.SelectedItem is object lbItem) {
lb.ScrollIntoView(lbItem);
} else if (input is DataGrid dg && dg.SelectedItem is object dgItem) {
dg.ScrollIntoView(dgItem);
}
} }
public static void SelectListBoxItem(ListBox lb, Func<object?, object?> getId, object? id) { public static void SelectItemWithPk(Selector input, params object?[] pk) {
lb.SelectedItem = GetItemFromSource(lb.ItemsSource, getId, id); SelectItemWithHash(input, Utils.GetEntityIdentifier(pk));
lb.ScrollIntoView(lb.SelectedItem);
} }
public static void SelectListBoxItem(ListBox lb, object? item, Func<object?, object?> getId) { public static void SelectItem(Selector input, object? item) {
SelectListBoxItem(lb, getId, getId(item)); SelectItemWithHash(input, Utils.GetEntityIdentifier(item));
} }
public static IEnumerable<object?> GetItemsFromSource(IEnumerable source, Func<object?, object?> getId, IEnumerable<object?> ids) { public static IEnumerable<object?> GetItemsFromSource(IEnumerable source, IEnumerable<int?> ids) {
if (source == null) if (source == null)
return Array.Empty<object>(); return [];
return source.Cast<object>().Where(i => ids.Any(c => c?.Equals(getId(i)) ?? false)); return source.Cast<object>().Where(i => ids.Any(c => c == Utils.GetEntityIdentifier(i)));
} }
public static IEnumerable<object?> GetItemsFromSource(IEnumerable source, IEnumerable<object?>? items, Func<object?, object?> getId) { public static IEnumerable<object?> GetItemsFromSource(IEnumerable source, IEnumerable<object?>? items) {
if (items == null) if (items == null)
return Array.Empty<object>(); return [];
return GetItemsFromSource(source, getId, items.Select(i => getId(i))); return GetItemsFromSource(source, items.Select(Utils.GetEntityIdentifier));
} }
public static void SelectCheckComboBoxItems(Xceed.Wpf.Toolkit.CheckComboBox ccb, Func<object?, object?> getId, IEnumerable<object?>? ids) { public static void SelectItems(ListBox lb, IEnumerable<int?>? ids) {
ccb.SelectedItems.Clear(); lb.SelectedItems.Clear();
if (ids == null) return; if (ids == null) return;
foreach (var id in ids) foreach (var id in ids)
ccb.SelectedItems.Add(GetItemFromSource(ccb.ItemsSource, getId, id)); lb.SelectedItems.Add(GetItemFromSource(lb.ItemsSource, id));
} }
public static void SelectCheckComboBoxItems(Xceed.Wpf.Toolkit.CheckComboBox ccb, IEnumerable<object>? items, Func<object?, object?> getId) { public static void SelectItems(ListBox lb, IEnumerable<object>? items) {
SelectCheckComboBoxItems(ccb, getId, items?.Select(i => getId(i))); SelectItems(lb, items?.Select(Utils.GetEntityIdentifier));
} }
public static object? GetInputValue(Control input) { public static int? GetInputHashCode(Control input) {
if (input is TextBox tb) { if (input is TextBox tb) {
return tb.Text; return Utils.GetEntityIdentifier(tb.Text);
} else if (input is ComboBox sb) { } else if (input is ComboBox sb) {
return sb.SelectedItem; return Utils.GetEntityIdentifier(sb.SelectedItem);
} else if (input is Xceed.Wpf.Toolkit.CheckComboBox ccb) { } else if (input is ListBox lb) {
return ccb.SelectedItems.Cast<object>().ToArray(); return Utils.GetEntityIdentifier(lb.SelectedItems);
} else if (input is CheckBox cb) { } else if (input is CheckBox cb) {
return cb.IsChecked?.ToString(); return Utils.GetEntityIdentifier(cb.IsChecked);
} else if (input is RadioButton rb) { } else if (input is RadioButton rb) {
return rb.IsChecked?.ToString(); return Utils.GetEntityIdentifier(rb.IsChecked);
} else { } else {
return null; return null;
} }
} }
public static void InitializeDelayTimer(TextBox tb, Action<object, TextChangedEventArgs> handler) {
var timer = new DispatcherTimer {
Interval = TimeSpan.FromMilliseconds(250)
};
timer.Tick += (object? sender, EventArgs evt) => {
timer.Stop();
var (oSender, oEvent) = ((object, TextChangedEventArgs))timer.Tag;
handler(oSender, oEvent);
};
tb.TextChanged += (object sender, TextChangedEventArgs evt) => {
timer.Stop();
timer.Tag = (sender, evt);
timer.Start();
};
}
} }
} }

View File

@ -22,20 +22,20 @@ namespace Elwig.Helpers.Export {
"""; """;
var c = App.Client; var c = App.Client;
var (a1, a2) = Utils.SplitAddress(c.Address); var (a1, a2) = Utils.SplitAddress(c.Address);
_clientData = $"{c.LfbisNr};{c.NameFull};;{a1};{a2};{c.Plz};{c.Ort}"; _clientData = $"{c.LfbisNr};{c.NameFull};;{a1};\t{a2};{c.Plz};{c.Ort}";
} }
public async Task ExportAsync(int year) { public async Task ExportAsync(int year) {
using var cnx = await AppDbContext.ConnectAsync(); using var cnx = await AppDbContext.ConnectAsync();
using var cmd = cnx.CreateCommand(); using var cmd = cnx.CreateCommand();
cmd.CommandText = $""" cmd.CommandText = $"""
SELECT lfbis_nr, family_name, name, billing_name, address, plz, ort, area, SELECT lfbis_nr, name, other_names, billing_name, address, plz, ort, area,
date, weight, type, sortid, qualid, year, hkid, kmw, oe date, weight, type, sortid, qualid, year, hkid, kmw, oe
FROM v_bki_delivery FROM v_bki_delivery
WHERE year = {year} WHERE year = {year}
"""; """;
var r = await cmd.ExecuteReaderAsync(); var r = await cmd.ExecuteReaderAsync();
List<Row> rows = new(); List<Row> rows = [];
while (await r.ReadAsync()) { while (await r.ReadAsync()) {
rows.Add(new( rows.Add(new(
(r.IsDBNull(0) ? null : r.GetString(0), r.IsDBNull(1) ? null : r.GetString(1), r.IsDBNull(2) ? null : r.GetString(2), r.IsDBNull(3) ? null : r.GetString(3), r.GetString(4), r.GetInt32(5), r.GetString(6), r.GetInt32(7)), (r.IsDBNull(0) ? null : r.GetString(0), r.IsDBNull(1) ? null : r.GetString(1), r.IsDBNull(2) ? null : r.GetString(2), r.IsDBNull(3) ? null : r.GetString(3), r.GetString(4), r.GetInt32(5), r.GetString(6), r.GetInt32(7)),
@ -57,7 +57,7 @@ namespace Elwig.Helpers.Export {
var (n1, n2) = billingName == null ? (familyName, name) : Utils.SplitName(billingName, familyName); var (n1, n2) = billingName == null ? (familyName, name) : Utils.SplitName(billingName, familyName);
var (a1, a2) = Utils.SplitAddress(address); var (a1, a2) = Utils.SplitAddress(address);
var memberData = $"{lfBisNr};{n1};{n2};{a1};{a2};{plz};{ort}"; var memberData = $"{lfBisNr};{n1};{n2};{a1};\t{a2};{plz};{ort}";
var deliveryData = $"{string.Join(".", date.Split("-").Reverse())};{weight};TB;{(type == "W" ? "J" : "")};{(type == "R" ? "J" : "")};{sortid};;;{qualid};{year};{hkid};{kmw:0.0};{oe:0}"; var deliveryData = $"{string.Join(".", date.Split("-").Reverse())};{weight};TB;{(type == "W" ? "J" : "")};{(type == "R" ? "J" : "")};{sortid};;;{qualid};{year};{hkid};{kmw:0.0};{oe:0}";
var vollData = $"N;;;{area / 10_000.0}"; var vollData = $"N;;;{area / 10_000.0}";

View File

@ -1,15 +1,17 @@
using Elwig.Models;
using Elwig.Models.Dtos; using Elwig.Models.Dtos;
using Elwig.Models.Entities; using Elwig.Models.Entities;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Security; using System.Security;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Elwig.Helpers.Export { namespace Elwig.Helpers.Export {
public class Ebics(PaymentVar variant, string filename, int version) : IBankingExporter { public class Ebics(PaymentVar variant, string filename, int version, Ebics.AddressMode mode = Ebics.AddressMode.Full) : IBankingExporter {
public enum AddressMode { Omit = 0, Lines = 1, Full = 2 }
public static string FileExtension => "xml"; public static string FileExtension => "xml";
@ -19,6 +21,7 @@ namespace Elwig.Helpers.Export {
private readonly string Name = variant.Name; private readonly string Name = variant.Name;
private readonly int AvNr = variant.AvNr; private readonly int AvNr = variant.AvNr;
private readonly int Version = version; private readonly int Version = version;
private readonly AddressMode ShowAddresses = mode;
public void Dispose() { public void Dispose() {
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
@ -35,8 +38,11 @@ namespace Elwig.Helpers.Export {
} }
public async Task ExportAsync(IEnumerable<Transaction> transactions, IProgress<double>? progress = null) { public async Task ExportAsync(IEnumerable<Transaction> transactions, IProgress<double>? progress = null) {
if (transactions.Any(tx => tx.Amount < 0)) if (transactions.Any(tx => tx.Amount < 0)) {
throw new ArgumentException("Tranaction amount may not be negative"); throw new ArgumentException("Tranaction amount may not be negative");
} else if (App.Client.Iban == null) {
throw new ArgumentException("Client IBAN has to be set");
}
progress?.Report(0.0); progress?.Report(0.0);
var nbOfTxs = transactions.Count(); var nbOfTxs = transactions.Count();
int count = nbOfTxs + 2, i = 0; int count = nbOfTxs + 2, i = 0;
@ -77,12 +83,19 @@ namespace Elwig.Helpers.Export {
<PmtId><EndToEndId>{id}</EndToEndId></PmtId> <PmtId><EndToEndId>{id}</EndToEndId></PmtId>
<Amt><InstdAmt Ccy="{tx.Currency}">{Transaction.FormatAmount(tx.Amount)}</InstdAmt></Amt> <Amt><InstdAmt Ccy="{tx.Currency}">{Transaction.FormatAmount(tx.Amount)}</InstdAmt></Amt>
<Cdtr> <Cdtr>
<Nm>{SecurityElement.Escape(a.Name[..Math.Min(140, a.Name.Length)])}</Nm> <Nm>{SecurityElement.Escape(a.FullName[..Math.Min(140, a.FullName.Length)])}</Nm>
<PstlAdr> """);
<StrtNm>{a1?[..Math.Min(70, a1.Length)]}</StrtNm><BldgNb>{SecurityElement.Escape(a2?[..Math.Min(16, a2.Length)])}</BldgNb> if (ShowAddresses != AddressMode.Omit) {
<PstCd>{a.PostalDest.AtPlz?.Plz}</PstCd><TwnNm>{SecurityElement.Escape(a.PostalDest.AtPlz?.Ort.Name)}</TwnNm> var full = ShowAddresses == AddressMode.Full;
<Ctry>{a.PostalDest.Country.Alpha2}</Ctry> await Writer.WriteLineAsync($"""
</PstlAdr> <PstlAdr>{(full ? "" : $"\r\n <Ctry>{a.PostalDest.Country.Alpha2}</Ctry>")}
{(full ? $"<StrtNm>{SecurityElement.Escape(a1?[..Math.Min(70, a1.Length)])}</StrtNm> <BldgNb>{SecurityElement.Escape(a2?[..Math.Min(16, a2.Length)])}</BldgNb>" :
$"<AdrLine>{SecurityElement.Escape(a.Address[..Math.Min(70, a.Address.Length)])}</AdrLine>")}
<{(full ? "PstCd" : "AdrLine")}>{a.PostalDest.AtPlz?.Plz}{(full ? "</PstCd> <TwnNm>" : " ")}{SecurityElement.Escape(a.PostalDest.AtPlz?.Ort.Name)}</{(full ? "TwnNm" : "AdrLine")}>
{(full ? $" <Ctry>{a.PostalDest.Country.Alpha2}</Ctry>\r\n " : "")}</PstlAdr>
""");
}
await Writer.WriteLineAsync($"""
</Cdtr> </Cdtr>
<CdtrAcct><Id><IBAN>{tx.Member.Iban!}</IBAN></Id></CdtrAcct> <CdtrAcct><Id><IBAN>{tx.Member.Iban!}</IBAN></Id></CdtrAcct>
<RmtInf><Ustrd>{SecurityElement.Escape(info)}</Ustrd></RmtInf> <RmtInf><Ustrd>{SecurityElement.Escape(info)}</Ustrd></RmtInf>

View File

@ -0,0 +1,667 @@
using System.IO.Compression;
using System.IO;
using System.Threading.Tasks;
using Elwig.Models.Entities;
using System.Collections.Generic;
using System;
using System.Text.Json.Nodes;
using System.Linq;
using System.Windows;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
namespace Elwig.Helpers.Export {
public static class ElwigData {
public enum ImportMode { Auto, Interactively, FromBranches }
public static readonly string ImportedTxt = Path.Combine(App.DataPath, "imported.txt");
private static readonly JsonSerializerOptions JsonOpts = new() { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
public static async Task<string[]> GetImportedFiles() {
try {
return await File.ReadAllLinesAsync(ImportedTxt, Utils.UTF8);
} catch {
return [];
}
}
public static async Task AddImportedFiles(params string[] filenames) {
await File.AppendAllLinesAsync(ImportedTxt, filenames, Utils.UTF8);
}
public static Task Import(string filename, ImportMode mode) => Import([filename], mode);
public static async Task Import(IEnumerable<string> filenames, ImportMode mode) {
try {
Dictionary<string, Branch> branches;
Dictionary<int, int> currentDids;
Dictionary<string, int> currentLsNrs;
Dictionary<int, List<WbRd>> currentWbRde;
Dictionary<int, AT_Kg> kgs;
using (var ctx = new AppDbContext()) {
branches = await ctx.Branches.ToDictionaryAsync(b => b.ZwstId);
currentDids = await ctx.Deliveries
.GroupBy(d => d.Year)
.ToDictionaryAsync(g => g.Key, g => g.Max(d => d.DId));
currentLsNrs = await ctx.Deliveries
.ToDictionaryAsync(d => d.LsNr, d => d.DId);
currentWbRde = await ctx.WbRde
.GroupBy(r => r.KgNr)
.ToDictionaryAsync(g => g.Key, g => g.ToList());
kgs = await ctx.Katastralgemeinden.Include(k => k.WbKg).ToDictionaryAsync(k => k.KgNr);
}
var data = new List<(
List<Member> Members,
List<BillingAddr> BillingAddresses,
List<MemberTelNr> TelephoneNumbers,
List<MemberEmailAddr> EmailAddresses,
List<AreaCom> AreaCommitments,
List<WbRd> Riede,
List<Delivery> Deliveries,
List<DeliveryPart> DeliveryParts,
List<DeliveryPartModifier> Modifiers)>();
var metaData = new List<(string FileName, string ZwstId, string Device,
int? MemberNum, string? MemberFilters,
int? AreaComNum, string? AreaComFilters,
int? DeliveryNum, string? DeliveryFilters)>();
foreach (var filename in filenames) {
// TODO read encrypted files
using var zip = ZipFile.Open(filename, ZipArchiveMode.Read);
var version = zip.GetEntry("version");
using (var reader = new StreamReader(version!.Open(), Utils.UTF8)) {
if (await reader.ReadToEndAsync() != "elwig:1")
throw new FileFormatException($"Ungültige Export-Datei ({filename})");
}
var metaJson = zip.GetEntry("meta.json");
var meta = await JsonNode.ParseAsync(metaJson!.Open());
var memberCount = meta!["members"]?["count"]?.AsValue().GetValue<int>();
var memberFilters = meta!["members"]?["filters"]?.AsArray().Select(f => f!.AsValue().GetValue<string>()).ToArray();
var areaComCount = meta!["area_commitments"]?["count"]?.AsValue().GetValue<int>();
var areaComFilters = meta!["area_commitments"]?["filters"]?.AsArray().Select(f => f!.AsValue().GetValue<string>()).ToArray();
var deliveryCount = meta!["deliveries"]?["count"]?.AsValue().GetValue<int>();
var deliveryFilters = meta!["deliveries"]?["filters"]?.AsArray().Select(f => f!.AsValue().GetValue<string>()).ToArray();
metaData.Add((Path.GetFileName(filename),
meta["zwstid"]!.AsValue().GetValue<string>(), meta["device"]!.AsValue().GetValue<string>(),
memberCount, memberFilters != null ? string.Join(" / ", memberFilters) : null,
areaComCount, areaComFilters != null ? string.Join(" / ", areaComFilters) : null,
deliveryCount, deliveryFilters != null ? string.Join(" / ", deliveryFilters) : null));
data.Add(new([], [], [], [], [], [], [], new([], [])));
var r = data[^1];
var membersJson = zip.GetEntry("members.json");
if (membersJson != null) {
using var reader = new StreamReader(membersJson.Open(), Utils.UTF8);
string? line;
while ((line = await reader.ReadLineAsync()) != null) {
var obj = JsonNode.Parse(line)!.AsObject();
var (m, b, telNrs, emailAddrs) = obj.ToMember(kgs);
r.Members.Add(m);
if (b != null) r.BillingAddresses.Add(b);
r.TelephoneNumbers.AddRange(telNrs);
r.EmailAddresses.AddRange(emailAddrs);
}
}
var areaComsJson = zip.GetEntry("area_commitments.json");
if (areaComsJson != null) {
using var reader = new StreamReader(areaComsJson.Open(), Utils.UTF8);
string? line;
while ((line = await reader.ReadLineAsync()) != null) {
var obj = JsonNode.Parse(line)!.AsObject();
var (areaCom, wbrd) = obj.ToAreaCom(kgs, currentWbRde);
r.AreaCommitments.Add(areaCom);
if (wbrd != null) {
currentWbRde[wbrd.KgNr].Add(wbrd);
r.Riede.Add(wbrd);
}
}
}
var deliveriesJson = zip.GetEntry("deliveries.json");
if (deliveriesJson != null) {
using var reader = new StreamReader(deliveriesJson.Open(), Utils.UTF8);
string? line;
while ((line = await reader.ReadLineAsync()) != null) {
var obj = JsonNode.Parse(line)!.AsObject();
var (d, parts, mods) = obj.ToDelivery(currentLsNrs, currentDids);
r.Deliveries.Add(d);
r.DeliveryParts.AddRange(parts);
r.Modifiers.AddRange(mods);
}
}
}
var importedMembers = new List<(string FileName, string ZwstId, string Device, int New, int Overwritten, int NotImported, string Filters)>();
var importedAreaComs = new List<(string FileName, string ZwstId, string Device, int Imported, int NotImported, string Filters)>();
var importedDeliveries = new List<(string FileName, string ZwstId, string Device, int New, int Overwritten, int NotImported, string Filters)>();
foreach (var ((members, billingAddresses, telephoneNumbers, emailAddresses, areaCommitments, riede, deliveries, deliveryParts, modifiers), meta) in data.Zip(metaData)) {
var branch = branches[meta.ZwstId];
var device = meta.Device;
using var ctx = new AppDbContext();
var mgnrs = members.Select(m => m.MgNr).ToList();
var duplicateMgNrs = await ctx.Members
.Where(m => mgnrs.Contains(m.MgNr))
.Select(m => m.MgNr)
.ToListAsync();
bool importNewMembers = false, importDuplicateMembers = false;
if (mode == ImportMode.Interactively) {
if (mgnrs.Count - duplicateMgNrs.Count > 0)
importNewMembers = ImportQuestion(branch.Name, device, "Mitglieder", false, mgnrs.Count - duplicateMgNrs.Count);
} else {
importNewMembers = true;
}
if (duplicateMgNrs.Count > 0)
importDuplicateMembers = ImportQuestion(branch.Name, device, "Mitglieder", true, duplicateMgNrs.Count);
var fbnrs = areaCommitments.Select(c => c.FbNr).ToList();
var duplicateFbNrs = await ctx.AreaCommitments
.Where(c => fbnrs.Contains(c.FbNr))
.Select(c => c.FbNr)
.ToListAsync();
var lsnrs = deliveries.Select(d => d.LsNr).ToList();
var duplicateLsNrs = await ctx.Deliveries
.Where(d => lsnrs.Contains(d.LsNr))
.Select(d => d.LsNr)
.ToListAsync();
var duplicateDIds = deliveries
.Where(d => duplicateLsNrs.Contains(d.LsNr))
.Select(d => (d.Year, d.DId))
.ToList();
var allowedDuplicateLsNrs = new List<string>();
bool importNewDeliveries = false, importDuplicateDeliveries = false;
if (mode == ImportMode.Interactively) {
if (lsnrs.Count - duplicateLsNrs.Count > 0)
importNewDeliveries = ImportQuestion(branch.Name, device, "Lieferungen", false, lsnrs.Count - duplicateLsNrs.Count);
if (duplicateLsNrs.Count > 0)
importDuplicateDeliveries = ImportQuestion(branch.Name, device, "Lieferungen", true, duplicateLsNrs.Count);
} else if (mode == ImportMode.FromBranches) {
importNewDeliveries = true;
if (duplicateLsNrs.Count > 0) {
allowedDuplicateLsNrs = await ctx.Deliveries
.Where(d => lsnrs.Contains(d.LsNr) && d.ZwstId == branch.ZwstId)
.Select(d => d.LsNr)
.ToListAsync();
if (duplicateLsNrs.Count - allowedDuplicateLsNrs.Count > 0)
importDuplicateDeliveries = ImportQuestion(branch.Name, device, "Lieferungen", true, duplicateLsNrs.Count - allowedDuplicateLsNrs.Count);
}
} else {
importNewDeliveries = true;
if (duplicateLsNrs.Count > 0)
importDuplicateDeliveries = ImportQuestion(branch.Name, device, "Lieferungen", true, duplicateLsNrs.Count);
}
if (importDuplicateMembers) {
ctx.RemoveRange(ctx.BillingAddresses.Where(a => duplicateMgNrs.Contains(a.MgNr)));
ctx.RemoveRange(ctx.MemberTelephoneNrs.Where(n => duplicateMgNrs.Contains(n.MgNr)));
ctx.RemoveRange(ctx.MemberEmailAddrs.Where(a => duplicateMgNrs.Contains(a.MgNr)));
ctx.UpdateRange(members.Where(m => duplicateMgNrs.Contains(m.MgNr)));
ctx.AddRange(billingAddresses.Where(a => duplicateMgNrs.Contains(a.MgNr)));
ctx.AddRange(telephoneNumbers.Where(n => duplicateMgNrs.Contains(n.MgNr)));
ctx.AddRange(emailAddresses.Where(a => duplicateMgNrs.Contains(a.MgNr)));
ctx.UpdateRange(areaCommitments.Where(c => duplicateMgNrs.Contains(c.MgNr) && duplicateFbNrs.Contains(c.FbNr)));
ctx.AddRange(areaCommitments.Where(c => duplicateMgNrs.Contains(c.MgNr) && !duplicateFbNrs.Contains(c.FbNr)));
}
if (importNewMembers) {
ctx.AddRange(members.Where(m => !duplicateMgNrs.Contains(m.MgNr)));
ctx.AddRange(billingAddresses.Where(a => !duplicateMgNrs.Contains(a.MgNr)));
ctx.AddRange(telephoneNumbers.Where(n => !duplicateMgNrs.Contains(n.MgNr)));
ctx.AddRange(emailAddresses.Where(a => !duplicateMgNrs.Contains(a.MgNr)));
ctx.UpdateRange(areaCommitments.Where(c => !duplicateMgNrs.Contains(c.MgNr) && duplicateFbNrs.Contains(c.FbNr)));
ctx.AddRange(areaCommitments.Where(c => !duplicateMgNrs.Contains(c.MgNr) && !duplicateFbNrs.Contains(c.FbNr)));
}
if (members.Count > 0) {
var n = importNewMembers ? members.Count - duplicateMgNrs.Count : 0;
var o = importDuplicateMembers ? duplicateMgNrs.Count : 0;
importedMembers.Add((meta.FileName, meta.ZwstId, meta.Device, n, o, members.Count - n - o, meta.MemberFilters));
}
if (areaCommitments.Count > 0) {
ctx.AddRange(riede);
var imported = areaCommitments.Where(c => (importNewMembers && !duplicateMgNrs.Contains(c.MgNr)) || (importDuplicateMembers && duplicateMgNrs.Contains(c.MgNr))).ToList();
importedAreaComs.Add((meta.FileName, meta.ZwstId, meta.Device, imported.Count, areaCommitments.Count - imported.Count, meta.AreaComFilters));
}
if (allowedDuplicateLsNrs.Count > 0) {
var dids = deliveries
.Where(d => allowedDuplicateLsNrs.Contains(d.LsNr))
.Select(d => (d.Year, d.DId))
.ToList();
ctx.RemoveRange(ctx.DeliveryParts
.Where(p => allowedDuplicateLsNrs.Contains(p.Delivery.LsNr))
.SelectMany(p => p.PartModifiers));
ctx.RemoveRange(ctx.DeliveryParts.Where(p => allowedDuplicateLsNrs.Contains(p.Delivery.LsNr)));
ctx.UpdateRange(deliveries.Where(d => dids.Contains((d.Year, d.DId))));
ctx.AddRange(deliveryParts.Where(p => dids.Contains((p.Year, p.DId))));
ctx.AddRange(modifiers.Where(m => dids.Contains((m.Year, m.DId))));
}
if (importDuplicateDeliveries) {
var l = duplicateLsNrs.Except(allowedDuplicateLsNrs).ToList();
var dids = deliveries
.Where(d => l.Contains(d.LsNr))
.Select(d => (d.Year, d.DId))
.ToList();
ctx.RemoveRange(ctx.DeliveryParts
.Where(p => l.Contains(p.Delivery.LsNr))
.SelectMany(p => p.PartModifiers));
ctx.RemoveRange(ctx.DeliveryParts.Where(p => l.Contains(p.Delivery.LsNr)));
ctx.UpdateRange(deliveries.Where(d => dids.Contains((d.Year, d.DId))));
ctx.AddRange(deliveryParts.Where(p => dids.Contains((p.Year, p.DId))));
ctx.AddRange(modifiers.Where(m => dids.Contains((m.Year, m.DId))));
}
if (importNewDeliveries) {
ctx.AddRange(deliveries.Where(d => !duplicateDIds.Contains((d.Year, d.DId))));
ctx.AddRange(deliveryParts.Where(p => !duplicateDIds.Contains((p.Year, p.DId))));
ctx.AddRange(modifiers.Where(m => !duplicateDIds.Contains((m.Year, m.DId))));
}
if (deliveries.Count > 0) {
var n = importNewDeliveries ? deliveries.Count - duplicateDIds.Count : 0;
var o = allowedDuplicateLsNrs.Count + (importDuplicateDeliveries ? duplicateDIds.Count - allowedDuplicateLsNrs.Count : 0);
importedDeliveries.Add((meta.FileName, meta.ZwstId, meta.Device, n, o, deliveries.Count - n - o, meta.DeliveryFilters));
}
await ctx.SaveChangesAsync();
await AddImportedFiles(Path.GetFileName(meta.FileName));
}
App.HintContextChange();
MessageBox.Show(
$"Das importieren der Daten war erfolgreich!\n" +
$"Folgendes wurde importiert:\n" +
string.Join("\n", [
$"Mitglieder: {importedMembers.Sum(d => d.New + d.Overwritten)}",
..importedMembers.Select(d =>
$" {d.FileName} ({d.New + d.Overwritten})\n" +
$" ({d.New} neu, {d.Overwritten} überschrieben, {d.NotImported} nicht importiert)\n" +
$" Zweigstelle: {branches[d.ZwstId].Name} (Gerät {d.Device})\n" +
$" Filter: {d.Filters}"),
$"Flächenbindungen: {importedAreaComs.Sum(d => d.Imported)}",
..importedAreaComs.Select(d =>
$" {d.FileName} ({d.Imported})\n" +
$" ({d.Imported} importiert, {d.NotImported} nicht importiert)\n" +
$" Zweigstelle: {branches[d.ZwstId].Name} (Gerät {d.Device})\n" +
$" Filter: {d.Filters}"),
$"Lieferungen: {importedDeliveries.Sum(d => d.New + d.Overwritten)}",
..importedDeliveries.Select(d =>
$" {d.FileName} ({d.New + d.Overwritten})\n" +
$" ({d.New} neu, {d.Overwritten} überschr., {d.NotImported} nicht importiert)\n" +
$" Zwst.: {branches[d.ZwstId].Name} (Gerät {d.Device})\n" +
$" Filter: {d.Filters}")
]),
"Importieren erfolgreich",
MessageBoxButton.OK, MessageBoxImage.Information);
} catch (Exception exc) {
var str = "Der Eintrag konnte nicht in der Datenbank aktualisiert werden!\n\n" + exc.Message;
if (exc.InnerException != null) str += "\n\n" + exc.InnerException.Message;
MessageBox.Show(str, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
GC.Collect();
GC.WaitForPendingFinalizers();
}
private static bool ImportQuestion(string branch, string device, string subject, bool duplicate, int number) {
return MessageBox.Show(
$"Sollen {number} {(duplicate ? "" : "neue ")}{subject} durch die Zweigstelle\n" +
$"{branch} (Gerät {device}) {(duplicate ? "überschrieben" : "importiert")} werden?",
$"{subject} importieren",
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.Yes
) == MessageBoxResult.Yes;
}
public static Task Export(string filename, IEnumerable<Member> members, IEnumerable<string> filters) {
return new ElwigExport {
Members = (members, filters)
}.Export(filename);
}
public static Task Export(string filename, IEnumerable<Member> members, IEnumerable<AreaCom> areaComs, IEnumerable<string> filters) {
return new ElwigExport {
Members = (members, filters),
AreaComs = (areaComs, ["von exportierten Mitgliedern"]),
}.Export(filename);
}
public static Task Export(string filename, IEnumerable<Delivery> deliveries, IEnumerable<string> filters) {
return new ElwigExport {
Deliveries = (deliveries, filters)
}.Export(filename);
}
public class ElwigExport {
public (IEnumerable<Member> Members, IEnumerable<string> Filters)? Members { get; set; }
public (IEnumerable<AreaCom> AreaComs, IEnumerable<string> Filters)? AreaComs { get; set; }
public (IEnumerable<Delivery> Deliveries, IEnumerable<string> Filters)? Deliveries { get; set; }
public async Task Export(string filename) {
File.Delete(filename);
using var zip = ZipFile.Open(filename, ZipArchiveMode.Create);
var version = zip.CreateEntry("version", CompressionLevel.NoCompression);
using (var writer = new StreamWriter(version.Open(), Utils.UTF8)) {
await writer.WriteAsync("elwig:1");
}
var meta = zip.CreateEntry("meta.json", CompressionLevel.NoCompression);
using (var writer = new StreamWriter(meta.Open(), Utils.UTF8)) {
var obj = new JsonObject {
["timestamp"] = $"{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}",
["zwstid"] = App.ZwstId,
["device"] = Environment.MachineName,
};
if (Members != null)
obj["members"] = new JsonObject {
["count"] = Members.Value.Members.Count(),
["filters"] = new JsonArray(Members.Value.Filters.Select(f => (JsonNode)f).ToArray()),
};
if (AreaComs != null)
obj["area_commitments"] = new JsonObject {
["count"] = AreaComs.Value.AreaComs.Count(),
["filters"] = new JsonArray(AreaComs.Value.Filters.Select(f => (JsonNode)f).ToArray()),
};
if (Deliveries != null)
obj["deliveries"] = new JsonObject {
["count"] = Deliveries.Value.Deliveries.Count(),
["parts"] = Deliveries.Value.Deliveries.Sum(d => d.Parts.Count),
["filters"] = new JsonArray(Deliveries.Value.Filters.Select(f => (JsonNode)f).ToArray()),
};
await writer.WriteAsync(obj.ToJsonString(JsonOpts));
}
// TODO encrypt files
if (Members != null) {
var json = zip.CreateEntry("members.json", CompressionLevel.SmallestSize);
using var writer = new StreamWriter(json.Open(), Utils.UTF8);
foreach (var m in Members.Value.Members) {
await writer.WriteLineAsync(m.ToJson().ToJsonString(JsonOpts));
}
}
if (AreaComs != null) {
var json = zip.CreateEntry("area_commitments.json", CompressionLevel.SmallestSize);
using var writer = new StreamWriter(json.Open(), Utils.UTF8);
foreach (var c in AreaComs.Value.AreaComs) {
await writer.WriteLineAsync(c.ToJson().ToJsonString(JsonOpts));
}
}
if (Deliveries != null) {
var json = zip.CreateEntry("deliveries.json", CompressionLevel.SmallestSize);
using var writer = new StreamWriter(json.Open(), Utils.UTF8);
foreach (var d in Deliveries.Value.Deliveries) {
await writer.WriteLineAsync(d.ToJson().ToJsonString(JsonOpts));
}
}
}
}
public static JsonObject ToJson(this Member m) {
return new JsonObject {
["mgnr"] = m.MgNr,
["predecessor_mgnr"] = m.PredecessorMgNr,
["name"] = m.Name,
["prefix"] = m.Prefix,
["given_name"] = m.GivenName,
["middle_names"] = m.MiddleName,
["suffix"] = m.Suffix,
["attn"] = m.ForTheAttentionOf,
["birthday"] = m.Birthday,
["entry_date"] = m.EntryDate != null ? $"{m.EntryDate:yyyy-MM-dd}" : null,
["exit_date"] = m.ExitDate != null ? $"{m.ExitDate:yyyy-MM-dd}" : null,
["business_shares"] = m.BusinessShares,
["accounting_nr"] = m.AccountingNr,
["zwstid"] = m.ZwstId,
["lfbis_nr"] = m.LfbisNr,
["ustid_nr"] = m.UstIdNr,
["juridical_pers"] = m.IsJuridicalPerson,
["volllieferant"] = m.IsVollLieferant,
["buchführend"] = m.IsBuchführend,
["organic"] = m.IsOrganic,
["funktionär"] = m.IsFunktionär,
["active"] = m.IsActive,
["deceased"] = m.IsDeceased,
["iban"] = m.Iban,
["bic"] = m.Bic,
["default_kgnr"] = m.DefaultKgNr,
["contact_postal"] = m.ContactViaPost,
["contact_email"] = m.ContactViaEmail,
["address"] = new JsonObject {
["address"] = m.Address,
["postal_dest"] = m.PostalDestId,
["country"] = m.CountryNum,
},
["billing_address"] = m.BillingAddress != null ? new JsonObject {
["name"] = m.BillingAddress.FullName,
["address"] = m.BillingAddress.Address,
["postal_dest"] = m.BillingAddress.PostalDestId,
["country"] = m.BillingAddress.CountryNum,
} : null,
["telephone_numbers"] = new JsonArray(m.TelephoneNumbers.OrderBy(n => n.Nr).Select(n => {
var obj = new JsonObject {
["number"] = n.Number,
["type"] = n.Type,
};
if (n.Comment != null) obj["comment"] = n.Comment;
return obj;
}).ToArray()),
["email_addresses"] = new JsonArray(m.EmailAddresses.OrderBy(a => a.Nr).Select(a => {
var obj = new JsonObject {
["address"] = a.Address,
};
if (a.Comment != null) obj["comment"] = a.Comment;
return obj;
}).ToArray()),
["comment"] = m.Comment,
};
}
public static (Member, BillingAddr?, List<MemberTelNr>, List<MemberEmailAddr>) ToMember(this JsonNode json, Dictionary<int, AT_Kg> kgs) {
var mgnr = json["mgnr"]!.AsValue().GetValue<int>();
var kgnr = json["default_kgnr"]?.AsValue().GetValue<int>();
if (kgnr != null && !kgs.Values.Any(k => k.WbKg?.KgNr == kgnr)) {
throw new ArgumentException($"Für KG {(kgs.TryGetValue(kgnr.Value, out var k) ? k.Name : "?")} ({kgnr:00000}) ist noch keine Großlage festgelegt!\n(Stammdaten → Herkunftshierarchie)");
}
return (new Member {
MgNr = mgnr,
PredecessorMgNr = json["predecessor_mgnr"]?.AsValue().GetValue<int>(),
Name = json["name"]?.AsValue().GetValue<string>() ?? json["family_name"]!.AsValue().GetValue<string>(),
Prefix = json["prefix"]?.AsValue().GetValue<string>(),
GivenName = json["given_name"]?.AsValue().GetValue<string>(),
MiddleName = json["middle_names"]?.AsValue().GetValue<string>(),
Suffix = json["suffix"]?.AsValue().GetValue<string>(),
ForTheAttentionOf = json["attn"]?.AsValue().GetValue<string>(),
Birthday = json["birthday"]?.AsValue().GetValue<string>(),
EntryDateString = json["entry_date"]?.AsValue().GetValue<string>(),
ExitDateString = json["exit_date"]?.AsValue().GetValue<string>(),
BusinessShares = json["business_shares"]?.AsValue().GetValue<int>() ?? 0,
AccountingNr = json["accounting_nr"]?.AsValue().GetValue<string>(),
ZwstId = json["zwstid"]?.AsValue().GetValue<string>(),
LfbisNr = json["lfbis_nr"]?.AsValue().GetValue<string>(),
UstIdNr = json["ustid_nr"]?.AsValue().GetValue<string>(),
IsJuridicalPerson = json["juridical_pers"]?.AsValue().GetValue<bool>() ?? false,
IsVollLieferant = json["volllieferant"]?.AsValue().GetValue<bool>() ?? false,
IsBuchführend = json["buchführend"]?.AsValue().GetValue<bool>() ?? false,
IsOrganic = json["organic"]?.AsValue().GetValue<bool>() ?? false,
IsFunktionär = json["funktionär"]?.AsValue().GetValue<bool>() ?? false,
IsActive = json["active"]?.AsValue().GetValue<bool>() ?? false,
IsDeceased = json["deceased"]?.AsValue().GetValue<bool>() ?? false,
Iban = json["iban"]?.AsValue().GetValue<string>(),
Bic = json["bic"]?.AsValue().GetValue<string>(),
CountryNum = json["address"]!["country"]!.AsValue().GetValue<int>(),
PostalDestId = json["address"]!["postal_dest"]!.AsValue().GetValue<string>(),
Address = json["address"]!["address"]!.AsValue().GetValue<string>(),
DefaultKgNr = kgnr,
ContactViaPost = json["contact_postal"]?.AsValue().GetValue<bool>() ?? false,
ContactViaEmail = json["contact_email"]?.AsValue().GetValue<bool>() ?? false,
Comment = json["comment"]?.AsValue().GetValue<string>(),
}, json["billing_address"] is JsonObject a ? new BillingAddr {
MgNr = mgnr,
FullName = a["name"]!.AsValue().GetValue<string>(),
CountryNum = a["country"]!.AsValue().GetValue<int>(),
PostalDestId = a["postal_dest"]!.AsValue().GetValue<string>(),
Address = a["address"]!.AsValue().GetValue<string>(),
} : null, json["telephone_numbers"]!.AsArray().Select(n => n!.AsObject()).Select((n, i) => new MemberTelNr {
MgNr = mgnr,
Nr = i + 1,
Type = n["type"]!.AsValue().GetValue<string>(),
Number = n["number"]!.AsValue().GetValue<string>(),
Comment = n["comment"]?.AsValue().GetValue<string>(),
}).ToList(), json["email_addresses"]!.AsArray().Select(a => a!.AsObject()).Select((a, i) => new MemberEmailAddr {
MgNr = mgnr,
Nr = i + 1,
Address = a["address"]!.AsValue().GetValue<string>(),
Comment = a["comment"]?.AsValue().GetValue<string>(),
}).ToList());
}
public static JsonObject ToJson(this AreaCom c) {
return new JsonObject {
["fbnr"] = c.FbNr,
["mgnr"] = c.MgNr,
["vtrgid"] = c.VtrgId,
["cultid"] = c.CultId,
["area"] = c.Area,
["kgnr"] = c.KgNr,
["gstnr"] = c.GstNr,
["ried"] = c.Rd?.Name,
["year_from"] = c.YearFrom,
["year_to"] = c.YearTo,
["comment"] = c.Comment,
};
}
public static (AreaCom, WbRd?) ToAreaCom(this JsonNode json, Dictionary<int, AT_Kg> kgs, Dictionary<int, List<WbRd>> riede) {
var kgnr = json["kgnr"]!.AsValue().GetValue<int>();
var ried = json["ried"]?.AsValue().GetValue<string>();
WbRd? rd = null;
bool newRd = false;
if (ried != null) {
var rde = riede[kgnr] ?? throw new ArgumentException($"Für KG {(kgs.TryGetValue(kgnr, out var k) ? k.Name : "?")} ({kgnr:00000}) ist noch keine Großlage festgelegt!\n(Stammdaten → Herkunftshierarchie)");
rd = rde.FirstOrDefault(r => r.Name == ried);
if (rd == null) {
newRd = true;
rd = new WbRd {
KgNr = kgnr,
RdNr = (rde.Count == 0 ? 0 : rde.Max(r => r.RdNr)) + 1,
Name = ried,
};
rde.Add(rd);
}
}
return (new AreaCom {
FbNr = json["fbnr"]!.AsValue().GetValue<int>(),
MgNr = json["mgnr"]!.AsValue().GetValue<int>(),
VtrgId = json["vtrgid"]!.AsValue().GetValue<string>(),
CultId = json["cultid"]?.AsValue().GetValue<string>(),
Area = json["area"]!.AsValue().GetValue<int>(),
KgNr = kgnr,
GstNr = json["gstnr"]?.AsValue().GetValue<string>() ?? "-",
RdNr = rd?.RdNr,
YearFrom = json["year_from"]?.AsValue().GetValue<int>(),
YearTo = json["year_to"]?.AsValue().GetValue<int>(),
Comment = json["comment"]?.AsValue().GetValue<string>(),
}, newRd ? rd : null);
}
public static JsonObject ToJson(this Delivery d) {
return new JsonObject {
["lsnr"] = d.LsNr,
["year"] = d.Year,
["date"] = $"{d.Date:yyyy-MM-dd}",
["zwstid"] = d.ZwstId,
["lnr"] = d.LNr,
["time"] = d.Time != null ? $"{d.Time:HH:mm:ss}" : null,
["mgnr"] = d.MgNr,
["parts"] = new JsonArray(d.Parts.OrderBy(p => p.DPNr).Select(p => {
var obj = new JsonObject {
["dpnr"] = p.DPNr,
["sortid"] = p.SortId,
["attrid"] = p.AttrId,
["cultid"] = p.CultId,
["weight"] = p.Weight,
["kmw"] = p.Kmw,
["qualid"] = p.QualId,
["hkid"] = p.HkId,
["kgnr"] = p.KgNr,
["rdnr"] = p.RdNr,
["net_weight"] = p.IsNetWeight,
["manual_weighing"] = p.IsManualWeighing,
["modids"] = new JsonArray(p.Modifiers.Select(m => (JsonNode)m.ModId).ToArray()),
["comment"] = p.Comment,
};
if (p.IsSplCheck) obj["spl_check"] = p.IsSplCheck;
if (p.IsHandPicked != null) obj["hand_picked"] = p.IsHandPicked;
if (p.IsLesewagen != null) obj["lesewagen"] = p.IsLesewagen;
if (p.IsGebunden != null) obj["gebunden"] = p.IsGebunden;
if (p.Temperature != null) obj["temperature"] = p.Temperature;
if (p.Acid != null) obj["acid"] = p.Acid;
if (p.ScaleId != null) obj["scale_id"] = p.ScaleId;
if (p.WeighingData != null) obj["weighing_data"] = JsonNode.Parse(p.WeighingData);
if (p.WeighingReason != null) obj["weighing_reason"] = p.WeighingReason;
return obj;
}).ToArray()),
["comment"] = d.Comment,
};
}
public static (Delivery, List<DeliveryPart>, List<DeliveryPartModifier>) ToDelivery(this JsonNode json, Dictionary<string, int> currentLsNrs, Dictionary<int, int> currentDids) {
var year = json["year"]!.AsValue().GetValue<int>();
var lsnr = json["lsnr"]!.AsValue().GetValue<string>();
var did = currentLsNrs.GetValueOrDefault(lsnr, -1);
if (did == -1) {
if (!currentDids.ContainsKey(year)) currentDids[year] = 0;
did = ++currentDids[year];
}
currentLsNrs[lsnr] = did;
return (new Delivery {
Year = year,
DId = did,
DateString = json["date"]!.AsValue().GetValue<string>(),
TimeString = json["time"]?.AsValue().GetValue<string>(),
ZwstId = json["zwstid"]!.AsValue().GetValue<string>(),
LNr = json["lnr"]!.AsValue().GetValue<int>(),
LsNr = lsnr,
MgNr = json["mgnr"]!.AsValue().GetValue<int>(),
Comment = json["comment"]?.AsValue().GetValue<string>(),
}, json["parts"]!.AsArray().Select(p => p!.AsObject()).Select(p => new DeliveryPart {
Year = year,
DId = did,
DPNr = p["dpnr"]!.AsValue().GetValue<int>(),
SortId = p["sortid"]!.AsValue().GetValue<string>(),
AttrId = p["attrid"]?.AsValue().GetValue<string>(),
CultId = p["cultid"]?.AsValue().GetValue<string>(),
Weight = p["weight"]!.AsValue().GetValue<int>(),
Kmw = p["kmw"]!.AsValue().GetValue<double>(),
QualId = p["qualid"]!.AsValue().GetValue<string>(),
HkId = p["hkid"]!.AsValue().GetValue<string>(),
KgNr = p["kgnr"]?.AsValue().GetValue<int>(),
RdNr = p["rdnr"]?.AsValue().GetValue<int>(),
IsNetWeight = p["net_weight"]!.AsValue().GetValue<bool>(),
IsManualWeighing = p["manual_weighing"]!.AsValue().GetValue<bool>(),
Comment = p["comment"]?.AsValue().GetValue<string>(),
IsSplCheck = p["spl_check"]?.AsValue().GetValue<bool>() ?? false,
IsHandPicked = p["hand_picked"]?.AsValue().GetValue<bool>(),
IsLesewagen = p["lesewagen"]?.AsValue().GetValue<bool>(),
IsGebunden = p["gebunden"]?.AsValue().GetValue<bool>(),
Temperature = p["temperature"]?.AsValue().GetValue<double>(),
Acid = p["acid"]?.AsValue().GetValue<double>(),
ScaleId = p["scale_id"]?.AsValue().GetValue<string>(),
WeighingData = p["weighing_data"]?.AsObject().ToJsonString(JsonOpts),
WeighingReason = p["weighing_reason"]?.AsValue().GetValue<string>(),
}).ToList(), json["parts"]!.AsArray().SelectMany(p => p!["modids"]!.AsArray().Select(m => new DeliveryPartModifier {
Year = year,
DId = did,
DPNr = p["dpnr"]!.AsValue().GetValue<int>(),
ModId = m!.AsValue().GetValue<string>(),
})).ToList());
}
}
}

View File

@ -1,4 +1,4 @@
using Elwig.Models.Dtos; using Elwig.Models.Dtos;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.Entity.Core.Common.CommandTrees.ExpressionBuilder; using System.Data.Entity.Core.Common.CommandTrees.ExpressionBuilder;
@ -6,6 +6,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Security;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Elwig.Helpers.Export { namespace Elwig.Helpers.Export {
@ -20,7 +21,7 @@ namespace Elwig.Helpers.Export {
FileName = filename; FileName = filename;
File.Delete(filename); File.Delete(filename);
ZipArchive = ZipFile.Open(FileName, ZipArchiveMode.Create); ZipArchive = ZipFile.Open(FileName, ZipArchiveMode.Create);
_tables = new(); _tables = [];
} }
public void Dispose() { public void Dispose() {
@ -101,7 +102,12 @@ namespace Elwig.Helpers.Export {
<style:style style:name="header" style:family="table-cell" style:parent-style-name="default"> <style:style style:name="header" style:family="table-cell" style:parent-style-name="default">
<style:table-cell-properties style:text-align-source="fix" style:repeat-content="false"/> <style:table-cell-properties style:text-align-source="fix" style:repeat-content="false"/>
<style:paragraph-properties fo:text-align="center"/> <style:paragraph-properties fo:text-align="center"/>
<style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold" fo:font-size="16pt"/> <style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold" fo:font-size="16pt"/>
</style:style>
<style:style style:name="subheader" style:family="table-cell" style:parent-style-name="default">
<style:table-cell-properties style:text-align-source="fix" style:repeat-content="false"/>
<style:paragraph-properties fo:text-align="center"/>
<style:text-properties fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"/>
</style:style> </style:style>
<style:style style:name="th" style:family="table-cell" style:parent-style-name="default"> <style:style style:name="th" style:family="table-cell" style:parent-style-name="default">
<style:table-cell-properties style:text-align-source="fix" style:repeat-content="false" style:vertical-align="middle"/> <style:table-cell-properties style:text-align-source="fix" style:repeat-content="false" style:vertical-align="middle"/>
@ -122,6 +128,18 @@ namespace Elwig.Helpers.Export {
<style:style style:name="N5" style:family="table-cell" style:parent-style-name="default" style:data-style-name="NN5"/> <style:style style:name="N5" style:family="table-cell" style:parent-style-name="default" style:data-style-name="NN5"/>
<number:number-style style:name="NN6"><number:number number:decimal-places="6" number:min-decimal-places="6" number:min-integer-digits="1" number:grouping="true"/></number:number-style> <number:number-style style:name="NN6"><number:number number:decimal-places="6" number:min-decimal-places="6" number:min-integer-digits="1" number:grouping="true"/></number:number-style>
<style:style style:name="N6" style:family="table-cell" style:parent-style-name="default" style:data-style-name="NN6"/> <style:style style:name="N6" style:family="table-cell" style:parent-style-name="default" style:data-style-name="NN6"/>
<number:date-style style:name="_date"><number:day number:style="long"/><number:text>.</number:text><number:month number:style="long"/><number:text>.</number:text><number:year number:style="long"/></number:date-style>
<style:style style:name="date" style:family="table-cell" style:parent-style-name="default" style:data-style-name="_date"/>
<number:time-style style:name="_time"><number:hours number:style="long"/><number:text>:</number:text><number:minutes number:style="long"/><number:text>:</number:text><number:seconds number:style="long"/></number:time-style>
<style:style style:name="time" style:family="table-cell" style:parent-style-name="default" style:data-style-name="_time"/>
<number:date-style style:name="_datetime">
<number:day number:style="long"/><number:text>.</number:text><number:month number:style="long"/><number:text>.</number:text><number:year number:style="long"/>
<number:text> </number:text>
<number:hours number:style="long"/><number:text>:</number:text><number:minutes number:style="long"/><number:text>:</number:text><number:seconds number:style="long"/>
</number:date-style>
<style:style style:name="datetime" style:family="table-cell" style:parent-style-name="default" style:data-style-name="_datetime"/>
<number:boolean-style style:name="_boolean"><number:boolean/></number:boolean-style>
<style:style style:name="boolean" style:family="table-cell" style:parent-style-name="default" style:data-style-name="_boolean"/>
</office:automatic-styles> </office:automatic-styles>
<office:body> <office:body>
<office:spreadsheet> <office:spreadsheet>
@ -161,8 +179,8 @@ namespace Elwig.Helpers.Export {
await writer.WriteAsync($""" await writer.WriteAsync($"""
<config:config-item-map-entry config:name="{tbl}"> <config:config-item-map-entry config:name="{tbl}">
<config:config-item config:name="VerticalSplitMode" config:type="short">2</config:config-item> <config:config-item config:name="VerticalSplitMode" config:type="short">2</config:config-item>
<config:config-item config:name="VerticalSplitPosition" config:type="int">4</config:config-item> <config:config-item config:name="VerticalSplitPosition" config:type="int">5</config:config-item>
<config:config-item config:name="PositionBottom" config:type="int">4</config:config-item> <config:config-item config:name="PositionBottom" config:type="int">5</config:config-item>
</config:config-item-map-entry> </config:config-item-map-entry>
"""); """);
@ -198,6 +216,9 @@ namespace Elwig.Helpers.Export {
FormatCell(table.FullName, colSpan: totalSpan, style: "header") + FormatCell(table.FullName, colSpan: totalSpan, style: "header") +
$" </table:table-row>\r\n" + $" </table:table-row>\r\n" +
$" <table:table-row>\r\n" + $" <table:table-row>\r\n" +
FormatCell(table.Subtitle, colSpan: totalSpan, style: "subheader") +
$" </table:table-row>\r\n" +
$" <table:table-row>\r\n" +
$" <table:table-cell table:number-columns-repeated=\"{totalSpan}\"/>\r\n" + $" <table:table-cell table:number-columns-repeated=\"{totalSpan}\"/>\r\n" +
$" </table:table-row>\r\n" + $" </table:table-row>\r\n" +
$" <table:table-row>\r\n"); $" <table:table-row>\r\n");
@ -251,8 +272,8 @@ namespace Elwig.Helpers.Export {
protected static string FormatCell(object? data, int rowSpan = 1, int colSpan = 1, string? style = "default", bool isCovered = false, string?[]? units = null) { protected static string FormatCell(object? data, int rowSpan = 1, int colSpan = 1, string? style = "default", bool isCovered = false, string?[]? units = null) {
if (data?.GetType().IsValueType == true && data.GetType().Name.StartsWith("ValueTuple")) if (data?.GetType().IsValueType == true && data.GetType().Name.StartsWith("ValueTuple"))
return string.Join("", data.GetType().GetFields().Zip(units ?? Array.Empty<string?>()) return string.Join("", data.GetType().GetFields().Zip(units ?? [])
.Select(p => FormatCell(p.First.GetValue(data), rowSpan, colSpan, style, isCovered, new[] { p.Second })) .Select(p => FormatCell(p.First.GetValue(data), rowSpan, colSpan, style, isCovered, [p.Second]))
); );
var add = (style != null ? $" table:style-name=\"{style}\"" : "") + (rowSpan > 1 || colSpan > 1 ? $" table:number-rows-spanned=\"{rowSpan}\" table:number-columns-spanned=\"{colSpan}\"" : ""); var add = (style != null ? $" table:style-name=\"{style}\"" : "") + (rowSpan > 1 || colSpan > 1 ? $" table:number-rows-spanned=\"{rowSpan}\" table:number-columns-spanned=\"{colSpan}\"" : "");
@ -262,22 +283,38 @@ namespace Elwig.Helpers.Export {
string c; string c;
if (data == null) { if (data == null) {
c = $"<{ct}{add}/>"; c = $"<{ct}{add}/>";
} else if (data is bool b) {
add = string.Join(' ', add.Split(' ').Select(p => p.StartsWith("table:style-name=") ? $"table:style-name=\"boolean\"" : p));
c = $"<{ct} office:value-type=\"boolean\" calcext:value-type=\"boolean\" office:boolean-value=\"{(b ? "true" : "false")}\"{add}><text:p>{(b ? "Ja" : "Nein")}</text:p></{ct}>";
} else if (data is DateOnly date) {
add = string.Join(' ', add.Split(' ').Select(p => p.StartsWith("table:style-name=") ? $"table:style-name=\"date\"" : p));
c = $"<{ct} office:value-type=\"date\" calcext:value-type=\"date\" office:date-value=\"{date:yyyy-MM-dd}\"{add}><text:p>{date:dd.MM.yyyy}</text:p></{ct}>";
} else if (data is TimeOnly time) {
add = string.Join(' ', add.Split(' ').Select(p => p.StartsWith("table:style-name=") ? $"table:style-name=\"time\"" : p));
c = $"<{ct} office:value-type=\"time\" calcext:value-type=\"time\" office:time-value=\"{time:\\P\\THH\\Hmm\\Mss\\S}\"{add}><text:p>{time:HH:mm:ss}</text:p></{ct}>";
} else if (data is DateTime dt) {
add = string.Join(' ', add.Split(' ').Select(p => p.StartsWith("table:style-name=") ? $"table:style-name=\"datetime\"" : p));
c = $"<{ct} office:value-type=\"date\" calcext:value-type=\"date\" office:date-value=\"{dt:yyyy-MM-dd\\THH:mm:ss}\"{add}><text:p>{dt:dd.MM.yyyy HH:mm:ss}</text:p></{ct}>";
} else if (data is decimal || data is float || data is double || data is byte || data is char || } else if (data is decimal || data is float || data is double || data is byte || data is char ||
data is short || data is ushort || data is int || data is uint || data is long || data is ulong) { data is short || data is ushort || data is int || data is uint || data is long || data is ulong) {
double v = double.Parse(data?.ToString() ?? "0"); // use default culture for ToString and Parse()! double v = double.Parse(data?.ToString() ?? "0"); // use default culture for ToString and Parse()!
if (units != null && units.Length > 0) { if (units != null && units.Length > 0) {
int n = -1; int n = -1;
switch (units[0]) { switch (units[0]) {
case "#": n = 0; data = $"{v:N0}"; break;
case "%": n = 1; data = $"{v:N1}"; break; case "%": n = 1; data = $"{v:N1}"; break;
case "€": n = 2; data = $"{v:N2}"; break; case "€": n = 2; data = $"{v:N2}"; break;
case "€/kg": n = 4; data = $"{v:N4}"; break;
case "°KMW": n = 1; data = $"{v:N1}"; break; case "°KMW": n = 1; data = $"{v:N1}"; break;
case "°Oe": n = 0; data = $"{v:N0}"; break; case "°Oe": n = 0; data = $"{v:N0}"; break;
case "m²": n = 0; data = $"{v:N0}"; break;
case "kg": n = 0; data = $"{v:N0}"; break;
} }
if (n >= 0) add = string.Join(" ", add.Split(" ").Select(p => p.StartsWith("table:style-name=") ? $"table:style-name=\"N{n}\"" : p)); if (n >= 0) add = string.Join(' ', add.Split(' ').Select(p => p.StartsWith("table:style-name=") ? $"table:style-name=\"N{n}\"" : p));
} }
c = $"<{ct} office:value-type=\"float\" calcext:value-type=\"float\" office:value=\"{v.ToString(CultureInfo.InvariantCulture)}\"{add}><text:p>{data}</text:p></{ct}>"; c = $"<{ct} office:value-type=\"float\" calcext:value-type=\"float\" office:value=\"{v.ToString(CultureInfo.InvariantCulture)}\"{add}><text:p>{data}</text:p></{ct}>";
} else { } else {
c = $"<{ct} office:value-type=\"string\" calcext:value-type=\"string\"{add}><text:p>{data}</text:p></{ct}>"; c = $"<{ct} office:value-type=\"string\" calcext:value-type=\"string\"{add}><text:p>{SecurityElement.Escape(data.ToString())}</text:p></{ct}>";
} }
return $" {c}\r\n" + (colSpan > 1 ? $" <table:covered-table-cell table:number-rows-repeated=\"{colSpan - 1}\"/>\r\n" : ""); return $" {c}\r\n" + (colSpan > 1 ? $" <table:covered-table-cell table:number-rows-repeated=\"{colSpan - 1}\"/>\r\n" : "");

View File

@ -0,0 +1,5 @@
namespace Elwig.Helpers {
public enum ExportMode {
Show, SaveList, SavePdf, Print, Email, Export, Upload
}
}

View File

@ -0,0 +1,99 @@
using System;
using System.IO;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace Elwig.Helpers {
static partial class Extensions {
[LibraryImport("msvcrt.dll")]
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
private static unsafe partial int memcmp(void* b1, void* b2, long count);
public static unsafe int CompareBuffers(char[] buffer1, int offset1, char[] buffer2, int offset2, int count) {
fixed (char* b1 = buffer1, b2 = buffer2) {
return memcmp(b1 + offset1, b2 + offset2, count);
}
}
public static string? ReadUntil(this StreamReader reader, string delimiter) {
return ReadUntil(reader, delimiter.ToCharArray());
}
public static string? ReadUntil(this StreamReader reader, char delimiter) {
return ReadUntil(reader, [ delimiter ]);
}
public static string? ReadUntil(this StreamReader reader, char[] delimiter) {
var buf = new char[512];
int bufSize = 0, ret;
while (!reader.EndOfStream && bufSize < buf.Length - 1) {
if ((ret = reader.Read()) == -1)
return null;
buf[bufSize++] = (char)ret;
if (bufSize >= delimiter.Length && CompareBuffers(buf, bufSize - delimiter.Length, delimiter, 0, delimiter.Length) == 0)
return new string(buf, 0, bufSize);
}
return null;
}
public static Task<string?> ReadUntilAsync(this StreamReader reader, string delimiter) {
return ReadUntilAsync(reader, delimiter.ToCharArray());
}
public static Task<string?> ReadUntilAsync(this StreamReader reader, char delimiter) {
return ReadUntilAsync(reader, [ delimiter ]);
}
public static async Task<string?> ReadUntilAsync(this StreamReader reader, char[] delimiter) {
var buf = new char[512];
int bufSize = 0;
var tmpBuf = new char[1];
while (!reader.EndOfStream && bufSize < buf.Length - 1) {
if ((await reader.ReadAsync(tmpBuf, 0, 1)) != 1)
return null;
buf[bufSize++] = tmpBuf[0];
if (bufSize >= delimiter.Length && CompareBuffers(buf, bufSize - delimiter.Length, delimiter, 0, delimiter.Length) == 0)
return new string(buf, 0, bufSize);
}
return null;
}
public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long>? progress = null, CancellationToken cancellationToken = default) {
ArgumentNullException.ThrowIfNull(source);
if (!source.CanRead) throw new ArgumentException("Has to be readable", nameof(source));
ArgumentNullException.ThrowIfNull(destination);
if (!destination.CanWrite) throw new ArgumentException("Has to be writable", nameof(destination));
ArgumentOutOfRangeException.ThrowIfNegative(bufferSize);
var buffer = new byte[bufferSize];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0) {
await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
progress?.Report(totalBytesRead);
}
}
public static async Task DownloadAsync(this HttpClient client, string requestUri, Stream destination, IProgress<double>? progress = null, CancellationToken cancellationToken = default) {
using var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
response.EnsureSuccessStatusCode();
var contentLength = response.Content.Headers.ContentLength;
using var download = await response.Content.ReadAsStreamAsync(cancellationToken);
if (progress == null || !contentLength.HasValue) {
await download.CopyToAsync(destination, cancellationToken);
return;
}
var relativeProgress = new Progress<long>(totalBytes => progress.Report((double)totalBytes / contentLength.Value));
await download.CopyToAsync(destination, 81920, relativeProgress, cancellationToken);
progress.Report(100.0);
}
}
}

View File

@ -1,24 +0,0 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Elwig.Helpers {
public static class HttpClientExtensions {
public static async Task DownloadAsync(this HttpClient client, string requestUri, Stream destination, IProgress<double>? progress = null, CancellationToken cancellationToken = default) {
using var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
var contentLength = response.Content.Headers.ContentLength;
using var download = await response.Content.ReadAsStreamAsync(cancellationToken);
if (progress == null || !contentLength.HasValue) {
await download.CopyToAsync(destination, cancellationToken);
return;
}
var relativeProgress = new Progress<long>(totalBytes => progress.Report((double)totalBytes / contentLength.Value));
await download.CopyToAsync(destination, 81920, relativeProgress, cancellationToken);
progress.Report(100.0);
}
}
}

View File

@ -8,16 +8,12 @@ using System.Windows;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Linq; using System.Linq;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using PdfiumViewer;
using System.Drawing.Printing;
namespace Elwig.Helpers.Printing { namespace Elwig.Helpers.Printing {
public static class Pdf { public static class Pdf {
private static readonly string PdfToPrinter = new string[] { App.ExePath }
.Union(Environment.GetEnvironmentVariable("PATH")?.Split(';') ?? [])
.Select(x => Path.Combine(x, "PDFtoPrinter.exe"))
.Where(File.Exists)
.FirstOrDefault() ?? throw new FileNotFoundException("PDFtoPrinter executable not found");
private static readonly string WinziPrint = new string[] { App.ExePath } private static readonly string WinziPrint = new string[] { App.ExePath }
.Union(Environment.GetEnvironmentVariable("PATH")?.Split(';') ?? []) .Union(Environment.GetEnvironmentVariable("PATH")?.Split(';') ?? [])
.Select(x => Path.Combine(x, "WinziPrint.exe")) .Select(x => Path.Combine(x, "WinziPrint.exe"))
@ -59,7 +55,7 @@ namespace Elwig.Helpers.Printing {
progress?.Report(0.0); progress?.Report(0.0);
using var client = new TcpClient("127.0.0.1", 30983); using var client = new TcpClient("127.0.0.1", 30983);
using var stream = client.GetStream(); using var stream = client.GetStream();
await stream.WriteAsync(Encoding.UTF8.GetBytes( await stream.WriteAsync(Utils.UTF8.GetBytes(
"-e utf-8;-p;" + (doublePaged ? "-2;" : "") + "-e utf-8;-p;" + (doublePaged ? "-2;" : "") +
$"{string.Join(';', htmlPath)};{pdfPath}" + $"{string.Join(';', htmlPath)};{pdfPath}" +
"\r\n")); "\r\n"));
@ -92,17 +88,24 @@ namespace Elwig.Helpers.Printing {
}); });
} }
public static async Task Print(string path, int copies = 1) { public static async Task Print(string path, int copies = 1, bool doublePaged = false) {
await Print(path, new() {
Copies = (short)copies,
Collate = true,
Duplex = doublePaged ? Duplex.Vertical : Duplex.Simplex,
});
}
public static Task Print(string path, PrinterSettings settings) {
try { try {
var p = new Process() { StartInfo = new() { FileName = PdfToPrinter } }; using var doc = PdfDocument.Load(path);
p.StartInfo.ArgumentList.Add(path); using var printDoc = doc.CreatePrintDocument(PdfPrintMode.CutMargin);
p.StartInfo.ArgumentList.Add("/s"); printDoc.PrinterSettings = settings;
p.StartInfo.ArgumentList.Add($"copies={copies}"); printDoc.Print();
p.Start();
await p.WaitForExitAsync();
} catch (Exception e) { } catch (Exception e) {
MessageBox.Show("Beim Drucken ist ein Fehler aufgetreten:\n\n" + e.Message, "Fehler beim Drucken"); MessageBox.Show("Beim Drucken ist ein Fehler aufgetreten:\n\n" + e.Message, "Fehler beim Drucken", MessageBoxButton.OK, MessageBoxImage.Error);
} }
return Task.CompletedTask;
} }
} }
} }

View File

@ -1,25 +0,0 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Elwig.Helpers {
public static class StreamExtensions {
public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long>? progress = null, CancellationToken cancellationToken = default) {
ArgumentNullException.ThrowIfNull(source);
if (!source.CanRead) throw new ArgumentException("Has to be readable", nameof(source));
ArgumentNullException.ThrowIfNull(destination);
if (!destination.CanWrite) throw new ArgumentException("Has to be writable", nameof(destination));
ArgumentOutOfRangeException.ThrowIfNegative(bufferSize);
var buffer = new byte[bufferSize];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0) {
await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
progress?.Report(totalBytesRead);
}
}
}
}

View File

@ -18,7 +18,19 @@ using System.Text.Json.Nodes;
using System.IO; using System.IO;
using MailKit.Net.Smtp; using MailKit.Net.Smtp;
using MailKit.Security; using MailKit.Security;
using OpenTK.Compute.OpenCL; using Microsoft.EntityFrameworkCore;
using System.Reflection;
using System.Collections;
using Elwig.Documents;
using MimeKit;
using System.Windows.Input;
using LinqKit;
using System.Linq.Expressions;
using Elwig.Models;
using Microsoft.Win32;
using System.Globalization;
using System.Threading;
using System.Windows.Markup;
namespace Elwig.Helpers { namespace Elwig.Helpers {
public static partial class Utils { public static partial class Utils {
@ -27,7 +39,8 @@ namespace Elwig.Helpers {
public static int CurrentYear => DateTime.Now.Year; public static int CurrentYear => DateTime.Now.Year;
public static int CurrentNextSeason => DateTime.Now.Year - (DateTime.Now.Month <= 3 ? 1 : 0); public static int CurrentNextSeason => DateTime.Now.Year - (DateTime.Now.Month <= 3 ? 1 : 0);
public static int CurrentLastSeason => DateTime.Now.Year - (DateTime.Now.Month <= 7 ? 1 : 0); public static int CurrentLastSeason => DateTime.Now.Year - (DateTime.Now.Month <= 6 ? 1 : 0);
public static int FollowingSeason => DateTime.Now.Year + (DateTime.Now.Month >= 11 ? 1 : 0);
public static DateTime Today => (DateTime.Now.Hour >= 3) ? DateTime.Today : DateTime.Today.AddDays(-1); public static DateTime Today => (DateTime.Now.Hour >= 3) ? DateTime.Today : DateTime.Today.AddDays(-1);
[GeneratedRegex("^serial://([A-Za-z0-9]+):([0-9]+)(,([5-9]),([NOEMSnoems]),(0|1|1\\.5|2|))?$", RegexOptions.Compiled)] [GeneratedRegex("^serial://([A-Za-z0-9]+):([0-9]+)(,([5-9]),([NOEMSnoems]),(0|1|1\\.5|2|))?$", RegexOptions.Compiled)]
@ -71,7 +84,7 @@ namespace Elwig.Helpers {
return PhoneNrTypes.Where(t => t.Key == type).Select(t => t.Value).FirstOrDefault(type); return PhoneNrTypes.Where(t => t.Key == type).Select(t => t.Value).FirstOrDefault(type);
} }
private static readonly string[] TempWildcards = ["*.html", "*.pdf", "*.exe"]; private static readonly string[] TempWildcards = ["*.html", "*.pdf", "*.exe", "*.zip"];
private static readonly ushort[] Crc16ModbusTable = [ private static readonly ushort[] Crc16ModbusTable = [
0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
@ -108,6 +121,23 @@ namespace Elwig.Helpers {
0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040, 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040,
]; ];
public static void OverrideCulture() {
var locale = new CultureInfo("de-AT", false);
locale.NumberFormat.CurrencyGroupSeparator = Utils.GroupSeparator;
locale.NumberFormat.NumberGroupSeparator = Utils.GroupSeparator;
locale.NumberFormat.PercentGroupSeparator = Utils.GroupSeparator;
CultureInfo.CurrentCulture = locale;
CultureInfo.CurrentUICulture = locale;
Thread.CurrentThread.CurrentCulture = locale;
Thread.CurrentThread.CurrentUICulture = locale;
CultureInfo.DefaultThreadCurrentCulture = locale;
CultureInfo.DefaultThreadCurrentUICulture = locale;
FrameworkElement.LanguageProperty.OverrideMetadata(
typeof(FrameworkElement),
new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.Name))
);
}
public static SerialPort OpenSerialConnection(string connection) { public static SerialPort OpenSerialConnection(string connection) {
var m = SerialRegex.Match(connection); var m = SerialRegex.Match(connection);
if (!m.Success) if (!m.Success)
@ -187,7 +217,7 @@ namespace Elwig.Helpers {
} }
public static void MailTo(string emailAddress) { public static void MailTo(string emailAddress) {
MailTo(new string[] { emailAddress }); MailTo([emailAddress]);
} }
public static void MailTo(IEnumerable<string> emailAddresses) { public static void MailTo(IEnumerable<string> emailAddresses) {
@ -257,9 +287,9 @@ namespace Elwig.Helpers {
return d.ShowDialog() == true ? (d.Weight, d.Reason) : null; return d.ShowDialog() == true ? (d.Weight, d.Reason) : null;
} }
public static int? ShowAbwertenDialog(string lsnr, string name, int weight) { public static (string?, int[])? ShowDeliverySplittingDialog(Delivery delivery) {
var d = new AbwertenDialog(lsnr, name, weight); var d = new DeliverySplittingDialog(delivery);
return d.ShowDialog() == true ? d.Weight : null; return d.ShowDialog() == true ? (d.MgNr?.ToString() ?? d.LsNr, d.Weights ?? []) : null;
} }
public static double? ShowLinearPriceIncreaseDialog() { public static double? ShowLinearPriceIncreaseDialog() {
@ -267,11 +297,6 @@ namespace Elwig.Helpers {
return d.ShowDialog() == true ? d.Price : null; return d.ShowDialog() == true ? d.Price : null;
} }
public static string? ShowDeliveryExtractionDialog(string lsnr, string name, bool single, IEnumerable<string> lsnrs) {
var d = new DeliveryExtractionDialog(lsnr, name, single, lsnrs);
return d.ShowDialog() == true ? d.AddTo : null;
}
public static Footer GenerateFooter(string lineBreak, string seperator) { public static Footer GenerateFooter(string lineBreak, string seperator) {
return new Footer(lineBreak, seperator); return new Footer(lineBreak, seperator);
} }
@ -318,7 +343,7 @@ namespace Elwig.Helpers {
> 0 => "+", > 0 => "+",
}; };
public static double AggregateDeliveryPartsKmw(IEnumerable<DeliveryPart> parts) public static double AggregateDeliveryPartsKmw(IEnumerable<IDelivery> parts)
=> parts.Aggregate( => parts.Aggregate(
(Weight: 0, Kmw: 0.0), (Weight: 0, Kmw: 0.0),
(sum, item) => ( (sum, item) => (
@ -338,7 +363,7 @@ namespace Elwig.Helpers {
} }
public static (string, string?) SplitName(string fullName, string? familyName) { public static (string, string?) SplitName(string fullName, string? familyName) {
if (familyName == null || familyName == "") return (fullName, null); if (string.IsNullOrWhiteSpace(familyName)) return (fullName, null);
var p0 = fullName.IndexOf(familyName, StringComparison.CurrentCultureIgnoreCase); var p0 = fullName.IndexOf(familyName, StringComparison.CurrentCultureIgnoreCase);
if (p0 == -1) return (fullName, null); if (p0 == -1) return (fullName, null);
var p1 = fullName.IndexOf(" und "); var p1 = fullName.IndexOf(" und ");
@ -352,8 +377,10 @@ namespace Elwig.Helpers {
var p3 = fullName.LastIndexOf(' ', p2 - 1); var p3 = fullName.LastIndexOf(' ', p2 - 1);
return (fullName[0..p3], fullName[(p3 + 1)..^0]); return (fullName[0..p3], fullName[(p3 + 1)..^0]);
} }
} else { } else if (p0 + familyName.Length >= fullName.Length || fullName[p0 + familyName.Length] == ' ') {
return (familyName, fullName.Replace(familyName, "").Replace(" ", " ").Trim()); return (familyName, fullName.Replace(familyName, "").Replace(" ", " ").Trim());
} else {
return (fullName, null);
} }
} }
@ -399,19 +426,54 @@ namespace Elwig.Helpers {
return InternetGetConnectedState(out var _, 0); return InternetGetConnectedState(out var _, 0);
} }
public static HttpClient GetHttpClient(string? username = null, string? password = null, string? accept = null) {
var client = new HttpClient() {
Timeout = TimeSpan.FromSeconds(5),
};
client.DefaultRequestHeaders.Accept.Clear();
if (accept != null)
client.DefaultRequestHeaders.Accept.Add(new(accept));
if (username != null || password != null)
client.DefaultRequestHeaders.Authorization = new("Basic", Convert.ToBase64String(Utils.UTF8.GetBytes($"{username}:{password}")));
return client;
}
public static async Task<(string Version, string Url, long Size)?> GetLatestInstallerUrl(string url) { public static async Task<(string Version, string Url, long Size)?> GetLatestInstallerUrl(string url) {
try { try {
using var client = new HttpClient() { using var client = GetHttpClient(accept: "application/json");
Timeout = TimeSpan.FromSeconds(5), using var res = await client.GetAsync(url);
}; if (!res.IsSuccessStatusCode)
var res = JsonNode.Parse(await client.GetStringAsync(url)); return null;
var data = res!["data"]![0]!; var resJson = JsonNode.Parse(await res.Content.ReadAsStringAsync());
return ((string)data["version"]!, (string)data["url"]!, (int)data["size"]!); var data = resJson!["data"]!.AsArray()[^1]!;
return ((string)data["version"]!, (string)data["url"]!, (long)data["size"]!);
} catch { } catch {
return null; return null;
} }
} }
public static async Task UploadExportData(string zip, string url, string username, string password) {
if (url.StartsWith("https://elwig.at/clients/"))
url = "https://sync.elwig.at/" + url[25..];
if (!url.EndsWith('/')) url += "/";
using var client = GetHttpClient(username, password, accept: "application/json");
var content = new StreamContent(new FileStream(zip, FileMode.Open, FileAccess.Read));
content.Headers.ContentType = new("application/zip");
using var res = await client.PutAsync(url + Path.GetFileName(zip), content);
res.EnsureSuccessStatusCode();
}
public static async Task<JsonArray> GetExportMetaData(string url, string username, string password) {
if (url.StartsWith("https://elwig.at/clients/"))
url = "https://sync.elwig.at/" + url[25..];
using var client = GetHttpClient(username, password, accept: "application/json");
using var res = await client.GetAsync(url);
res.EnsureSuccessStatusCode();
var resJson = JsonNode.Parse(await res.Content.ReadAsStringAsync());
var data = resJson!["data"]!;
return data.AsArray();
}
public static void CleanupTempFiles() { public static void CleanupTempFiles() {
var dir = new DirectoryInfo(App.TempPath); var dir = new DirectoryInfo(App.TempPath);
foreach (var file in TempWildcards.SelectMany(dir.EnumerateFiles)) { foreach (var file in TempWildcards.SelectMany(dir.EnumerateFiles)) {
@ -432,5 +494,179 @@ namespace Elwig.Helpers {
await client.AuthenticateAsync(username, password); await client.AuthenticateAsync(username, password);
return client; return client;
} }
public static async Task<bool> SendEmail(Member member, string subject, string text, IEnumerable<Document> docs) {
if (App.Config.Smtp == null)
return false;
SmtpClient? client = null;
try {
Mouse.OverrideCursor = Cursors.AppStarting;
client = await GetSmtpClient();
using var msg = new MimeMessage();
msg.From.Add(new MailboxAddress(App.Client.NameFull, App.Config.Smtp.Value.From));
msg.To.AddRange(member.EmailAddresses.OrderBy(a => a.Nr).Select(a => new MailboxAddress(member.AdministrativeName, a.Address)));
msg.Subject = subject;
var body = new Multipart("mixed") {
new TextPart("plain") { Text = text }
};
foreach (var doc in docs) {
var name = NormalizeFileName(doc.Title);
body.Add(doc.AsEmailAttachment($"{name}.pdf"));
}
msg.Body = body;
await client!.SendAsync(msg);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
return false;
} finally {
if (client != null)
await client.DisconnectAsync(true);
client?.Dispose();
Mouse.OverrideCursor = null;
}
return true;
}
public static async Task ExportDocument(Document doc, ExportMode mode, string? filename = null, (Member, string, string)? emailData = null) {
if (mode == ExportMode.Print && !App.Config.Debug) {
await doc.Generate();
await doc.Print();
} else if (mode == ExportMode.Email && emailData is (Member, string, string) e) {
await doc.Generate();
var success = await SendEmail(e.Item1, e.Item2, e.Item3, [doc]);
if (success)
MessageBox.Show("Die E-Mail wurde erfolgreich verschickt!", "E-Mail verschickt",
MessageBoxButton.OK, MessageBoxImage.Information);
} else if (mode == ExportMode.SavePdf) {
var d = new SaveFileDialog() {
FileName = $"{NormalizeFileName(filename ?? doc.Title)}.pdf",
DefaultExt = "pdf",
Filter = "PDF-Datei (*.pdf)|*.pdf",
Title = $"{doc.Title} speichern unter - Elwig"
};
if (d.ShowDialog() == true) {
await doc.Generate();
doc.SaveTo(d.FileName);
Process.Start("explorer.exe", d.FileName);
}
} else {
await doc.Generate();
doc.Show();
}
}
public static List<EventLogEntry> GetLogEntries() {
using var log = new EventLog {
Log = "Application",
Source = ".NET Runtime",
};
return log.Entries.Cast<EventLogEntry>()
.Where(e => e.Message.StartsWith("Application: Elwig.exe"))
.ToList();
}
public static int GetEntityIdetifierForPk(params object?[] primaryKey) {
var pk = primaryKey.Select(k => k?.GetHashCode() ?? 0).ToArray();
return ((IStructuralEquatable)pk).GetHashCode(EqualityComparer<int>.Default);
}
public static int? GetEntityIdentifier(object? obj) {
if (obj == null) {
return null;
} else if (obj is IEnumerable list && obj is not string) {
var arr = list.Cast<object>().Select(GetEntityIdentifier).ToArray();
return ((IStructuralEquatable)arr).GetHashCode(EqualityComparer<int?>.Default);
} else if (obj.GetType().GetCustomAttribute(typeof(PrimaryKeyAttribute), true) is PrimaryKeyAttribute pkAttr) {
var pk = pkAttr.PropertyNames.Select(name => obj.GetType().GetProperty(name)!.GetValue(obj)?.GetHashCode() ?? 0).ToArray();
return ((IStructuralEquatable)pk).GetHashCode(EqualityComparer<int>.Default);
} else {
return obj.GetHashCode();
}
}
public static Expression<Func<AreaCom, bool>> ActiveAreaCommitments() => ActiveAreaCommitments(CurrentYear);
public static Expression<Func<AreaCom, bool>> ActiveAreaCommitments(int year) =>
c => (c.YearFrom == null || c.YearFrom <= year) && (c.YearTo == null || c.YearTo >= year);
public static IQueryable<AreaCom> ActiveAreaCommitments(IQueryable<AreaCom> query) => ActiveAreaCommitments(query, CurrentYear);
public static IQueryable<AreaCom> ActiveAreaCommitments(IQueryable<AreaCom> query, int year) =>
query.Where(ActiveAreaCommitments(year));
public static IEnumerable<AreaCom> ActiveAreaCommitments(IEnumerable<AreaCom> query) => ActiveAreaCommitments(query, CurrentYear);
public static IEnumerable<AreaCom> ActiveAreaCommitments(IEnumerable<AreaCom> query, int year) =>
query.Where(c => ActiveAreaCommitments(year).Invoke(c));
public static async Task<(DateTime DateTime, string Type, int MgNr, string Name, string[] Addresses, string Subject, string[] Attachments)[]> GetSentMails(DateOnly? fromDate = null, DateOnly? toDate = null) {
var f = $"{fromDate:yyyy-MM-dd}";
var t = $"{toDate:yyyy-MM-dd}";
try {
var rows = new List<(DateTime, string, int, string, string[], string, string[])>();
var filenames = Directory.GetFiles(App.MailsPath, "????.csv")
.Where(n => fromDate == null || Path.GetFileName(n).CompareTo($"{fromDate.Value.Year}.csv") >= 0)
.Where(n => toDate == null || Path.GetFileName(n).CompareTo($"{toDate.Value.Year}.csv") <= 0)
.Order();
foreach (var filename in filenames) {
using var reader = new StreamReader(filename, Utils.UTF8);
string? line;
while ((line = await reader.ReadLineAsync()) != null) {
try {
if (line.Length < 20 || line[10] != ';' || line[19] != ';')
continue;
var date = line[..10];
if (fromDate != null && date.CompareTo(f) < 0) {
continue;
} else if (toDate != null && date.CompareTo(t) > 0) {
break;
}
var p = line.Split(';');
rows.Add((
DateOnly.ParseExact(p[0], "yyyy-MM-dd").ToDateTime(TimeOnly.ParseExact(p[1], "HH:mm:ss")),
p[2],
int.Parse(p[3]),
p[4],
p[5].Split(',').Select(a => a.Replace(" | ", "\n")).ToArray(),
p[6],
p[7].Split(',')
));
} catch {
continue;
}
}
}
return [.. rows];
} catch {
return [];
}
}
public static async Task AddSentMails(IEnumerable<(string Type, int MgNr, string Name, string[] Addresses, string Subject, string[] Attachments)> data) {
var now = DateTime.Now;
var filename = Path.Combine(App.MailsPath, $"{now.Year}.csv");
await File.AppendAllLinesAsync(filename, data.Select(d =>
$"{now:yyyy-MM-dd;HH:mm:ss};{d.Type};" +
$"{d.MgNr};{d.Name.Replace(';', ' ')};" +
$"{string.Join(',', d.Addresses.Select(a => a.Replace(';', ' ').Replace(',', ' ').Replace("\n", " | ")))};" +
$"{d.Subject.Replace(';', ' ')};" +
$"{string.Join(',', d.Attachments.Select(a => a.Replace(';', ' ').Replace(',', ' ')))}"
), Utils.UTF8);
}
public static async Task<string?> FindSentMailBody(DateTime target) {
var dt = $"{target:yyyy-MM-dd_HH-mm-ss}_";
var filename = Directory.GetFiles(App.MailsPath, "????-??-??_??-??-??_*.txt")
.Where(n => Path.GetFileName(n).CompareTo(dt) <= 0)
.Order()
.LastOrDefault();
if (filename == null)
return null;
return await File.ReadAllTextAsync(filename, Utils.UTF8);
}
public static async Task AddSentMailBody(string subject, string body, int recipients) {
var filename = Path.Combine(App.MailsPath, $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{NormalizeFileName(subject)}.txt");
await File.WriteAllTextAsync(filename, $"# {subject}\r\n# Vorgesehene Empfänger: {recipients}\r\n\r\n" + body, Utils.UTF8);
}
} }
} }

View File

@ -8,7 +8,7 @@ namespace Elwig.Helpers {
public static class Validator { public static class Validator {
private static readonly Dictionary<string, string[][]> PHONE_NRS = new() { private static readonly Dictionary<string, string[][]> PHONE_NRS = new() {
{ "43", new string[][] { { "43", [
[], [],
["57", "59"], ["57", "59"],
[ [
@ -17,17 +17,17 @@ namespace Elwig.Helpers {
"650", "651", "652", "653", "655", "657", "659", "660", "661", "650", "651", "652", "653", "655", "657", "659", "660", "661",
"663", "664", "665", "666", "667", "668", "669", "67", "68", "69" "663", "664", "665", "666", "667", "668", "669", "67", "68", "69"
] ]
} }, ] },
{ "49", Array.Empty<string[]>() }, { "49", [] },
{ "48", Array.Empty<string[]>() }, { "48", [] },
{ "420", Array.Empty<string[]>() }, { "420", [] },
{ "421", Array.Empty<string[]>() }, { "421", [] },
{ "36", Array.Empty<string[]>() }, { "36", [] },
{ "386", Array.Empty<string[]>() }, { "386", [] },
{ "39", Array.Empty<string[]>() }, { "39", [] },
{ "33", Array.Empty<string[]>() }, { "33", [] },
{ "41", Array.Empty<string[]>() }, { "41", [] },
{ "423", Array.Empty<string[]>() }, { "423", [] },
}; };
public static ValidationResult CheckInteger(TextBox input, bool required) { public static ValidationResult CheckInteger(TextBox input, bool required) {
@ -128,13 +128,14 @@ namespace Elwig.Helpers {
return new(true, null); return new(true, null);
} }
public static ValidationResult CheckPlz(TextBox input, bool required, AppDbContext ctx) { public static ValidationResult CheckPlz(TextBox input, bool required) {
CheckInteger(input, false, 4); CheckInteger(input, false, 4);
if (!required && input.Text.Length == 0) { if (!required && input.Text.Length == 0) {
return new(true, null); return new(true, null);
} else if (input.Text.Length != 4) { } else if (input.Text.Length != 4) {
return new(false, "PLZ zu kurz"); return new(false, "PLZ zu kurz");
} }
using var ctx = new AppDbContext();
int plz = int.Parse(input.Text); int plz = int.Parse(input.Text);
if (ctx.Postleitzahlen.Find(plz) == null) { if (ctx.Postleitzahlen.Find(plz) == null) {
return new(false, "Ungültige PLZ"); return new(false, "Ungültige PLZ");
@ -413,7 +414,7 @@ namespace Elwig.Helpers {
} }
} }
public static ValidationResult CheckMgNr(TextBox input, bool required, AppDbContext ctx) { public static ValidationResult CheckMgNr(TextBox input, bool required) {
var res = CheckInteger(input, required); var res = CheckInteger(input, required);
if (!res.IsValid) { if (!res.IsValid) {
return res; return res;
@ -421,6 +422,7 @@ namespace Elwig.Helpers {
return new(true, null); return new(true, null);
} }
using var ctx = new AppDbContext();
int nr = int.Parse(input.Text); int nr = int.Parse(input.Text);
if (!ctx.MgNrExists(nr).GetAwaiter().GetResult()) { if (!ctx.MgNrExists(nr).GetAwaiter().GetResult()) {
return new(false, "Ungültige Mitgliedsnummer"); return new(false, "Ungültige Mitgliedsnummer");
@ -429,7 +431,7 @@ namespace Elwig.Helpers {
return new(true, null); return new(true, null);
} }
public static ValidationResult CheckNewMgNr(TextBox input, bool required, AppDbContext ctx, Member? m) { public static ValidationResult CheckNewMgNr(TextBox input, bool required, Member? m) {
var res = CheckInteger(input, required); var res = CheckInteger(input, required);
if (!res.IsValid) { if (!res.IsValid) {
return res; return res;
@ -437,6 +439,7 @@ namespace Elwig.Helpers {
return new(true, null); return new(true, null);
} }
using var ctx = new AppDbContext();
int nr = int.Parse(input.Text); int nr = int.Parse(input.Text);
if (nr != m?.MgNr && ctx.MgNrExists(nr).GetAwaiter().GetResult()) { if (nr != m?.MgNr && ctx.MgNrExists(nr).GetAwaiter().GetResult()) {
return new(false, "Mitgliedsnummer wird bereits verwendet"); return new(false, "Mitgliedsnummer wird bereits verwendet");
@ -445,7 +448,7 @@ namespace Elwig.Helpers {
return new(true, null); return new(true, null);
} }
public static ValidationResult CheckSortId(TextBox input, bool required, AppDbContext ctx) { public static ValidationResult CheckSortId(TextBox input, bool required) {
var res = CheckUpperCase(input, required, 3); var res = CheckUpperCase(input, required, 3);
if (!res.IsValid) { if (!res.IsValid) {
return res; return res;
@ -453,6 +456,7 @@ namespace Elwig.Helpers {
return new(true, null); return new(true, null);
} }
using var ctx = new AppDbContext();
if (input.Text.Length < 2 || !ctx.SortIdExists(input.Text[0..2]).GetAwaiter().GetResult()) { if (input.Text.Length < 2 || !ctx.SortIdExists(input.Text[0..2]).GetAwaiter().GetResult()) {
return new(false, "Ungültige Sorte"); return new(false, "Ungültige Sorte");
} else if (input.Text.Length >= 3) { } else if (input.Text.Length >= 3) {
@ -465,8 +469,9 @@ namespace Elwig.Helpers {
return new(true, null); return new(true, null);
} }
public static ValidationResult CheckPredecessorMgNr(TextBox input, bool required, AppDbContext ctx) { public static ValidationResult CheckPredecessorMgNr(TextBox input, bool required) {
var res = CheckInteger(input, required); var res = CheckInteger(input, required);
using var ctx = new AppDbContext();
if (!res.IsValid) { if (!res.IsValid) {
return res; return res;
} else if (!required && input.Text.Length == 0) { } else if (!required && input.Text.Length == 0) {
@ -575,7 +580,7 @@ namespace Elwig.Helpers {
} }
} }
public static ValidationResult CheckFbNr(TextBox input, bool required, AppDbContext ctx, AreaCom? c) { public static ValidationResult CheckFbNr(TextBox input, bool required, AreaCom? c) {
var res = CheckInteger(input, required); var res = CheckInteger(input, required);
if (!res.IsValid) { if (!res.IsValid) {
return res; return res;
@ -583,6 +588,7 @@ namespace Elwig.Helpers {
return new(true, null); return new(true, null);
} }
using var ctx = new AppDbContext();
int nr = int.Parse(input.Text); int nr = int.Parse(input.Text);
if (nr != c?.FbNr && ctx.FbNrExists(nr).GetAwaiter().GetResult()) { if (nr != c?.FbNr && ctx.FbNrExists(nr).GetAwaiter().GetResult()) {
return new(false, "Flächenbindungsnummer wird bereits verwendet"); return new(false, "Flächenbindungsnummer wird bereits verwendet");

View File

@ -1,4 +1,4 @@
using System; using System;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@ -26,6 +26,8 @@ namespace Elwig.Helpers.Weighing {
Model = model; Model = model;
IsReady = true; IsReady = true;
HasFillingClearance = false; HasFillingClearance = false;
Stream.WriteTimeout = -1;
Stream.ReadTimeout = -1;
BackgroundThread = new Thread(new ParameterizedThreadStart(BackgroundLoop)); BackgroundThread = new Thread(new ParameterizedThreadStart(BackgroundLoop));
BackgroundThread.Start(); BackgroundThread.Start();
} }
@ -46,7 +48,8 @@ namespace Elwig.Helpers.Weighing {
while (IsRunning) { while (IsRunning) {
try { try {
var data = await Receive(); var data = await Receive();
RaiseWeighingEvent(new WeighingEventArgs(data)); if (data != null)
RaiseWeighingEvent(new WeighingEventArgs(data.Value));
} catch (Exception ex) { } catch (Exception ex) {
MessageBox.Show($"Beim Wiegen ist ein Fehler Aufgetreten:\n\n{ex.Message}", "Waagenfehler", MessageBox.Show($"Beim Wiegen ist ein Fehler Aufgetreten:\n\n{ex.Message}", "Waagenfehler",
MessageBoxButton.OK, MessageBoxImage.Error); MessageBoxButton.OK, MessageBoxImage.Error);
@ -54,13 +57,20 @@ namespace Elwig.Helpers.Weighing {
} }
} }
protected async Task<WeighingResult> Receive() { protected async Task<WeighingResult?> Receive() {
string? line = null; var line = "";
using (var reader = new StreamReader(Stream, Encoding.ASCII, false, -1, true)) { while (line.Length < 33) {
line = await reader.ReadLineAsync(); var ch = Reader.Read();
if (LogPath != null) await File.AppendAllTextAsync(LogPath, $"{line}\r\n"); if (ch == -1) {
return null;
} else if (line.Length > 0 || ch == ' ') {
line += char.ToString((char)ch);
}
} }
if (line == null || line.Length != 33 || line[0] != ' ' || line[9] != ' ' || line[15] != ' ' || line[20] != ' ' || line[32] != ' ') { if (LogPath != null) await File.AppendAllTextAsync(LogPath, line);
if (line == null || line == "") {
return null;
} else if (line.Length != 33 || line[0] != ' ' || line[9] != ' ' || line[15] != ' ' || line[20] != ' ' || line[32] != ' ') {
throw new IOException($"Invalid event from scale: '{line}'"); throw new IOException($"Invalid event from scale: '{line}'");
} }
@ -77,7 +87,7 @@ namespace Elwig.Helpers.Weighing {
identNr = identNr.Length > 0 && identNr != "0" ? identNr : null; identNr = identNr.Length > 0 && identNr != "0" ? identNr : null;
var parsedDate = DateOnly.Parse(date); var parsedDate = DateOnly.Parse(date);
return new() { return new() {
Weight = int.Parse(netto), NetWeight = int.Parse(netto),
WeighingId = identNr, WeighingId = identNr,
FullWeighingId = identNr != null ? $"{parsedDate:yyyy-MM-dd}/{identNr}" : null, FullWeighingId = identNr != null ? $"{parsedDate:yyyy-MM-dd}/{identNr}" : null,
Date = parsedDate, Date = parsedDate,

View File

@ -0,0 +1,126 @@
using System;
using System.IO;
using System.IO.Ports;
using System.Text;
using System.Threading.Tasks;
namespace Elwig.Helpers.Weighing {
public class GassnerScale : Scale, ICommandScale {
public string Manufacturer => "Gassner";
public int InternalScaleNr => 1;
public string Model { get; private set; }
public string ScaleId { get; private set; }
public bool IsReady { get; private set; }
public bool HasFillingClearance { get; private set; }
public GassnerScale(string id, string model, string cnx, string? empty = null, string? filling = null, int? limit = null, string? log = null) :
base(cnx, empty, filling, limit, log) {
ScaleId = id;
Model = model;
IsReady = true;
HasFillingClearance = false;
Stream.WriteTimeout = 250;
Stream.ReadTimeout = 11000;
}
protected Task SendCommand(char command) => SendCommand(Convert.ToByte(command));
protected async Task SendCommand(byte command) {
byte[] cmd = [command];
await Stream.WriteAsync(cmd);
if (LogPath != null) await File.AppendAllTextAsync(LogPath, Encoding.ASCII.GetString(cmd));
}
protected async Task<string> ReceiveResponse() {
var line = await Reader.ReadUntilAsync('\x03');
if (LogPath != null) await File.AppendAllTextAsync(LogPath, $"{line}\r\n");
if (line == null || line.Length < 4 || !line.StartsWith('\x02')) {
throw new IOException("Invalid response from scale");
}
var status = line[1..3];
if (status[0] == 'E' || status[1] != 'S') {
string msg = $"Unbekannter Fehler (Fehler code {status})";
switch (status[1]) {
case 'M': msg = "Waage in Bewegung"; break;
}
throw new IOException($"Waagenfehler {status}: {msg}");
} else if (status[0] != ' ') {
throw new IOException($"Invalid response from scale (error code {status})");
}
return line[1..^1];
}
protected async Task<WeighingResult> Weigh(bool incIdentNr) {
await SendCommand(incIdentNr ? '\x05' : '?');
string record = await ReceiveResponse();
if (record.Length != 45)
throw new IOException("Invalid response from scale: Received record has invalid size");
var line = record[2..];
var brutto = line[ 0.. 7].Trim();
var tara = line[ 7..14].Trim();
var netto = line[14..21].Trim();
var scaleNr = line[21..23].Trim();
var identNr = line[23..29].Trim();
var date = line[29..37];
var time = line[37..43];
identNr = identNr.Length > 0 && identNr != "0" ? identNr : null;
return new() {
GrossWeight = int.Parse(brutto),
TareWeight = int.Parse(tara),
NetWeight = int.Parse(netto),
WeighingId = identNr,
FullWeighingId = identNr,
Date = DateOnly.TryParseExact(date, "yyyyMMdd", out var d) ? d : null,
Time = TimeOnly.TryParseExact(time, "HHmmss", out var t) ? t : null,
};
}
public async Task<WeighingResult> Weigh() {
return await Weigh(true);
}
public async Task<WeighingResult> GetCurrentWeight() {
return await Weigh(false);
}
public async Task Empty() {
SerialPort? p = ControlSerialEmpty ?? Serial;
if (EmptyMode == Output.RTS && p != null) {
p.RtsEnable = true;
await Task.Delay(EmptyDelay);
p.RtsEnable = false;
} else if (EmptyMode == Output.DTR && p != null) {
p.DtrEnable = true;
await Task.Delay(EmptyDelay);
p.DtrEnable = false;
}
}
protected Task SetFillingClearance(bool status) {
SerialPort? p = ControlSerialFilling ?? Serial;
if (FillingClearanceMode == Output.RTS && p != null) {
p.RtsEnable = status;
} else if (FillingClearanceMode == Output.DTR && p != null) {
p.DtrEnable = status;
}
return Task.CompletedTask;
}
public async Task GrantFillingClearance() {
await SetFillingClearance(true);
}
public async Task RevokeFillingClearance() {
await SetFillingClearance(false);
}
public Task SetDateAndTime(DateTime dateTime) {
throw new NotImplementedException("Für Waagen vom Typ 'Gassner' ist diese Funktion noch nicht implementiert");
}
}
}

View File

@ -1,8 +1,9 @@
using System.Threading.Tasks; using System;
using System.Threading.Tasks;
namespace Elwig.Helpers.Weighing { namespace Elwig.Helpers.Weighing {
/// <summary> /// <summary>
/// Interface for controlling a a scale which responds to commands sent to it /// Interface for controlling a weighing scale which responds to commands sent to it
/// </summary> /// </summary>
public interface ICommandScale : IScale { public interface ICommandScale : IScale {
/// <summary> /// <summary>
@ -31,5 +32,10 @@ namespace Elwig.Helpers.Weighing {
/// Revoke clearance to fill the scale container /// Revoke clearance to fill the scale container
/// </summary> /// </summary>
Task RevokeFillingClearance(); Task RevokeFillingClearance();
/// <summary>
/// Try to set date and time on the scale
/// </summary>
Task SetDateAndTime(DateTime dateTime);
} }
} }

View File

@ -1,6 +1,6 @@
namespace Elwig.Helpers.Weighing { namespace Elwig.Helpers.Weighing {
/// <summary> /// <summary>
/// Interface for controlling a a scale which automatically sends weighing updates /// Interface for controlling a weighing scale which automatically sends weighing updates
/// </summary> /// </summary>
public interface IEventScale : IScale { public interface IEventScale : IScale {

View File

@ -2,7 +2,7 @@ using System;
namespace Elwig.Helpers.Weighing { namespace Elwig.Helpers.Weighing {
/// <summary> /// <summary>
/// Interface for controlling a industrial scale (industrial terminal, "IT") /// Interface for controlling a industrial weighing scale (industrial terminal, "IT")
/// </summary> /// </summary>
public interface IScale : IDisposable { public interface IScale : IDisposable {
/// <summary> /// <summary>

View File

@ -1,7 +1,8 @@
using System.IO.Ports; using System.IO.Ports;
using System.IO; using System.IO;
using System.Net.Sockets; using System.Net.Sockets;
using System; using System;
using System.Text;
namespace Elwig.Helpers.Weighing { namespace Elwig.Helpers.Weighing {
public abstract class Scale : IDisposable { public abstract class Scale : IDisposable {
@ -12,6 +13,7 @@ namespace Elwig.Helpers.Weighing {
protected SerialPort? ControlSerialEmpty = null, ControlSerialFilling = null; protected SerialPort? ControlSerialEmpty = null, ControlSerialFilling = null;
protected TcpClient? Tcp = null; protected TcpClient? Tcp = null;
protected Stream Stream; protected Stream Stream;
protected StreamReader Reader;
protected readonly Output? EmptyMode = null; protected readonly Output? EmptyMode = null;
protected readonly Output? FillingClearanceMode = null; protected readonly Output? FillingClearanceMode = null;
@ -26,6 +28,8 @@ namespace Elwig.Helpers.Weighing {
return new SysTecITScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log); return new SysTecITScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log);
} else if (config.Type == "Avery-Async") { } else if (config.Type == "Avery-Async") {
return new AveryEventScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log); return new AveryEventScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log);
} else if (config.Type == "Gassner") {
return new GassnerScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log);
} else { } else {
throw new ArgumentException($"Invalid scale type: \"{config.Type}\""); throw new ArgumentException($"Invalid scale type: \"{config.Type}\"");
} }
@ -41,6 +45,7 @@ namespace Elwig.Helpers.Weighing {
} else { } else {
throw new ArgumentException($"Unsupported scheme: \"{cnx.Split(':')[0]}\""); throw new ArgumentException($"Unsupported scheme: \"{cnx.Split(':')[0]}\"");
} }
Reader = new(Stream, Encoding.ASCII, false, 512);
LogPath = log; LogPath = log;
@ -73,6 +78,7 @@ namespace Elwig.Helpers.Weighing {
} }
public void Dispose() { public void Dispose() {
Reader.Close();
Stream.Close(); Stream.Close();
Serial?.Close(); Serial?.Close();
ControlSerialEmpty?.Close(); ControlSerialEmpty?.Close();

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