Compare commits

..

172 Commits

Author SHA1 Message Date
e9e90b1e98 Bump version to 1.0.3.0
All checks were successful
Test / Run tests (push) Successful in 3m4s
Deploy / Build and Deploy (push) Successful in 2m8s
2026-01-16 14:43:52 +01:00
640dbf705e Printing/Pdf: Fix detection of WinziPrint.exe to isolate tests properly
All checks were successful
Test / Run tests (push) Successful in 2m11s
2026-01-16 01:26:35 +01:00
42121fe7da Elwig: Update dependencies
Some checks failed
Test / Run tests (push) Has been cancelled
2026-01-16 00:35:50 +01:00
d45c3f867f Tests: Update dependencies 2026-01-16 00:35:38 +01:00
a90be2644d [#50] MailWindow: Add button to cancel document generation
Some checks failed
Test / Run tests (push) Has been cancelled
2026-01-16 00:22:11 +01:00
01739ba42e ClientParameters: Fix storing of MailSendPostal and MailSendEmail 2026-01-16 00:20:15 +01:00
7bea4d9ee0 BillingVariant: Fix NULL/0 error introduced in c7a2f2241d
All checks were successful
Test / Run tests (push) Successful in 2m12s
2026-01-15 17:56:12 +01:00
b6497fd422 PaymentVariantsWindow: Select last variant by default and ask user before deleting
All checks were successful
Test / Run tests (push) Successful in 2m33s
2026-01-15 14:03:00 +01:00
f141485537 Documents: Only show header and footer on BusinessDocuments 2026-01-15 14:01:21 +01:00
b31603554a Document: Add IsPreview to indicate that a document may only be viewed by internal staff
All checks were successful
Test / Run tests (push) Successful in 2m51s
2026-01-15 13:26:44 +01:00
90def81cc5 PaymentVariantSummary: Fix statistical sum for geb weight
All checks were successful
Test / Run tests (push) Successful in 2m52s
2026-01-14 09:15:27 +01:00
87903227b5 [#66] Services: Add filters for import/export/upload 2026-01-03 20:49:00 +01:00
6d6776f0f9 DeliveryAdminWindow: Complete tooltip in search bar and warning when no season is found 2026-01-03 20:48:39 +01:00
bf3cc2ea1e [#71] Weighing: Fix reconnection behaviour when COM port is connected/disconnected 2026-01-03 20:48:39 +01:00
c1697dc4f3 [#66] MainWindow: Merge both sync buttons into a single one 2026-01-03 20:47:51 +01:00
3b335a568e AbbDbContext: Move SqliteConnection extensions to Extensions class 2026-01-03 20:47:45 +01:00
beacdab54f [#66] Services: Add SyncService 2026-01-03 20:47:14 +01:00
da05a49e10 DeliveryService: Add modifiers when splitting delivery parts
All checks were successful
Test / Run tests (push) Successful in 2m31s
2025-12-18 18:12:14 +01:00
5cec5b3556 QueryWindow: Allow users to export query result to csv file
All checks were successful
Test / Run tests (push) Successful in 1m47s
2025-12-16 16:55:52 +01:00
9af498287d Database: Add v_member view
All checks were successful
Test / Run tests (push) Successful in 2m32s
2025-12-16 15:58:45 +01:00
452f246f24 [#73] DeliveryAdminWindow: Add Liefermengen Excel output 2025-12-15 00:03:45 +01:00
e97c29db43 Dtos: Rename MemberDeliveryPerVarietyData to MemberDeliveryYieldsPerVarietyData 2025-12-14 23:14:40 +01:00
3419113dec DeliveryAdminWindow: Restrict handling of modifiers to specific clients
All checks were successful
Test / Run tests (push) Successful in 2m19s
2025-12-13 08:30:10 +01:00
bf6297f63b DeliveryPart: Add Unloading type instead of IsLesewagen
All checks were successful
Test / Run tests (push) Successful in 1m54s
2025-12-12 12:35:48 +01:00
f228ba3019 AppDbContext: Fix silent fail in ExecuteBatch() 2025-12-11 16:08:11 +01:00
495aa8a691 DeliveryNote: Add option to redact modifier value
All checks were successful
Test / Run tests (push) Successful in 2m26s
2025-12-11 13:05:40 +01:00
811916a8b9 Controls/CheckComboBox: Apply AllItemsSelectedContent only when more than 1 items are selected 2025-12-11 13:05:40 +01:00
3b6333a6a2 Tests: Update dependencies
All checks were successful
Test / Run tests (push) Successful in 2m22s
2025-11-29 12:34:33 +01:00
9b37330362 Elwig: Update dependencies 2025-11-29 12:34:33 +01:00
889a17b21c Upgrade to .NET 10 2025-11-29 12:34:33 +01:00
ac6d559e5d Helpers/Utils: Fix mail log for single mails 2025-11-29 12:34:29 +01:00
6d80cca241 Bump version to 1.0.2.0
All checks were successful
Test / Run tests (push) Successful in 1m44s
Deploy / Build and Deploy (push) Successful in 1m36s
2025-11-10 00:15:46 +01:00
9dc225d3e4 Export/VCard: Add Escape method
All checks were successful
Test / Run tests (push) Successful in 1m54s
2025-11-10 00:13:53 +01:00
0d513f7bff Tests: Update dependencies
All checks were successful
Test / Run tests (push) Successful in 2m3s
2025-11-09 23:49:42 +01:00
b10c744bf9 Elwig: Update dependencies 2025-11-09 23:49:33 +01:00
e6367da286 App: Add SerialPortWatcher
Some checks failed
Test / Run tests (push) Has been cancelled
2025-11-09 23:47:38 +01:00
01f4480a08 MemberAdminWindow: Add feature to export .vcf files
All checks were successful
Test / Run tests (push) Successful in 1m42s
2025-10-31 17:19:09 +01:00
e9722c790c Bump version to 1.0.1.5
All checks were successful
Test / Run tests (push) Successful in 1m45s
Deploy / Build and Deploy (push) Successful in 1m42s
2025-10-29 08:16:59 +01:00
af98c32026 MailWindow: Fix crash when using Button in Leseabschluss
Some checks failed
Test / Run tests (push) Has been cancelled
2025-10-29 08:14:37 +01:00
7300b30cf5 Bump version to 1.0.1.4
All checks were successful
Test / Run tests (push) Successful in 1m44s
Deploy / Build and Deploy (push) Successful in 1m55s
2025-10-28 13:17:58 +01:00
428cd6ddc2 BillingVariant: Assume price to be 0 for undefined vaributes
All checks were successful
Test / Run tests (push) Successful in 1m56s
2025-10-28 13:15:24 +01:00
2de8af878b MailWindow: Fix crash when generating docs for no-email and postal-where-no-email
All checks were successful
Test / Run tests (push) Successful in 2m31s
2025-10-28 11:37:13 +01:00
34d95eab9d Bump version to 1.0.1.3
All checks were successful
Test / Run tests (push) Successful in 1m47s
Deploy / Build and Deploy (push) Successful in 1m43s
2025-10-13 20:40:55 +02:00
548aeb2ce9 DeliveryAdminWindow: Show delivery (part) comments in list 2025-10-13 20:39:09 +02:00
7edd888aa2 DeliveryAncmtAdminWindow: Increase performace by aggregating AnnouncedWeight
All checks were successful
Test / Run tests (push) Successful in 2m23s
2025-10-07 20:41:15 +02:00
a0d4f19f30 DeliveryAdminWindow: Include attribute and cultivation in wine variety column
All checks were successful
Test / Run tests (push) Successful in 2m24s
2025-10-05 21:21:03 +02:00
67ba342c28 App: Delay auto update check even more
All checks were successful
Test / Run tests (push) Successful in 2m15s
2025-09-29 16:14:16 +02:00
1b69fcb16a Bump version to 1.0.1.2
All checks were successful
Test / Run tests (push) Successful in 1m48s
Deploy / Build and Deploy (push) Successful in 1m40s
2025-09-25 22:51:35 +02:00
c8a95422af Export/Database: Add version and meta.json entry to zip export
All checks were successful
Test / Run tests (push) Successful in 2m13s
2025-09-25 21:08:11 +02:00
9d02f18bac ElwigData: Fail more gracefully when single files may not be processed
All checks were successful
Test / Run tests (push) Successful in 1m47s
2025-09-25 12:08:43 +02:00
f9ee2cb120 Bump version to 1.0.1.1
All checks were successful
Test / Run tests (push) Successful in 1m43s
Deploy / Build and Deploy (push) Successful in 1m38s
2025-09-21 22:15:47 +02:00
b27b89f599 DeliveryAdminWindow: Improve responsiveness by caching wine quality levels
All checks were successful
Test / Run tests (push) Successful in 1m54s
2025-09-21 22:09:10 +02:00
bfbd0a6a22 README: Add about sections 2025-09-21 15:15:57 +02:00
e2de7a1f1c DeliveryAdminWindow: Unselect wine quality level on invalid gradation 2025-09-19 10:33:19 +02:00
3f769eb7d7 Bump version to 1.0.1.0
All checks were successful
Test / Run tests (push) Successful in 1m58s
Deploy / Build and Deploy (push) Successful in 1m53s
2025-09-18 23:44:15 +02:00
8bc053053c App: Add auto update check on network change to active
All checks were successful
Test / Run tests (push) Successful in 1m55s
2025-09-18 23:37:10 +02:00
a0dcaf7b4f Elwig: Update database to include new BKI wine varieties
All checks were successful
Test / Run tests (push) Successful in 2m43s
2025-09-18 23:29:38 +02:00
844fc5217a Utils: Add User-Agent to default http client
All checks were successful
Test / Run tests (push) Successful in 1m53s
2025-09-17 15:09:28 +02:00
542afa5892 Bump version to 1.0.0.6
All checks were successful
Test / Run tests (push) Successful in 1m38s
Deploy / Build and Deploy (push) Successful in 1m37s
2025-09-17 09:58:20 +02:00
9e02b15ff1 Elwig: Fix extensions in SaveFileDialog
All checks were successful
Test / Run tests (push) Successful in 1m49s
2025-09-17 09:55:19 +02:00
463769b549 App: Fix auto selection of weighing mode
All checks were successful
Test / Run tests (push) Successful in 2m17s
2025-09-17 09:33:34 +02:00
2c383d0c55 AboutWindow: Fix typo in description 2025-09-17 09:33:11 +02:00
5ba74c8368 Bump version to 1.0.0.5
All checks were successful
Test / Run tests (push) Successful in 1m40s
Deploy / Build and Deploy (push) Successful in 1m36s
2025-09-15 23:14:49 +02:00
3c9b3c2db1 Windows: Add AboutWindow
All checks were successful
Test / Run tests (push) Successful in 1m39s
2025-09-15 23:09:52 +02:00
98f8907817 Elwig: Update dependencies
All checks were successful
Test / Run tests (push) Successful in 1m56s
2025-09-15 21:37:12 +02:00
44dcc5e19f Setup: Update dependencies 2025-09-15 21:36:57 +02:00
d7012ebfa1 Config: Add option to specify weighing mode
All checks were successful
Test / Run tests (push) Successful in 2m8s
2025-09-15 21:16:36 +02:00
a9b5317e79 DeliveryAdminWindow: Make GerebeltGewogenInput required in Matzen
All checks were successful
Test / Run tests (push) Successful in 1m49s
2025-09-15 11:28:57 +02:00
f02598760f Export: Implement exporting and importing of sqlite3 and sql files
All checks were successful
Test / Run tests (push) Successful in 2m15s
2025-09-15 10:53:27 +02:00
4b8cd2a0d7 Bump version to 1.0.0.4
All checks were successful
Test / Run tests (push) Successful in 1m40s
Deploy / Build and Deploy (push) Successful in 1m45s
2025-09-01 22:03:46 +02:00
104798d4f2 DeliveryAdminWindow: Enable WineQualityLevelInput in Übernahme
All checks were successful
Test / Run tests (push) Successful in 2m14s
2025-09-01 21:55:22 +02:00
4653a4f129 LogWindow: Improve loading time
All checks were successful
Test / Run tests (push) Successful in 1m46s
2025-08-20 16:33:25 +02:00
07f9a0f522 Utils: Fix thread error when sending emails
All checks were successful
Test / Run tests (push) Successful in 2m27s
2025-08-19 15:42:47 +02:00
7e21d9e2e4 Bump version to 1.0.0.3
All checks were successful
Deploy / Build and Deploy (push) Successful in 1m42s
2025-08-11 23:01:15 +02:00
4e2eca295d Tests: Update dependencies
All checks were successful
Test / Run tests (push) Successful in 1m45s
2025-08-11 22:50:12 +02:00
1f165055c1 Elwig: Update dependencies 2025-08-11 22:50:12 +02:00
7b953fa73e [#70] ElwigData: Sync WbKg 2025-08-11 22:50:04 +02:00
d3157e4d48 ElwigData: Check zip file integrity
All checks were successful
Test / Run tests (push) Successful in 1m59s
2025-08-06 12:31:19 +02:00
73fe4531cc [#69] Elwig: Add functionality to upload and replace database
All checks were successful
Test / Run tests (push) Successful in 2m23s
2025-08-06 10:50:33 +02:00
b6c03892b1 Helpers: Fix Issues for importing/exporting
All checks were successful
Test / Run tests (push) Successful in 1m58s
2025-08-05 23:45:59 +02:00
91950ad9fd Bump version to 1.0.0.2
All checks were successful
Test / Run tests (push) Successful in 1m55s
Deploy / Build and Deploy (push) Successful in 1m39s
2025-08-05 19:19:45 +02:00
77c3f388e7 Elwig: Add SQLitePCLRaw.bundle_e_sqlite3 explicitly
Some checks failed
Test / Run tests (push) Successful in 1m52s
Deploy / Build and Deploy (push) Failing after 12s
2025-08-05 19:06:09 +02:00
6f3e1a8905 Bump version to 1.0.0.1
All checks were successful
Test / Run tests (push) Successful in 1m54s
Deploy / Build and Deploy (push) Successful in 1m49s
2025-08-05 18:57:43 +02:00
466c8a322c Elwig: Update dependencies
All checks were successful
Test / Run tests (push) Successful in 2m28s
2025-08-05 18:35:24 +02:00
ab7c7404e2 ElwigData: Update error message for user 2025-08-05 18:33:33 +02:00
ea3ea5ca83 Bump version to 1.0.0.0
All checks were successful
Deploy / Build and Deploy (push) Successful in 2m29s
2025-07-30 17:37:25 +02:00
93e920f61a Add GNU GPLv3 as license 2025-07-29 23:53:53 +02:00
9b48242f0e Setup: Update dependencies
All checks were successful
Test / Run tests (push) Successful in 2m32s
2025-07-29 23:43:42 +02:00
7153bfab6f Tests: Update dependencies 2025-07-29 23:43:27 +02:00
4b27ebf81b Elwig: Update dependencies 2025-07-29 23:43:17 +02:00
c26e4a2dbd [#61] Windows: Use more red and green color to indicate type of WineVariety
All checks were successful
Test / Run tests (push) Successful in 2m27s
2025-07-28 20:38:20 +02:00
fad1e28c06 Documents: Add Bio-KSt. to footer
All checks were successful
Test / Run tests (push) Successful in 2m24s
2025-07-23 17:07:10 +02:00
c61c2e3fcd [#64] AreaComAdminWindow: Add dialog for modifying/deleting
All checks were successful
Test / Run tests (push) Successful in 2m21s
2025-07-09 18:14:27 +02:00
267aa3d47c Dialogs: Rename AreaComDialog to AreaComTransferDialog
Some checks failed
Test / Run tests (push) Has been cancelled
2025-07-09 16:56:08 +02:00
7e9a27c75d Services: Move App.HintContextChange() to Windows
Some checks failed
Test / Run tests (push) Has been cancelled
2025-07-09 16:07:47 +02:00
53d82604a1 AreaComAdminWindow: Enhance visual appearance
All checks were successful
Test / Run tests (push) Successful in 1m53s
2025-07-09 00:39:01 +02:00
bb65a1f20e [#62] CreditNote: Add longer custom text to bottom
All checks were successful
Test / Run tests (push) Successful in 1m53s
2025-07-08 16:06:12 +02:00
c6748055fd [#61] Windows: Use red and green color to indicate type of WineVariety
All checks were successful
Test / Run tests (push) Successful in 2m17s
2025-07-08 15:27:13 +02:00
e6746f76b1 Windows: Small fixes 2025-07-08 15:27:13 +02:00
e9f389b885 DeliveryService: Fix spacing in tooltip 2025-07-08 15:27:13 +02:00
919ab3356d [#57] MailWindow: Use Task.Run() 2025-07-08 15:27:13 +02:00
39060f6a1e [#57] PaymentAdjustmentWindow: Use Task.Run() 2025-07-08 15:26:57 +02:00
070dda1bde [#57] ChartWindow: Use Task.Run() 2025-07-07 23:53:08 +02:00
b6bfc60ab8 [#57] DeliveryScheduleAdminWindow: Use Task.Run() 2025-07-07 23:39:55 +02:00
8aaa22fcb4 [#57] DeliveryAncmtAdminWindow: Use Task.Run() 2025-07-07 23:36:08 +02:00
64832e9a53 [#57] PaymentVariantsWindow: Use Task.Run()
All checks were successful
Test / Run tests (push) Successful in 1m49s
2025-07-07 23:31:03 +02:00
7c9844e277 [#57] MainWindow: Use Task.Run()
All checks were successful
Test / Run tests (push) Successful in 1m40s
2025-07-07 22:45:28 +02:00
19d2208fc3 [#57] AreaComAdminWindow: Use Task.Run() 2025-07-07 22:45:28 +02:00
38d0ff969d BaseDataWindow: Use Cursors.Wait instead of Cursors.AppStarting 2025-07-07 22:45:28 +02:00
fb2b998d1b [#57] DeliveryAdminWindow: Use Task.Run() 2025-07-07 22:45:28 +02:00
10d5f49f63 [#57] MemberAdminWinodw: Use Task.Run() 2025-07-07 22:45:28 +02:00
41811925be PaymentVariantsWindow: Add ViewModel and Service
All checks were successful
Test / Run tests (push) Successful in 1m58s
2025-07-07 22:44:30 +02:00
97300b6e30 [#65] MailLogWindow: Fix initial loading
All checks were successful
Test / Run tests (push) Successful in 1m49s
2025-07-03 13:03:01 +02:00
c6c8fd9b68 ChartWindow: Automatically calculate variant when saved 2025-07-03 13:03:01 +02:00
913129f155 PaymentVariantsWindow: Add estimates for uncommited variants 2025-07-03 13:02:57 +02:00
953532cae4 MemberService: Filter area commitments by CurrentLastSeason instead of CurrentYear
All checks were successful
Test / Run tests (push) Successful in 2m15s
2025-06-24 13:31:58 +02:00
267797b55d AreaComDialog: Fix property name (MaintainYearFrom) 2025-06-24 13:30:04 +02:00
4234c7f994 Elwig: Add ITime/XTime to entities and allow to export/import CTime/MTime
All checks were successful
Test / Run tests (push) Successful in 2m30s
2025-06-17 18:06:05 +02:00
3493ff6df1 Tests: Add UnitTests folder
All checks were successful
Test / Run tests (push) Successful in 2m24s
2025-06-03 20:57:53 +02:00
623f55f5b0 App: Rename ExePath to InstallPath
All checks were successful
Test / Run tests (push) Successful in 2m13s
2025-05-27 11:43:34 +02:00
b580e1bf79 App: Use environment variables for paths
All checks were successful
Test / Run tests (push) Successful in 1m42s
2025-05-26 15:18:32 +02:00
b08ca63bed [#59] Documents: Move Documents install folder from ProgramData to Program Files
All checks were successful
Test / Run tests (push) Successful in 2m19s
2025-05-26 15:12:49 +02:00
6a5f4cefa9 [#58] PaymentVariantSummary: Add 'attributlos gebunden' columns 2025-05-23 15:43:39 +02:00
0f3ce39f35 Elwig: Use 'Menge' instead of 'Gewicht' where applicable
All checks were successful
Test / Run tests (push) Successful in 1m59s
2025-05-20 15:55:11 +02:00
336aef5c70 CreditNoteDeliveryData: Include WeighingModifier only if delivery modifiers are considered 2025-05-20 13:09:29 +02:00
0dff3986b7 PaymentVariantsWindow: Add note, that modifiers include Rebelzuschlag
All checks were successful
Test / Run tests (push) Successful in 2m30s
2025-05-20 12:07:28 +02:00
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 9930e6173c
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
180 changed files with 7706 additions and 2369 deletions

View File

@@ -1,7 +1,7 @@
name: Deploy
on:
push:
tags: ["v[0-9]+.[0-9]+.[0-9]+"]
tags: ["v[0-9]+.[0-9]+.[0-9]+.[0-9]+"]
jobs:
deploy:
name: Build and Deploy

View File

@@ -2,21 +2,432 @@
Changelog
=========
[v1.0.3.0][v1.0.3.0] (2026-01-16) {#v1.0.3.0}
---------------------------------------------
### Neue Funktionen {#v1.0.3.0-features}
* Bei Zu-/Abschlägen ist es nun möglich den konkreten Wert dieser auf Lieferscheinen nicht anzuzeigen (Stammdaten -> Saisons). (495aa8a691)
* Bei Teillieferungen kann nun die Art der Anlieferung angegeben werden: Planenwagen/Kipper, Lesewagen, Kiste(n). (bf6297f63b, 3419113dec, 6d6776f0f9)
* Im Lieferungen-Fenster (`DeliveryAdminWindow`) können nun die Gesamtmengen der gefilterten Lieferungen nach Mitglied und Sorte als Excel-Liste exportiert werden ("Liefermengen"). ([#73][i73], e97c29db43)
* Im Datenbankabfragen-Fenster (`QueryWindow`) gibt es die Möglichkeit die Ergebnisse als CSV-Datei zu exportieren. (9af498287d, 5cec5b3556)
* PDF-Dokumente wurden überarbeitet:
* Traubengutschriften können nur noch ausgedruckt bzw. verschickt werden, wenn die zugehörige Auszahlungsvariante festgesetzt ist. (b31603554a)
* Auf Dokumenten für interne Verwendung (alle außer an Mitglieder adressierte) wurden Kopf- und Fußzeile auf ein Minimum beschränkt. (f141485537)
* Das Synchronisieren zwischen Zweigstellen ist für den Benutzer nun wesentlich vereinfacht. Geänderte Daten von Mitgliedern, Flächenbindungen oder Lieferungen werden beim Synchronisieren automatsich hochgeladen bzw. heruntergeladen. ([#66][i66], 3b335a568e)
* Im Rundschreiben-Fenster (`MailWindow`) wurde ein Knopf zum Abbrechen des Generierens hinzugefügt. ([#50][i50])
### Behobene Fehler {#v1.0.3.0-bugfixes}
* Einzelne (nicht über das Rundschreiben-Fenster) verschickte E-Mails werde korrekt im Ausgangsprotokoll angeführt. (ac6d559e5d)
* Bei automatischen Datenbank-Updates sind auftretende Fehler ignoriert worden. (f228ba3019)
* Beim Aufteilen von Lieferungen sind Zu-/Abschläge nicht auf neu erstellen Teillieferungen übernommen worden. (da05a49e10)
* Das Verbinden und Trennen von Waagen mittels Serial-/COM-Port ist nun bei laufendem Programm möglich. ([#71][i71])
* In den Auszahlungsvarianten-Daten (`PaymentVariantSummary`) ist die statistische Summe der gebundenen und ungebundenen Menge ohne die Spalte _attributlos gebunden_ berechnet worden. (90def81cc5)
* Seit [v0.13.7](#v0.13.7) (2025-01-21) wurde auch bei der ersten Auszahlung der Saison ein "Bisher berücksichtigt: € 0,00" auf den Traubengutschriften angeführt. (7bea4d9ee0)
* Das Speichern der Parameter `MAIL_SEND_POSTAL` und `MAIL_SEND_EMAIL` war fehlerhaft. (01739ba42e)
* Bessere Isolation der automatisierten Dokumenten-Tests. (640dbf705e)
### Sonstiges {#v1.0.3.0-misc}
* Im Lieferungen-Fenster (`DeliveryAdminWindow`) wird bei Zu-/Abschlägen nur "Alle" angezeigt, wenn mehr als 1 ausgewählt sind. (811916a8b9)
* Im Auszahlungsvarianten-Fenster (`PaymentVariantsWindow`) wird die letzte Variante standardmäßig ausgewählt und beim Löschen wird um Bestätigung gebeten. (b6497fd422)
* Abhängigkeiten aktualisiert. (889a17b21c, 9b37330362, 3b6333a6a2, d45c3f867f, 42121fe7da)
[v1.0.3.0]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.3.0
[i50]: https://git.necronda.net/winzer/elwig/issues/50
[i66]: https://git.necronda.net/winzer/elwig/issues/60
[i71]: https://git.necronda.net/winzer/elwig/issues/71
[i73]: https://git.necronda.net/winzer/elwig/issues/73
[v1.0.2.0][v1.0.2.0] (2025-11-10) {#v1.0.2.0}
---------------------------------------------
### Neue Funktionen {#v1.0.2.0-features}
* Im Mitglieder-Fenster (`MemberAdminWindow`) können Kontaktdaten der Mitglieder als .vcf-Datei exportiert werden. (01f4480a08, 9dc225d3e4)
### Sonstiges {#v1.0.2.0-misc}
* Wenn ein Serial-/COM-Port-USB-Adapter an- oder abgesteckt wird, wird das nun automatisch erkannt. (e6367da286)
* Abhängigkeiten aktualisiert. (b10c744bf9, 0d513f7bff)
[v1.0.2.0]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.2.0
[v1.0.1.5][v1.0.1.5] (2025-10-29) {#v1.0.1.5}
---------------------------------------------
### Behobene Fehler {#v1.0.1.5-bugfixes}
* Im Rundschreiben-Fenster (`MailWindow`) kam es zu einem Absturz, wenn man das Fenster über den "Anlieferungsbestätigung"-Knopf im Leseabschluss-Abschnitt geöffnet hat. (af98c32026)
[v1.0.1.5]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.1.5
[v1.0.1.4][v1.0.1.4] (2025-10-28) {#v1.0.1.4}
---------------------------------------------
### Behobene Fehler {#v1.0.1.4-bugfixes}
* Im Rundschreiben-Fenster (`MailWindow`) kam es zu einem Absturz, wenn man die Zustelloptionen "Post zusenden an Mitglieder, die keine E-Mail erhalten würden" und "E-Mail zusenden an niemanden" kombiniert hat. (2de8af878b)
### Sonstiges {#v1.0.1.4-misc}
* Im Auszahlungsvariante-Fenster (`ChartWindow`) gibt es keine Fehlermeldung mehr wenn nicht für alle Sorten ein Preis definiert ist, nur noch eine Warnung. (428cd6ddc2)
[v1.0.1.4]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.1.4
[v1.0.1.3][v1.0.1.3] (2025-10-13) {#v1.0.1.3}
---------------------------------------------
### Neue Funktionen {#v1.0.1.3-features}
* In der Liste des Lieferungen-Fenster (`DeliveryAdminWindow`) werden
* statt ausschließlich der Sorte auch Attribut und Bewirtschaftungsart angezeigt. (a0d4f19f30)
* Kommentare der Lieferungen (und Teillieferungen) angezeigt. (548aeb2ce9)
### Sonstiges {#v1.0.1.3-misc}
* Verzögerung der Überprüfung auf automatische Updates auf einige Sekunden verlängert. (67ba342c28)
* Verbesserung der Ladezeiten im Anmeldungen-Fenster (`DeliveryAncmtAdminWindow`). (7edd888aa2)
[v1.0.1.3]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.1.3
[v1.0.1.2][v1.0.1.2] (2025-09-25) {#v1.0.1.2}
---------------------------------------------
### Behobene Fehler {#v1.0.1.2-bugfixes}
* Beim automatischen Importieren/Synchronisieren wird bei einem Fehlerfall der Benutzer verständigt, aber der Vorgang nicht abgebrochen. (9d02f18bac)
### Sonstiges {#v1.0.1.2-misc}
* Beim Sichern der Datenbank werden Meta-Informationen in der ZIP-Datei gespeichert. (c8a95422af)
[v1.0.1.2]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.1.2
[v1.0.1.1][v1.0.1.1] (2025-09-21) {#v1.0.1.1}
---------------------------------------------
### Sonstiges {#v1.0.1.1-misc}
* Eingabe von Sorten und Qualitätsstufen im Übernahme-Fenster (`DeliveryAdminWindows`) verbessert. (e2de7a1f1c, b27b89f599)
[v1.0.1.1]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.1.1
[v1.0.1.0][v1.0.1.0] (2025-09-18) {#v1.0.1.0}
---------------------------------------------
### Neue Funktionen {#v1.0.1.0-features}
* Neue Weinsorten gemäß Kürzelliste der Bundeskellereiinspektion hinzugefügt. (a0dcaf7b4f)
### Sonstiges {#v1.0.1.0-misc}
* HTTP-Anfragen haben jetzt das Feld `User-Agent` gesetzt. (844fc5217a)
* Auto-Update-Funktion wird auch beim Erlangen von Netzwerkverbindung ausgeführt. (8bc053053c)
[v1.0.1.0]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.1.0
[v1.0.0.6][v1.0.0.6] (2025-09-17) {#v1.0.0.6}
---------------------------------------------
### Behobene Fehler {#v1.0.0.6-bugfixes}
* Die automatische Erkennung des Wiege-Modus hat im WKW nicht funktioniert. (463769b549)
### Sonstiges {#v1.0.0.6-misc}
* Tippfehler im Über-Fenster (`AboutWindow`) behoben. (2c383d0c55)
* `SaveFileDialog` mit Multi-File-Extensions verbessert. (9e02b15ff1)
[v1.0.0.6]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.0.6
[v1.0.0.5][v1.0.0.5] (2025-09-15) {#v1.0.0.5}
---------------------------------------------
### Neue Funktionen {#v1.0.0.5-features}
* Die Datenbank kann im Haupt-Fenster (`MainWindow`) gesichert und wiederhergestellt werden. (f02598760f)
### Sonstiges {#v1.0.0.5-misc}
* In der WGM ist eine Auswahl eines Zu/-Abschlags im Übernahme-Fenster (`DeliveryAdminWindow`) nun erforderlich. (a9b5317e79)
* In der Konfigurationsdatei kann im `[general]` Block `weighing = gross`, `weighing = net`, oder `weighing = box` angegeben werden. (d7012ebfa1)
* Über-Fenser (`AboutWindow`) hinzugefügt. (3c9b3c2db1)
* Abhängigkeiten aktualisiert. (44dcc5e19f, 98f8907817)
[v1.0.0.5]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.0.5
[v1.0.0.4][v1.0.0.4] (2025-09-01) {#v1.0.0.4}
---------------------------------------------
### Behobene Fehler {#v1.0.0.4-bugfixes}
* Absturz beim Verschicken von einzelnen E-Mails außerhalb des Rundschreiben-Fensters (`MailWindow`) behoben. (07f9a0f522)
### Sonstiges {#v1.0.0.4-misc}
* Ladezeit des Fehler-Protokoll-Fensters (`LogWindow`) verbessert. (4653a4f129)
* Im Übernahme-Fenster (`DeliveryAdminWindow`) ist es nun möglich die Qualitätsstufe zu verändern. (104798d4f2)
[v1.0.0.4]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.0.4
[v1.0.0.3][v1.0.0.3] (2025-08-11) {#v1.0.0.3}
---------------------------------------------
### Neue Funktionen {#v1.0.0.3-features}
* Im Haupt-Fenster (`MainWindow`) ist es nun möglich die gesamte Datenbank zu hochzuladen bzw. herunterzuladen. ([#69][i69])
### Sonstiges {#v1.0.0.3-misc}
* Die Intigrität von Elwig-Export-Dateien (`.elwig.zip`) und anderen `.zip` Dateien wir nun überprüft. (d3157e4d48)
* Die Herkunftshierarchie wird nun auch automatisch synchronisiert (benötigt für Stamm-KG bei Mitglied, Ried/KG bei Flächenbindung, und Herkunft bei Lieferung). ([#70][i70], b6c03892b1)
* Abhängigkeiten aktualisiert. (1f165055c1, 4e2eca295d)
[v1.0.0.3]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.0.3
[i69]: https://git.necronda.net/winzer/elwig/issues/69
[i70]: https://git.necronda.net/winzer/elwig/issues/70
[v1.0.0.2][v1.0.0.2] (2025-08-05) {#v1.0.0.2}
---------------------------------------------
### Sonstiges {#v1.0.0.2-misc}
* Explizit native SQLite-Bibliothek hinzugefügt. (77c3f388e7)
[v1.0.0.2]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.0.2
[v1.0.0.1][v1.0.0.1] (2025-08-05) {#v1.0.0.1}
---------------------------------------------
### Sonstiges {#v1.0.0.1-misc}
* Abhängigkeiten aktualisiert. (466c8a322c)
* Angepasste Fehlermeldung, wenn Importieren fehlschlägt. (ab7c7404e2)
[v1.0.0.1]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.0.1
[v1.0.0.0][v1.0.0.0] (2025-07-30) {#v1.0.0.0}
---------------------------------------------
### Neue Funktionen {#v1.0.0.0-features}
* Es wird nun gespeichert, wann Mitglieder/Flächenbindungen/Lieferungen importiert bzw. exporiert wurden. (4234c7f994)
* Im Auszahlungsvarianten-Fenster (`PaymentVariantsWindow`) wird der Auszahlungsbetrag für nicht-festgesetzte Varianten nun überschlagsmäßig angezeigt. (913129f155)
* Wenn eine Variante im Auszahlungsvariante-Fenster (`ChartWindow`) gespeichert wird, wird sie nun automatisch auch berechnet. (c6c8fd9b68)
* Überall wo Weinsorten vorkommen werden diese jetzt rot oder grün eingefärbt angezeigt. ([#61][i61])
* Das Flächenbindungs-Fenster (`AreaComAdminWindow`) wurde überarbeitet. (53d82604a1, 267aa3d47c, [#64][i64])
* Auf allen Dokumenten wird der Bio-Kontrollstellen-Code in der Fußzeile angeführt (sofern dieser in den Stammdaten gesetzt wurde). (fad1e28c06)
### Behobene Fehler {#v1.0.0.0-bugfixes}
* Bei Traubengutschriften (`CreditNote`) wurde der Rebelzuschlag immer angeführt, auch wenn dieser in der zugrundeliegenden Berechnung nicht berücksichtigt wurde. (336aef5c70)
* In den Variantendaten einer Auszahlungsvariante (`PaymentVariantSummary`) wurde neben den Spalten _gebunden_ und _ungebunden_ noch _attributlos gebunden_ hinzugefügt. Ohne diese neue Spalte wären die Werte der anderen beiden falsch. ([#58][i58])
* Das erste Laden des Ausgangs-Protokoll-Fensters (`MailLogWindow`) hat nicht funktioniert. ([#65][i65])
* Im Lieferungen-Fenster (`DeliveryAdminWindow`) und im Mitglieder-Fenster (`MemberAdminWindow`) wird der Tool-Tip für Gewicht/Gradation mit korrektem Layout angezeigt. (e9f389b885)
* Bei Traubengutschriften (`CreditNote`) werden längere Freitexte vollständig angezeigt statt abgeschnitten. ([#62][i62])
### Sonstiges {#v1.0.0.0-misc}
* Im Auszahlungsvarianten-Fenster (`PaymentVariantsWindow`) wird angeführt, dass "Zu- und Abschläge bei Lieferungen" auch den Rebelzuschlag umfassen. (0dff3986b7)
* Im gesamten Program wird an passenden Stellen statt "Gewicht" nun "Menge" verwendet. (0f3ce39f35)
* Verbesserungen der Code-Qualität und der Leistung. ([#59][i59], b580e1bf79, 623f55f5b0, 3493ff6df1, 267797b55d, 41811925be, [#57][i57], 38d0ff969d, e6746f76b1, 7e9a27c75d)
* Im Mitglieder-Fenster (`MemberAdminWindow`) werden die Flächenbindungen jetzt für die momentane Saison angezeigt statt für das aktuelle Jahr. (953532cae4)
* Abhängigkeiten aktualisiert. (4b27ebf81b, 7153bfab6f, 9b48242f0e)
* GPLv3 Lizenz hinzugefügt. (93e920f61a)
[v1.0.0.0]: https://git.necronda.net/winzer/elwig/releases/tag/v1.0.0.0
[i57]: https://git.necronda.net/winzer/elwig/issues/57
[i58]: https://git.necronda.net/winzer/elwig/issues/58
[i59]: https://git.necronda.net/winzer/elwig/issues/59
[i61]: https://git.necronda.net/winzer/elwig/issues/61
[i62]: https://git.necronda.net/winzer/elwig/issues/62
[i64]: https://git.necronda.net/winzer/elwig/issues/64
[i65]: https://git.necronda.net/winzer/elwig/issues/65
[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 {#0.13.2-features}
### Neue Funktionen {#v0.13.2-features}
* Im Lieferungen-Fenster den Menüpunkt _Abwerteliste_ (`DeliveryDepreciationList`) hinzugefügt. (3cbffdbf27)
* Im Lieferungen-Fenster den Menüpunkt _Abwertungsliste_ (`DeliveryDepreciationList`) hinzugefügt. (3cbffdbf27)
### Behobene Fehler {#0.13.2-bugfixes}
### 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 {#0.13.2-misc}
### 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)
@@ -32,15 +443,15 @@ Changelog
[v0.13.1][v0.13.1] (2024-09-29) {#v0.13.1}
------------------------------------------
### Neue Funktionen {#0.13.1-features}
### 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 {#0.13.1-bugfixes}
### 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 {#0.13.1-misc}
### Sonstiges {#v0.13.1-misc}
* Abhängigkeiten aktualisiert. (b6ae1f5675)
@@ -55,7 +466,7 @@ Changelog
> [!NOTE]
> Ab dieser Version verhält sich die Berechnung der Unterlieferungen bei Flächenbindungen anders.
### Behobene Fehler {#0.13.0-bugfixes}
### 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)
@@ -132,7 +543,7 @@ Changelog
### Neue Funktionen {#v0.11.2-features}
* In der Anmeldeliste (`DeliveryAncmtList`) wird die Stamm-KG des Mitgliedes angegeben. (165770fa37)
* Im Hauptmenü wurde im Menüpunkt _Hilfe_ ein Fehler-Protokoll-Fenster (`LogWindow`) hinzugefügt. (526e951029)
* Im Haupt-Fenster (`MainWindow`) wurde im Menüpunkt _Hilfe_ ein Fehler-Protokoll-Fenster (`LogWindow`) hinzugefügt. (526e951029)
### Behobene Fehler {#v0.11.2-bugfixes}
@@ -239,7 +650,7 @@ Changelog
### Behobene Fehler {#v0.10.6-bugfixes}
* Der Titel des Flächenbindungs-Fensters (`AreaComAdminWindow`) ist jetzt _Flächenbindungen_, nicht mehr _Lieferungen_. (ee1315929c)
* 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)
@@ -556,7 +967,7 @@ Changelog
### Behobene Fehler {#v0.8.4-bugfixes}
* Falls beim Schließen des Hauptmenüs ein anderes Fenster im Bearbeiten-/Erstellen-Modus war führte das zu einem Absturz. (70f8276808)
* 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}

View File

@@ -30,23 +30,37 @@
<DataTemplate x:Key="WineVarietyTemplateCollapsed">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Name}" Foreground="{Binding Color}"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="WineVarietyTemplateExpanded">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding SortId}" MinWidth="36" Margin="0,0,10,0"/>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding SortIdFormat}" Foreground="{Binding Color}" MinWidth="36" Margin="0,0,10,0"/>
<TextBlock Text="{Binding Name}" Foreground="{Binding Color}"/>
<TextBlock Text="{Binding CommentFormat}" FontSize="10" VerticalAlignment="Bottom" Margin="0,0,0,2"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="AreaCommitmentTypeTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding WineVar.Name}" Foreground="{Binding WineVar.Color}"/>
<TextBlock Text="{Binding WineAttr.Name}" Margin="5,0,0,0"/>
<TextBlock Text="{Binding Discriminator}" Margin="5,0,0,0"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="ModifierTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" MinWidth="250" Margin="0,0,10,0"/>
<TextBlock Text="{Binding ValueStr}"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="PublicModifierTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" MinWidth="250" Margin="0,0,10,0"/>
<TextBlock Text="{Binding PublicValueStr}"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="WineAttributeTemplate">
<StackPanel Orientation="Horizontal">

View File

@@ -1,20 +1,22 @@
using System;
using System.Data;
using System.Linq;
using System.Windows;
using System.IO;
using Elwig.Helpers;
using Elwig.Helpers.Weighing;
using System.Collections.Generic;
using System.Windows.Threading;
using System.Reflection;
using Elwig.Helpers.Printing;
using Elwig.Windows;
using Elwig.Dialogs;
using System.Threading.Tasks;
using Elwig.Helpers;
using Elwig.Helpers.Billing;
using Elwig.Helpers.Export;
using Elwig.Helpers.Printing;
using Elwig.Helpers.Weighing;
using Elwig.Models.Entities;
using Elwig.Windows;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
namespace Elwig {
public partial class App : Application {
@@ -24,10 +26,15 @@ namespace Elwig {
public static bool ForceShutdown { get; private set; } = false;
private readonly DispatcherTimer _autoUpdateTimer = new() { Interval = TimeSpan.FromHours(1) };
public readonly SerialPortWatcher SerialPortWatcher = new();
public static readonly string DataPath = @"C:\ProgramData\Elwig\";
public static readonly string DataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "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 InstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Elwig");
public static readonly string DocumentsPath = (Assembly.GetEntryAssembly()?.Location.Contains(@"\bin\") ?? false) ?
Path.Combine(Assembly.GetEntryAssembly()!.Location.Split(@"\bin\")[0], "../Elwig/Documents") :
Path.Combine(InstallPath, "resources/Documents");
public static readonly string TempPath = Path.Combine(Path.GetTempPath(), "Elwig");
public static Config Config { get; private set; } = new(ConfigPath);
@@ -42,9 +49,10 @@ namespace Elwig {
public static string? BranchPhoneNr { get; private set; }
public static string? BranchFaxNr { get; private set; }
public static string? BranchMobileNr { get; private set; }
public static IList<IScale> Scales { get; private set; }
public static IList<ICommandScale> CommandScales => Scales.Where(s => s is ICommandScale).Cast<ICommandScale>().ToList();
public static IList<IEventScale> EventScales => Scales.Where(s => s is IEventScale).Cast<IEventScale>().ToList();
public static IList<IScale> Scales { get; private set; } = [];
public static IList<ICommandScale> CommandScales => [.. Scales.Where(s => s is ICommandScale).Cast<ICommandScale>()];
public static IList<IEventScale> EventScales => [.. Scales.Where(s => s is IEventScale).Cast<IEventScale>()];
public static ClientParameters Client { get; set; }
public static Dispatcher MainDispatcher { get; private set; }
@@ -56,6 +64,7 @@ namespace Elwig {
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Directory.CreateDirectory(TempPath);
Directory.CreateDirectory(DataPath);
Directory.CreateDirectory(MailsPath);
MainDispatcher = Dispatcher;
Scales = [];
CurrentApp = this;
@@ -80,7 +89,7 @@ namespace Elwig {
}
protected override async void OnStartup(StartupEventArgs evt) {
Version = new 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.0");
try {
await AppDbUpdater.CheckDb();
@@ -121,14 +130,18 @@ namespace Elwig {
if (Config.UpdateAuto && Config.UpdateUrl != null) {
if (Utils.HasInternetConnectivity()) {
Utils.RunBackground("Auto Updater", async () => {
await Task.Delay(500);
await Task.Delay(1000);
await CheckForUpdates();
});
}
NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
_autoUpdateTimer.Tick += new EventHandler(OnAutoUpdateTimer);
_autoUpdateTimer.Start();
}
SerialPortWatcher.SerialPortConnected += OnSerialPortConnected;
SerialPortWatcher.SerialPortDisconnected += OnSerialPortDisconnected;
var list = new List<IScale>();
foreach (var s in Config.Scales) {
try {
@@ -136,7 +149,7 @@ namespace Elwig {
} catch (Exception e) {
list.Add(new InvalidScale(s.Id));
if (s.Required)
MessageBox.Show($"Unable to create scale {s.Id}:\n\n{e.Message}", "Scale Error",
MessageBox.Show($"Verbindung zu Waage {s.Id} konnte nicht hergestellt werden:\n\n{e.Message}", "Waagenfehler",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
@@ -144,7 +157,7 @@ namespace Elwig {
if (Config.Branch != null) {
if (!branches.ContainsKey(Config.Branch.ToLower())) {
MessageBox.Show("Invalid branch name in config!", "Invalid Branch Config", MessageBoxButton.OK, MessageBoxImage.Error);
MessageBox.Show("Ungültige Zweigstelle in Konfigurationsdatei!", "Ungültige Zweigstelle", MessageBoxButton.OK, MessageBoxImage.Error);
Shutdown();
} else {
SetBranch(branches[Config.Branch.ToLower()]);
@@ -152,10 +165,20 @@ namespace Elwig {
} else if (branches.Count == 1) {
SetBranch(branches.First().Value);
} else {
MessageBox.Show("Unable to determine local branch!", "Invalid Branch Config", MessageBoxButton.OK, MessageBoxImage.Error);
MessageBox.Show("Erkennen der lokalen Zweigstelle nicht möglich!", "Ungültige Zweigstelle", MessageBoxButton.OK, MessageBoxImage.Error);
Shutdown();
}
if (Config.WeighingMode == null) {
if (Client.IsMatzen || Client.IsWolkersdorf) {
Config.WeighingMode = WeighingMode.Net;
} else if (Client.IsHaugsdorf || Client.IsSitzendorf) {
Config.WeighingMode = WeighingMode.Box;
} else if (Client.IsBaden || Client.IsGrInzersdorf) {
Config.WeighingMode = WeighingMode.Gross;
}
}
base.OnStartup(evt);
var window = new MainWindow();
@@ -163,6 +186,7 @@ namespace Elwig {
}
private async void Application_Exit(object sender, ExitEventArgs evt) {
SerialPortWatcher.Dispose();
foreach (var s in EventScales) {
s.Dispose();
}
@@ -195,10 +219,12 @@ namespace Elwig {
var ch = CurrentLastWrite;
if (ch > CurrentApp.LastChanged)
CurrentApp.LastChanged = ch;
foreach (Window w in CurrentApp.Windows) {
if (w is not ContextWindow c) continue;
MainDispatcher.BeginInvoke(c.HintContextChange);
}
MainDispatcher.Invoke(() => {
foreach (Window w in CurrentApp.Windows) {
if (w is not ContextWindow c) continue;
MainDispatcher.BeginInvoke(c.HintContextChange);
}
});
}
private void OnAutoUpdateTimer(object? sender, EventArgs? evt) {
@@ -210,6 +236,63 @@ namespace Elwig {
}
}
private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs evt) {
if (!evt.IsAvailable) return;
if (Utils.HasInternetConnectivity()) {
Utils.RunBackground("Auto Updater", async () => {
await Task.Delay(2000);
await CheckForUpdates();
});
}
}
private void OnSerialPortConnected(object? sender, string name) {
for (var i = 0; i < Config.Scales.Count; i++) {
var s = Config.Scales[i];
if (s.Connection?.StartsWith($"serial://{name}:") ?? false) {
if (Scales[i] is InvalidScale) {
try {
Scales[i] = Scale.FromConfig(s);
MessageBox.Show($"Verbindung zu Waage {s.Id} wieder hergestellt!", $"Waage {s.Id}", MessageBoxButton.OK, MessageBoxImage.Information);
} catch (Exception e) {
Scales[i] = new InvalidScale(s.Id);
MessageBox.Show($"Verbindung zu Waage {s.Id} konnte nicht hergestellt werden:\n\n{e.Message}", "Waagenfehler",
MessageBoxButton.OK, MessageBoxImage.Error);
}
} else if (Scales[i] is IEventScale) {
MessageBox.Show($"Verbindung zu Waage {s.Id} wieder hergestellt!", $"Waage {s.Id}", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
}
UpdateScales();
}
private void OnSerialPortDisconnected(object? sender, string name) {
for (var i = 0; i < Config.Scales.Count; i++) {
var s = Config.Scales[i];
if ((s.Connection?.StartsWith($"serial://{name}:") ?? false) && Scales[i] is not InvalidScale) {
MessageBox.Show($"Verbindung zu Waage {s.Id} unterbrochen!", $"Waagen {s.Id}", MessageBoxButton.OK, MessageBoxImage.Warning);
if (Scales[i] is ICommandScale) {
try {
Scales[i].Dispose();
} catch {
// ignore
}
Scales[i] = new InvalidScale(s.Id);
}
}
}
UpdateScales();
}
public static void UpdateScales() {
foreach (Window w in CurrentApp.Windows) {
if (w is DeliveryAdminWindow t && t.ViewModel.IsReceipt) {
t.UpdateScales();
}
}
}
public static async Task CheckForUpdates(bool showAlert = false) {
if (Config.UpdateUrl == null) return;
var latest = await Utils.GetLatestInstallerUrl(Config.UpdateUrl);
@@ -232,6 +315,19 @@ namespace Elwig {
}
}
public static async Task ReplaceDatabase(string filename) {
try {
await Task.Run(async () => {
await Database.Import(filename);
});
MessageBox.Show("Das Ersetzen war erfolgreich!\n\nBitte starten Sie Elwig neu!", "Datenbank ersetzen", MessageBoxButton.OK, MessageBoxImage.Information);
ForceShutdown = true;
Current.Shutdown();
} catch (Exception exc) {
MessageBox.Show("Fehler beim Ersetzen:\n\n" + exc.Message, "Datenbank ersetzen", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private static T FocusWindow<T>(Func<T> constructor, Predicate<T>? selector = null) where T : Window {
foreach (Window w in CurrentApp.Windows) {
if (w is T t && (selector == null || selector(t))) {

View File

@@ -124,19 +124,24 @@ namespace Elwig.Controls {
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 {
AllItemsSelected = null;
}
if (SelectedItems.Count > 1 && SelectedItems.Count == ItemsSource.Cast<object>().Count() && AllItemsSelectedContent != null) {
_textBox.Text = AllItemsSelectedContent;
} else if (SelectedItems.Count == 0) {
_textBox.Text = "";
} 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));
}

View File

@@ -0,0 +1,71 @@
<Window x:Class="Elwig.Dialogs.AreaComModifyDialog"
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 bearbeiten" Height="220" 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">
Soll die Flächenbindung (<Run Text="{Binding Area}"/> m², <Run Text="{Binding OrigYearFrom}"/><Run Text="{Binding OrigYearTo}"/>) rückwirkend bearbeitet werden,<LineBreak/>
oder erst ab der angegebenen Saison?
</TextBlock>
<TextBlock x:Name="QuestionBlock2" TextAlignment="Center" Margin="0,10,0,0" Visibility="Hidden"
HorizontalAlignment="Center" VerticalAlignment="Top">
Soll die Flächenbindung (<Run Text="{Binding Area}"/> m², <Run Text="{Binding OrigYearFrom}"/><Run Text="{Binding OrigYearTo}"/>) rückwirkend gelöscht werden,<LineBreak/>
oder erst ab der angegebenen Saison?
</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="RetroactiveInput" Content="Rückwirkend bearbeiten" Margin="0,80,0,0"
HorizontalAlignment="Center" VerticalAlignment="Top"
Checked="RetroactiveInput_Changed" Unchecked="RetroactiveInput_Changed"
IsChecked="{Binding ModifyRetroactively}"/>
<TextBlock x:Name="DescBlock1" Margin="0,105,0,0" TextAlignment="Center"
HorizontalAlignment="Center" VerticalAlignment="Top">
Die Flächenbindung bleibt bis inkl. Saison <Bold><Run x:Name="CancelSeason1"/></Bold> unverändert,<LineBreak/>
und wird ab inkl. Saison <Bold><Run x:Name="NewSeason1"/></Bold> in geänderter Form übernommen.
</TextBlock>
<TextBlock x:Name="DescBlock2" Margin="0,105,0,0" TextAlignment="Center" Visibility="Hidden"
HorizontalAlignment="Center" VerticalAlignment="Top">
Die Flächenbindung bleibt bis inklusive Saison <Bold><Run x:Name="CancelSeason2"/></Bold> gültig.
</TextBlock>
<Button x:Name="ConfirmButton" Content="Bestätigen" Margin="10,10,115,10" Grid.Column="1"
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,62 @@
using Elwig.Helpers;
using System.Windows;
using System.Windows.Controls;
namespace Elwig.Dialogs {
public partial class AreaComModifyDialog : Window {
public int? YearTo { get; set; }
public bool ModifyRetroactively { get; set; }
public string OrigYearFrom { get; set; }
public string OrigYearTo { get; set; }
public string Area { get; set; }
public AreaComModifyDialog(int? yearFrom, int? yearTo, int area, bool delete) {
Area = $"{area:N0}";
OrigYearFrom = $"{yearFrom}";
OrigYearTo = $"{yearTo}";
InitializeComponent();
Title = delete ? "Flächenbindung löschen" : "Flächenbindung bearbeiten";
RetroactiveInput.Content = delete ? "Rückwirkend löschen" : "Rückwirkend bearbeiten";
if (delete) {
QuestionBlock1.Visibility = Visibility.Hidden;
QuestionBlock2.Visibility = Visibility.Visible;
DescBlock1.Visibility = Visibility.Hidden;
DescBlock2.Visibility = Visibility.Visible;
}
if ((yearTo.HasValue && yearTo < Utils.CurrentNextSeason) || (yearFrom.HasValue && yearFrom >= Utils.CurrentNextSeason)) {
RetroactiveInput.IsChecked = true;
} else {
SeasonInput.Text = $"{Utils.CurrentNextSeason}";
}
}
private void SeasonInput_TextChanged(object sender, TextChangedEventArgs evt) {
YearTo = SeasonInput.Value! - 1;
CancelSeason1.Text = $"{YearTo}";
CancelSeason2.Text = $"{YearTo}";
NewSeason1.Text = $"{YearTo + 1}";
}
private void RetroactiveInput_Changed(object sender, RoutedEventArgs evt) {
if (ModifyRetroactively) {
SeasonInput.IsEnabled = false;
SeasonInput.Text = "";
YearTo = null;
DescBlock1.Visibility = Visibility.Hidden;
DescBlock2.Visibility = Visibility.Hidden;
} else {
SeasonInput.IsEnabled = true;
SeasonInput.Text = $"{Utils.CurrentNextSeason}";
DescBlock1.Visibility = QuestionBlock1.Visibility;
DescBlock2.Visibility = QuestionBlock2.Visibility;
}
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
DialogResult = true;
Close();
}
}
}

View File

@@ -1,4 +1,4 @@
<Window x:Class="Elwig.Dialogs.AreaComDialog"
<Window x:Class="Elwig.Dialogs.AreaComTransferDialog"
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"
@@ -51,7 +51,7 @@
<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}"/>
IsChecked="{Binding MaintainYearFrom}"/>
<TextBlock x:Name="DescBlock1" Margin="0,105,0,0" TextAlignment="Center"
HorizontalAlignment="Center" VerticalAlignment="Top">

View File

@@ -3,16 +3,16 @@ using System.Windows;
using System.Windows.Controls;
namespace Elwig.Dialogs {
public partial class AreaComDialog : Window {
public partial class AreaComTransferDialog : Window {
public int CancelSeason { get; set; }
public int SuccessorSeason => CancelSeason + 1;
public bool MaintainYearTo { get; set; }
public bool MaintainYearFrom { get; set; }
public string AreaComNum { get; set; }
public string Area { get; set; }
public AreaComDialog(string name, int areaComNum, int area) {
public AreaComTransferDialog(string name, int areaComNum, int area) {
CancelSeason = Utils.FollowingSeason - 1;
AreaComNum = $"{areaComNum:N0}";
Area = $"{area:N0}";
@@ -29,7 +29,7 @@ namespace Elwig.Dialogs {
SeasonLabel.Margin = new(0, 40, 100, 0);
}
public AreaComDialog(string name, string successorName, int areaComNum, int area) {
public AreaComTransferDialog(string name, string successorName, int areaComNum, int area) {
CancelSeason = Utils.FollowingSeason - 1;
AreaComNum = $"{areaComNum:N0}";
Area = $"{area:N0}";
@@ -43,13 +43,13 @@ namespace Elwig.Dialogs {
CancelSeason = (int)SeasonInput.Value!;
CancelSeason1.Text = $"{CancelSeason}";
CancelSeason2.Text = $"{CancelSeason}";
TransferSeason.Text = MaintainYearTo ? "" : $"{SuccessorSeason}";
DescBlock1Season.Text = MaintainYearTo ? "dem originalen Beginn der FB" : "inkl. Saison ";
TransferSeason.Text = MaintainYearFrom ? "" : $"{SuccessorSeason}";
DescBlock1Season.Text = MaintainYearFrom ? "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 ";
TransferSeason.Text = MaintainYearFrom ? "" : $"{SuccessorSeason}";
DescBlock1Season.Text = MaintainYearFrom ? "dem originalen Beginn der FB" : "inkl. Saison ";
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {

View File

@@ -19,6 +19,16 @@ namespace Elwig.Documents {
Member = m;
Location = App.BranchLocation;
IncludeSender = includeSender;
var c = App.Client;
Header = $"<div class='name'>{c.Name}</div><div class='suffix'>{c.NameSuffix}</div><div class='type'>{c.NameTypeFull}</div>";
Footer = Utils.GenerateFooter("<br/>", " \u00b7 ")
.Item(c.NameFull).NextLine()
.Item(c.Address).Item($"{c.Plz} {c.Ort}").Item("Österreich").Item("Tel.", c.PhoneNr).Item("Fax", c.FaxNr).NextLine()
.Item(c.EmailAddress != null ? $"<a href=\"mailto:{c.Name} {c.NameSuffix} <{c.EmailAddress}>\">{c.EmailAddress}</a>" : null)
.Item(c.Website != null ? $"<a href=\"http://{c.Website}/\">{c.Website}</a>" : null)
.Item("Betriebs-Nr.", c.LfbisNr).Item("Bio-KSt.", c.OrganicAuthority).NextLine()
.Item("UID", c.UstIdNr).Item("BIC", c.Bic).Item("IBAN", c.Iban)
.ToString();
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>" +
$"<thead><tr><th colspan='2'>Mitglied</th></tr></thead><tbody>" +

View File

@@ -2,7 +2,7 @@
@inherits TemplatePage<Elwig.Documents.BusinessDocument>
@model Elwig.Documents.BusinessDocument
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\BusinessDocument.css"/>
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\BusinessDocument.css"/>
<div class="info-wrapper">
<div class="address-wrapper">
<div class="sender">

View File

@@ -39,6 +39,7 @@ namespace Elwig.Documents {
Data = data;
Payment = p;
Credit = p.Credit;
IsPreview = Payment == null || Credit == null;
var season = p.Variant.Season;
if (considerCustomModifiers) {
CustomPayment = ctx.CustomPayments.Find(p.Year, p.MgNr);
@@ -48,7 +49,7 @@ namespace Elwig.Documents {
if (CustomPayment?.ModComment != null) {
MemberModifier = CustomPayment.ModComment;
} else if (mod != null) {
MemberModifier = $"{mod.Name} ({mod.ValueStr})";
MemberModifier = $"{mod.Name} ({mod.PublicValueStr})";
} else {
MemberModifier = "Sonstige Zu-/Abschläge";
}

View File

@@ -3,7 +3,7 @@
@inherits TemplatePage<Elwig.Documents.CreditNote>
@model Elwig.Documents.CreditNote
@{ Layout = "BusinessDocument"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\CreditNote.css"/>
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\CreditNote.css" />
<main>
<h1>@Model.Title</h1>
<table class="credit">
@@ -136,7 +136,10 @@
}
</tbody>
<tbody style="break-inside: avoid;">
@{ decimal penalty = 0; }
@{
decimal penalty = 0;
string? comment = null;
}
@if (Model.MemberUnderDeliveries != null && Model.MemberUnderDeliveries.Count() > 0) {
<tr class="small">
@@ -158,7 +161,13 @@
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));
comment = Model.CustomPayment.Comment;
string text = (Model.CustomPayment.Amount.Value < 0 ? "Weitere Abzüge" : "Weitere Zuschläge") + (comment != null ? "*" : "");
if (comment != null && comment!.Length <= 30) {
text = comment;
comment = null;
}
@Raw(FormatRow(text, Model.CustomPayment.Amount.Value, add: true));
penalty += Model.CustomPayment.Amount.Value;
}
@@ -176,6 +185,9 @@
}
</tbody>
</table>
@if (comment != null) {
<p>* @comment</p>
}
<p>Überweisung erfolgt auf Konto @(Elwig.Helpers.Utils.FormatIban(Model.Member.Iban ?? "-")).</p>
<div style="margin-top: 1em;">
@if (Model.Text != null) {

View File

@@ -2,7 +2,7 @@
@inherits TemplatePage<Elwig.Documents.DeliveryAncmtList>
@model Elwig.Documents.DeliveryAncmtList
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\DeliveryAncmtList.css" />
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\DeliveryAncmtList.css" />
<main>
<h1>Anmeldeliste</h1>
<h2>@Model.Filter</h2>
@@ -24,7 +24,7 @@
<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>
<th>Menge</th>
</tr>
<tr>
<th class="unit">[kg]</th>

View File

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

View File

@@ -3,7 +3,7 @@
@inherits TemplatePage<Elwig.Documents.DeliveryConfirmation>
@model Elwig.Documents.DeliveryConfirmation
@{ Layout = "BusinessDocument"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\DeliveryConfirmation.css"/>
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\DeliveryConfirmation.css" />
<main>
<h1>@Model.Title</h1>
<table class="delivery-confirmation">
@@ -30,7 +30,7 @@
<th rowspan="2" style="text-align: left;">Qualitätsstufe</th>
<th colspan="2">Gradation</th>
<th colspan="2">Flächenbindung</th>
<th>Gewicht</th>
<th>Menge</th>
<th rowspan="3" style="padding: 0;">
<svg width="10" height="40" xmlns="http://www.w3.org/2000/svg">
<text x="-40" y="4" transform="rotate(270)" font-size="8pt" font-style="italic" font-family="Times New Roman"

View File

@@ -2,7 +2,7 @@
@inherits TemplatePage<Elwig.Documents.DeliveryDepreciationList>
@model Elwig.Documents.DeliveryDepreciationList
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\DeliveryDepreciationList.css" />
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\DeliveryDepreciationList.css" />
<main>
<h1>Abwertungsliste</h1>
<h2>@Model.Filter</h2>
@@ -27,7 +27,7 @@
<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>
<th>Menge</th>
</tr>
<tr>
<th class="unit">[°Oe]</th>

View File

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

View File

@@ -2,7 +2,7 @@
@inherits TemplatePage<Elwig.Documents.DeliveryJournal>
@model Elwig.Documents.DeliveryJournal
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\DeliveryJournal.css"/>
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\DeliveryJournal.css"/>
<main>
<h1>Lieferjournal</h1>
<h2>@Model.Filter</h2>
@@ -29,7 +29,7 @@
<th rowspan="2" style="text-align: left;">Mitglied</th>
<th rowspan="2" style="text-align: left;">Sorte</th>
<th colspan="2">Gradation</th>
<th>Gewicht</th>
<th>Menge</th>
</tr>
<tr>
<th class="unit">[°Oe]</th>

View File

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

View File

@@ -2,7 +2,7 @@
@inherits TemplatePage<Elwig.Documents.DeliveryNote>
@model Elwig.Documents.DeliveryNote
@{ Layout = "BusinessDocument"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\DeliveryNote.css" />
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\DeliveryNote.css" />
<main>
<h1>@Model.Title</h1>
<table class="delivery large">
@@ -24,7 +24,7 @@
<th class="main" rowspan="2" colspan="2">Attribut</th>
<th class="main" rowspan="2">Qualitätsstufe</th>
<th colspan="2">Gradation</th>
<th>Gewicht</th>
<th>Menge</th>
</tr>
<tr>
<th class="unit">[°Oe]</th>
@@ -55,7 +55,7 @@
@if (part.Modifiers.Count() > 0) {
var first = true;
foreach (var mod in part.Modifiers) {
<tr class="tight @(first ? "first" : "")"><td></td><td>@Raw(first ? "<i>Zu-/Abschläge:</i>" : "")</td><td colspan="3"><b>@mod.Name</b></td><td style="white-space: pre;">@mod.ValueStr</td></tr>
<tr class="tight @(first ? "first" : "")"><td></td><td>@Raw(first ? "<i>Zu-/Abschläge:</i>" : "")</td><td colspan="3"><b>@mod.Name</b></td><td style="white-space: pre;">@mod.PublicValueStr</td></tr>
first = false;
}
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using Elwig.Helpers.Printing;
using MimeKit;
using System.Threading;
namespace Elwig.Documents {
public abstract partial class Document : IDisposable {
@@ -18,12 +19,13 @@ namespace Elwig.Documents {
protected string? _pdfPath;
protected string? PdfPath => _pdfPath ?? _pdfFile?.FilePath;
public int? TotalPages { get; private set; }
public int? Pages => TotalPages / (DoublePaged ? 2 : 1);
public int? Pages => TotalPages / (IsDoublePaged ? 2 : 1);
public bool ShowFoldMarks = App.Config.Debug;
public bool DoublePaged = false;
public bool IsDoublePaged = false;
public bool IsPreview = false;
public string DataPath;
public string DocumentsPath;
public int CurrentNextSeason;
public string? DocumentId;
public string Title;
@@ -34,19 +36,12 @@ namespace Elwig.Documents {
public Document(string title) {
var c = App.Client;
DataPath = App.DataPath;
DocumentsPath = App.DocumentsPath;
CurrentNextSeason = Utils.CurrentNextSeason;
Title = title;
Author = c.NameFull;
Header = $"<div class='name'>{c.Name}</div><div class='suffix'>{c.NameSuffix}</div><div class='type'>{c.NameTypeFull}</div>";
Footer = Utils.GenerateFooter("<br/>", " \u00b7 ")
.Item(c.NameFull).NextLine()
.Item(c.Address).Item($"{c.Plz} {c.Ort}").Item("Österreich").Item("Tel.", c.PhoneNr).Item("Fax", c.FaxNr).NextLine()
.Item(c.EmailAddress != null ? $"<a href=\"mailto:{c.Name} {c.NameSuffix} <{c.EmailAddress}>\">{c.EmailAddress}</a>" : null)
.Item(c.Website != null ? $"<a href=\"http://{c.Website}/\">{c.Website}</a>" : null)
.Item("Betriebs-Nr.", c.LfbisNr).Item("UID", c.UstIdNr).NextLine()
.Item("BIC", c.Bic).Item("IBAN", c.Iban)
.ToString();
Header = "";
Footer = Utils.GenerateFooter("<br/>", " \u00b7 ").Item(c.NameFull).ToString();
Date = DateOnly.FromDateTime(Utils.Today);
}
@@ -104,7 +99,7 @@ namespace Elwig.Documents {
return await Html.CompileRenderAsync(name, this); ;
}
public async Task Generate(IProgress<double>? progress = null) {
public async Task Generate(CancellationToken? cancelToken = null, IProgress<double>? progress = null) {
if (_pdfFile != null)
return;
progress?.Report(0.0);
@@ -114,36 +109,50 @@ namespace Elwig.Documents {
var pdf = new TempFile("pdf");
var tmpHtmls = new List<TempFile>();
var tmpFiles = new List<string>();
var n = m.Documents.Count();
int i = 0;
foreach (var doc in m.Documents) {
if (doc is PdfDocument) {
tmpFiles.Add(doc.PdfPath!);
continue;
try {
var n = m.Documents.Count();
int i = 0;
foreach (var doc in m.Documents) {
if (doc is PdfDocument) {
tmpFiles.Add(doc.PdfPath!);
continue;
}
if (cancelToken?.IsCancellationRequested ?? false)
throw new OperationCanceledException("Dokumentenerzeugung abgebrochen!");
var tmpHtml = new TempFile("html");
await File.WriteAllTextAsync(tmpHtml.FilePath, await doc.Render(), Utils.UTF8);
tmpHtmls.Add(tmpHtml);
tmpFiles.Add((doc is Letterhead ? "#" : "") + tmpHtml.FileName);
i++;
progress?.Report(GenerationProportion * 100 * i / n);
}
progress?.Report(GenerationProportion * 100);
var pages = await Pdf.Convert(tmpFiles, pdf.FileName, IsDoublePaged, cancelToken, new Progress<double>(v => progress?.Report(GenerationProportion * 100 + v * (1 - GenerationProportion))));
TotalPages = pages.Pages;
_pdfFile = pdf;
} catch {
pdf.Dispose();
throw;
} finally {
foreach (var tmp in tmpHtmls) {
tmp.Dispose();
}
var tmpHtml = new TempFile("html");
await File.WriteAllTextAsync(tmpHtml.FilePath, await doc.Render(), Utils.UTF8);
tmpHtmls.Add(tmpHtml);
tmpFiles.Add((doc is Letterhead ? "#" : "") + tmpHtml.FileName);
i++;
progress?.Report(GenerationProportion * 100 * i / n);
}
progress?.Report(GenerationProportion * 100);
var pages = await Pdf.Convert(tmpFiles, pdf.FileName, DoublePaged, new Progress<double>(v => progress?.Report(GenerationProportion * 100 + v * (1 - GenerationProportion))));
TotalPages = pages.Pages;
foreach (var tmp in tmpHtmls) {
tmp.Dispose();
}
_pdfFile = pdf;
} else {
if (cancelToken?.IsCancellationRequested ?? false)
throw new OperationCanceledException("Dokumentenerzeugung abgebrochen!");
var pdf = new TempFile("pdf");
using (var tmpHtml = new TempFile("html")) {
try {
using var tmpHtml = new TempFile("html");
await File.WriteAllTextAsync(tmpHtml.FilePath, await Render(), Utils.UTF8);
progress?.Report(50.0);
var pages = await Pdf.Convert(tmpHtml.FilePath, pdf.FilePath, DoublePaged);
var pages = await Pdf.Convert(tmpHtml.FilePath, pdf.FilePath, IsDoublePaged, cancelToken);
TotalPages = pages.Pages;
_pdfFile = pdf;
} catch {
pdf.Dispose();
throw;
}
_pdfFile = pdf;
}
progress?.Report(100.0);
}
@@ -155,7 +164,7 @@ namespace Elwig.Documents {
public async Task Print(int copies = 1) {
if (PdfPath == null) throw new InvalidOperationException("Pdf file has not been generated yet");
await Pdf.Print(PdfPath, copies, DoublePaged);
await Pdf.Print(PdfPath, copies, IsDoublePaged);
}
public void Show() {

View File

@@ -7,10 +7,10 @@
<title>@Model.Title</title>
<meta name="author" value="@Model.Author"/>
<meta charset="UTF-8"/>
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\Document.css"/>
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\Document.Page.css"/>
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\Document.Table.css"/>
@if (Model.DoublePaged) {
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\Document.css" />
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\Document.Page.css" />
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\Document.Table.css" />
@if (Model.IsDoublePaged) {
<style>
@@page :left {
margin: 25mm 25mm 35mm 20mm;
@@ -34,14 +34,14 @@
<div class="pre-footer">
<span class="date">@($"{Model.Date:dddd, d. MMMM yyyy}")</span>
<span class="doc-id">@Model.DocumentId</span>
<span class="page"></span>
<span><span class="page"></span>@Raw(Model.IsPreview ? " <b>(vorläufig)</b>" : "")</span>
</div>
<footer>@Raw(Model.Footer)</footer>
</div>
@if (Model.DoublePaged) {
@if (Model.IsDoublePaged) {
<div class="footer-wrapper left">
<div class="pre-footer">
<span class="page"></span>
<span>@Raw(Model.IsPreview ? "<b>(vorläufig)</b> " : "")<span class="page"></span></span>
<span class="doc-id">@Model.DocumentId</span>
<span class="date">@($"{Model.Date:dddd, d. MMMM yyyy}")</span>
</div>

View File

@@ -3,7 +3,7 @@
@inherits TemplatePage<Elwig.Documents.MemberDataSheet>
@model Elwig.Documents.MemberDataSheet
@{ Layout = "BusinessDocument"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\MemberDataSheet.css" />
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\MemberDataSheet.css" />
<main>
<h1>@Model.Title</h1>
<table class="member border">

View File

@@ -2,7 +2,7 @@
@inherits TemplatePage<Elwig.Documents.MemberList>
@model Elwig.Documents.MemberList
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\MemberList.css"/>
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\MemberList.css" />
<main>
<h1>Mitgliederliste</h1>
<h2>@Model.Filter</h2>

View File

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

View File

@@ -27,6 +27,7 @@ namespace Elwig.Documents {
Data = data;
CurrencySymbol = v.Season.Currency.Symbol ?? v.Season.Currency.Code;
MemberNum = v.Credits.Count;
IsPreview = MemberNum == 0;
DeliveryNum = v.DeliveryPartPayments.DistinctBy(p => p.DeliveryPart.Delivery).Count();
DeliveryPartNum = v.DeliveryPartPayments.Count;
ModifierStat = AppDbContext.GetModifierStats(v.Year, v.AvNr).GetAwaiter().GetResult();

View File

@@ -3,7 +3,7 @@
@inherits TemplatePage<Elwig.Documents.PaymentVariantSummary>
@model Elwig.Documents.PaymentVariantSummary
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\PaymentVariantSummary.css" />
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\PaymentVariantSummary.css" />
<main>
<h1>Auszahlungsvariante Lese @Model.Variant.Year</h1>
<h2>@Model.Variant.Name</h2>
@@ -111,8 +111,8 @@
<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);
var minWei = weiRows.Min(r => r.Ungeb.MinPrice);
var maxWei = weiRows.Max(r => r.Ungeb.MaxPrice);
}
<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>
@@ -123,8 +123,8 @@
<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);
var minPrice = quwRows.Min(r => r.Ungeb.MinPrice);
var maxPrice = quwRows.Max(r => r.Ungeb.MaxPrice);
}
<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>
@@ -135,8 +135,8 @@
<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);
.Where(r => r.Geb.MaxPrice != null && r.Ungeb.MinPrice != null)
.Select(r => r.Geb.MaxPrice - r.Ungeb.MinPrice);
var minGeb = gebRows.Min();
var maxGeb = gebRows.Max();
}
@@ -164,14 +164,14 @@
<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>
<td colspan="2" class="number">@($"{Model.Data.Rows.Sum(r => r.Geb.Weight + r.LowGeb.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>
<td colspan="2" class="number tborder">@($"{Model.Data.Rows.Sum(r => r.Ungeb.Weight + r.LowGeb.Weight + r.Geb.Weight):N0}") kg</td>
</tr>
</tbody>
</table>
@@ -219,19 +219,22 @@
</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;"/>
<col style="width: 19mm;"/>
<col style="width: 18mm;"/>
<col style="width: 15mm;"/>
<col style="width: 18mm;"/>
<col style="width: 15mm;"/>
<col style="width: 18mm;"/>
<col style="width: 15mm;"/>
<col style="width: 22mm;"/>
</colgroup>
<thead>
<tr>
<th rowspan="2" style="text-align: left;">Qualitätsstufe</th>
<th>Gradation</th>
<th colspan="2">ungebunden</th>
<th colspan="2">attributlos gebunden</th>
<th colspan="2">gebunden</th>
<th>Gesamt</th>
</tr>
@@ -241,6 +244,8 @@
<th>[@(Model.CurrencySymbol)/kg]</th>
<th>[kg]</th>
<th>[@(Model.CurrencySymbol)/kg]</th>
<th>[kg]</th>
<th>[@(Model.CurrencySymbol)/kg]</th>
<th>[@(Model.CurrencySymbol)]</th>
</tr>
</thead>
@@ -258,6 +263,8 @@
<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.LowGeb.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>
@@ -267,9 +274,11 @@
<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.Ungeb.MaxPrice != null ? $"{row.Ungeb.MaxPrice:N4}" : "-")</td>
<td class="number">@(row.LowGeb.Weight != 0 ? $"{row.LowGeb.Weight:N0}" : "-")</td>
<td class="number">@(row.LowGeb.MaxPrice != null ? $"{row.LowGeb.MaxPrice: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.Geb.MaxPrice != null ? $"{row.Geb.MaxPrice:N4}" : "-")</td>
<td class="number">@($"{row.Amount:N2}")</td>
</tr>
lastHdr = hdr;

View File

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

View File

@@ -3,7 +3,7 @@
@inherits TemplatePage<Elwig.Documents.WineQualityStatistics>
@model Elwig.Documents.WineQualityStatistics
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\WineQualityStatistics.css"/>
<link rel="stylesheet" href="file:///@Raw(Model.DocumentsPath)\WineQualityStatistics.css" />
<main>
<h1>Qualitätsstatistik</h1>
<h2>@Model.Filter</h2>

View File

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

View File

@@ -2,12 +2,12 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<PreserveCompilationContext>true</PreserveCompilationContext>
<ApplicationIcon>Resources\Images\Elwig.ico</ApplicationIcon>
<Version>0.13.2</Version>
<Version>1.0.3.0</Version>
<SatelliteResourceLanguages>de-AT</SatelliteResourceLanguages>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ApplicationManifest>app.manifest</ApplicationManifest>
@@ -20,26 +20,25 @@
<EmbeddedResource Include="Resources\Sql\*" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="call fetch-resources.bat" />
</Target>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
<PackageReference Include="LinqKit" Version="1.3.0" />
<PackageReference Include="MailKit" Version="4.8.0" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="6.0.33" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="8.0.0" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2792.45" />
<PackageReference Include="NJsonSchema" Version="11.0.2" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="LinqKit" Version="1.3.9" />
<PackageReference Include="MailKit" Version="4.14.1" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="6.0.36" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="10.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="10.0.2" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3650.58" />
<PackageReference Include="NJsonSchema" Version="11.5.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="ScottPlot.WPF" Version="5.0.39" />
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
<PackageReference Include="ScottPlot.WPF" Version="5.1.57" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.2" />
<PackageReference Include="System.IO.Hashing" Version="10.0.2" />
<PackageReference Include="System.IO.Ports" Version="10.0.2" />
<PackageReference Include="System.Management" Version="10.0.2" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.2" />
</ItemGroup>
</Project>

View File

@@ -10,7 +10,6 @@ using Microsoft.Data.Sqlite;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using Elwig.Models.Dtos;
using System.Reflection;
using System.Data;
namespace Elwig.Helpers {
@@ -63,10 +62,12 @@ namespace Elwig.Helpers {
public DbSet<PaymentDeliveryPart> PaymentDeliveryParts { get; private set; }
public DbSet<PaymentCustom> CustomPayments { 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<AreaComUnderDeliveryRowSingle> AreaComUnderDeliveryRows { get; private set; }
public DbSet<MemberDeliveryPerVariantRowSingle> MemberDeliveryPerVariantRows { get; private set; }
public DbSet<MemberDeliveryPerVarietyRowSingle> MemberDeliveryPerVariantRows { get; private set; }
public DbSet<MemberAreaComsRowSingle> MemberAreaComsRows { get; private set; }
public DbSet<CreditNoteDeliveryRowSingle> CreditNoteDeliveryRows { get; private set; }
public DbSet<CreditNoteRowSingle> CreditNoteRows { get; private set; }
public DbSet<WeightBreakdownRow> WeightBreakDownRows { get; private set; }
@@ -115,39 +116,6 @@ namespace Elwig.Helpers {
return cnx;
}
public static async Task ExecuteBatch(SqliteConnection cnx, string sql) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = sql;
await (await cmd.ExecuteReaderAsync()).CloseAsync();
}
public static async Task ExecuteEmbeddedScript(SqliteConnection cnx, Assembly asm, string name) {
using var stream = asm.GetManifestResourceStream(name) ?? throw new FileNotFoundException("Unable to load embedded resource");
using var reader = new StreamReader(stream);
await ExecuteBatch(cnx, await reader.ReadToEndAsync());
}
public static async Task<object?> ExecuteScalar(SqliteConnection cnx, string sql) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = sql;
return await cmd.ExecuteScalarAsync();
}
public static async Task<(string Table, long RowId, string Parent, long FkId)[]> ForeignKeyCheck(SqliteConnection cnx) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = "PRAGMA foreign_key_check";
using var reader = await cmd.ExecuteReaderAsync();
var list = new List<(string, long, string, long)>();
while (await reader.ReadAsync()) {
var table = reader.GetString(0);
var rowid = reader.GetInt64(1);
var parent = reader.GetString(2);
var fkid = reader.GetInt64(3);
list.Add((table, rowid, parent, fkid));
}
return [.. list];
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
optionsBuilder.UseSqlite(ConnectionString);
optionsBuilder.UseLazyLoadingProxies();
@@ -246,13 +214,6 @@ namespace Elwig.Helpers {
return c + 1;
}
public async Task<WineQualLevel> GetWineQualityLevel(double kmw) {
return await WineQualityLevels
.Where(q => !q.IsPredicate && (q.MinKmw == null || q.MinKmw <= kmw))
.OrderBy(q => q.MinKmw)
.LastAsync();
}
public void UpdateDeliveryPartModifiers(DeliveryPart part, IEnumerable<Modifier> oldModifiers, IEnumerable<Modifier> newModifiers) {
foreach (var m in Modifiers.Where(m => m.Year == part.Year)) {
var mod = new DeliveryPartModifier {

View File

@@ -9,17 +9,17 @@ namespace Elwig.Helpers {
public static class AppDbUpdater {
// Don't forget to update value in Tests/fetch-resources.bat!
public static readonly int RequiredSchemaVersion = 31;
public static readonly int RequiredSchemaVersion = 36;
private static int VersionOffset = 0;
public static async Task<Version> CheckDb() {
using var cnx = AppDbContext.Connect();
var applId = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA application_id") ?? 0;
var applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id") ?? 0;
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 cnx.ExecuteScalar("PRAGMA schema_version") ?? 0;
VersionOffset = (int)(schemaVers % 100);
if (VersionOffset != 0) {
// schema was modified manually/externally
@@ -27,12 +27,12 @@ namespace Elwig.Helpers {
}
await UpdateDbSchema(cnx, (int)(schemaVers / 100), RequiredSchemaVersion);
var userVers = (long?)await AppDbContext.ExecuteScalar(cnx, "PRAGMA user_version") ?? 0;
var v = new Version((int)(userVers >> 24), (int)((userVers >> 16) & 0xFF), (int)(userVers & 0xFFFF));
var userVers = (long?)await cnx.ExecuteScalar("PRAGMA user_version") ?? 0;
var v = new Version((int)(userVers >> 24), (int)((userVers >> 16) & 0xFF), (int)((userVers >> 8) & 0xFF), (int)(userVers & 0xFF));
if (App.Version > v) {
long vers = (App.Version.Major << 24) | (App.Version.Minor << 16) | App.Version.Build;
await AppDbContext.ExecuteBatch(cnx, $"PRAGMA user_version = {vers}");
long vers = (App.Version.Major << 24) | (App.Version.Minor << 16) | (App.Version.Build << 8) | App.Version.Revision;
await cnx.ExecuteBatch($"PRAGMA user_version = {vers}");
}
return v;
@@ -67,20 +67,20 @@ namespace Elwig.Helpers {
if (toExecute.Count == 0)
return;
await AppDbContext.ExecuteBatch(cnx, """
await cnx.ExecuteBatch("""
PRAGMA locking_mode = EXCLUSIVE;
BEGIN EXCLUSIVE;
""");
foreach (var script in toExecute) {
await AppDbContext.ExecuteEmbeddedScript(cnx, asm, script);
await cnx.ExecuteEmbeddedScript(asm, script);
}
var violations = await AppDbContext.ForeignKeyCheck(cnx);
var violations = await cnx.ForeignKeyCheck();
if (violations.Length > 0) {
throw new Exception($"Foreign key violations ({violations.Length}):\n" + string.Join("\n", violations
.Select(v => $"{v.Table} - {v.RowId} - {v.Parent} - {v.FkId}")));
}
await AppDbContext.ExecuteBatch(cnx, $"""
await cnx.ExecuteBatch($"""
COMMIT;
VACUUM;
PRAGMA schema_version = {toVersion * 100 + VersionOffset};

View File

@@ -27,7 +27,7 @@ namespace Elwig.Helpers.Billing {
public async Task FinishSeason() {
using var cnx = await AppDbContext.ConnectAsync();
await AppDbContext.ExecuteBatch(cnx, $"""
await cnx.ExecuteBatch($"""
UPDATE season
SET (start_date, end_date) = (SELECT MIN(date), MAX(date) FROM delivery WHERE year = {Year})
WHERE year = {Year};
@@ -37,7 +37,7 @@ namespace Elwig.Helpers.Billing {
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();
await AppDbContext.ExecuteBatch(cnx, $"""
await cnx.ExecuteBatch($"""
UPDATE member
SET business_shares = member.business_shares - h.business_shares
FROM member_history h
@@ -66,7 +66,7 @@ namespace Elwig.Helpers.Billing {
public async Task UnAdjustBusinessShares() {
using var cnx = await AppDbContext.ConnectAsync();
await AppDbContext.ExecuteBatch(cnx, $"""
await cnx.ExecuteBatch($"""
UPDATE member
SET business_shares = member.business_shares - h.business_shares
FROM member_history h
@@ -157,9 +157,9 @@ namespace Elwig.Helpers.Billing {
lastMgNr = mgnr;
}
await AppDbContext.ExecuteBatch(cnx, $"UPDATE delivery_part_bucket SET value = 0 WHERE year = {Year}");
await cnx.ExecuteBatch($"UPDATE delivery_part_bucket SET value = 0 WHERE year = {Year}");
if (inserts.Count > 0) {
await AppDbContext.ExecuteBatch(cnx, $"""
await cnx.ExecuteBatch($"""
INSERT INTO delivery_part_bucket (year, did, dpnr, bktnr, discr, value)
VALUES {string.Join(",\n ", inserts.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '{i.Item4}', {i.Item5})"))}
ON CONFLICT DO UPDATE
@@ -237,7 +237,7 @@ namespace Elwig.Helpers.Billing {
if (needed == 0) break;
}
await AppDbContext.ExecuteBatch(cnx, $"""
await cnx.ExecuteBatch($"""
INSERT INTO delivery_part_bucket (year, did, dpnr, bktnr, discr, value)
VALUES {string.Join(",\n ", posChanges.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '', {i.Item4})"))}
ON CONFLICT DO UPDATE

View File

@@ -299,22 +299,30 @@ namespace Elwig.Helpers.Billing {
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);
if (!data.ContainsKey("default")) {
foreach (var (v, ks) in rev1) {
if ((ks.Count >= vaributes.Count() * 0.5 && useDefault) || ks.Count == vaributes.Count()) {
foreach (var k in ks) data.Remove(k);
if ((ks.Count > vaributes.Count() * 0.5 && useDefault) || ks.Count == vaributes.Count()) {
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;
CollapsePaymentData(data, vaributes, useDefault);
CollapsePaymentData(data, originalData, vaributes, useDefault);
return;
}
}
foreach (var (v, ks) in rev2) {
if ((ks.Count >= vaributes.Count() * 0.5 && useDefault) || ks.Count == vaributes.Count()) {
foreach (var k in ks) data.Remove(k);
if ((ks.Count > vaributes.Count() * 0.5 && useDefault) || ks.Count == vaributes.Count()) {
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;
CollapsePaymentData(data, vaributes, useDefault);
CollapsePaymentData(data, originalData, vaributes, useDefault);
return;
}
}
@@ -330,16 +338,26 @@ namespace Elwig.Helpers.Billing {
var len = vaributes.Count(e => $"{e.AttrId}{(e.CultId != null && e.CultId != "" ? "-" : "")}{e.CultId}" == idx);
foreach (var (v, ks) in rev1) {
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);
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) {
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);
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;
}
}
}
}
@@ -355,13 +373,22 @@ namespace Elwig.Helpers.Billing {
} else if (k.Contains("/-")) {
data.Remove(k, out var 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);
var keyVaributes = data
.Select(e => e.Key.Split('-')[0])
.Where(e => e.Length > 0 && e != "default")
.Select(e => e.Key)
.Where(e => e.Length > 0 && !e.Contains('-') && e != "default")
.Distinct()
.ToList();
foreach (var idx in keyVaributes) {
@@ -419,8 +446,8 @@ namespace Elwig.Helpers.Billing {
}
}
CollapsePaymentData(payment, vaributes ?? payment.Select(e => new RawVaribute(e.Key)).ToList(), useDefaultPayment);
CollapsePaymentData(qualityWei, vaributes ?? qualityWei.Select(e => new RawVaribute(e.Key)).ToList(), useDefaultQuality);
CollapsePaymentData(payment, payment.DeepClone().AsObject(), vaributes ?? payment.Select(e => new RawVaribute(e.Key)).ToList(), useDefaultPayment);
CollapsePaymentData(qualityWei, qualityWei.DeepClone().AsObject(), vaributes ?? qualityWei.Select(e => new RawVaribute(e.Key)).ToList(), useDefaultQuality);
var data = new JsonObject {
["mode"] = "elwig",

View File

@@ -20,13 +20,19 @@ namespace Elwig.Helpers.Billing {
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 strictPrices = true, bool? honorGebunden = null, bool? allowAttrsIntoLower = null, bool? avoidUnderDeliveries = null) {
using var cnx = await AppDbContext.ConnectAsync();
using var tx = await cnx.BeginTransactionAsync();
await CalculateBuckets(honorGebunden, allowAttrsIntoLower, avoidUnderDeliveries, cnx);
await DeleteInDb(cnx);
await SetCalcTime(cnx);
await CalculatePrices(cnx);
KeyNotFoundException? exception = null;
try {
await CalculatePrices(cnx, strictPrices);
} catch (KeyNotFoundException e) {
if (strictPrices) throw;
exception = e;
}
if (Data.ConsiderDelieryModifiers) {
await CalculateDeliveryModifiers(cnx);
}
@@ -34,26 +40,28 @@ namespace Elwig.Helpers.Billing {
await CalculateMemberModifiers(cnx);
}
await tx.CommitAsync();
if (exception != null)
throw exception;
}
public async Task Commit() {
await Revert();
using var cnx = await AppDbContext.ConnectAsync();
await AppDbContext.ExecuteBatch(cnx, $"""
await cnx.ExecuteBatch($"""
INSERT INTO credit (year, tgnr, mgnr, avnr, net_amount, prev_net_amount, vat, modifiers, prev_modifiers)
SELECT s.year,
COALESCE(t.tgnr, 0) + ROW_NUMBER() OVER(ORDER BY m.mgnr) AS tgnr,
m.mgnr,
v.avnr,
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, 0, ROUND(lp.amount / POW(10, s.precision - 2))) AS prev_net_amount,
IIF(m.buchführend, s.vat_normal, s.vat_flatrate) AS vat,
ROUND(IIF({Data.ConsiderTotalPenalty}, COALESCE(b.total_penalty, 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,
lc.modifiers AS prev_modifiers
IIF(lc.amount < 0, 0, lc.modifiers) AS prev_modifiers
FROM season s
JOIN payment_variant v ON v.year = s.year
LEFT JOIN payment_variant l ON l.year = s.year
@@ -74,27 +82,27 @@ namespace Elwig.Helpers.Billing {
LEFT JOIN payment_custom x ON (x.year, x.mgnr) = (s.year, m.mgnr)
WHERE s.year = {Year} AND v.avnr = {AvNr};
""");
await AppDbContext.ExecuteBatch(cnx, $"""
await cnx.ExecuteBatch($"""
UPDATE payment_variant SET test_variant = FALSE WHERE (year, avnr) = ({Year}, {AvNr});
""");
}
public async Task Revert() {
using var cnx = await AppDbContext.ConnectAsync();
await AppDbContext.ExecuteBatch(cnx, $"""
await cnx.ExecuteBatch($"""
DELETE FROM credit WHERE (year, avnr) = ({Year}, {AvNr});
UPDATE payment_variant SET test_variant = TRUE WHERE (year, avnr) = ({Year}, {AvNr});
""");
}
protected async Task SetCalcTime(SqliteConnection cnx) {
await AppDbContext.ExecuteBatch(cnx, $"""
await cnx.ExecuteBatch($"""
UPDATE payment_variant SET calc_time = UNIXEPOCH() WHERE (year, avnr) = ({Year}, {AvNr})
""");
}
protected async Task DeleteInDb(SqliteConnection cnx) {
await AppDbContext.ExecuteBatch(cnx, $"""
await cnx.ExecuteBatch($"""
DELETE FROM payment_delivery_part_bucket WHERE (year, avnr) = ({Year}, {AvNr});
DELETE FROM payment_delivery_part WHERE (year, avnr) = ({Year}, {AvNr});
DELETE FROM payment_member WHERE (year, avnr) = ({Year}, {AvNr});
@@ -108,7 +116,7 @@ namespace Elwig.Helpers.Billing {
var multiplier = 0.50;
var includePredecessor = true;
var modName = "Treue%";
await AppDbContext.ExecuteBatch(cnx, $"""
await cnx.ExecuteBatch($"""
INSERT INTO payment_member (year, avnr, mgnr, net_amount, mod_abs, mod_rel)
SELECT c.year, {AvNr}, s.mgnr, 0,
ROUND(s.sum * COALESCE(m.abs, 0)),
@@ -130,7 +138,7 @@ namespace Elwig.Helpers.Billing {
mod_rel = mod_rel + excluded.mod_rel
""");
}
await AppDbContext.ExecuteBatch(cnx, $"""
await cnx.ExecuteBatch($"""
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
@@ -142,7 +150,8 @@ namespace Elwig.Helpers.Billing {
""");
}
protected async Task CalculatePrices(SqliteConnection cnx) {
protected async Task CalculatePrices(SqliteConnection cnx, bool strict = true) {
var invalid = new HashSet<string>();
var parts = new List<(int Year, int DId, int DPNr, int BktNr, string SortId, string? AttrId, string? CultId, string Discr, int Value, double Oe, double Kmw, string QualId, bool AttrAreaCom)>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"""
@@ -172,21 +181,31 @@ namespace Elwig.Helpers.Billing {
var payAttrId = (part.Discr is "" or "_") ? null : part.Discr;
var attrId = part.AttrAreaCom ? payAttrId : part.AttrId;
var geb = !ungeb && (payAttrId == attrId || !part.AttrAreaCom);
var price = Data.CalculatePrice(part.SortId, attrId, part.CultId, part.QualId, geb, part.Oe, part.Kmw);
decimal price = 0;
try {
price = Data.CalculatePrice(part.SortId, attrId, part.CultId, part.QualId, geb, part.Oe, part.Kmw);
} catch (KeyNotFoundException e) {
invalid.Add(e.Message.Split('\'')[1]);
}
var priceL = PaymentVariant.Season.DecToDb(price);
inserts.Add((part.Year, part.DId, part.DPNr, part.BktNr, priceL, priceL * part.Value));
}
await AppDbContext.ExecuteBatch(cnx, $"""
var msg = invalid.Count == 0 ? null : "Für folgende Sorten wurde noch keine Preiskurve festgelegt: " + string.Join(", ", invalid);
if (msg != null && strict)
throw new KeyNotFoundException(msg);
await cnx.ExecuteBatch($"""
INSERT INTO payment_delivery_part_bucket (year, did, dpnr, bktnr, avnr, price, amount)
VALUES {string.Join(",\n ", inserts.Select(i => $"({i.Year}, {i.DId}, {i.DPNr}, {i.BktNr}, {AvNr}, {i.Price}, {i.Amount})"))};
""");
if (msg != null)
throw new KeyNotFoundException(msg);
}
protected async Task CalculateDeliveryModifiers(SqliteConnection cnx) {
var netMod = Data.NetWeightModifier.ToString().Replace(',', '.');
var grossMod = Data.GrossWeightModifier.ToString().Replace(',', '.');
await AppDbContext.ExecuteBatch(cnx, $"""
await cnx.ExecuteBatch($"""
INSERT INTO payment_delivery_part (year, did, dpnr, avnr, net_amount, mod_abs, mod_rel)
SELECT d.year, d.did, d.dpnr, {AvNr}, 0, 0, IIF(d.net_weight, {netMod}, {grossMod})
FROM delivery_part d

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Media;
namespace Elwig.Helpers.Billing {
public class GraphEntry {
@@ -40,6 +41,7 @@ namespace Elwig.Helpers.Billing {
public string VaributeStringSimple => (Abgewertet ? "Abgew.: " : "") + (Vaributes.Count != 0 ? (Vaributes.Count >= 25 ? "Restliche Sorten" : string.Join(", ", Vaributes.Select(c => c.Listing))) : "-");
public string VaributeString => Vaributes.Count != 0 ? string.Join("\n", Vaributes.Select(c => c.FullName)) : "-";
public string VaributeStringChange => (Abgewertet ? "A." : "") + string.Join(",", Vaributes.Select(c => c.Listing));
public Brush? Color => Vaributes.Select(v => v.Variety?.Color).Distinct().SingleOrDefault();
private readonly int Precision;
public GraphEntry(int id, int precision, BillingData.CurveMode mode) {

View File

@@ -19,10 +19,6 @@ namespace Elwig.Helpers {
public bool IsSitzendorf => IsWinzerkeller && App.ZwstId == "S";
public bool IsGrInzersdorf => IsWeinland;
public bool HasNetWeighing(string? zwstId) => IsMatzen || (IsWinzerkeller && zwstId == "W");
public bool HasNetWeighing(Branch? b) => HasNetWeighing(b?.ZwstId);
public bool HasNetWeighing() => HasNetWeighing(App.ZwstId);
public string NameToken;
public string NameShort;
public string Name;
@@ -51,6 +47,7 @@ namespace Elwig.Helpers {
public string? Bic;
public string? UstIdNr;
public string? LfbisNr;
public string? OrganicAuthority;
public string? PhoneNr;
public string? FaxNr;
@@ -67,6 +64,12 @@ namespace Elwig.Helpers {
public string? TextEmailSubject;
public string? TextEmailBody;
public bool MailIncludeNonDeliverers;
public bool MailDoublePaged;
public int MailSendPostal;
public int MailSendEmail;
public int MailOrdering;
public int ExportEbicsVersion;
public int ExportEbicsAddress;
@@ -103,6 +106,7 @@ namespace Elwig.Helpers {
UstIdNr = parameters.GetValueOrDefault("CLIENT_USTIDNR");
Bic = parameters.GetValueOrDefault("CLIENT_BIC");
Iban = parameters.GetValueOrDefault("CLIENT_IBAN");
OrganicAuthority = parameters.GetValueOrDefault("CLIENT_ORGANIC_AUTHORITY");
switch (parameters.GetValueOrDefault("MODE_DELIVERYNOTE_STATS", "SHORT")?.ToUpper()) {
case "NONE": ModeDeliveryNoteStats = 0; break;
@@ -117,7 +121,7 @@ namespace Elwig.Helpers {
case "KMW/5": ModeWineQualityStatistics = 3; break;
case "KMW/10": ModeWineQualityStatistics = 4; break;
}
switch (parameters.GetValueOrDefault("ORDERING_MEMBERLIST", "")?.ToUpper()) {
switch (parameters.GetValueOrDefault("ORDERING_MEMBERLIST", "MGNR")?.ToUpper()) {
case "MGNR": OrderingMemberList = 0; break;
case "NAME": OrderingMemberList = 1; break;
case "KG": OrderingMemberList = 2; break;
@@ -135,6 +139,31 @@ namespace Elwig.Helpers {
TextEmailBody = parameters.GetValueOrDefault("TEXT_EMAIL_BODY");
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;
@@ -177,6 +206,25 @@ namespace Elwig.Helpers {
case 1: orderingMemberList = "NAME"; break;
case 2: orderingMemberList = "KG"; break;
}
string mailSendPostal = "WISH";
switch (MailSendPostal) {
case 0: mailSendPostal = "NONE"; break;
case 1: mailSendPostal = "NO_EMAIL"; break;
case 2: mailSendPostal = "WISH"; break;
case 3: mailSendPostal = "ALL"; break;
}
string mailSendEmail = "WISH";
switch (MailSendEmail) {
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;
@@ -203,6 +251,7 @@ namespace Elwig.Helpers {
("CLIENT_USTIDNR", UstIdNr),
("CLIENT_BIC", Bic),
("CLIENT_IBAN", Iban),
("CLIENT_ORGANIC_AUTHORITY", OrganicAuthority),
("MODE_DELIVERYNOTE_STATS", deliveryNoteStats),
("MODE_WINEQUALITYSTATISTICS", modeWineQualityStatistics),
("ORDERING_MEMBERLIST", orderingMemberList),
@@ -212,6 +261,11 @@ namespace Elwig.Helpers {
("TEXT_CREDITNOTE", TextCreditNote),
("TEXT_EMAIL_SUBJECT", TextEmailSubject),
("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),

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -5,6 +6,8 @@ using Microsoft.Extensions.Configuration;
namespace Elwig.Helpers {
public enum WeighingMode { Gross, Net, Box }
public record struct ScaleConfig {
public string Id;
public string? Type;
@@ -41,6 +44,7 @@ namespace Elwig.Helpers {
public string DatabaseFile = App.DataPath + "database.sqlite3";
public string? DatabaseLog = null;
public string? Branch = null;
public WeighingMode? WeighingMode;
public string? UpdateUrl = null;
public bool UpdateAuto = false;
public string? SyncUrl = null;
@@ -74,6 +78,8 @@ namespace Elwig.Helpers {
DatabaseLog = log != null ? Path.Combine(Path.GetDirectoryName(FileName) ?? App.DataPath, log) : null;
Branch = config["general:branch"];
Debug = TrueValues.Contains(config["general:debug"]?.ToLower());
var weighing = config["general:weighing"];
WeighingMode = weighing != null && Enum.TryParse<WeighingMode>(weighing, true, out var w) ? w : null;
UpdateUrl = config["update:url"];
UpdateAuto = TrueValues.Contains(config["update:auto"]?.ToLower());
SyncUrl = config["sync:url"];

View File

@@ -5,6 +5,7 @@ using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Threading;
using Brush = System.Windows.Media.Brush;
using Brushes = System.Windows.Media.Brushes;
@@ -234,5 +235,21 @@ namespace Elwig.Helpers {
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

@@ -15,7 +15,9 @@ namespace Elwig.Helpers.Export {
protected readonly char Separator;
protected string? Header;
public Csv(string filename, char separator = ';') : this(filename, separator, Utils.UTF8) { }
public Csv(string filename, char separator = ';') :
this(filename, separator, Utils.UTF8) {
}
public Csv(string filename, char separator, Encoding encoding) {
_writer = new StreamWriter(filename, false, encoding);
@@ -58,4 +60,22 @@ namespace Elwig.Helpers.Export {
public abstract string FormatRow(T row);
}
public class CsvSimple : Csv<IEnumerable<object?>> {
public CsvSimple(string filename, char separator, Encoding encoding) :
base(filename, separator, encoding) {
}
public CsvSimple(string filename, char separator = ';') :
base(filename, separator) {
}
public override string FormatRow(IEnumerable<object?> row) {
return string.Join(Separator, row.Select(i => {
var str = $"{i}";
return str.Contains(Separator) || str.Contains('\n') ? $"\"{str.Replace("\"", "\"\"")}\"" : str;
}));
}
}
}

View File

@@ -0,0 +1,243 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
namespace Elwig.Helpers.Export {
public static class Database {
private static async Task<(long? ApplicationId, string? UserVersion, long? SchemaVersion, long FileSize)> GetMeta() {
long size = new FileInfo(App.Config.DatabaseFile).Length;
using var cnx = await AppDbContext.ConnectAsync();
var applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id");
var userVers = (long?)await cnx.ExecuteScalar("PRAGMA user_version");
var schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version");
return (applId, userVers != null ? $"{userVers >> 24}.{(userVers >> 16) & 0xFF}.{(userVers >> 8) & 0xFF}.{userVers & 0xFF}" : null, schemaVers, size);
}
public static async Task ExportSqlite(string filename, bool zipFile) {
if (zipFile) {
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 (applId, userVers, schemaVers, size) = await GetMeta();
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,
["database"] = new JsonObject {
["application_id"] = applId,
["user_version"] = userVers,
["schema_version"] = schemaVers,
["file_size"] = size,
},
};
await writer.WriteAsync(obj.ToJsonString(Utils.JsonOpts));
}
var db = zip.CreateEntryFromFile(App.Config.DatabaseFile, "database.sqlite3", CompressionLevel.SmallestSize);
} else {
File.Copy(App.Config.DatabaseFile, filename, true);
}
}
public static async Task ExportSql(string filename, bool zipFile) {
if (zipFile) {
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 (applId, userVers, schemaVers, size) = await GetMeta();
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,
["database"] = new JsonObject {
["application_id"] = applId,
["user_version"] = userVers,
["schema_version"] = schemaVers,
["file_size"] = size,
},
};
await writer.WriteAsync(obj.ToJsonString(Utils.JsonOpts));
}
var sql = zip.CreateEntry("database.sql", CompressionLevel.SmallestSize);
using (var writer = new StreamWriter(sql.Open(), Utils.UTF8)) {
await ExportSql(writer);
}
} else {
using var stream = File.OpenWrite(filename);
using var writer = new StreamWriter(stream, Utils.UTF8);
await ExportSql(writer);
}
}
public static async Task ExportSql(StreamWriter writer) {
using var cnx = await AppDbContext.ConnectAsync();
var tables = new List<(string Name, string Sql)>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = "SELECT name, sql FROM sqlite_schema WHERE type = 'table'";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
tables.Add((reader.GetString(0), reader.GetString(1)));
}
}
var applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id") ?? 0;
var userVers = (long?)await cnx.ExecuteScalar("PRAGMA user_version") ?? 0;
var schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version") ?? 0;
await writer.WriteLineAsync($"-- Elwig database dump, {DateTime.Now:yyyy-MM-dd, HH:mm:ss}");
await writer.WriteLineAsync($"-- {Environment.MachineName}, Zwst. {App.BranchName}, {App.Client.Name}");
await writer.WriteLineAsync("BEGIN TRANSACTION;");
await writer.WriteLineAsync("PRAGMA foreign_keys=OFF;");
await writer.WriteLineAsync($"PRAGMA application_id=0x{applId:X8};");
await writer.WriteLineAsync($"PRAGMA user_version=0x{userVers:X8};");
foreach (var t in tables) {
await writer.WriteAsync(t.Sql);
await writer.WriteLineAsync(";");
var columnNames = new List<string>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"PRAGMA table_info({t.Name})";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
columnNames.Add(reader.GetString(1));
}
}
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"SELECT {string.Join(',', columnNames)} FROM {t.Name}";
using var reader = await cmd.ExecuteReaderAsync();
var columns = await reader.GetColumnSchemaAsync();
var values = new object[reader.FieldCount];
while (await reader.ReadAsync()) {
await writer.WriteAsync($"INSERT INTO {t.Name} VALUES (");
reader.GetValues(values);
for (int i = 0; i < columns.Count; i++) {
var c = columns[i];
var v = values[i];
if (i > 0) await writer.WriteAsync(",");
if (v == null || v is DBNull) {
await writer.WriteAsync("NULL");
} else if (c.DataTypeName == "TEXT") {
await writer.WriteAsync($"'{v.ToString()?.Replace("'", "''")}'");
} else {
await writer.WriteAsync(v.ToString()?.Replace(',', '.'));
}
}
await writer.WriteLineAsync(");");
}
}
}
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = "SELECT sql FROM sqlite_schema WHERE type != 'table' AND sql IS NOT NULL";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
await writer.WriteAsync(reader.GetString(0));
await writer.WriteLineAsync(";");
}
}
await writer.WriteLineAsync($"PRAGMA schema_version={schemaVers};");
await writer.WriteLineAsync("PRAGMA foreign_keys=ON;");
await writer.WriteLineAsync("COMMIT;");
await writer.WriteLineAsync("VACUUM;");
}
public static async Task Import(string filename) {
if (filename.EndsWith(".sql")) {
await ImportSql(filename, false);
} else if (filename.EndsWith(".sql.zip")) {
await ImportSql(filename, true);
} else if (filename.EndsWith(".sqlite3")) {
await ImportSqlite(filename, false);
} else if (filename.EndsWith(".sqlite3.zip")) {
await ImportSqlite(filename, true);
} else {
throw new ArgumentException($"Unknown file extension for importing: '{filename}'");
}
}
public static async Task ImportSql(string filename, bool zipFile = false) {
if (zipFile) {
using var zip = ZipFile.Open(filename, ZipArchiveMode.Read);
await zip.CheckIntegrity();
foreach (var entry in zip.Entries) {
if (entry.Name.EndsWith(".sql")) {
using var stream = entry.Open();
using var reader = new StreamReader(stream, Utils.UTF8);
await ImportSql(reader);
return;
}
}
throw new FileFormatException("ZIP archive has to contain at least one .sql file");
} else {
using var stream = File.Open(filename, FileMode.Open);
using var reader = new StreamReader(stream, Utils.UTF8);
await ImportSql(reader);
}
}
public static async Task ImportSqlite(string filename, bool zipFile = false) {
if (zipFile) {
var newName = Path.ChangeExtension(App.Config.DatabaseFile, ".new.sqlite3");
try {
using var zip = ZipFile.Open(filename, ZipArchiveMode.Read);
await zip.CheckIntegrity();
foreach (var entry in zip.Entries) {
if (entry.Name.EndsWith(".sqlite3")) {
entry.ExtractToFile(newName);
await ImportSqlite(newName);
return;
}
}
throw new FileFormatException("ZIP archive has to contain at least one .sqlite3 file");
} finally {
if (File.Exists(newName)) File.Delete(newName);
}
}
var oldName = Path.ChangeExtension(App.Config.DatabaseFile, ".old.sqlite3");
File.Move(App.Config.DatabaseFile, oldName, true);
File.Move(filename, App.Config.DatabaseFile, false);
using var cnx = await AppDbContext.ConnectAsync();
await cnx.ExecuteBatch("VACUUM");
}
public static async Task ImportSql(StreamReader reader) {
var newName = Path.ChangeExtension(App.Config.DatabaseFile, ".new.sqlite3");
File.Delete(newName);
try {
using (var cnx = await AppDbContext.ConnectAsync($"Data Source=\"{newName}\"; Mode=ReadWriteCreate; Foreign Keys=False; Cache=Default; Pooling=False")) {
await cnx.ExecuteBatch(await reader.ReadToEndAsync());
}
await ImportSqlite(newName);
} finally {
if (File.Exists(newName)) File.Delete(newName);
}
}
}
}

View File

@@ -1,14 +1,14 @@
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;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using System.Windows;
namespace Elwig.Helpers.Export {
public static class ElwigData {
@@ -17,8 +17,6 @@ namespace Elwig.Helpers.Export {
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);
@@ -40,6 +38,7 @@ namespace Elwig.Helpers.Export {
Dictionary<string, int> currentLsNrs;
Dictionary<int, List<WbRd>> currentWbRde;
Dictionary<int, AT_Kg> kgs;
List<WbGl> currentWbGls;
using (var ctx = new AppDbContext()) {
branches = await ctx.Branches.ToDictionaryAsync(b => b.ZwstId);
@@ -51,6 +50,7 @@ namespace Elwig.Helpers.Export {
currentWbRde = await ctx.WbRde
.GroupBy(r => r.KgNr)
.ToDictionaryAsync(g => g.Key, g => g.ToList());
currentWbGls = await ctx.WbGls.ToListAsync();
kgs = await ctx.Katastralgemeinden.Include(k => k.WbKg).ToDictionaryAsync(k => k.KgNr);
}
@@ -61,9 +61,12 @@ namespace Elwig.Helpers.Export {
List<MemberEmailAddr> EmailAddresses,
List<AreaCom> AreaCommitments,
List<WbRd> Riede,
List<WbKg> WbKgs,
List<WbGl> WbGls,
List<Delivery> Deliveries,
List<DeliveryPart> DeliveryParts,
List<DeliveryPartModifier> Modifiers)>();
List<DeliveryPartModifier> Modifiers,
Dictionary<string, List<(int Id1, int Id2, DateTime CreatedAt, DateTime ModifiedAt)>> Timestamps)>();
var metaData = new List<(string FileName, string ZwstId, string Device,
int? MemberNum, string? MemberFilters,
@@ -71,71 +74,117 @@ namespace Elwig.Helpers.Export {
int? DeliveryNum, string? DeliveryFilters)>();
foreach (var filename in filenames) {
// TODO read encrypted files
using var zip = ZipFile.Open(filename, ZipArchiveMode.Read);
try {
data.Add(new([], [], [], [], [], [], [], new([], [], [], [], new() {
["member"] = [],
["area_commitment"] = [],
["delivery"] = [],
})));
var r = data[^1];
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})");
}
// TODO read encrypted files
using var zip = ZipFile.Open(filename, ZipArchiveMode.Read);
await zip.CheckIntegrity();
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 version = zip.GetEntry("version");
using (var reader = new StreamReader(version!.Open(), Utils.UTF8)) {
if (await reader.ReadToEndAsync() != "elwig:1")
throw new FileFormatException($"Ungültige Elwig-Export-Datei ({filename})");
}
}
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 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));
var wbKgsJson = zip.GetEntry("wb_kgs.json");
if (wbKgsJson != null) {
using var reader = new StreamReader(wbKgsJson.Open(), Utils.UTF8);
string? line;
while ((line = await reader.ReadLineAsync()) != null) {
var obj = JsonNode.Parse(line)!.AsObject();
var (k, g) = obj.ToWbKg(currentWbGls);
r.WbKgs.Add(k);
if (g != null) {
currentWbGls[g.GlNr] = g;
r.WbGls.Add(g);
}
}
}
}
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 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, timestamps) = obj.ToMember(kgs);
r.Members.Add(m);
if (b != null) r.BillingAddresses.Add(b);
r.TelephoneNumbers.AddRange(telNrs);
r.EmailAddresses.AddRange(emailAddrs);
if (timestamps.HasValue)
r.Timestamps["member"].Add((m.MgNr, 0, timestamps.Value.CreatedAt, timestamps.Value.ModifiedAt));
}
}
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, timestamps) = obj.ToAreaCom(currentWbRde);
r.AreaCommitments.Add(areaCom);
if (wbrd != null) {
r.Riede.Add(wbrd);
}
if (timestamps.HasValue)
r.Timestamps["area_commitment"].Add((areaCom.FbNr, 0, timestamps.Value.CreatedAt, timestamps.Value.ModifiedAt));
}
}
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, rde, timestamps) = obj.ToDelivery(currentLsNrs, currentDids, kgs, currentWbRde);
r.Deliveries.Add(d);
r.DeliveryParts.AddRange(parts);
r.Modifiers.AddRange(mods);
r.Riede.AddRange(rde);
if (timestamps.HasValue)
r.Timestamps["delivery"].Add((d.Year, d.DId, timestamps.Value.CreatedAt, timestamps.Value.ModifiedAt));
}
}
} catch (Exception exc) when (
exc is InvalidDataException ||
exc is FileFormatException ||
exc is FileNotFoundException ||
exc is IOException) {
data.RemoveAt(data.Count - 1);
var str = $"Die Elwig-Export-Datei '{Path.GetFileName(filename)}' konnte nicht verarbeitet werden und wird übersprungen.\n\n" + exc.Message;
if (exc.InnerException != null) str += "\n\n" + exc.InnerException.Message;
MessageBox.Show(str, "Fehler beim Importieren", MessageBoxButton.OK, MessageBoxImage.Error);
await AddImportedFiles(Path.GetFileName(filename));
} catch (Exception exc) {
data.RemoveAt(data.Count - 1);
var str = $"Die Elwig-Export-Datei '{Path.GetFileName(filename)}' konnte nicht verarbeitet werden. Soll sie in Zukunft übersprungen werden?\n\n" + exc.Message;
if (exc.InnerException != null) str += "\n\n" + exc.InnerException.Message;
var r = MessageBox.Show(str, "Fehler beim Importieren", MessageBoxButton.YesNo, MessageBoxImage.Error, MessageBoxResult.No);
if (r == MessageBoxResult.Yes) {
await AddImportedFiles(Path.GetFileName(filename));
}
}
}
@@ -144,12 +193,18 @@ namespace Elwig.Helpers.Export {
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)) {
foreach (var ((members, billingAddresses, telephoneNumbers, emailAddresses, areaCommitments, riede, wbKgs, wbGls, deliveries, deliveryParts, modifiers, timestamps), meta) in data.Zip(metaData)) {
var branch = branches[meta.ZwstId];
var device = meta.Device;
using var ctx = new AppDbContext();
var kgnrs = wbKgs.Select(k => k.KgNr).ToList();
var duplicateKgNrs = await ctx.WbKgs
.Where(k => kgnrs.Contains(k.KgNr))
.Select(k => k.KgNr)
.ToListAsync();
var mgnrs = members.Select(m => m.MgNr).ToList();
var duplicateMgNrs = await ctx.Members
.Where(m => mgnrs.Contains(m.MgNr))
@@ -203,6 +258,12 @@ namespace Elwig.Helpers.Export {
importDuplicateDeliveries = ImportQuestion(branch.Name, device, "Lieferungen", true, duplicateLsNrs.Count);
}
if (importDuplicateMembers || importNewMembers || importDuplicateDeliveries || importNewDeliveries) {
ctx.AddRange(wbGls);
ctx.UpdateRange(wbKgs.Where(k => duplicateKgNrs.Contains(k.KgNr)));
ctx.AddRange(wbKgs.Where(k => !duplicateKgNrs.Contains(k.KgNr)));
}
if (importDuplicateMembers) {
ctx.RemoveRange(ctx.BillingAddresses.Where(a => duplicateMgNrs.Contains(a.MgNr)));
ctx.RemoveRange(ctx.MemberTelephoneNrs.Where(n => duplicateMgNrs.Contains(n.MgNr)));
@@ -272,6 +333,26 @@ namespace Elwig.Helpers.Export {
}
await ctx.SaveChangesAsync();
var primaryKeys = new Dictionary<string, string>() {
["member"] = "mgnr, 0",
["area_commitment"] = "fbnr, 0",
["delivery"] = "year, did",
};
var updateStmts = timestamps
.SelectMany(e => e.Value.Select(m => $"UPDATE {e.Key} " +
$"SET ctime = {((DateTimeOffset)m.CreatedAt.ToUniversalTime()).ToUnixTimeSeconds()}, " +
$"mtime = {((DateTimeOffset)m.ModifiedAt.ToUniversalTime()).ToUnixTimeSeconds()} " +
$"WHERE ({primaryKeys[e.Key]}) = ({m.Id1}, {m.Id2});"));
using var cnx = AppDbContext.Connect();
await cnx.ExecuteBatch($"""
BEGIN;
UPDATE client_parameter SET value = '0' WHERE param = 'ENABLE_TIME_TRIGGERS';
{string.Join("\n", updateStmts)}
UPDATE client_parameter SET value = '1' WHERE param = 'ENABLE_TIME_TRIGGERS';
COMMIT;
""");
await AddImportedFiles(Path.GetFileName(meta.FileName));
}
App.HintContextChange();
@@ -302,9 +383,9 @@ namespace Elwig.Helpers.Export {
"Importieren erfolgreich",
MessageBoxButton.OK, MessageBoxImage.Information);
} catch (Exception exc) {
var str = "Der Eintrag konnte nicht in der Datenbank aktualisiert werden!\n\n" + exc.Message;
var str = "Der Eintrag konnte nicht in der Datenbank aktualisiert werden!\n\nEvtl. muss die Datenbank manuell auf dieses Gerät kopieren werden.\n\n" + exc.Message;
if (exc.InnerException != null) str += "\n\n" + exc.InnerException.Message;
MessageBox.Show(str, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
MessageBox.Show(str, "Fehler beim Importieren", MessageBoxButton.OK, MessageBoxImage.Error);
}
GC.Collect();
GC.WaitForPendingFinalizers();
@@ -319,26 +400,30 @@ namespace Elwig.Helpers.Export {
) == MessageBoxResult.Yes;
}
public static Task Export(string filename, IEnumerable<Member> members, IEnumerable<string> filters) {
public static Task Export(string filename, IEnumerable<Member> members, IEnumerable<WbKg> wbKgs, IEnumerable<string> filters) {
return new ElwigExport {
Members = (members, filters)
Members = (members, filters),
WbKgs = (wbKgs, ["von exportierten Mitgliedern"]),
}.Export(filename);
}
public static Task Export(string filename, IEnumerable<Member> members, IEnumerable<AreaCom> areaComs, IEnumerable<string> filters) {
public static Task Export(string filename, IEnumerable<Member> members, IEnumerable<AreaCom> areaComs, IEnumerable<WbKg> wbKgs, IEnumerable<string> filters) {
return new ElwigExport {
Members = (members, filters),
AreaComs = (areaComs, ["von exportierten Mitgliedern"]),
WbKgs = (wbKgs, ["von exportierten Mitgliedern und Flächenbindungen"]),
}.Export(filename);
}
public static Task Export(string filename, IEnumerable<Delivery> deliveries, IEnumerable<string> filters) {
public static Task Export(string filename, IEnumerable<Delivery> deliveries, IEnumerable<WbKg> wbKgs, IEnumerable<string> filters) {
return new ElwigExport {
Deliveries = (deliveries, filters)
Deliveries = (deliveries, filters),
WbKgs = (wbKgs, ["von exportierten Lieferungen"]),
}.Export(filename);
}
public class ElwigExport {
public (IEnumerable<WbKg> WbKgs, IEnumerable<string> Filters)? WbKgs { get; set; }
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; }
@@ -359,6 +444,12 @@ namespace Elwig.Helpers.Export {
["zwstid"] = App.ZwstId,
["device"] = Environment.MachineName,
};
if (WbKgs != null) {
obj["wb_kgs"] = new JsonObject {
["count"] = WbKgs.Value.WbKgs.Count(),
["filters"] = new JsonArray(WbKgs.Value.Filters.Select(f => (JsonNode)f).ToArray()),
};
}
if (Members != null)
obj["members"] = new JsonObject {
["count"] = Members.Value.Members.Count(),
@@ -375,34 +466,69 @@ namespace Elwig.Helpers.Export {
["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));
await writer.WriteAsync(obj.ToJsonString(Utils.JsonOpts));
}
// TODO encrypt files
if (WbKgs != null) {
var json = zip.CreateEntry("wb_kgs.json", CompressionLevel.SmallestSize);
using var writer = new StreamWriter(json.Open(), Utils.UTF8);
foreach (var k in WbKgs.Value.WbKgs) {
await writer.WriteLineAsync(k.ToJson().ToJsonString(Utils.JsonOpts));
}
}
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));
await writer.WriteLineAsync(m.ToJson().ToJsonString(Utils.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));
await writer.WriteLineAsync(c.ToJson().ToJsonString(Utils.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));
await writer.WriteLineAsync(d.ToJson().ToJsonString(Utils.JsonOpts));
}
}
}
}
public static JsonObject ToJson(this WbKg k) {
return new JsonObject {
["kgnr"] = k.KgNr,
["großlage"] = k.Gl?.Name,
};
}
public static (WbKg, WbGl?) ToWbKg(this JsonNode json, List<WbGl> gls) {
var grosslage = json["großlage"]?.AsValue().GetValue<string>();
WbGl? gl = null;
bool newGl = false;
if (grosslage != null) {
gl = gls.FirstOrDefault(g => g.Name == grosslage);
if (gl == null) {
newGl = true;
gl = new WbGl {
GlNr = (gls.Count == 0 ? 1 : gls.Max(g => g.GlNr)) + 1,
Name = grosslage,
};
gls[gl.GlNr] = gl;
}
}
return (new WbKg {
KgNr = json["kgnr"]!.AsValue().GetValue<int>(),
GlNr = gl?.GlNr,
}, newGl ? gl : null);
}
public static JsonObject ToJson(this Member m) {
return new JsonObject {
["mgnr"] = m.MgNr,
@@ -460,15 +586,19 @@ namespace Elwig.Helpers.Export {
return obj;
}).ToArray()),
["comment"] = m.Comment,
["created_at"] = $"{m.CreatedAt:yyyy-MM-ddTHH:mm:ssK}",
["modified_at"] = $"{m.ModifiedAt:yyyy-MM-ddTHH:mm:ssK}",
};
}
public static (Member, BillingAddr?, List<MemberTelNr>, List<MemberEmailAddr>) ToMember(this JsonNode json, Dictionary<int, AT_Kg> kgs) {
public static (Member, BillingAddr?, List<MemberTelNr>, List<MemberEmailAddr>, (DateTime CreatedAt, DateTime ModifiedAt)?) 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)");
}
var createdAt = json["created_at"]?.AsValue().GetValue<string>();
var modifiedAt = json["modified_at"]?.AsValue().GetValue<string>();
return (new Member {
MgNr = mgnr,
PredecessorMgNr = json["predecessor_mgnr"]?.AsValue().GetValue<int>(),
@@ -502,6 +632,7 @@ namespace Elwig.Helpers.Export {
ContactViaPost = json["contact_postal"]?.AsValue().GetValue<bool>() ?? false,
ContactViaEmail = json["contact_email"]?.AsValue().GetValue<bool>() ?? false,
Comment = json["comment"]?.AsValue().GetValue<string>(),
ImportedAt = DateTime.Now,
}, json["billing_address"] is JsonObject a ? new BillingAddr {
MgNr = mgnr,
FullName = a["name"]!.AsValue().GetValue<string>(),
@@ -519,7 +650,10 @@ namespace Elwig.Helpers.Export {
Nr = i + 1,
Address = a["address"]!.AsValue().GetValue<string>(),
Comment = a["comment"]?.AsValue().GetValue<string>(),
}).ToList());
}).ToList(),
createdAt == null || modifiedAt == null ? null :
(DateTime.ParseExact(createdAt, "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture, DateTimeStyles.None),
DateTime.ParseExact(modifiedAt, "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture, DateTimeStyles.None)));
}
public static JsonObject ToJson(this AreaCom c) {
@@ -535,27 +669,32 @@ namespace Elwig.Helpers.Export {
["year_from"] = c.YearFrom,
["year_to"] = c.YearTo,
["comment"] = c.Comment,
["created_at"] = $"{c.CreatedAt:yyyy-MM-ddTHH:mm:ssK}",
["modified_at"] = $"{c.ModifiedAt:yyyy-MM-ddTHH:mm:ssK}",
};
}
public static (AreaCom, WbRd?) ToAreaCom(this JsonNode json, Dictionary<int, AT_Kg> kgs, Dictionary<int, List<WbRd>> riede) {
public static (AreaCom, WbRd?, (DateTime CreatedAt, DateTime ModifiedAt)?) ToAreaCom(this JsonNode json, 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)");
var rde = riede.GetValueOrDefault(kgnr, []);
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,
RdNr = (rde.Count == 0 ? 1 : rde.Max(r => r.RdNr)) + 1,
Name = ried,
};
rde.Add(rd);
riede[rd.KgNr] = rde;
}
}
var createdAt = json["created_at"]?.AsValue().GetValue<string>();
var modifiedAt = json["modified_at"]?.AsValue().GetValue<string>();
return (new AreaCom {
FbNr = json["fbnr"]!.AsValue().GetValue<int>(),
MgNr = json["mgnr"]!.AsValue().GetValue<int>(),
@@ -564,11 +703,15 @@ namespace Elwig.Helpers.Export {
Area = json["area"]!.AsValue().GetValue<int>(),
KgNr = kgnr,
GstNr = json["gstnr"]?.AsValue().GetValue<string>() ?? "-",
RdNr = rd?.RdNr,
RdNr = rd?.RdNr ?? json["rdnr"]?.AsValue().GetValue<int>(),
YearFrom = json["year_from"]?.AsValue().GetValue<int>(),
YearTo = json["year_to"]?.AsValue().GetValue<int>(),
Comment = json["comment"]?.AsValue().GetValue<string>(),
}, newRd ? rd : null);
ImportedAt = DateTime.Now,
}, newRd ? rd : null,
createdAt == null || modifiedAt == null ? null :
(DateTime.ParseExact(createdAt, "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture, DateTimeStyles.None),
DateTime.ParseExact(modifiedAt, "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture, DateTimeStyles.None)));
}
public static JsonObject ToJson(this Delivery d) {
@@ -591,16 +734,18 @@ namespace Elwig.Helpers.Export {
["qualid"] = p.QualId,
["hkid"] = p.HkId,
["kgnr"] = p.KgNr,
["rdnr"] = p.RdNr,
["ried"] = p.Rd?.Name,
["net_weight"] = p.IsNetWeight,
["manual_weighing"] = p.IsManualWeighing,
["modids"] = new JsonArray(p.Modifiers.Select(m => (JsonNode)m.ModId).ToArray()),
["comment"] = p.Comment,
["created_at"] = $"{p.CreatedAt:yyyy-MM-ddTHH:mm:ssK}",
["modified_at"] = $"{p.ModifiedAt:yyyy-MM-ddTHH:mm:ssK}",
};
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.Unloading != null) obj["unloading"] = p.Unloading;
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;
@@ -609,10 +754,12 @@ namespace Elwig.Helpers.Export {
return obj;
}).ToArray()),
["comment"] = d.Comment,
["created_at"] = $"{d.CreatedAt:yyyy-MM-ddTHH:mm:ssK}",
["modified_at"] = $"{d.ModifiedAt:yyyy-MM-ddTHH:mm:ssK}",
};
}
public static (Delivery, List<DeliveryPart>, List<DeliveryPartModifier>) ToDelivery(this JsonNode json, Dictionary<string, int> currentLsNrs, Dictionary<int, int> currentDids) {
public static (Delivery, List<DeliveryPart>, List<DeliveryPartModifier>, List<WbRd>, (DateTime CreatedAt, DateTime ModifiedAt)?) ToDelivery(this JsonNode json, Dictionary<string, int> currentLsNrs, Dictionary<int, int> currentDids, Dictionary<int, AT_Kg> kgs, Dictionary<int, List<WbRd>> riede) {
var year = json["year"]!.AsValue().GetValue<int>();
var lsnr = json["lsnr"]!.AsValue().GetValue<string>();
var did = currentLsNrs.GetValueOrDefault(lsnr, -1);
@@ -621,6 +768,9 @@ namespace Elwig.Helpers.Export {
did = ++currentDids[year];
}
currentLsNrs[lsnr] = did;
var createdAt = json["created_at"]?.AsValue().GetValue<string>();
var modifiedAt = json["modified_at"]?.AsValue().GetValue<string>();
var wbRde = new List<WbRd>();
return (new Delivery {
Year = year,
DId = did,
@@ -631,37 +781,61 @@ namespace Elwig.Helpers.Export {
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>(),
ImportedAt = DateTime.Now,
}, json["parts"]!.AsArray().Select(p => p!.AsObject()).Select(p => {
var kgnr = p["kgnr"]!.AsValue().GetValue<int>();
var ried = p["ried"]?.AsValue().GetValue<string>();
WbRd? rd = null;
if (ried != null) {
var rde = riede.GetValueOrDefault(kgnr, []);
rd = rde.FirstOrDefault(r => r.Name == ried);
if (rd == null) {
rd = new WbRd {
KgNr = kgnr,
RdNr = (rde.Count == 0 ? 1 : rde.Max(r => r.RdNr)) + 1,
Name = ried,
};
rde.Add(rd);
riede[rd.KgNr] = rde;
wbRde.Add(rd);
}
}
return 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 = rd?.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>(),
IsGebunden = p["gebunden"]?.AsValue().GetValue<bool>(),
Unloading = p["unloading"]?.AsValue().GetValue<string>() ?? ((p["lesewagen"]?.AsValue().GetValue<bool>() ?? false) ? DeliveryPart.Pumped : null),
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(Utils.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());
})).ToList(),
wbRde,
createdAt == null || modifiedAt == null ? null :
(DateTime.ParseExact(createdAt, "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture, DateTimeStyles.None),
DateTime.ParseExact(modifiedAt, "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture, DateTimeStyles.None)));
}
}
}

View File

@@ -301,6 +301,7 @@ namespace Elwig.Helpers.Export {
if (units != null && units.Length > 0) {
int n = -1;
switch (units[0]) {
case "#": n = 0; data = $"{v:N0}"; break;
case "%": n = 1; data = $"{v:N1}"; break;
case "€": n = 2; data = $"{v:N2}"; break;
case "€/kg": n = 4; data = $"{v:N4}"; break;
@@ -316,7 +317,7 @@ namespace Elwig.Helpers.Export {
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-columns-repeated=\"{colSpan - 1}\"/>\r\n" : "");
}
}
}

View File

@@ -0,0 +1,69 @@
using Elwig.Models.Entities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Elwig.Helpers.Export {
public class VCard : IExporter<Member> {
public static string FileExtension => "vcf";
private readonly StreamWriter _writer;
public VCard(string filename) : this(filename, Utils.UTF8) { }
public VCard(string filename, Encoding encoding) {
_writer = new StreamWriter(filename, false, encoding);
}
public void Dispose() {
GC.SuppressFinalize(this);
_writer.Dispose();
}
public ValueTask DisposeAsync() {
GC.SuppressFinalize(this);
return _writer.DisposeAsync();
}
public async Task ExportAsync(IEnumerable<Member> data, IProgress<double>? progress = null) {
progress?.Report(0.0);
int count = data.Count() + 1, i = 0;
foreach (var row in data) {
var billingAddr = row.BillingAddress != null ? $"ADR;TYPE=work;LANGUAGE=de;LABEL=\"{Escape(row.BillingAddress.FullName)}\\n{Escape(row.BillingAddress.Address)}\\n{row.BillingAddress.PostalDest.AtPlz?.Plz} {Escape(row.BillingAddress.PostalDest.AtPlz?.Ort.Name)}\\nÖsterreich\":;;{Escape(row.BillingAddress.Address)};{Escape(row.BillingAddress.PostalDest.AtPlz?.Ort.Name)};;{row.BillingAddress.PostalDest.AtPlz?.Plz};Österreich\r\n" : null;
var tel = string.Join("", row.TelephoneNumbers
.Where(n => n.Type != "fax")
.Select(n => $"TEL;TYPE={(n.Type == "mobile" ? "cell" : "voice")}:{Escape(n.Number)}\r\n"));
var email = string.Join("", row.EmailAddresses.Select(a => $"EMAIL:{Escape(a.Address)}\r\n"));
await _writer.WriteLineAsync($"""
BEGIN:VCARD
VERSION:4.0
UID:mg{row.MgNr}@{App.Client.NameToken.ToLower()}.elwig.at
NOTE:MgNr. {row.MgNr}
FN:{Escape(row.AdministrativeName)}
N:{Escape(row.Name)};{Escape(row.GivenName)};{Escape(row.MiddleName)};{Escape(row.Prefix)};{Escape(row.Suffix)}
KIND:{(row.IsJuridicalPerson ? "org" : "individual")}
ADR{(billingAddr == null ? "" : ";TYPE=home")};LANGUAGE=de;LABEL="{Escape(row.Address)}\n{row.PostalDest.AtPlz?.Plz} {Escape(row.PostalDest.AtPlz?.Ort.Name)}\nÖsterreich":;;{Escape(row.Address)};{Escape(row.PostalDest.AtPlz?.Ort.Name)};;{row.PostalDest.AtPlz?.Plz};Österreich
{billingAddr}{tel}{email}REV:{row.ModifiedAt.ToUniversalTime():yyyyMMdd\THHmmss\Z}
END:VCARD
""");
progress?.Report(100.0 * ++i / count);
}
await _writer.FlushAsync();
progress?.Report(100.0);
}
public void Export(IEnumerable<Member> data, IProgress<double>? progress = null) {
ExportAsync(data, progress).GetAwaiter().GetResult();
}
private static string? Escape(string? text) {
return text?.Replace("\\", "\\\\").Replace(",", "\\,").Replace(";", "\\;").Replace("\n", "\\n").Replace("\r", "");
}
}
}

View File

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

View File

@@ -1,12 +1,17 @@
using Microsoft.Data.Sqlite;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.IO.Hashing;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace Elwig.Helpers {
static partial class Extensions {
public static partial class Extensions {
[LibraryImport("msvcrt.dll")]
[UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
@@ -95,5 +100,50 @@ namespace Elwig.Helpers {
await download.CopyToAsync(destination, 81920, relativeProgress, cancellationToken);
progress.Report(100.0);
}
public static async Task CheckIntegrity(this ZipArchive zip) {
var crc = new Crc32();
foreach (var entry in zip.Entries) {
crc.Reset();
using var stream = entry.Open();
await crc.AppendAsync(stream);
if (crc.GetCurrentHashAsUInt32() != entry.Crc32)
throw new InvalidDataException($"CRC-32 mismatch in '{entry.FullName}'");
}
}
public static async Task ExecuteBatch(this SqliteConnection cnx, string sql) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = sql;
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.NextResultAsync()) ;
}
public static async Task ExecuteEmbeddedScript(this SqliteConnection cnx, Assembly asm, string name) {
using var stream = asm.GetManifestResourceStream(name) ?? throw new FileNotFoundException("Unable to load embedded resource");
using var reader = new StreamReader(stream);
await ExecuteBatch(cnx, await reader.ReadToEndAsync());
}
public static async Task<object?> ExecuteScalar(this SqliteConnection cnx, string sql) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = sql;
return await cmd.ExecuteScalarAsync();
}
public static async Task<(string Table, long RowId, string Parent, long FkId)[]> ForeignKeyCheck(this SqliteConnection cnx) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = "PRAGMA foreign_key_check";
using var reader = await cmd.ExecuteReaderAsync();
var list = new List<(string, long, string, long)>();
while (await reader.ReadAsync()) {
var table = reader.GetString(0);
var rowid = reader.GetInt64(1);
var parent = reader.GetString(2);
var fkid = reader.GetInt64(3);
list.Add((table, rowid, parent, fkid));
}
return [.. list];
}
}
}

View File

@@ -10,7 +10,7 @@ namespace Elwig.Helpers.Printing {
public static async Task Init(Action? evtHandler = null) {
var e = new RazorLightEngineBuilder()
.UseFileSystemProject(App.DataPath + "resources")
.UseFileSystemProject(App.DocumentsPath)
.UseMemoryCachingProvider()
.Build();

View File

@@ -1,24 +1,28 @@
using System.Threading.Tasks;
using Elwig.Windows;
using System.Diagnostics;
using PdfiumViewer;
using System;
using System.IO;
using System.Collections.Generic;
using System.Windows;
using System.Text.RegularExpressions;
using System.Diagnostics;
using System.Drawing.Printing;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using PdfiumViewer;
using System.Drawing.Printing;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace Elwig.Helpers.Printing {
public static class Pdf {
private static readonly string WinziPrint = new string[] { App.ExePath }
.Union(Environment.GetEnvironmentVariable("PATH")?.Split(';') ?? [])
.Select(x => Path.Combine(x, "WinziPrint.exe"))
.Where(File.Exists)
.FirstOrDefault() ?? throw new FileNotFoundException("WiniPrint executable not found");
private static readonly string WinziPrint = (Assembly.GetEntryAssembly()?.Location.Contains(@"\bin\") ?? false) ?
Path.Combine(Assembly.GetEntryAssembly()!.Location.Split(@"\bin\")[0], "../Installer/Files/WinziPrint.exe") :
new string[] { App.InstallPath }
.Union(Environment.GetEnvironmentVariable("PATH")?.Split(';') ?? [])
.Select(x => Path.Combine(x, "WinziPrint.exe"))
.Where(File.Exists)
.FirstOrDefault() ?? throw new FileNotFoundException("WiniPrint executable not found");
private static Process? WinziPrintProc;
public static bool IsReady => WinziPrintProc != null;
@@ -46,24 +50,45 @@ namespace Elwig.Helpers.Printing {
return Task.CompletedTask;
}
public static async Task<(int Pages, IEnumerable<int> PerDoc)> Convert(string htmlPath, string pdfPath, bool doublePaged = false, IProgress<double>? progress = null) {
return await Convert([htmlPath], pdfPath, doublePaged, progress);
public static async Task<(int Pages, IEnumerable<int> PerDoc)> Convert(string htmlPath, string pdfPath, bool doublePaged = false, CancellationToken? cancelToken = null, IProgress<double>? progress = null) {
return await Convert([htmlPath], pdfPath, doublePaged, cancelToken, progress);
}
public static async Task<(int Pages, IEnumerable<int> PerDoc)> Convert(IEnumerable<string> htmlPath, string pdfPath, bool doublePaged = false, IProgress<double>? progress = null) {
public static async Task<(int Pages, IEnumerable<int> PerDoc)> Convert(IEnumerable<string> htmlPath, string pdfPath, bool doublePaged = false, CancellationToken? cancelToken = null, IProgress<double>? progress = null) {
if (WinziPrintProc == null) throw new InvalidOperationException("The WinziPrint process has not been initialized yet");
progress?.Report(0.0);
using var client = new TcpClient("127.0.0.1", 30983);
using var stream = client.GetStream();
string cnxId;
using var reader = new StreamReader(stream);
var first = await reader.ReadLineAsync() ?? throw new IOException("Invalid response from WinziPrint");
if (first.StartsWith("id:")) {
cnxId = first[3..].Trim();
} else {
throw new IOException("Invalid response from WinziPrint");
}
await stream.WriteAsync(Utils.UTF8.GetBytes(
"-e utf-8;-p;" + (doublePaged ? "-2;" : "") +
$"{string.Join(';', htmlPath)};{pdfPath}" +
"\r\n"));
using var reader = new StreamReader(stream);
bool cancelled = false;
while (true) {
if (!cancelled && (cancelToken?.IsCancellationRequested ?? false)) {
try {
using var cancelClient = new TcpClient("127.0.0.1", 30983);
using var cancelStream = cancelClient.GetStream();
using var cancelReader = new StreamReader(cancelStream);
await cancelReader.ReadLineAsync();
await cancelStream.WriteAsync(Utils.UTF8.GetBytes($"cancel;{cnxId}\r\n"));
} catch { }
cancelled = true;
}
var line = await reader.ReadLineAsync() ?? throw new IOException("Invalid response from WinziPrint");
if (line.StartsWith("error:")) {
throw new IOException($"WinziPrint: {line[6..].Trim()}");
var msg = line[6..].Trim();
if (msg == "aborted")
throw new OperationCanceledException("Dokumentenerzeugung abgebrochen!");
throw new IOException($"WinziPrint: {msg}");
} else if (line.StartsWith("progress:")) {
var parts = line[9..].Trim().Split('/').Select(int.Parse).ToArray();
progress?.Report(100.0 * parts[0] / parts[1]);

View File

@@ -0,0 +1,60 @@
using System;
using System.IO.Ports;
using System.Linq;
using System.Management;
namespace Elwig.Helpers {
public sealed class SerialPortWatcher : IDisposable {
private readonly ManagementEventWatcher _deviceArrivalWatcher;
private readonly ManagementEventWatcher _deviceRemovalWatcher;
private string[] _knownPorts;
public event EventHandler<string>? SerialPortConnected;
public event EventHandler<string>? SerialPortDisconnected;
public SerialPortWatcher() {
_knownPorts = SerialPort.GetPortNames();
_deviceArrivalWatcher = new ManagementEventWatcher("SELECT * FROM Win32_DeviceChangeEvent WHERE EventType = 2");
_deviceArrivalWatcher.EventArrived += OnDeviceArrived;
_deviceRemovalWatcher = new ManagementEventWatcher("SELECT * FROM Win32_DeviceChangeEvent WHERE EventType = 3");
_deviceRemovalWatcher.EventArrived += OnDeviceRemoved;
_deviceArrivalWatcher.Start();
_deviceRemovalWatcher.Start();
}
private void OnDeviceArrived(object sender, EventArrivedEventArgs evt) {
App.MainDispatcher.Invoke(() => {
// "synchronized"
string[] currentPorts = SerialPort.GetPortNames();
var newPorts = currentPorts.Except(_knownPorts).ToArray();
foreach (var port in newPorts)
SerialPortConnected?.Invoke(this, port);
_knownPorts = currentPorts;
});
}
private void OnDeviceRemoved(object sender, EventArrivedEventArgs evt) {
App.MainDispatcher.Invoke(() => {
// "synchronized"
string[] currentPorts = SerialPort.GetPortNames();
var removedPorts = _knownPorts.Except(currentPorts).ToArray();
foreach (var port in removedPorts)
SerialPortDisconnected?.Invoke(this, port);
_knownPorts = currentPorts;
});
}
public void Dispose() {
try {
_deviceArrivalWatcher?.Stop();
_deviceRemovalWatcher?.Stop();
} finally {
_deviceArrivalWatcher?.Dispose();
_deviceRemovalWatcher?.Dispose();
}
}
}
}

View File

@@ -1,41 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.IO.Ports;
using System.Net.Sockets;
using Elwig.Dialogs;
using System.Text;
using System.Numerics;
using Elwig.Models.Entities;
using Elwig.Documents;
using Elwig.Helpers.Billing;
using System.Runtime.InteropServices;
using System.Net.Http;
using System.Text.Json.Nodes;
using System.IO;
using Elwig.Models;
using Elwig.Models.Entities;
using LinqKit;
using MailKit.Net.Smtp;
using MailKit.Security;
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 MimeKit;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Linq.Expressions;
using System.Net.Http;
using System.Net.Sockets;
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Markup;
namespace Elwig.Helpers {
public static partial class Utils {
public static readonly Encoding UTF8 = new UTF8Encoding(false, true);
public static readonly Encoding UTF8BOM = new UTF8Encoding(true, true);
public static readonly JsonSerializerOptions JsonOpts = new() { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
public static int CurrentYear => DateTime.Now.Year;
public static int CurrentNextSeason => DateTime.Now.Year - (DateTime.Now.Month <= 3 ? 1 : 0);
@@ -430,12 +432,13 @@ namespace Elwig.Helpers {
var client = new HttpClient() {
Timeout = TimeSpan.FromSeconds(5),
};
client.DefaultRequestHeaders.UserAgent.Clear();
client.DefaultRequestHeaders.UserAgent.ParseAdd($"Elwig/{App.Version} ({App.Client.NameToken}, {App.BranchName}, {Environment.MachineName}, {Environment.OSVersion})");
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}")));
client.DefaultRequestHeaders.Authorization = new("Basic", Convert.ToBase64String(Utils.UTF8.GetBytes($"{username}:{password}")));
return client;
}
@@ -454,15 +457,19 @@ namespace Elwig.Helpers {
}
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));
using 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();
@@ -495,48 +502,67 @@ namespace Elwig.Helpers {
public static async Task<bool> SendEmail(Member member, string subject, string text, IEnumerable<Document> docs) {
if (App.Config.Smtp == null)
return false;
return await Task.Run(async () => {
await AddSentMailBody(subject, text, 1);
SmtpClient? client = null;
try {
client = await GetSmtpClient();
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"));
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);
await AddSentMails([(
"email", member.MgNr, member.AdministrativeName,
member.EmailAddresses.OrderBy(a => a.Nr).Select(a => a.Address).ToArray(),
subject, docs.Select(d => d.Title).ToArray()
)]);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
return false;
} finally {
if (client != null)
await client.DisconnectAsync(true);
client?.Dispose();
}
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;
return true;
});
}
public static async Task ExportDocument(Document doc, ExportMode mode, string? filename = null, (Member, string, string)? emailData = null) {
public static async Task ExportDocument(Document doc, ExportMode mode, string? filename = null, (Member Member, string Subject, string Text)? emailData = null) {
if (mode == ExportMode.Print && !App.Config.Debug) {
if (doc.IsPreview) {
MessageBox.Show("Dieses Dokument ist als vorläufig markiert und kann daher nicht ausgedruckt werden!",
"Vorläufiges Dokument", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
await doc.Generate();
await doc.Print();
} else if (mode == ExportMode.Email && emailData is (Member, string, string) e) {
if (doc.IsPreview) {
MessageBox.Show("Dieses Dokument ist als vorläufig markiert und kann daher nicht verschickt werden!",
"Vorläufiges Dokument", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
await doc.Generate();
var success = await SendEmail(e.Item1, e.Item2, e.Item3, [doc]);
var success = await SendEmail(e.Member, e.Subject, e.Text, [doc]);
if (success)
MessageBox.Show("Die E-Mail wurde erfolgreich verschickt!", "E-Mail verschickt",
MessageBox.Show("Die E-Mail wurde erfolgreich verschickt!\n\nEs kann einige Minuten dauern, bis die E-Mail im Posteingang des Empfängers aufscheint.", "E-Mail verschickt",
MessageBoxButton.OK, MessageBoxImage.Information);
} else if (mode == ExportMode.SavePdf) {
if (doc.IsPreview) {
MessageBox.Show("Dieses Dokument ist als vorläufig markiert und sollte daher nicht langfristig gespeichert werden!",
"Vorläufiges Dokument", MessageBoxButton.OK, MessageBoxImage.Warning);
}
var d = new SaveFileDialog() {
FileName = $"{NormalizeFileName(filename ?? doc.Title)}.pdf",
DefaultExt = "pdf",
@@ -559,9 +585,7 @@ namespace Elwig.Helpers {
Log = "Application",
Source = ".NET Runtime",
};
return log.Entries.Cast<EventLogEntry>()
.Where(e => e.Message.StartsWith("Application: Elwig.exe"))
.ToList();
return [.. log.Entries.OfType<EventLogEntry>().Where(e => e.InstanceId == 1026).Where(e => e.Message.StartsWith("Application: Elwig.exe"))];
}
public static int GetEntityIdetifierForPk(params object?[] primaryKey) {
@@ -594,5 +618,76 @@ namespace Elwig.Helpers {
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)[..19].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

@@ -628,5 +628,55 @@ namespace Elwig.Helpers {
return new(true, null);
}
public static ValidationResult CheckOrganicAuthorityCode(TextBox input, bool required) {
string text = "";
int pos = input.CaretIndex;
for (int i = 0, v = 0; i < input.Text.Length; i++) {
char ch = char.ToUpper(input.Text[i]);
if (v < 2 && char.IsAsciiLetter(ch)) {
v++;
text += ch;
} else if ((v == 2 || v == 6) && ch == '-') {
v++;
text += ch;
} else if (v >= 2 && char.IsLetterOrDigit(ch)) {
if (text.StartsWith("AT")) {
if (v == 3 && ch == 'B' || v == 4 && ch == 'I' || v == 5 && ch == 'O') {
v++;
text += ch;
} else if (v > 6 && char.IsAsciiDigit(ch)) {
v++;
text += ch;
}
} else {
v++;
text += ch;
}
}
if (i == input.CaretIndex - 1) {
pos = text.Length;
} else if (text.StartsWith("AT") && v >= 10) {
break;
}
}
input.Text = text;
input.CaretIndex = pos;
if (text.Length == 0)
return required ? new(false, "Bio-Kontrollstellen-Code ist nicht optional") : new(true, null);
if (text.StartsWith("AT")) {
if (text.Length != 10) {
return new(false, "Bio-Kontrollstellen-Code ist ungültig");
}
} else {
return new(false, "Not implemented yet");
}
return new(true, null);
}
}
}

View File

@@ -15,15 +15,17 @@ namespace Elwig.Helpers.Weighing {
public bool IsReady { get; private set; }
public bool HasFillingClearance { get; private set; }
public event IEventScale.EventHandler<WeighingEventArgs> WeighingEvent;
public event IEventScale.EventHandler<WeighingEventArgs>? WeighingEvent;
private bool IsRunning = true;
private readonly Thread BackgroundThread;
private readonly string Connection;
public AveryEventScale(string id, string model, string cnx, string? empty = null, string? filling = null, int? limit = null, string? log = null) :
base(cnx, empty, filling, limit, log) {
public AveryEventScale(string id, string model, string cnx, string? log = null, bool required = true) :
base(cnx, null, null, null, log, true, !required) {
ScaleId = id;
Model = model;
Connection = cnx;
IsReady = true;
HasFillingClearance = false;
Stream.WriteTimeout = -1;
@@ -50,19 +52,49 @@ namespace Elwig.Helpers.Weighing {
var data = await Receive();
if (data != null)
RaiseWeighingEvent(new WeighingEventArgs(data.Value));
} catch (ThreadInterruptedException) {
// ignore
} catch (IOException) {
await Task.Delay(500);
await Reconnect();
} catch (TimeoutException) {
await Task.Delay(500);
await Reconnect();
} 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} ({ex.GetType().Name})", "Waagenfehler",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
protected async Task Reconnect() {
try { Reader.Close(); } catch { }
try { Stream.Close(); } catch { }
try { Serial?.Close(); } catch { }
while (IsRunning) {
try {
if (Connection.StartsWith("serial:")) {
Serial = Utils.OpenSerialConnection(Connection);
Stream = Serial.BaseStream;
} else if (Connection.StartsWith("tcp:")) {
Tcp = Utils.OpenTcpConnection(Connection);
Stream = Tcp.GetStream();
}
Reader = new(Stream, Encoding.ASCII, false, 512);
break;
} catch {
// ignore
}
await Task.Delay(1000);
}
}
protected async Task<WeighingResult?> Receive() {
var line = "";
while (line.Length < 33) {
var ch = Reader.Read();
if (ch == -1) {
return null;
throw new IOException("Connection closed");
} else if (line.Length > 0 || ch == ' ') {
line += char.ToString((char)ch);
}
@@ -71,7 +103,7 @@ namespace Elwig.Helpers.Weighing {
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 FormatException($"Invalid event from scale: '{line}'");
}
var date = line[ 1.. 9];
@@ -81,7 +113,7 @@ namespace Elwig.Helpers.Weighing {
var unit = line[30..32];
if (unit != "kg") {
throw new IOException($"Unsupported unit in weighing event: '{unit}'");
throw new WeighingException($"Unsupported unit in weighing event: '{unit}'");
}
identNr = identNr.Length > 0 && identNr != "0" ? identNr : null;

View File

@@ -36,7 +36,7 @@ namespace Elwig.Helpers.Weighing {
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");
throw new FormatException("Invalid response from scale");
}
var status = line[1..3];
@@ -45,9 +45,9 @@ namespace Elwig.Helpers.Weighing {
switch (status[1]) {
case 'M': msg = "Waage in Bewegung"; break;
}
throw new IOException($"Waagenfehler {status}: {msg}");
throw new WeighingException($"Waagenfehler {status}: {msg}");
} else if (status[0] != ' ') {
throw new IOException($"Invalid response from scale (error code {status})");
throw new WeighingException($"Invalid response from scale (error code {status})");
}
return line[1..^1];
@@ -57,7 +57,7 @@ namespace Elwig.Helpers.Weighing {
await SendCommand(incIdentNr ? '\x05' : '?');
string record = await ReceiveResponse();
if (record.Length != 45)
throw new IOException("Invalid response from scale: Received record has invalid size");
throw new FormatException("Invalid response from scale: Received record has invalid size");
var line = record[2..];
var brutto = line[ 0.. 7].Trim();

View File

@@ -1,8 +1,9 @@
using System.IO.Ports;
using System.IO;
using System.Net.Sockets;
using System;
using System.IO;
using System.IO.Ports;
using System.Net.Sockets;
using System.Text;
using System.Windows;
namespace Elwig.Helpers.Weighing {
public abstract class Scale : IDisposable {
@@ -27,7 +28,7 @@ namespace Elwig.Helpers.Weighing {
if (config.Type == "SysTec-IT") {
return new SysTecITScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log);
} 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.Log, config.Required);
} else if (config.Type == "Gassner") {
return new GassnerScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log);
} else {
@@ -35,10 +36,17 @@ namespace Elwig.Helpers.Weighing {
}
}
protected Scale(string cnx, string? empty, string? filling, int? limit, string? log) {
protected Scale(string cnx, string? empty, string? filling, int? limit, string? log, bool softFail = false, bool failSilent = false) {
if (cnx.StartsWith("serial:")) {
Serial = Utils.OpenSerialConnection(cnx);
Stream = Serial.BaseStream;
try {
Serial = Utils.OpenSerialConnection(cnx);
} catch (Exception e) {
if (!softFail) throw;
if (!failSilent)
MessageBox.Show($"Verbindung zu Waage konnte nicht hergestellt werden:\n\n{e.Message}", "Waagenfehler",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
Stream = Serial?.BaseStream ?? Stream.Null;
} else if (cnx.StartsWith("tcp:")) {
Tcp = Utils.OpenTcpConnection(cnx);
Stream = Tcp.GetStream();

View File

@@ -34,14 +34,14 @@ namespace Elwig.Helpers.Weighing {
var line = await Reader.ReadUntilAsync("\r\n");
if (LogPath != null) await File.AppendAllTextAsync(LogPath, line);
if (line == null || line.Length < 4 || !line.StartsWith('<') || !line.EndsWith(">\r\n")) {
throw new IOException("Invalid response from scale");
throw new FormatException("Invalid response from scale");
}
var error = line[1..3];
string msg = $"Unbekannter Fehler (Fehler code {error})";
if (error[0] == '0') {
if (error[1] != '0') {
throw new IOException($"Invalid response from scale (error code {error})");
throw new WeighingException($"Invalid response from scale (error code {error})");
}
} else if (error[0] == '1') {
switch (error[1]) {
@@ -52,21 +52,21 @@ namespace Elwig.Helpers.Weighing {
case '6': msg = "Drucker nicht bereit"; break;
case '7': msg = "Druckmuster enthält ungültiges Kommando"; break;
}
throw new IOException($"Waagenfehler {error}: {msg}");
throw new WeighingException($"Waagenfehler {error}: {msg}");
} else if (error[0] == '2') {
switch (error[1]) {
case '0': msg = "Brutto negativ"; break;
}
throw new IOException($"Fehler {error}: {msg}");
throw new WeighingException($"Fehler {error}: {msg}");
} else if (error[0] == '3') {
switch (error[1]) {
case '1': msg = "Übertragunsfehler"; break;
case '2': msg = "Ungültiger Befehl"; break;
case '3': msg = "Ungültiger Parameter"; break;
}
throw new IOException($"Kommunikationsfehler {error}: {msg}");
throw new WeighingException($"Kommunikationsfehler {error}: {msg}");
} else {
throw new IOException($"Invalid response from scale (error code {error})");
throw new WeighingException($"Invalid response from scale (error code {error})");
}
return line[1..^3];
@@ -76,7 +76,7 @@ namespace Elwig.Helpers.Weighing {
await SendCommand(incIdentNr ? $"RN{InternalScaleNr}" : $"RM{InternalScaleNr}");
string record = await ReceiveResponse();
if (record.Length != 62)
throw new IOException("Invalid response from scale: Received record has invalid size");
throw new FormatException("Invalid response from scale: Received record has invalid size");
var line = record[2..];
var status = line[ 0.. 2];
@@ -94,9 +94,9 @@ namespace Elwig.Helpers.Weighing {
var crc16 = line[52..60].Trim();
if (Utils.CalcCrc16Modbus(record[..54]) != ushort.Parse(crc16)) {
throw new IOException($"Invalid response from scale: Invalid CRC16 checksum ({crc16} != {Utils.CalcCrc16Modbus(record[..54])})");
throw new WeighingException($"Invalid response from scale: Invalid CRC16 checksum ({crc16} != {Utils.CalcCrc16Modbus(record[..54])})");
} else if (unit != "kg") {
throw new IOException($"Unsupported unit in weighing response: '{unit}'");
throw new WeighingException($"Unsupported unit in weighing response: '{unit}'");
}
identNr = identNr.Length > 0 && identNr != "0" ? identNr : null;

View File

@@ -0,0 +1,6 @@
using System;
namespace Elwig.Helpers.Weighing {
public class WeighingException(string? message = null) : Exception(message) {
}
}

View File

@@ -45,7 +45,7 @@ namespace Elwig.Models.Dtos {
LEFT JOIN AT_plz_dest p ON p.id = m.postal_dest
LEFT JOIN AT_ort o ON o.okz = p.okz
JOIN v_under_delivery u ON (u.mgnr, u.bucket, u.year) = (m.mgnr, c.bucket, c.year)
WHERE c.year = {year} AND m.active = 1
WHERE c.year = {year}
ORDER BY m.mgnr, c.bucket
""").ToListAsync();
}

View File

@@ -27,14 +27,14 @@ namespace Elwig.Models.Dtos {
MgNr = mgnr;
}
public static async Task<IDictionary<int, CreditNoteDeliveryData>> ForPaymentVariant(DbSet<CreditNoteDeliveryRowSingle> table, DbSet<Season> seasons, int year, int avnr) {
var variant = (await seasons.FindAsync(year))?.PaymentVariants.Where(v => v.AvNr == avnr).SingleOrDefault();
public static async Task<IDictionary<int, CreditNoteDeliveryData>> ForPaymentVariant(DbSet<CreditNoteDeliveryRowSingle> table, DbSet<PaymentVar> paymentVariants, int year, int avnr) {
var variant = await paymentVariants.FindAsync(year, avnr);
BillingData? varData = null;
try { varData = variant != null ? BillingData.FromJson(variant.Data) : null; } catch { }
return (await FromDbSet(table, year, avnr))
.GroupBy(
r => new { r.Year, r.AvNr, r.MgNr, r.TgNr, r.DId, r.DPNr },
(k, g) => new CreditNoteDeliveryRow(g, seasons, varData?.NetWeightModifier ?? 0.0, varData?.GrossWeightModifier ?? 0.0))
(k, g) => new CreditNoteDeliveryRow(g, variant, varData))
.GroupBy(
r => new { r.Year, r.AvNr, r.MgNr, r.TgNr },
(k, g) => new CreditNoteDeliveryData(g, k.Year, k.TgNr, mgnr: k.MgNr))
@@ -86,13 +86,13 @@ namespace Elwig.Models.Dtos {
public decimal? Amount;
public double WeighingModifier;
public CreditNoteDeliveryRow(IEnumerable<CreditNoteDeliveryRowSingle> rows, DbSet<Season> seasons, double netWeightModifier, double grossWeightModifier) {
public CreditNoteDeliveryRow(IEnumerable<CreditNoteDeliveryRowSingle> rows, PaymentVar paymentVariant, BillingData? varData) {
var f = rows.First();
Year = f.Year;
TgNr = f.TgNr;
AvNr = f.AvNr;
MgNr = f.MgNr;
var season = seasons.Find(Year);
var season = paymentVariant.Season;
LsNr = f.LsNr;
DPNr = f.DPNr;
@@ -115,7 +115,7 @@ namespace Elwig.Models.Dtos {
b.Price != null ? season?.DecFromDb((long)b.Price) : null,
b.Amount != null ? season?.DecFromDb((long)b.Amount) : null))
.ToArray();
WeighingModifier = f.NetWeight ? netWeightModifier : grossWeightModifier;
WeighingModifier = (varData == null || !varData.ConsiderDelieryModifiers) ? 0 : f.NetWeight ? varData.NetWeightModifier : varData.GrossWeightModifier;
Amount = f.TotalAmount != null ? season?.DecFromDb((long)f.TotalAmount) : null;
var netAmount = f.NetAmount != null ? season?.DecFromDb((long)f.NetAmount) : null;
var amt = netAmount * (decimal)(1.0 + WeighingModifier);

View File

@@ -17,9 +17,9 @@ namespace Elwig.Models.Dtos {
("Name2", "Vorname", null, 40),
("DefaultKg", "Ort", null, 40),
("SortId", "Sorte", null, 10),
("Weight", "Gewicht", "kg", 20),
("CreatedTimestamp", "Angemeldet", null, 35),
("ModifiedTimestamp", "Geändert", null, 35),
("Weight", "Menge", "kg", 20),
("CreatedAt", "Angemeldet", null, 35),
("ModifiedAt", "Geändert", null, 35),
("Status", "Status", null, 20),
];
@@ -47,8 +47,8 @@ namespace Elwig.Models.Dtos {
public string? DefaultKg;
public string SortId;
public string Variety;
public DateTime CreatedTimestamp;
public DateTime? ModifiedTimestamp;
public DateTime CreatedAt;
public DateTime? ModifiedAt;
public int Weight;
public string? Status;
@@ -65,10 +65,10 @@ namespace Elwig.Models.Dtos {
DefaultKg = m.DefaultKg?.Name;
SortId = a.SortId;
Variety = a.Variety.Name;
CreatedTimestamp = a.CreatedTimestamp;
ModifiedTimestamp = a.ModifiedTimestamp == a.CreatedTimestamp ? null : a.ModifiedTimestamp;
CreatedAt = a.CreatedAt;
ModifiedAt = a.ModifiedAt == a.CreatedAt ? null : a.ModifiedAt;
Weight = a.Weight;
Status = s.AncmtTo == null ? null : a.CreatedTimestamp >= s.AncmtTo ? "verspät." : "ok";
Status = s.AncmtTo == null ? null : a.CreatedAt >= s.AncmtTo ? "verspät." : "ok";
}
}
}

View File

@@ -16,7 +16,7 @@ namespace Elwig.Models.Dtos {
("QualityLevel", "Qualitätsstufe", null, 25),
("Gradation", "Gradation", "°Oe|°KMW", 32),
("Buckets", "Flächenbindung", "|kg", 36),
("Weight", "Gewicht", "kg", 16),
("Weight", "Menge", "kg", 16),
];
private readonly int MgNr;

View File

@@ -24,7 +24,7 @@ namespace Elwig.Models.Dtos {
("CultId", "Bewirt.", null, 15),
("QualId", "Qualität", null, 15),
("Gradation", "Gradation", "°Oe|°KMW", 40),
("Weight", "Gewicht", "kg", 20),
("Weight", "Menge", "kg", 20),
("IsNetWeight", "Gerebelt", null, 20),
("HkId", "Herkunft", null, 20),
("Modifiers", "Zu-/Abschläge", null, 40),

View File

@@ -0,0 +1,105 @@
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;
namespace Elwig.Models.Dtos {
class MemberAreaComsData : DataTable<MemberAreaComsRow> {
private static readonly (string, string, string?, int)[] FieldNames = [
("MgNr", "MgNr.", null, 12),
("Name1", "Name", null, 40),
("Name2", "Vorname", null, 40),
("Address", "Adresse", null, 60),
("Plz", "PLZ", null, 10),
("Locality", "Ort", null, 60),
("SortIds", "Sorte", null, 12),
("AttrIds", "Attribut", null, 16),
("Areas", "Fläche", "m²", 22),
("DeliveryObligations", "Lieferpflicht", "kg", 22),
("DeliveryRights", "Lieferrecht", "kg", 22),
];
public MemberAreaComsData(IEnumerable<MemberAreaComsRow> rows, int year) :
base($"Flächenbindungen", $"Flächenbindungen pro Mitglied {year}", rows, FieldNames) {
}
public static async Task<MemberAreaComsData> ForSeason(DbSet<MemberAreaComsRowSingle> table, int year) {
return new MemberAreaComsData(
(await FromDbSet(table, year)).GroupBy(
r => r.MgNr,
(k, g) => new MemberAreaComsRow(g)
), year);
}
private static async Task<IEnumerable<MemberAreaComsRowSingle>> FromDbSet(DbSet<MemberAreaComsRowSingle> table, int year) {
return await table.FromSql($"""
SELECT m.mgnr, m.name AS name_1,
COALESCE(m.prefix || ' ', '') || m.given_name ||
COALESCE(' ' || m.middle_names, '') || COALESCE(' ' || m.suffix, '') AS name_2,
p.plz, o.name AS ort, m.address,
c.bucket, c.area, c.min_kg, c.max_kg
FROM v_area_commitment_bucket_strict c
LEFT JOIN member m ON m.mgnr = c.mgnr
LEFT JOIN AT_plz_dest p ON p.id = m.postal_dest
LEFT JOIN AT_ort o ON o.okz = p.okz
WHERE c.year = {year}
ORDER BY m.mgnr, c.bucket
""").ToListAsync();
}
}
public class MemberAreaComsRow {
public int MgNr;
public string Name1;
public string? Name2;
public string Address;
public int Plz;
public string Locality;
public string[] SortIds;
public string[] AttrIds;
public int[] Areas;
public int[] DeliveryObligations;
public int[] DeliveryRights;
public MemberAreaComsRow(IEnumerable<MemberAreaComsRowSingle> rows) {
var f = rows.First();
MgNr = f.MgNr;
Name1 = f.Name1;
Name2 = f.Name2;
Address = f.Address;
Plz = f.Plz;
Locality = f.Locality.Split(",")[0];
SortIds = rows.Select(r => r.VtrgId[..2]).ToArray();
AttrIds = rows.Select(r => r.VtrgId[2..]).ToArray();
Areas = rows.Select(r => r.Area).ToArray();
DeliveryObligations = rows.Select(r => r.MinKg).ToArray();
DeliveryRights = rows.Select(r => r.MaxKg).ToArray();
}
}
[Keyless]
public class MemberAreaComsRowSingle {
[Column("mgnr")]
public int MgNr { get; set; }
[Column("name_1")]
public required string Name1 { get; set; }
[Column("name_2")]
public string? Name2 { get; set; }
[Column("address")]
public required string Address { get; set; }
[Column("plz")]
public int Plz { get; set; }
[Column("ort")]
public required string Locality { get; set; }
[Column("bucket")]
public required string VtrgId { get; set; }
[Column("area")]
public int Area { get; set; }
[Column("min_kg")]
public int MinKg { get; set; }
[Column("max_kg")]
public int MaxKg { get; set; }
}
}

View File

@@ -0,0 +1,59 @@
using Elwig.Models.Entities;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Elwig.Models.Dtos {
public class MemberDeliveryData : DataTable<MemberDeliveryRow> {
private static readonly (string, string, string?, int?)[] FieldNames = [
("MgNr", "MgNr.", null, 12),
("Name1", "Name", null, 40),
("Name2", "Vorname", null, 40),
("Address", "Adresse", null, 60),
("Plz", "PLZ", null, 10),
("Locality", "Ort", null, 60),
("Weight", "Geliefert", "kg", 22),
];
public MemberDeliveryData(IEnumerable<MemberDeliveryRow> rows, List<string> filterNames) :
base("Liefermengen Gesamt", "Liefermengen pro Mitglied", string.Join(" / ", filterNames), rows, FieldNames) {
}
public static async Task<MemberDeliveryData> FromQuery(IQueryable<DeliveryPart> query, List<string> filterNames) {
return new((await query
.GroupBy(p => new {
p.Delivery.MgNr,
p.Delivery.Member.Name,
p.Delivery.Member.GivenName,
p.Delivery.Member.Address,
p.Delivery.Member.PostalDest.AtPlz!.Plz,
Ort = p.Delivery.Member.PostalDest.AtPlz!.Ort.Name,
})
.Select(g => new {
g.Key,
Weight = g.Sum(p => p.Weight),
}).ToListAsync())
.Select(g => new MemberDeliveryRow {
MgNr = g.Key.MgNr,
Name1 = g.Key.Name,
Name2 = g.Key.GivenName,
Address = g.Key.Address,
Plz = g.Key.Plz,
Locality = g.Key.Ort,
Weight = g.Weight,
}), filterNames);
}
}
public class MemberDeliveryRow {
public required int MgNr;
public required string Name1;
public required string? Name2;
public required string Address;
public required int Plz;
public required string Locality;
public required int Weight;
}
}

View File

@@ -0,0 +1,78 @@
using Elwig.Models.Entities;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Elwig.Models.Dtos {
public class MemberDeliveryPerVarietyData : DataTable<MemberDeliveryPerVarietyRow> {
private static readonly (string, string, string?, int?)[] FieldNames = [
("MgNr", "MgNr.", null, 12),
("Name1", "Name", null, 40),
("Name2", "Vorname", null, 40),
("Address", "Adresse", null, 60),
("Plz", "PLZ", null, 10),
("Locality", "Ort", null, 60),
("SortIds", "Sorte", null, 12),
("AttrIds", "Attribut", null, 16),
("CultIds", "Bewirt.", null, 16),
("Weights", "Geliefert", "kg", 22),
];
public MemberDeliveryPerVarietyData(IEnumerable<MemberDeliveryPerVarietyRow> rows, List<string> filterNames) :
base("Liefermengen pro Sorte", "Liefermengen pro Mitglied, Sorte, Attribut und Bewirtschaftungsart", string.Join(" / ", filterNames), rows, FieldNames) {
}
public static async Task<MemberDeliveryPerVarietyData> FromQuery(IQueryable<DeliveryPart> query, List<string> filterNames) {
return new((await query
.GroupBy(p => new {
p.Delivery.MgNr,
p.Delivery.Member.Name,
p.Delivery.Member.GivenName,
p.Delivery.Member.Address,
p.Delivery.Member.PostalDest.AtPlz!.Plz,
Ort = p.Delivery.Member.PostalDest.AtPlz!.Ort.Name,
p.SortId,
p.AttrId,
p.CultId,
})
.Select(g => new {
g.Key,
Weight = g.Sum(p => p.Weight),
})
.ToListAsync()).GroupBy(g => new {
g.Key.MgNr,
g.Key.Name,
g.Key.GivenName,
g.Key.Address,
g.Key.Plz,
g.Key.Ort,
}).Select(g => new MemberDeliveryPerVarietyRow {
MgNr = g.Key.MgNr,
Name1 = g.Key.Name,
Name2 = g.Key.GivenName,
Address = g.Key.Address,
Plz = g.Key.Plz,
Locality = g.Key.Ort,
SortIds = [.. g.Select(d => d.Key.SortId)],
AttrIds = [.. g.Select(d => d.Key.AttrId)],
CultIds = [.. g.Select(d => d.Key.CultId)],
Weights = [.. g.Select(d => d.Weight)],
}), filterNames);
}
}
public class MemberDeliveryPerVarietyRow {
public required int MgNr;
public required string Name1;
public required string? Name2;
public required string Address;
public required int Plz;
public required string Locality;
public required string[] SortIds;
public required string?[] AttrIds;
public required string?[] CultIds;
public required int[] Weights;
}
}

View File

@@ -1,12 +1,11 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;
namespace Elwig.Models.Dtos {
public class MemberDeliveryPerVariantData : DataTable<MemberDeliveryPerVariantRow> {
public class MemberDeliveryYieldsPerVarietyData : DataTable<MemberDeliveryYieldsPerVarietyRow> {
private static readonly (string, string, string?, int)[] FieldNames = [
("MgNr", "MgNr.", null, 12),
@@ -22,20 +21,19 @@ namespace Elwig.Models.Dtos {
("Yields", "Ertrag", "kg/ha", 22),
];
public MemberDeliveryPerVariantData(IEnumerable<MemberDeliveryPerVariantRow> rows, int year) :
public MemberDeliveryYieldsPerVarietyData(IEnumerable<MemberDeliveryYieldsPerVarietyRow> rows, int year) :
base($"Liefermengen", $"Liefermengen pro Mitglied, Sorte und Attribut {year}", rows, FieldNames) {
}
public static async Task<MemberDeliveryPerVariantData> ForSeason(DbSet<MemberDeliveryPerVariantRowSingle> table, int year) {
return new MemberDeliveryPerVariantData(
public static async Task<MemberDeliveryYieldsPerVarietyData> ForSeason(DbSet<MemberDeliveryPerVarietyRowSingle> table, int year) {
return new MemberDeliveryYieldsPerVarietyData(
(await FromDbSet(table, year)).GroupBy(
r => r.MgNr,
(k, g) => new MemberDeliveryPerVariantRow(g)
(k, g) => new MemberDeliveryYieldsPerVarietyRow(g)
), year);
}
private static async Task<IEnumerable<MemberDeliveryPerVariantRowSingle>> FromDbSet(DbSet<MemberDeliveryPerVariantRowSingle> table, int year) {
private static async Task<IEnumerable<MemberDeliveryPerVarietyRowSingle>> FromDbSet(DbSet<MemberDeliveryPerVarietyRowSingle> table, int year) {
return await table.FromSql($"""
SELECT m.mgnr, m.name AS name_1,
COALESCE(m.prefix || ' ', '') || m.given_name ||
@@ -69,7 +67,7 @@ namespace Elwig.Models.Dtos {
}
}
public class MemberDeliveryPerVariantRow {
public class MemberDeliveryYieldsPerVarietyRow {
public int MgNr;
public string Name1;
public string? Name2;
@@ -82,7 +80,7 @@ namespace Elwig.Models.Dtos {
public int[] Weights;
public int?[] Yields => Weights.Zip(Areas).Select(i => i.Second > 0 ? (int?)i.First * 10_000 / i.Second : null).ToArray();
public MemberDeliveryPerVariantRow(IEnumerable<MemberDeliveryPerVariantRowSingle> rows) {
public MemberDeliveryYieldsPerVarietyRow(IEnumerable<MemberDeliveryPerVarietyRowSingle> rows) {
var f = rows.First();
MgNr = f.MgNr;
Name1 = f.Name1;
@@ -98,7 +96,7 @@ namespace Elwig.Models.Dtos {
}
[Keyless]
public class MemberDeliveryPerVariantRowSingle {
public class MemberDeliveryPerVarietyRowSingle {
[Column("mgnr")]
public int MgNr { get; set; }
[Column("name_1")]

View File

@@ -33,14 +33,13 @@ namespace Elwig.Models.Dtos {
m.business_shares * s.min_kg_per_bs AS min_kg,
m.business_shares * s.max_kg_per_bs AS max_kg,
COALESCE(SUM(d.weight), 0) AS sum
FROM v_delivery d
LEFT JOIN member m ON m.mgnr = d.mgnr
FROM season s, member m
LEFT JOIN AT_plz_dest p ON p.id = m.postal_dest
LEFT JOIN AT_ort o ON o.okz = p.okz
LEFT JOIN season s ON s.year = d.year
LEFT JOIN v_delivery d ON (d.year, d.mgnr) = (s.year, m.mgnr)
WHERE s.year = {year} AND (m.active = TRUE OR d.weight > 0)
GROUP BY d.year, m.mgnr
ORDER BY 100.0 * sum / max_kg, m.mgnr;
GROUP BY s.year, m.mgnr
ORDER BY 100.0 * sum / max_kg, m.mgnr
""").ToListAsync();
return new OverUnderDeliveryData(rows, year);
}

View File

@@ -18,8 +18,9 @@ namespace Elwig.Models.Dtos {
("Cultivation", "Bewirt.", null, 20),
("QualityLevel", "Qualitätsstufe", null, 30),
("Oe", "Gradation", "°Oe", 20),
("Ungeb", "ungebunden", "kg|€/kg", 40),
("Geb", "gebunden", "kg|€/kg", 40),
("Ungeb", "ungebunden", "kg|€/kg|€/kg", 60),
("LowGeb", "attributlos gebunden", "kg|€/kg|€/kg", 60),
("Geb", "gebunden", "kg|€/kg|€/kg", 60),
("Amount", "Gesamt", "€", 25),
];
@@ -30,8 +31,9 @@ namespace Elwig.Models.Dtos {
string? Cultivation,
string QualityLevel,
double Oe,
(int Weight, decimal? Price) Ungeb,
(int Weight, decimal? Price) Geb,
(int Weight, decimal? MinPrice, decimal? MaxPrice) Ungeb,
(int Weight, decimal? MinPrice, decimal? MaxPrice) LowGeb,
(int Weight, decimal? MinPrice, decimal? MaxPrice) Geb,
decimal Amount
);
@@ -42,8 +44,9 @@ namespace Elwig.Models.Dtos {
public static async Task<PaymentVariantSummaryData> ForPaymentVariant(PaymentVar v, DbSet<PaymentVariantSummaryRow> table) {
return new(v, (await FromDbSet(table, v.Year, v.AvNr))
.Select(r => new PaymentRow(r.Type, r.Variety, r.Attribute, r.Cultivation, r.QualityLevel, r.Oe,
(r.WeightUngeb, r.PriceUngeb != null ? Utils.DecFromDb(r.PriceUngeb.Value, v.Season.Precision) : null),
(r.WeightGeb, r.PriceGeb != null ? Utils.DecFromDb(r.PriceGeb.Value, v.Season.Precision) : null),
(r.WeightUngeb, r.MinPriceUngeb != null ? Utils.DecFromDb(r.MinPriceUngeb.Value, v.Season.Precision) : null, r.MaxPriceUngeb != null ? Utils.DecFromDb(r.MaxPriceUngeb.Value, v.Season.Precision) : null),
(r.WeightLowGeb, r.MinPriceLowGeb != null ? Utils.DecFromDb(r.MinPriceLowGeb.Value, v.Season.Precision) : null, r.MaxPriceLowGeb != null ? Utils.DecFromDb(r.MaxPriceLowGeb.Value, v.Season.Precision) : null),
(r.WeightGeb, r.MinPriceGeb != null ? Utils.DecFromDb(r.MinPriceGeb.Value, v.Season.Precision) : null, r.MaxPriceGeb != null ? Utils.DecFromDb(r.MaxPriceGeb.Value, v.Season.Precision) : null),
Utils.DecFromDb(r.Amount, v.Season.Precision)))
.ToArray());
}
@@ -57,19 +60,23 @@ namespace Elwig.Models.Dtos {
q.name AS quality_level,
ROUND(kmw * (4.54 + 0.022 * kmw)) AS oe,
SUM(IIF(w.discr = '_', w.value, 0)) AS weight_ungeb,
MAX(IIF(w.discr = '_', b.price, NULL)) AS price_ungeb,
SUM(IIF(w.discr != '_', w.value, 0)) AS weight_geb,
MAX(IIF(w.discr != '_', b.price, NULL)) AS price_geb,
MIN(IIF(w.discr = '_', b.price, NULL)) AS min_price_ungeb,
MAX(IIF(w.discr = '_', b.price, NULL)) AS max_price_ungeb,
SUM(IIF(w.discr NOT IN (COALESCE(p.attrid, ''), '_'), w.value, 0)) AS weight_lowgeb,
MIN(IIF(w.discr NOT IN (COALESCE(p.attrid, ''), '_'), b.price, NULL)) AS min_price_lowgeb,
MAX(IIF(w.discr NOT IN (COALESCE(p.attrid, ''), '_'), b.price, NULL)) AS max_price_lowgeb,
SUM(IIF(w.discr = COALESCE(p.attrid, ''), w.value, 0)) AS weight_geb,
MIN(IIF(w.discr = COALESCE(p.attrid, ''), b.price, NULL)) AS min_price_geb,
MAX(IIF(w.discr = COALESCE(p.attrid, ''), b.price, NULL)) AS max_price_geb,
SUM(b.amount) AS amount
FROM payment_delivery_part_bucket b
LEFT JOIN delivery_part_bucket w ON (w.year, w.did, w.dpnr, w.bktnr) = (b.year, b.did, b.dpnr, b.bktnr)
LEFT JOIN delivery_part p ON (p.year, p.did, p.dpnr) = (b.year, b.did, b.dpnr)
LEFT JOIN delivery d ON (d.year, d.did) = (p.year, p.did)
LEFT JOIN wine_variety v ON v.sortid = p.sortid
LEFT JOIN wine_attribute a ON a.attrid = p.attrid
LEFT JOIN wine_cultivation c ON c.cultid = p.cultid
LEFT JOIN wine_quality_level q ON q.qualid = p.qualid
WHERE d.year = {year} AND b.avnr = {avnr}
WHERE p.year = {year} AND b.avnr = {avnr}
GROUP BY variety, attribute, cultivation, q.min_kmw, oe
ORDER BY variety, attribute, cultivation, q.min_kmw, oe
""").ToListAsync();
@@ -92,12 +99,22 @@ namespace Elwig.Models.Dtos {
public double Oe { get; set; }
[Column("weight_ungeb")]
public int WeightUngeb { get; set; }
[Column("price_ungeb")]
public long? PriceUngeb { get; set; }
[Column("min_price_ungeb")]
public long? MinPriceUngeb { get; set; }
[Column("max_price_ungeb")]
public long? MaxPriceUngeb { get; set; }
[Column("weight_lowgeb")]
public int WeightLowGeb { get; set; }
[Column("min_price_lowgeb")]
public long? MinPriceLowGeb { get; set; }
[Column("max_price_lowgeb")]
public long? MaxPriceLowGeb { get; set; }
[Column("weight_geb")]
public int WeightGeb { get; set; }
[Column("price_geb")]
public long? PriceGeb { get; set; }
[Column("min_price_geb")]
public long? MinPriceGeb { get; set; }
[Column("max_price_geb")]
public long? MaxPriceGeb { get; set; }
[Column("amount")]
public long Amount { get; set; }
}

View File

@@ -15,7 +15,7 @@ namespace Elwig.Models.Dtos {
public static IEnumerable<Transaction> FromPaymentVariant(PaymentVar variant) {
return variant.Credits
.Where(c => c.Member.Iban != null)
.Where(c => c.Member.Iban != null && c.Amount > 0)
.OrderBy(c => c.TgNr)
.Select(c => new Transaction(c))
.ToList();

View File

@@ -14,7 +14,7 @@ namespace Elwig.Models.Dtos {
("CultId", "Bewirt.", null, 15),
("QualId", "Qual.", null, 15),
("Geb", "gebunden", null, 20),
("Weight", "Gewicht", "kg", 20),
("Weight", "Menge", "kg", 20),
];
public WeightBreakdownData(IEnumerable<WeightBreakdownRow> rows, int year, string name) :

View File

@@ -0,0 +1,60 @@
using Elwig.Helpers;
using Elwig.Models.Entities;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using static Elwig.Models.Dtos.WineLocalityStatisticsData;
namespace Elwig.Models.Dtos {
class WineLocalityStatisticsData : DataTable<StatisticsRow> {
private static readonly (string, string, string?, int?)[] FieldNames = [
("Branch", "Zwst.", null, 30),
("KgNr", "KgNr.", null, 15),
("Name", "Katastralgemeinde", null, 50),
("Members", "Mitgl.", "#", 15),
("Deliveries", "Lfrg.", "#", 15),
("Parts", "Teill.", "#", 15),
("Weight", "Menge", "kg", 20),
("Gradation", "Gradation", "°Oe|°KMW", 30),
];
public record struct StatisticsRow(
string Branch,
int? KgNr,
string? Name,
int Members,
int Deliveries,
int Parts,
int Weight,
(double Oe, double Kmw) Gradation
);
public WineLocalityStatisticsData(IEnumerable<StatisticsRow> rows, List<string> filterNames) :
base("Lieferstatistik pro Ort", "Lieferstatistik pro Ort", string.Join(" / ", filterNames), rows, FieldNames) {
}
public static async Task<WineLocalityStatisticsData> FromQuery(IQueryable<DeliveryPart> query, List<string> filterNames) {
return new((await query
.GroupBy(p => new {
Branch = p.Delivery.Branch.Name,
p.Kg!.KgNr,
Kg = p.Kg!.AtKg.Name,
}, (k, g) => new {
k.Branch,
KgNr = (int?)k.KgNr,
Kg = (string?)k.Kg,
Members = g.Select(p => p.Delivery.Member).Distinct().Count(),
Deliveries = g.Select(p => p.Delivery).Distinct().Count(),
Parts = g.Count(),
Weight = g.Sum(p => p.Weight),
Kmw = g.Sum(p => p.Kmw * p.Weight) / g.Sum(p => p.Weight),
})
.OrderByDescending(g => g.Weight)
.ThenBy(g => g.KgNr)
.ToListAsync()).Select(g => new StatisticsRow(g.Branch, g.KgNr, g.Kg, g.Members, g.Deliveries, g.Parts, g.Weight, (Utils.KmwToOe(g.Kmw), Math.Round(g.Kmw, 1)))), filterNames);
}
}
}

View File

@@ -41,14 +41,36 @@ namespace Elwig.Models.Entities {
public string? Comment { get; set; }
[Column("ctime"), DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public long CTime { get; private set; }
public long CTime { get; set; }
[NotMapped]
public DateTime CreatedTimestamp => DateTimeOffset.FromUnixTimeSeconds(CTime).LocalDateTime;
public DateTime CreatedAt {
get => DateTimeOffset.FromUnixTimeSeconds(CTime).LocalDateTime;
set => CTime = ((DateTimeOffset)value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("mtime"), DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public long MTime { get; private set; }
public long MTime { get; set; }
[NotMapped]
public DateTime ModifiedTimestamp => DateTimeOffset.FromUnixTimeSeconds(MTime).LocalDateTime;
public DateTime ModifiedAt {
get => DateTimeOffset.FromUnixTimeSeconds(MTime).LocalDateTime;
set => MTime = ((DateTimeOffset)value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("xtime")]
public long? XTime { get; set; }
[NotMapped]
public DateTime? ExportedAt {
get => XTime == null ? null : DateTimeOffset.FromUnixTimeSeconds(XTime.Value).LocalDateTime;
set => XTime = value == null ? null : ((DateTimeOffset)value.Value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("itime")]
public long? ITime { get; set; }
[NotMapped]
public DateTime? ImportedAt {
get => ITime == null ? null : DateTimeOffset.FromUnixTimeSeconds(ITime.Value).LocalDateTime;
set => ITime = value == null ? null : ((DateTimeOffset)value.Value.ToUniversalTime()).ToUnixTimeSeconds();
}
[ForeignKey("MgNr")]
public virtual Member Member { get; private set; } = null!;

View File

@@ -84,14 +84,36 @@ namespace Elwig.Models.Entities {
}
[Column("ctime"), DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public long CTime { get; private set; }
public long CTime { get; set; }
[NotMapped]
public DateTime CreatedTimestamp => DateTimeOffset.FromUnixTimeSeconds(CTime).LocalDateTime;
public DateTime CreatedAt {
get => DateTimeOffset.FromUnixTimeSeconds(CTime).LocalDateTime;
set => CTime = ((DateTimeOffset)value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("mtime"), DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public long MTime { get; private set; }
public long MTime { get; set; }
[NotMapped]
public DateTime ModifiedTimestamp => DateTimeOffset.FromUnixTimeSeconds(MTime).LocalDateTime;
public DateTime ModifiedAt {
get => DateTimeOffset.FromUnixTimeSeconds(MTime).LocalDateTime;
set => MTime = ((DateTimeOffset)value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("xtime")]
public long? XTime { get; set; }
[NotMapped]
public DateTime? ExportedAt {
get => XTime == null ? null : DateTimeOffset.FromUnixTimeSeconds(XTime.Value).LocalDateTime;
set => XTime = value == null ? null : ((DateTimeOffset)value.Value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("itime")]
public long? ITime { get; set; }
[NotMapped]
public DateTime? ImportedAt {
get => ITime == null ? null : DateTimeOffset.FromUnixTimeSeconds(ITime.Value).LocalDateTime;
set => ITime = value == null ? null : ((DateTimeOffset)value.Value.ToUniversalTime()).ToUnixTimeSeconds();
}
[ForeignKey("Year, AvNr, MgNr")]
public virtual PaymentMember Payment { get; private set; } = null!;

View File

@@ -1,9 +1,11 @@
using Elwig.Helpers;
using Elwig.Helpers.Billing;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Windows.Media;
using IndexAttribute = Microsoft.EntityFrameworkCore.IndexAttribute;
namespace Elwig.Models.Entities {
@@ -61,15 +63,42 @@ namespace Elwig.Models.Entities {
[Column("comment")]
public string? Comment { get; set; }
[Column("ctime"), DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public long CTime { get; private set; }
[NotMapped]
public DateTime CreatedTimestamp => DateTimeOffset.FromUnixTimeSeconds(CTime).LocalDateTime;
public string[] Comments => [.. Parts.Select(p => p.Comment).Prepend(Comment).Where(c => c != null).Cast<string>()];
[NotMapped]
public string CommentsString => string.Join(" / ", Comments);
[Column("ctime"), DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public long CTime { get; set; }
[NotMapped]
public DateTime CreatedAt {
get => DateTimeOffset.FromUnixTimeSeconds(CTime).LocalDateTime;
set => CTime = ((DateTimeOffset)value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("mtime"), DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public long MTime { get; private set; }
public long MTime { get; set; }
[NotMapped]
public DateTime ModifiedTimestamp => DateTimeOffset.FromUnixTimeSeconds(MTime).LocalDateTime;
public DateTime ModifiedAt {
get => DateTimeOffset.FromUnixTimeSeconds(MTime).LocalDateTime;
set => MTime = ((DateTimeOffset)value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("xtime")]
public long? XTime { get; set; }
[NotMapped]
public DateTime? ExportedAt {
get => XTime == null ? null : DateTimeOffset.FromUnixTimeSeconds(XTime.Value).LocalDateTime;
set => XTime = value == null ? null : ((DateTimeOffset)value.Value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("itime")]
public long? ITime { get; set; }
[NotMapped]
public DateTime? ImportedAt {
get => ITime == null ? null : DateTimeOffset.FromUnixTimeSeconds(ITime.Value).LocalDateTime;
set => ITime = value == null ? null : ((DateTimeOffset)value.Value.ToUniversalTime()).ToUnixTimeSeconds();
}
[ForeignKey("Year")]
public virtual Season Season { get; private set; } = null!;
@@ -85,16 +114,19 @@ namespace Elwig.Models.Entities {
public int Weight => Parts.Select(p => p.Weight).Sum();
public int FilteredWeight => FilteredParts.Select(p => p.Weight).Sum();
public IEnumerable<string> SortIds => Parts
.GroupBy(p => p.SortId)
public IEnumerable<RawVaribute> Vaributes => Parts
.GroupBy(p => (p.SortId, p.AttrId, p.CultId))
.OrderByDescending(g => g.Select(p => p.Weight).Sum())
.Select(g => g.Key);
public IEnumerable<string> FilteredSortIds => FilteredParts
.GroupBy(p => p.SortId)
.Select(g => new RawVaribute(g.Key.SortId, g.Key.AttrId, g.Key.CultId));
public IEnumerable<RawVaribute> FilteredVaributes => FilteredParts
.GroupBy(p => (p.SortId, p.AttrId, p.CultId))
.OrderByDescending(g => g.Select(p => p.Weight).Sum())
.Select(g => g.Key);
public string SortIdString => string.Join(", ", SortIds);
public string FilteredSortIdString => string.Join(", ", FilteredSortIds);
.Select(g => new RawVaribute(g.Key.SortId, g.Key.AttrId, g.Key.CultId));
public string VaributeString => string.Join(", ", Vaributes);
public string FilteredVaributeString => string.Join(", ", FilteredVaributes);
public Brush? Color => Parts.Select(p => p.Variety.Color).Distinct().SingleOrDefault();
public Brush? FilteredColor => FilteredParts.Select(p => p.Variety.Color).Distinct().SingleOrDefault();
public IEnumerable<string> Modifiers => Parts
.SelectMany(p => p.Modifiers)

View File

@@ -27,14 +27,36 @@ namespace Elwig.Models.Entities {
public required string Type { get; set; }
[Column("ctime"), DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public long CTime { get; private set; }
public long CTime { get; set; }
[NotMapped]
public DateTime CreatedTimestamp => DateTimeOffset.FromUnixTimeSeconds(CTime).LocalDateTime;
public DateTime CreatedAt {
get => DateTimeOffset.FromUnixTimeSeconds(CTime).LocalDateTime;
set => CTime = ((DateTimeOffset)value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("mtime"), DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public long MTime { get; private set; }
public long MTime { get; set; }
[NotMapped]
public DateTime ModifiedTimestamp => DateTimeOffset.FromUnixTimeSeconds(MTime).LocalDateTime;
public DateTime ModifiedAt {
get => DateTimeOffset.FromUnixTimeSeconds(MTime).LocalDateTime;
set => MTime = ((DateTimeOffset)value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("xtime")]
public long? XTime { get; set; }
[NotMapped]
public DateTime? ExportedAt {
get => XTime == null ? null : DateTimeOffset.FromUnixTimeSeconds(XTime.Value).LocalDateTime;
set => XTime = value == null ? null : ((DateTimeOffset)value.Value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("itime")]
public long? ITime { get; set; }
[NotMapped]
public DateTime? ImportedAt {
get => ITime == null ? null : DateTimeOffset.FromUnixTimeSeconds(ITime.Value).LocalDateTime;
set => ITime = value == null ? null : ((DateTimeOffset)value.Value.ToUniversalTime()).ToUnixTimeSeconds();
}
[ForeignKey("Year, DsNr")]
public virtual DeliverySchedule Schedule { get; private set; } = null!;

View File

@@ -9,6 +9,11 @@ using System.Text.Json.Nodes;
namespace Elwig.Models.Entities {
[Table("delivery_part"), PrimaryKey("Year", "DId", "DPNr")]
public class DeliveryPart : IDelivery {
public const string Dumper = "dumper";
public const string Pumped = "pumped";
public const string Box = "box";
[Column("year")]
public int Year { get; set; }
@@ -86,12 +91,27 @@ namespace Elwig.Models.Entities {
[Column("hand_picked")]
public bool? IsHandPicked { get; set; }
[Column("lesewagen")]
public bool? IsLesewagen { get; set; }
[Column("gebunden")]
public bool? IsGebunden { get; set; }
[Column("unloading")]
public string? Unloading { get; set; }
[NotMapped]
public bool IsDumper {
get => Unloading == Dumper;
set => Unloading = value ? Dumper : Unloading;
}
[NotMapped]
public bool IsPumped {
get => Unloading == Pumped;
set => Unloading = value ? Pumped : Unloading;
}
[NotMapped]
public bool IsBox {
get => Unloading == Box;
set => Unloading = value ? Box : Unloading;
}
[Column("temperature")]
public double? Temperature { get; set; }
@@ -129,14 +149,36 @@ namespace Elwig.Models.Entities {
public string? Comment { get; set; }
[Column("ctime"), DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public long CTime { get; private set; }
public long CTime { get; set; }
[NotMapped]
public DateTime CreatedTimestamp => DateTimeOffset.FromUnixTimeSeconds(CTime).LocalDateTime;
public DateTime CreatedAt {
get => DateTimeOffset.FromUnixTimeSeconds(CTime).LocalDateTime;
set => CTime = ((DateTimeOffset)value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("mtime"), DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public long MTime { get; private set; }
public long MTime { get; set; }
[NotMapped]
public DateTime ModifiedTimestamp => DateTimeOffset.FromUnixTimeSeconds(MTime).LocalDateTime;
public DateTime ModifiedAt {
get => DateTimeOffset.FromUnixTimeSeconds(MTime).LocalDateTime;
set => MTime = ((DateTimeOffset)value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("xtime")]
public long? XTime { get; set; }
[NotMapped]
public DateTime? ExportedAt {
get => XTime == null ? null : DateTimeOffset.FromUnixTimeSeconds(XTime.Value).LocalDateTime;
set => XTime = value == null ? null : ((DateTimeOffset)value.Value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("itime")]
public long? ITime { get; set; }
[NotMapped]
public DateTime? ImportedAt {
get => ITime == null ? null : DateTimeOffset.FromUnixTimeSeconds(ITime.Value).LocalDateTime;
set => ITime = value == null ? null : ((DateTimeOffset)value.Value.ToUniversalTime()).ToUnixTimeSeconds();
}
[InverseProperty(nameof(DeliveryPartModifier.Part))]
public virtual ICollection<DeliveryPartModifier> PartModifiers { get; private set; } = null!;

View File

@@ -42,7 +42,9 @@ namespace Elwig.Models.Entities {
[Column("max_weight")]
public int? MaxWeight { get; set; }
[NotMapped]
public int AnnouncedWeight => Announcements.Sum(a => a.Weight);
public int AnnouncedWeight => AnnouncedWeightOverride ?? Announcements.Sum(a => a.Weight);
[NotMapped]
public int? AnnouncedWeightOverride { get; set; }
[NotMapped]
public double? Percent => (double)AnnouncedWeight / MaxWeight * 100;

View File

@@ -145,14 +145,36 @@ namespace Elwig.Models.Entities {
public AT_Kg? DefaultKg => DefaultWbKg?.AtKg;
[Column("ctime"), DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public long CTime { get; private set; }
public long CTime { get; set; }
[NotMapped]
public DateTime CreatedTimestamp => DateTimeOffset.FromUnixTimeSeconds(CTime).LocalDateTime;
public DateTime CreatedAt {
get => DateTimeOffset.FromUnixTimeSeconds(CTime).LocalDateTime;
set => CTime = ((DateTimeOffset)value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("mtime"), DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public long MTime { get; private set; }
public long MTime { get; set; }
[NotMapped]
public DateTime ModifiedTimestamp => DateTimeOffset.FromUnixTimeSeconds(MTime).LocalDateTime;
public DateTime ModifiedAt {
get => DateTimeOffset.FromUnixTimeSeconds(MTime).LocalDateTime;
set => MTime = ((DateTimeOffset)value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("xtime")]
public long? XTime { get; set; }
[NotMapped]
public DateTime? ExportedAt {
get => XTime == null ? null : DateTimeOffset.FromUnixTimeSeconds(XTime.Value).LocalDateTime;
set => XTime = value == null ? null : ((DateTimeOffset)value.Value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("itime")]
public long? ITime { get; set; }
[NotMapped]
public DateTime? ImportedAt {
get => ITime == null ? null : DateTimeOffset.FromUnixTimeSeconds(ITime.Value).LocalDateTime;
set => ITime = value == null ? null : ((DateTimeOffset)value.Value.ToUniversalTime()).ToUnixTimeSeconds();
}
[ForeignKey("ZwstId")]
public virtual Branch? Branch { get; private set; }

View File

@@ -16,6 +16,9 @@ namespace Elwig.Models.Entities {
[Column("active")]
public bool IsActive { get; set; }
[Column("redacted")]
public bool IsRedacted { get; set; }
[Column("ordering")]
public int Ordering { get; set; }
@@ -46,6 +49,8 @@ namespace Elwig.Models.Entities {
(Rel != null) ? $"{Utils.GetSign(Rel.Value)}{(Math.Abs(Rel.Value) < 0.1m ? "\u2007" : "")}{Math.Abs(Rel.Value):0.00##\u00a0%}" :
"";
public string? PublicValueStr => IsRedacted ? null : ValueStr;
public override string ToString() {
return Name;
}

View File

@@ -46,14 +46,36 @@ namespace Elwig.Models.Entities {
public required string Data { get; set; }
[Column("ctime"), DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public long CTime { get; private set; }
public long CTime { get; set; }
[NotMapped]
public DateTime CreatedTimestamp => DateTimeOffset.FromUnixTimeSeconds(CTime).LocalDateTime;
public DateTime CreatedAt {
get => DateTimeOffset.FromUnixTimeSeconds(CTime).LocalDateTime;
set => CTime = ((DateTimeOffset)value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("mtime"), DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public long MTime { get; private set; }
public long MTime { get; set; }
[NotMapped]
public DateTime ModifiedTimestamp => DateTimeOffset.FromUnixTimeSeconds(MTime).LocalDateTime;
public DateTime ModifiedAt {
get => DateTimeOffset.FromUnixTimeSeconds(MTime).LocalDateTime;
set => MTime = ((DateTimeOffset)value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("xtime")]
public long? XTime { get; set; }
[NotMapped]
public DateTime? ExportedAt {
get => XTime == null ? null : DateTimeOffset.FromUnixTimeSeconds(XTime.Value).LocalDateTime;
set => XTime = value == null ? null : ((DateTimeOffset)value.Value.ToUniversalTime()).ToUnixTimeSeconds();
}
[Column("itime")]
public long? ITime { get; set; }
[NotMapped]
public DateTime? ImportedAt {
get => ITime == null ? null : DateTimeOffset.FromUnixTimeSeconds(ITime.Value).LocalDateTime;
set => ITime = value == null ? null : ((DateTimeOffset)value.Value.ToUniversalTime()).ToUnixTimeSeconds();
}
[ForeignKey("Year")]
public virtual Season Season { get; private set; } = null!;

View File

@@ -6,10 +6,10 @@ namespace Elwig.Models.Entities {
[Table("wb_gl"), PrimaryKey("GlNr")]
public class WbGl {
[Column("glnr")]
public int GlNr { get; private set; }
public int GlNr { get; set; }
[Column("name")]
public string Name { get; private set; } = null!;
public string Name { get; set; } = null!;
[InverseProperty(nameof(WbKg.Gl))]
public virtual ICollection<WbKg> Kgs { get; private set; } = null!;

View File

@@ -15,7 +15,7 @@ namespace Elwig.Models.Entities {
public virtual AT_Kg AtKg { get; private set; } = null!;
[ForeignKey("GlNr")]
public virtual WbGl Gl { get; private set; } = null!;
public virtual WbGl? Gl { get; private set; } = null!;
[InverseProperty(nameof(WbRd.Kg))]
public virtual ICollection<WbRd> Rds { get; private set; } = null!;

View File

@@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;
using System.Windows.Media;
namespace Elwig.Models.Entities {
[Table("wine_variety"), PrimaryKey("SortId")]
@@ -10,22 +11,32 @@ namespace Elwig.Models.Entities {
[Column("type")]
public string Type { get; private set; } = null!;
[Column("max_qualid")]
public string MaxQualId { get; private set; } = null!;
[ForeignKey("MaxQualId")]
public virtual WineQualLevel MaxQualityLevel { get; private set; } = null!;
[Column("name")]
public string Name { get; private set; } = null!;
[Column("comment")]
public string? Comment { get; private set; }
public string SortIdFormat => IsQuw ? SortId : $"({SortId})";
public string CommentFormat => (Comment != null) ? $" ({Comment})" : "";
public bool IsRed => Type == "R";
public bool IsWhite => Type == "W";
public bool IsQuw => MaxQualId == "QUW";
public Brush? Color => IsWhite ? Brushes.DarkGreen : IsRed ? Brushes.DarkRed : null;
public WineVar() { }
public WineVar(string sortId, string name) {
SortId = sortId;
Name = name;
MaxQualId = "QUW";
}
public override string ToString() {

View File

@@ -9,7 +9,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<PublishDir>bin\Publish</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
<TargetFramework>net8.0-windows</TargetFramework>
<TargetFramework>net10.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile>

View File

@@ -0,0 +1,33 @@
-- schema version 31 to 32
INSERT INTO client_parameter (param, value) VALUES ('ENABLE_TIME_TRIGGERS', '1');
ALTER TABLE member ADD COLUMN xtime INTEGER DEFAULT NULL;
ALTER TABLE member ADD COLUMN itime INTEGER DEFAULT NULL;
ALTER TABLE area_commitment ADD COLUMN xtime INTEGER DEFAULT NULL;
ALTER TABLE area_commitment ADD COLUMN itime INTEGER DEFAULT NULL;
ALTER TABLE delivery_announcement ADD COLUMN xtime INTEGER DEFAULT NULL;
ALTER TABLE delivery_announcement ADD COLUMN itime INTEGER DEFAULT NULL;
ALTER TABLE delivery ADD COLUMN xtime INTEGER DEFAULT NULL;
ALTER TABLE delivery ADD COLUMN itime INTEGER DEFAULT NULL;
ALTER TABLE delivery_part ADD COLUMN xtime INTEGER DEFAULT NULL;
ALTER TABLE delivery_part ADD COLUMN itime INTEGER DEFAULT NULL;
ALTER TABLE payment_variant ADD COLUMN xtime INTEGER DEFAULT NULL;
ALTER TABLE payment_variant ADD COLUMN itime INTEGER DEFAULT NULL;
ALTER TABLE credit ADD COLUMN xtime INTEGER DEFAULT NULL;
ALTER TABLE credit ADD COLUMN itime INTEGER DEFAULT NULL;
PRAGMA writable_schema = ON;
UPDATE sqlite_schema SET sql = REPLACE(REPLACE(sql,
' WHEN',
' WHEN (SELECT value FROM client_parameter WHERE param = ''ENABLE_TIME_TRIGGERS'') = 1 AND'),
'FOR EACH ROW' || char(10) ||
'BEGIN',
'FOR EACH ROW' || char(10) ||
' WHEN (SELECT value FROM client_parameter WHERE param = ''ENABLE_TIME_TRIGGERS'') = 1' || char(10) ||
'BEGIN')
WHERE type = 'trigger' AND name LIKE '%time%';
PRAGMA writable_schema = OFF;
PRAGMA schema_version = 3101;

View File

@@ -0,0 +1,17 @@
-- schema version 32 to 33
ALTER TABLE wine_variety ADD COLUMN max_qualid TEXT NOT NULL DEFAULT 'QUW';
UPDATE wine_quality_level SET qualid = 'ALW' WHERE qualid = 'AUL';
UPDATE wine_variety SET comment = 'Muscato' WHERE sortid = 'MO';
INSERT INTO wine_variety (sortid, type, max_qualid, name, comment) VALUES
('DR', 'W', 'QUW', 'Donauriesling', NULL),
('DV', 'W', 'QUW', 'Donauveltliner', NULL),
('BN', 'W', 'RSW', 'Bronner', NULL),
('CB', 'W', 'RSW', 'Cabernet Blanc', NULL),
('CJ', 'R', 'RSW', 'Cabernet Jura', NULL),
('JO', 'W', 'RSW', 'Johanniter', NULL),
('OR', 'W', 'RSW', 'Orangetraube', NULL),
('PI', 'R', 'RSW', 'Pinot Nova', NULL),
('RE', 'R', 'RSW', 'Regent', NULL),
('SI', 'W', 'RSW', 'Solaris', NULL);

View File

@@ -0,0 +1,3 @@
-- schema version 33 to 34
ALTER TABLE modifier ADD COLUMN redacted INTEGER NOT NULL CHECK (redacted IN (TRUE, FALSE)) DEFAULT FALSE;

View File

@@ -0,0 +1,6 @@
-- schema version 34 to 35
ALTER TABLE delivery_part ADD COLUMN unloading TEXT DEFAULT NULL;
UPDATE delivery_part SET unloading = 'pumped' WHERE lesewagen;
UPDATE delivery_part SET unloading = 'box' WHERE (SELECT zwstid IN ('H','S') FROM delivery d WHERE (d.year, d.did) = (delivery_part.year, delivery_part.did));
ALTER TABLE delivery_part DROP COLUMN lesewagen;

View File

@@ -0,0 +1,19 @@
-- schema version 35 to 36
CREATE VIEW v_member AS
SELECT m.mgnr, m.name,
(COALESCE(m.prefix || ' ', '') || m.given_name || COALESCE(' ' || m.middle_names, '') || COALESCE(' ' || m.suffix, '')) AS other_names,
m.address, p.plz, o.name AS locality,
a.name AS billing_name, a.address AS billing_address, p2.plz AS billing_plz, o2.name AS billing_locality,
k.name AS default_kg,
GROUP_CONCAT(e.address, ', ') AS email_addresses
FROM member m
LEFT JOIN AT_plz_dest p ON p.id = m.postal_dest
LEFT JOIN AT_ort o ON o.okz = p.okz
LEFT JOIN member_billing_address a ON a.mgnr = m.mgnr
LEFT JOIN AT_plz_dest p2 ON p2.id = a.postal_dest
LEFT JOIN AT_ort o2 ON o2.okz = p2.okz
LEFT JOIN AT_kg k ON k.kgnr = m.default_kgnr
LEFT JOIN member_email_address e ON e.mgnr = m.mgnr
GROUP BY m.mgnr
ORDER BY m.mgnr;

View File

@@ -48,6 +48,7 @@ namespace Elwig.Services {
var filterNotVar = new List<string>();
var filterAttr = new List<string>();
var filterNotAttr = new List<string>();
var filterSeasons = new List<int>();
var filter = vm.TextFilter;
if (filter.Count > 0) {
@@ -87,6 +88,10 @@ namespace Elwig.Services {
filter.RemoveAt(i--);
filterNames.Add($"ohne {var[e[1..3].ToUpper()].Name}");
filterNames.Add($"ohne Attribut {attrId[e[3..].ToUpper()].Name}");
} else if (e.Length == 4 && int.TryParse(e, out var year)) {
filterSeasons.Add(year);
filter.RemoveAt(i--);
filterNames.Add($"laufend {e}");
}
}
@@ -94,18 +99,20 @@ namespace Elwig.Services {
if (filterNotVar.Count > 0) areaComQuery = areaComQuery.Where(a => !filterNotVar.Contains(a.AreaComType.WineVar.SortId));
if (filterAttr.Count > 0) areaComQuery = areaComQuery.Where(a => a.AreaComType.WineAttr!.AttrId != null && filterAttr.Contains(a.AreaComType.WineAttr.AttrId));
if (filterNotAttr.Count > 0) areaComQuery = areaComQuery.Where(a => a.AreaComType.WineAttr!.AttrId == null || !filterNotAttr.Contains(a.AreaComType.WineAttr.AttrId));
foreach (var year in filterSeasons) areaComQuery = Utils.ActiveAreaCommitments(areaComQuery, year);
}
return (filterNames, areaComQuery, filter);
}
public static async Task<int> UpdateAreaCommitment(this AreaComAdminViewModel vm, int? oldFbNr) {
int newFbNr = (int)vm.FbNr!;
int newFbNr = vm.FbNr!.Value;
using (var ctx = new AppDbContext()) {
return await Task.Run(async () => {
using var ctx = new AppDbContext();
var a = new AreaCom {
FbNr = oldFbNr ?? newFbNr,
MgNr = (int)vm.MgNr!,
MgNr = vm.MgNr!.Value,
YearFrom = vm.YearFrom,
YearTo = vm.YearTo,
VtrgId = vm.AreaComType!.VtrgId,
@@ -113,8 +120,8 @@ namespace Elwig.Services {
Comment = string.IsNullOrEmpty(vm.Comment) ? null : vm.Comment,
KgNr = vm.Kg!.KgNr,
RdNr = vm.Rd?.RdNr,
GstNr = vm.GstNr!.Trim(),
Area = (int)vm.Area!,
GstNr = vm.GstNr?.Trim() ?? "-",
Area = vm.Area!.Value,
};
if (vm.Rd?.RdNr == 0) {
@@ -134,11 +141,9 @@ namespace Elwig.Services {
if (newFbNr != a.FbNr) {
await ctx.Database.ExecuteSqlAsync($"UPDATE area_commitment SET fbnr = {newFbNr} WHERE fbnr = {oldFbNr}");
}
}
App.HintContextChange();
return newFbNr;
return newFbNr;
});
}
private static void AddToolTipCell(Grid grid, string text, int row, int col, int colSpan = 1, bool bold = false, bool alignRight = false, bool alignCenter = false) {
@@ -247,5 +252,14 @@ namespace Elwig.Services {
}
return grid;
}
public static async Task DeleteAreaCom(int fbnr) {
await Task.Run(async () => {
using var ctx = new AppDbContext();
var l = (await ctx.AreaCommitments.FindAsync(fbnr))!;
ctx.Remove(l);
await ctx.SaveChangesAsync();
});
}
}
}

View File

@@ -37,8 +37,8 @@ namespace Elwig.Services {
vm.DeliverySchedule = (DeliverySchedule?)ControlUtils.GetItemFromSourceWithPk(vm.DeliveryScheduleSource, a.Year, a.DsNr);
vm.SortId = a.SortId;
vm.Weight = a.Weight;
vm.StatusAncmtCreated = $"{a.CreatedTimestamp:dd.MM.yyyy, HH:mm} ({a.Type})";
vm.StatusAncmtModified = a.ModifiedTimestamp != a.CreatedTimestamp ? $"{a.ModifiedTimestamp:dd.MM.yyyy, HH:mm}" : "-";
vm.StatusAncmtCreated = $"{a.CreatedAt:dd.MM.yyyy, HH:mm} ({a.Type})";
vm.StatusAncmtModified = a.ModifiedAt != a.CreatedAt ? $"{a.ModifiedAt:dd.MM.yyyy, HH:mm}" : "-";
}
public static async Task<(List<string>, IQueryable<DeliveryAncmt>, List<string>)> GetFilters(this DeliveryAncmtAdminViewModel vm, AppDbContext ctx) {
@@ -211,7 +211,8 @@ namespace Elwig.Services {
int newMgNr = vm.MgNr!.Value;
string newSortId = vm.SortId!;
using (var ctx = new AppDbContext()) {
return await Task.Run(async () => {
using var ctx = new AppDbContext();
var a = new DeliveryAncmt {
Year = oldYear ?? year,
DsNr = oldDsNr ?? dsnr,
@@ -232,11 +233,9 @@ namespace Elwig.Services {
if (oldDsNr != null && (oldYear != year || oldDsNr != dsnr || oldMgNr != newMgNr || oldSortId != newSortId)) {
await ctx.Database.ExecuteSqlAsync($"UPDATE delivery_announcement SET year = {year}, dsnr = {dsnr}, mgnr = {newMgNr}, sortid = {newSortId} WHERE (year, dsnr, mgnr, sortid) = ({a.Year}, {a.DsNr}, {a.MgNr}, {a.SortId})");
}
}
App.HintContextChange();
return (year, dsnr, newMgNr, newSortId);
return (year, dsnr, newMgNr, newSortId);
});
}
public static async Task GenerateDeliveryAncmtList(this DeliveryAncmtAdminViewModel vm, ExportSubject subject, ExportMode mode) {
@@ -269,25 +268,29 @@ namespace Elwig.Services {
Title = $"{DeliveryAncmtList.Name} speichern unter - Elwig"
};
if (d.ShowDialog() == true) {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
var data = await DeliveryAncmtListData.FromQuery(query, filterNames);
using var ods = new OdsFile(d.FileName);
await ods.AddTable(data);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
var data = await DeliveryAncmtListData.FromQuery(query, filterNames);
using var ods = new OdsFile(d.FileName);
await ods.AddTable(data);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
} else {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
var data = await DeliveryAncmtListData.FromQuery(query, filterNames);
using var doc = new DeliveryAncmtList(string.Join(" / ", filterNames), data);
await Utils.ExportDocument(doc, mode);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
var data = await DeliveryAncmtListData.FromQuery(query, filterNames);
using var doc = new DeliveryAncmtList(string.Join(" / ", filterNames), data);
await Utils.ExportDocument(doc, mode);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
}
@@ -326,7 +329,7 @@ namespace Elwig.Services {
var weight = await deliveryAncmts.SumAsync(p => p.Weight);
text = $"{weight:N0} kg";
AddToolTipRow(grid, 0, "Gewicht", null, weight, null, weight);
AddToolTipRow(grid, 0, "Menge", null, weight, null, weight);
if (await deliveryAncmts.AnyAsync()) {
var attrGroups = await deliveryAncmts

View File

@@ -152,7 +152,8 @@ namespace Elwig.Services {
public static async Task UpdateDeliverySchedule(this DeliveryScheduleAdminViewModel vm, int? oldYear, int? oldDsNr) {
int year = vm.Date!.Value.Year;
using (var ctx = new AppDbContext()) {
await Task.Run(async () => {
using var ctx = new AppDbContext();
var s = new DeliverySchedule {
Year = oldYear ?? year,
DsNr = oldDsNr ?? await ctx.NextDsNr(year),
@@ -181,9 +182,7 @@ namespace Elwig.Services {
.ToList(), vm.MainVarieties.Select(v => (v, 1)).Union(vm.OtherVarieties.Select(v => (v, 2))).ToList());
await ctx.SaveChangesAsync();
}
App.HintContextChange();
});
}
}
}

View File

@@ -15,10 +15,8 @@ using LinqKit;
using System.Globalization;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using System.IO;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using System.Windows.Controls;
using System.Net.Http;
namespace Elwig.Services {
public static class DeliveryService {
@@ -80,9 +78,9 @@ namespace Elwig.Services {
vm.PartComment = p.Comment ?? "";
vm.TemperatureString = (p.Temperature != null) ? $"{p.Temperature:N1}" : "";
vm.AcidString = (p.Acid != null) ? $"{p.Acid:N1}" : "";
vm.IsLesewagen = p.IsLesewagen ?? false;
vm.IsHandPicked = p.IsHandPicked;
vm.IsGebunden = p.IsGebunden;
vm.Unloading = p.Unloading;
vm.ScaleId = p.ScaleId;
vm.WeighingData = p.WeighingData;
@@ -188,14 +186,54 @@ namespace Elwig.Services {
prd = prd.And(p => p.IsNetWeight == true);
filter.RemoveAt(i--);
filterNames.Add("gerebelt gewogen");
} else if (e.Length >= 5 && e.Length <= 11 && "planenwagen".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
prd = prd.And(p => p.Unloading == DeliveryPart.Dumper);
filter.RemoveAt(i--);
filterNames.Add("Planenw./Kipper");
} else if (e.Length >= 6 && e.Length <= 12 && "!planenwagen".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
prd = prd.And(p => p.Unloading != DeliveryPart.Dumper);
filter.RemoveAt(i--);
filterNames.Add("kein Planenw./Kipper");
} else if (e.Length >= 4 && e.Length <= 6 && "kipper".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
prd = prd.And(p => p.Unloading == DeliveryPart.Dumper);
filter.RemoveAt(i--);
filterNames.Add("Planenw./Kipper");
} else if (e.Length >= 5 && e.Length <= 7 && "!kipper".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
prd = prd.And(p => p.Unloading != DeliveryPart.Dumper);
filter.RemoveAt(i--);
filterNames.Add("kein Planenw./Kipper");
} else if (e.Length >= 5 && e.Length <= 9 && "lesewagen".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
prd = prd.And(p => p.IsLesewagen == true);
prd = prd.And(p => p.Unloading == DeliveryPart.Pumped);
filter.RemoveAt(i--);
filterNames.Add("Lesewagen");
} else if (e.Length >= 6 && e.Length <= 10 && "!lesewagen".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
prd = prd.And(p => p.IsLesewagen == false);
prd = prd.And(p => p.Unloading != DeliveryPart.Pumped);
filter.RemoveAt(i--);
filterNames.Add("kein Lesewagen");
} else if (e.Length >= 5 && e.Length <= 6 && "kisten".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
prd = prd.And(p => p.Unloading == DeliveryPart.Box);
filter.RemoveAt(i--);
filterNames.Add("Kisten");
} else if (e.Length >= 6 && e.Length <= 7 && "!kisten".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
prd = prd.And(p => p.Unloading != DeliveryPart.Box);
filter.RemoveAt(i--);
filterNames.Add("keine Kisten");
} else if ("upload".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
prd = prd.And(p => (p.Delivery.XTime == null || p.Delivery.MTime > p.Delivery.XTime) && (p.Delivery.ITime == null || p.Delivery.MTime > p.Delivery.ITime));
filter.RemoveAt(i--);
filterNames.Add("geändert seit letztem Export");
} else if ("!upload".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
prd = prd.And(p => !((p.Delivery.XTime == null || p.Delivery.MTime > p.Delivery.XTime) && (p.Delivery.ITime == null || p.Delivery.MTime > p.Delivery.ITime)));
filter.RemoveAt(i--);
filterNames.Add("unverändert seit letztem Export");
} else if (">import".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
prd = prd.And(p => p.ITime != null && p.Delivery.MTime > p.Delivery.ITime);
filter.RemoveAt(i--);
filterNames.Add("geändert seit letztem Import");
} else if ("<import".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
prd = prd.And(p => p.Delivery.MTime <= p.Delivery.ITime);
filter.RemoveAt(i--);
filterNames.Add("unverändert seit letztem Import");
} else if (e.Length == 2 && var.ContainsKey(e.ToUpper())) {
filterVar.Add(e.ToUpper());
filter.RemoveAt(i--);
@@ -430,9 +468,10 @@ namespace Elwig.Services {
}
public static async Task<DeliveryPart> UpdateDeliveryPart(this DeliveryAdminViewModel vm, int? oldYear, int? oldDid, int? oldDpnr, bool dateHasChanged, bool timeHasChanged, bool timeIsDefault) {
DeliveryPart p;
return await Task.Run(async () => {
DeliveryPart p;
using (var ctx = new AppDbContext()) {
using var ctx = new AppDbContext();
int year = oldYear ?? Utils.CurrentYear;
int did = oldDid ?? await ctx.NextDId(year);
int dpnr = oldDpnr ?? await ctx.NextDPNr(year, did);
@@ -482,8 +521,8 @@ namespace Elwig.Services {
IsNetWeight = vm.IsNetWeight,
IsHandPicked = vm.IsHandPicked,
IsLesewagen = vm.IsLesewagen,
IsGebunden = vm.IsGebunden,
Unloading = vm.Unloading,
Temperature = vm.Temperature,
Acid = vm.Acid,
Comment = string.IsNullOrEmpty(vm.PartComment) ? null : vm.PartComment,
@@ -524,17 +563,16 @@ namespace Elwig.Services {
}
await ctx.SaveChangesAsync();
}
App.HintContextChange();
return p;
return p;
});
}
public static async Task<Delivery> SplitDeliveryToMember(int year, int did, int[] weights, int mgnr) {
Delivery n;
return await Task.Run(async () => {
Delivery n;
using (var ctx = new AppDbContext()) {
using var ctx = new AppDbContext();
bool anyLeft = false;
var d = (await ctx.Deliveries.FindAsync(year, did))!;
var lnr = await ctx.NextLNr(d.Date, d.ZwstId);
@@ -571,23 +609,28 @@ namespace Elwig.Services {
s.DPNr = dpnr++;
s.Weight = w;
ctx.Add(s);
ctx.AddRange(p.PartModifiers.Select(m => new DeliveryPartModifier() {
Year = s.Year,
DId = s.DId,
DPNr = s.DPNr,
ModId = m.ModId,
}));
}
}
await ctx.SaveChangesAsync();
if (!anyLeft)
await ctx.Database.ExecuteSqlAsync($"DELETE FROM delivery WHERE (year, did) = ({d.Year}, {d.DId})");
}
App.HintContextChange();
return n;
return n;
});
}
public static async Task<Delivery> SplitDeliveryToLsNr(int year, int did, int[] weights, string lsnr) {
Delivery n;
using (var ctx = new AppDbContext()) {
return await Task.Run(async () => {
Delivery n;
using var ctx = new AppDbContext();
var anyLeft = false;
n = (await ctx.Deliveries.FirstAsync(d => d.LsNr == lsnr))!;
var d = (await ctx.Deliveries.FindAsync(year, did))!;
@@ -610,21 +653,27 @@ namespace Elwig.Services {
s.DPNr = dpnr++;
s.Weight = w;
ctx.Add(s);
ctx.AddRange(p.PartModifiers.Select(m => new DeliveryPartModifier() {
Year = s.Year,
DId = s.DId,
DPNr = s.DPNr,
ModId = m.ModId,
}));
}
}
await ctx.SaveChangesAsync();
if (!anyLeft && n.LsNr != d.LsNr)
await ctx.Database.ExecuteSqlAsync($"DELETE FROM delivery WHERE (year, did) = ({d.Year}, {d.DId})");
}
App.HintContextChange();
return n;
return n;
});
}
public static async Task DepreciateDelivery(int year, int did, int[] weights) {
using (var ctx = new AppDbContext()) {
await Task.Run(async () => {
using var ctx = new AppDbContext();
var d = (await ctx.Deliveries.FindAsync(year, did))!;
var dpnr = await ctx.NextDPNr(year, did);
foreach (var (p, w) in d.Parts.ToList().Zip(weights)) {
@@ -645,24 +694,31 @@ namespace Elwig.Services {
n.QualId = "WEI";
n.HkId = "OEST";
ctx.Add(n);
ctx.AddRange(p.PartModifiers.Select(m => new DeliveryPartModifier() {
Year = n.Year,
DId = n.DId,
DPNr = n.DPNr,
ModId = m.ModId,
}));
}
}
await ctx.SaveChangesAsync();
}
App.HintContextChange();
});
}
public static async Task GenerateDeliveryNote(int year, int did, ExportMode mode) {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
using var ctx = new AppDbContext();
var d = (await ctx.Deliveries.FindAsync(year, did))!;
using var doc = new DeliveryNote(d, ctx);
await Utils.ExportDocument(doc, mode, d.LsNr, (d.Member, $"{DeliveryNote.Name} Nr. {d.LsNr}", $"Im Anhang finden Sie den {DeliveryNote.Name} Nr. {d.LsNr}"));
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
using var ctx = new AppDbContext();
var d = (await ctx.Deliveries.FindAsync(year, did))!;
using var doc = new DeliveryNote(d, ctx);
await Utils.ExportDocument(doc, mode, d.LsNr, (d.Member, $"{DeliveryNote.Name} Nr. {d.LsNr}", $"Im Anhang finden Sie den {DeliveryNote.Name} Nr. {d.LsNr}"));
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
@@ -707,14 +763,16 @@ namespace Elwig.Services {
Title = $"{DeliveryJournal.Name} speichern unter - Elwig"
};
if (d.ShowDialog() == true) {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
var data = await DeliveryJournalData.FromQuery(query, filterNames);
using var ods = new OdsFile(d.FileName);
await ods.AddTable(data);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
var data = await DeliveryJournalData.FromQuery(query, filterNames);
using var ods = new OdsFile(d.FileName);
await ods.AddTable(data);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
} else if (mode == ExportMode.Export) {
@@ -722,61 +780,53 @@ namespace Elwig.Services {
FileName = subject == ExportSubject.Selected ? $"Lieferung_{vm.SelectedDelivery?.LsNr}.elwig.zip" : $"Lieferungen_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip",
DefaultExt = "elwig.zip",
Filter = "Elwig-Export-Datei (*.elwig.zip)|*.elwig.zip",
Title = $"{DeliveryJournal.Name} speichern unter - Elwig"
Title = $"{DeliveryJournal.Name} speichern unter - Elwig",
AddExtension = false,
};
if (d.ShowDialog() == true) {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
await ElwigData.Export(d.FileName, await query
.Select(p => p.Delivery)
.Distinct()
.Include(d => d.Parts)
.ThenInclude(p => p.PartModifiers)
.AsSplitQuery()
.ToListAsync(), filterNames);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
if (!d.FileName.EndsWith(".elwig.zip")) d.FileName += ".elwig.zip";
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
var list = await query
.Select(p => p.Delivery)
.Distinct()
.Include(d => d.Parts).ThenInclude(p => p.PartModifiers)
.Include(d => d.Parts).ThenInclude(p => p.Rd)
.Include(d => d.Parts).ThenInclude(p => p.Kg!.Gl)
.AsSplitQuery()
.ToListAsync();
var wbKgs = list
.SelectMany(d => d.Parts)
.Where(p => p.Kg != null)
.Select(p => p.Kg!)
.DistinctBy(k => k.KgNr)
.OrderBy(k => k.KgNr)
.ToList();
await ElwigData.Export(d.FileName, list, wbKgs, filterNames);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
} else if (mode == ExportMode.Upload && App.Config.SyncUrl != null) {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
var filename = $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip";
var path = Path.Combine(App.TempPath, filename);
var list = await query
.Select(p => p.Delivery)
.Distinct()
.Include(d => d.Parts)
.ThenInclude(p => p.PartModifiers)
.AsSplitQuery()
.ToListAsync();
if (list.Count == 0) {
MessageBox.Show("Es wurden keine Lieferungen zum Hochladen ausgewählt!", "Lieferungen hochladen",
MessageBoxButton.OK, MessageBoxImage.Error);
} else {
await ElwigData.Export(path, list, filterNames);
await Utils.UploadExportData(path, App.Config.SyncUrl, App.Config.SyncUsername, App.Config.SyncPassword);
MessageBox.Show($"Hochladen von {list.Count:N0} Lieferungen erfolgreich!", "Lieferungen hochgeladen",
MessageBoxButton.OK, MessageBoxImage.Information);
}
} catch (HttpRequestException exc) {
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
} catch (TaskCanceledException exc) {
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
await SyncService.Upload(App.Config.SyncUrl, App.Config.SyncUrl, App.Config.SyncPassword, query, filterNames);
});
Mouse.OverrideCursor = null;
} else {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
var data = await DeliveryJournalData.FromQuery(query, filterNames);
using var doc = new DeliveryJournal(string.Join(" / ", filterNames), data);
await Utils.ExportDocument(doc, mode);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
var data = await DeliveryJournalData.FromQuery(query, filterNames);
using var doc = new DeliveryJournal(string.Join(" / ", filterNames), data);
await Utils.ExportDocument(doc, mode);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
}
@@ -798,17 +848,53 @@ namespace Elwig.Services {
throw new ArgumentException("Invalid value for ExportSubject");
}
Mouse.OverrideCursor = Cursors.AppStarting;
try {
var data = await WineQualityStatisticsData.FromQuery(query, App.Client.OrderingMemberList);
using var doc = new WineQualityStatistics(string.Join(" / ", filterNames), data);
await Utils.ExportDocument(doc, mode);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
var data = await WineQualityStatisticsData.FromQuery(query, App.Client.OrderingMemberList);
using var doc = new WineQualityStatistics(string.Join(" / ", filterNames), data);
await Utils.ExportDocument(doc, mode);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
public static async Task GenerateLocalityStatistics(this DeliveryAdminViewModel vm, ExportSubject subject) {
using var ctx = new AppDbContext();
IQueryable<DeliveryPart> query;
List<string> filterNames = [];
if (subject == ExportSubject.FromFilters) {
var (f, _, q, _, _) = await vm.GetFilters(ctx);
query = q;
filterNames.AddRange(f);
} else {
throw new ArgumentException("Invalid value for ExportSubject");
}
var d = new SaveFileDialog() {
FileName = $"Lieferstatistik-{vm.FilterSeason ?? Utils.CurrentLastSeason}.ods",
DefaultExt = "ods",
Filter = "OpenDocument Format Spreadsheet (*.ods)|*.ods",
Title = $"Lieferstatistik pro Ort speichern unter - Elwig"
};
if (d.ShowDialog() == true) {
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
using var ods = new OdsFile(d.FileName);
var tbl = await WineLocalityStatisticsData.FromQuery(query, filterNames);
await ods.AddTable(tbl);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
}
public static async Task GenerateDeliveryDepreciationList(this DeliveryAdminViewModel vm, ExportSubject subject, ExportMode mode) {
using var ctx = new AppDbContext();
IQueryable<DeliveryPart> query;
@@ -843,33 +929,83 @@ namespace Elwig.Services {
Title = $"{DeliveryDepreciationList.Name} speichern unter - Elwig"
};
if (d.ShowDialog() == true) {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
using var ods = new OdsFile(d.FileName);
var tblTotal = await DeliveryJournalData.FromQuery(query, filterNames);
tblTotal.FullName = DeliveryDepreciationList.Name;
tblTotal.Name = "Gesamt";
await ods.AddTable(tblTotal);
foreach (var branch in await ctx.Branches.OrderBy(b => b.Name).ToListAsync()) {
var tbl = await DeliveryJournalData.FromQuery(query.Where(p => p.Delivery.ZwstId == branch.ZwstId), filterNames);
tbl.FullName = DeliveryDepreciationList.Name;
tbl.Name = branch.Name;
await ods.AddTable(tbl);
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
using var ods = new OdsFile(d.FileName);
var tblTotal = await DeliveryJournalData.FromQuery(query, filterNames);
tblTotal.FullName = DeliveryDepreciationList.Name;
tblTotal.Name = "Gesamt";
await ods.AddTable(tblTotal);
foreach (var branch in await ctx.Branches.OrderBy(b => b.Name).ToListAsync()) {
var tbl = await DeliveryJournalData.FromQuery(query.Where(p => p.Delivery.ZwstId == branch.ZwstId), filterNames);
tbl.FullName = DeliveryDepreciationList.Name;
tbl.Name = branch.Name;
await ods.AddTable(tbl);
}
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
} else {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
var data = await DeliveryJournalData.FromQuery(query, filterNames);
using var doc = new DeliveryDepreciationList(string.Join(" / ", filterNames), data);
await Utils.ExportDocument(doc, mode);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
var data = await DeliveryJournalData.FromQuery(query, filterNames);
using var doc = new DeliveryDepreciationList(string.Join(" / ", filterNames), data);
await Utils.ExportDocument(doc, mode);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
}
public static async Task GenerateDeliveryDataList(this DeliveryAdminViewModel vm, ExportSubject subject, ExportMode mode) {
using var ctx = new AppDbContext();
IQueryable<DeliveryPart> query;
List<string> filterNames = [];
if (subject == ExportSubject.FromFilters) {
var (f, _, q, _, _) = await vm.GetFilters(ctx);
query = q;
filterNames.AddRange(f);
} else if (subject == ExportSubject.FromSeason) {
var year = vm.FilterSeason ?? Utils.CurrentLastSeason;
query = ctx.DeliveryParts
.Where(p => p.Year == year);
filterNames.Add($"{year}");
} else {
throw new ArgumentException("Invalid value for ExportSubject");
}
query = query
.OrderBy(p => p.Delivery.MgNr)
.ThenBy(p => p.SortId)
.ThenBy(p => p.AttrId)
.ThenBy(p => p.CultId);
var d = new SaveFileDialog() {
FileName = $"Liefermengen.ods",
DefaultExt = "ods",
Filter = "OpenDocument Format Spreadsheet (*.ods)|*.ods",
Title = $"Liefermengen speichern unter - Elwig"
};
if (d.ShowDialog() == true) {
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
using var ods = new OdsFile(d.FileName);
var tblTotal = await MemberDeliveryData.FromQuery(query, filterNames);
var tbl = await MemberDeliveryPerVarietyData.FromQuery(query, filterNames);
await ods.AddTable(tblTotal);
await ods.AddTable(tbl);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
}
@@ -914,7 +1050,7 @@ namespace Elwig.Services {
var weight = await deliveryParts.SumAsync(p => p.Weight);
wText = $"{weight:N0} kg";
wGrid.Add(("Gewicht", null, weight, null, weight));
wGrid.Add(("Menge", null, weight, null, weight));
if (await deliveryParts.AnyAsync()) {
var kmwMin = await deliveryParts.MinAsync(p => p.Kmw);
@@ -1015,7 +1151,7 @@ namespace Elwig.Services {
wGrid.ColumnDefinitions.Add(new() { Width = new(50) });
int rowNum = 0;
foreach (var row in weightData) {
if (rowNum == 1 || (rowNum != 0 && row.Item1 != null)) rowNum++;
if (rowNum != 0 && row.Item2 == null) rowNum++;
AddWeightToolTipRow(wGrid, rowNum++, row.Item1, row.Item2, row.Item3, row.Item4, row.Item5);
}
@@ -1030,7 +1166,7 @@ namespace Elwig.Services {
AddToolTipCell(gGrid, "Max.", 0, 4, 1, false, false, true);
rowNum = 1;
foreach (var row in gradationData) {
if (rowNum == 2 || (rowNum != 1 && row.Item1 != null)) rowNum++;
if (rowNum != 1 && row.Item2 == null) rowNum++;
AddGradationToolTipRow(gGrid, rowNum++, row.Item1, row.Item2, row.Item3, row.Item4, row.Item5);
}
@@ -1038,11 +1174,11 @@ namespace Elwig.Services {
}
public static async Task DeleteDelivery(string lsnr) {
using (var ctx = new AppDbContext()) {
await Task.Run(async () => {
using var ctx = new AppDbContext();
await ctx.Deliveries.Where(d => d.LsNr == lsnr).ExecuteDeleteAsync();
await ctx.SaveChangesAsync();
}
App.HintContextChange();
});
}
}
}

View File

@@ -1,20 +1,18 @@
using Elwig.Documents;
using Elwig.Helpers;
using Elwig.Helpers.Billing;
using Elwig.Helpers.Export;
using Elwig.Models.Dtos;
using Elwig.Models.Entities;
using Elwig.ViewModels;
using Microsoft.EntityFrameworkCore;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System;
using Microsoft.EntityFrameworkCore;
using Elwig.Documents;
using System.Windows.Input;
using System.Windows;
using Elwig.Helpers.Billing;
using Elwig.Models.Dtos;
using Elwig.Helpers.Export;
using Microsoft.Win32;
using Elwig.ViewModels;
using System.IO;
using System.Net.Http;
using System.Windows.Input;
namespace Elwig.Services {
public static class MemberService {
@@ -49,6 +47,11 @@ namespace Elwig.Services {
vm.StatusAreaCommitmentInfo = $"{Utils.CurrentLastSeason}";
vm.StatusAreaCommitmentToolTip = null;
vm.Age = "-";
vm.CreatedAt = "-";
vm.ModifiedAt = "-";
vm.ModifiedAtShort = "-";
vm.ExportedAt = "-";
vm.ImportedAt = "-";
}
public static void FillInputs(this MemberAdminViewModel vm, Member m) {
@@ -137,6 +140,12 @@ namespace Elwig.Services {
vm.ContactViaPost = m.ContactViaPost;
vm.ContactViaEmail = m.ContactViaEmail;
vm.CreatedAt = $"{m.CreatedAt:dd.MM.yyyy, HH:mm:ss}";
vm.ModifiedAt = $"{m.ModifiedAt:dd.MM.yyyy, HH:mm:ss}";
vm.ModifiedAtShort = $"{m.ModifiedAt:dd.MM.yyyy, HH:mm}";
vm.ExportedAt = m.ExportedAt == null ? "-" : $"{m.ExportedAt:dd.MM.yyyy, HH:mm:ss}";
vm.ImportedAt = m.ImportedAt == null ? "-" : $"{m.ImportedAt:dd.MM.yyyy, HH:mm:ss}";
vm.StatusDeliveriesLastSeasonInfo = $"{Utils.CurrentLastSeason - 1}";
vm.StatusDeliveriesLastSeason = "...";
vm.StatusDeliveriesLastSeasonToolTip = null;
@@ -212,6 +221,7 @@ namespace Elwig.Services {
var filterLfbisNr = new List<string>();
var filterUstIdNr = new List<string>();
var filterAreaCom = new List<string>();
var filterNotAreaCom = new List<string>();
var filter = vm.TextFilter;
if (filter.Count > 0) {
@@ -223,7 +233,15 @@ namespace Elwig.Services {
for (int i = 0; i < filter.Count; i++) {
var e = filter[i];
if (e.Length >= 5 && e.Length <= 10 && "funktionär".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
if (e.Equals("aktiv", StringComparison.CurrentCultureIgnoreCase)) {
memberQuery = memberQuery.Where(m => m.IsActive);
filter.RemoveAt(i--);
filterNames.Add("aktive Mitglieder");
} else if (e.Equals("!aktiv", StringComparison.CurrentCultureIgnoreCase)) {
memberQuery = memberQuery.Where(m => !m.IsActive);
filter.RemoveAt(i--);
filterNames.Add("inaktive Mitglieder");
} else if (e.Length >= 5 && e.Length <= 10 && "funktionär".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
memberQuery = memberQuery.Where(m => m.IsFunktionär);
filter.RemoveAt(i--);
filterNames.Add("Funktionäre");
@@ -295,6 +313,22 @@ namespace Elwig.Services {
memberQuery = memberQuery.Where(m => !m.ContactViaPost);
filter.RemoveAt(i--);
filterNames.Add("nicht Kontaktart Post");
} else if ("upload".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
memberQuery = memberQuery.Where(p => (p.XTime == null || p.MTime > p.XTime) && (p.ITime == null || p.MTime > p.ITime));
filter.RemoveAt(i--);
filterNames.Add("geändert seit letztem Export");
} else if ("!upload".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
memberQuery = memberQuery.Where(p => !((p.XTime == null || p.MTime > p.XTime) && (p.ITime == null || p.MTime > p.ITime)));
filter.RemoveAt(i--);
filterNames.Add("unverändert seit letztem Export");
} else if (">import".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
memberQuery = memberQuery.Where(p => p.MTime > p.ITime);
filter.RemoveAt(i--);
filterNames.Add("geändert seit letztem Import");
} else if ("<import".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
memberQuery = memberQuery.Where(p => p.MTime <= p.ITime);
filter.RemoveAt(i--);
filterNames.Add("unverändert seit letztem Import");
} else if (e.All(char.IsAsciiDigit) && mgnr.ContainsKey(e)) {
filterMgNr.Add(int.Parse(e));
filter.RemoveAt(i--);
@@ -314,10 +348,22 @@ namespace Elwig.Services {
memberQuery = memberQuery.Where(m => m.TelephoneNumbers.Any(t => t.Number.Replace(" ", "").StartsWith(e)));
filter.RemoveAt(i--);
filterNames.Add($"Tel.-Nr. {e}");
} else if (e.Length >= 5 && e.Length <= 14 && "flächenbindung".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
filterAreaCom.AddRange(areaComs.Keys);
filter.RemoveAt(i--);
filterNames.Add($"Flächenbindung");
} else if (e.Length >= 6 && e.Length <= 15 && "!flächenbindung".StartsWith(e, StringComparison.CurrentCultureIgnoreCase)) {
filterNotAreaCom.AddRange(areaComs.Keys);
filter.RemoveAt(i--);
filterNames.Add($"keine Flächenbindung");
} else if (areaComs.ContainsKey(e.ToUpper())) {
filterAreaCom.Add(e.ToUpper());
filter.RemoveAt(i--);
filterNames.Add($"Flächenbindung {e.ToUpper()}");
} else if (e.Length >= 3 && e[0] == '!' && areaComs.ContainsKey(e[1..].ToUpper())) {
filterNotAreaCom.Add(e[1..].ToUpper());
filter.RemoveAt(i--);
filterNames.Add($"ohne Flächenbindung {e[1..].ToUpper()}");
} else if (Validator.CheckLfbisNr(e)) {
filterLfbisNr.Add(e);
filter.RemoveAt(i--);
@@ -336,7 +382,8 @@ namespace Elwig.Services {
if (filterMgNr.Count > 0) memberQuery = memberQuery.Where(m => filterMgNr.Contains(m.MgNr));
if (filterKgNr.Count > 0) memberQuery = memberQuery.Where(m => m.DefaultKgNr != null && filterKgNr.Contains((int)m.DefaultKgNr));
if (filterZwst.Count > 0) memberQuery = memberQuery.Where(m => m.ZwstId != null && filterZwst.Contains(m.ZwstId));
if (filterAreaCom.Count > 0) memberQuery = memberQuery.Where(m => m.AreaCommitments.AsQueryable().Where(Utils.ActiveAreaCommitments()).Any(c => filterAreaCom.Contains(c.VtrgId)));
if (filterAreaCom.Count > 0) memberQuery = memberQuery.Where(m => m.AreaCommitments.AsQueryable().Where(Utils.ActiveAreaCommitments(Utils.CurrentLastSeason)).Any(c => filterAreaCom.Contains(c.VtrgId)));
if (filterNotAreaCom.Count > 0) memberQuery = memberQuery.Where(m => !m.AreaCommitments.AsQueryable().Where(Utils.ActiveAreaCommitments(Utils.CurrentLastSeason)).All(c => filterNotAreaCom.Contains(c.VtrgId)));
if (filterLfbisNr.Count > 0) memberQuery = memberQuery.Where(m => m.LfbisNr != null && filterLfbisNr.Contains(m.LfbisNr));
if (filterUstIdNr.Count > 0) memberQuery = memberQuery.Where(m => m.UstIdNr != null && filterUstIdNr.Contains(m.UstIdNr));
}
@@ -345,51 +392,57 @@ namespace Elwig.Services {
}
public static async Task GenerateMemberDataSheet(Member m, ExportMode mode) {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
using var ctx = new AppDbContext();
using var doc = new MemberDataSheet(m, ctx);
await Utils.ExportDocument(doc, mode, emailData: (m, MemberDataSheet.Name, "Im Anhang finden Sie das aktuelle Stammdatenblatt"));
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
using var ctx = new AppDbContext();
using var doc = new MemberDataSheet(m, ctx);
await Utils.ExportDocument(doc, mode, emailData: (m, MemberDataSheet.Name, "Im Anhang finden Sie das aktuelle Stammdatenblatt"));
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
public static async Task GenerateDeliveryConfirmation(Member m, int year, ExportMode mode) {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
var b = new Billing(year);
await b.FinishSeason();
await b.CalculateBuckets();
App.HintContextChange();
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
var b = new Billing(year);
await b.FinishSeason();
await b.CalculateBuckets();
App.HintContextChange();
using var ctx = new AppDbContext();
var data = await DeliveryConfirmationDeliveryData.ForMember(ctx.DeliveryParts, year, m);
using var doc = new DeliveryConfirmation(ctx, year, m, data);
await Utils.ExportDocument(doc, mode, emailData: (m, $"{DeliveryConfirmation.Name} {year}", $"Im Anhang finden Sie die Anlieferungsbestätigung {year}"));
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
using var ctx = new AppDbContext();
var data = await DeliveryConfirmationDeliveryData.ForMember(ctx.DeliveryParts, year, m);
using var doc = new DeliveryConfirmation(ctx, year, m, data);
await Utils.ExportDocument(doc, mode, emailData: (m, $"{DeliveryConfirmation.Name} {year}", $"Im Anhang finden Sie die Anlieferungsbestätigung {year}"));
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
public static async Task GenerateCreditNote(Member m, int year, int avnr, ExportMode mode) {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
using var ctx = new AppDbContext();
var v = (await ctx.PaymentVariants.FindAsync(year, avnr))!;
var data = await CreditNoteDeliveryData.ForPaymentVariant(ctx.CreditNoteDeliveryRows, ctx.Seasons, year, avnr);
var p = (await ctx.MemberPayments.FindAsync(year, avnr, m.MgNr))!;
var b = BillingData.FromJson((await ctx.PaymentVariants.FindAsync(year, avnr))!.Data);
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
using var ctx = new AppDbContext();
var v = (await ctx.PaymentVariants.FindAsync(year, avnr))!;
var data = await CreditNoteDeliveryData.ForPaymentVariant(ctx.CreditNoteDeliveryRows, ctx.PaymentVariants, year, avnr);
var p = (await ctx.MemberPayments.FindAsync(year, avnr, m.MgNr))!;
var b = BillingData.FromJson((await ctx.PaymentVariants.FindAsync(year, avnr))!.Data);
using var doc = new CreditNote(ctx, p, data[m.MgNr],
b.ConsiderContractPenalties, b.ConsiderTotalPenalty, b.ConsiderAutoBusinessShares, b.ConsiderCustomModifiers,
await ctx.GetMemberUnderDelivery(year, m.MgNr));
await Utils.ExportDocument(doc, mode, emailData: (m, $"{CreditNote.Name} {v.Name}", $"Im Anhang finden Sie die Traubengutschrift {v.Name}"));
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
using var doc = new CreditNote(ctx, p, data[m.MgNr],
b.ConsiderContractPenalties, b.ConsiderTotalPenalty, b.ConsiderAutoBusinessShares, b.ConsiderCustomModifiers,
await ctx.GetMemberUnderDelivery(year, m.MgNr));
await Utils.ExportDocument(doc, mode, emailData: (m, $"{CreditNote.Name} {v.Name}", $"Im Anhang finden Sie die Traubengutschrift {v.Name}"));
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
@@ -442,130 +495,146 @@ namespace Elwig.Services {
Title = $"{MemberList.Name} speichern unter - Elwig"
};
if (d.ShowDialog() == true) {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
var data = await MemberListData.FromQuery(query, filterNames, filterNames.Where(f => f.StartsWith("Flächenbindung")).Select(f => f.Split(' ')[^1]));
using var ods = new OdsFile(d.FileName);
await ods.AddTable(data);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
var data = await MemberListData.FromQuery(query, filterNames, filterNames.Where(f => f.StartsWith("Flächenbindung")).Select(f => f.Split(' ')[^1]));
using var ods = new OdsFile(d.FileName);
await ods.AddTable(data);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
} else if (mode == ExportMode.Vcf) {
var d = new SaveFileDialog() {
FileName = "Mitglieder.vcf",
DefaultExt = "vcf",
Filter = "vCard-Datei (*.vcf)|*.vcf",
Title = "Kontakte speichern unter - Elwig"
};
if (d.ShowDialog() == true) {
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
var members = await query
.OrderBy(m => m.MgNr)
.Include(m => m.BillingAddress)
.Include(m => m.TelephoneNumbers)
.Include(m => m.EmailAddresses)
.AsSplitQuery()
.ToListAsync();
using var exporter = new VCard(d.FileName);
await exporter.ExportAsync(members);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
} else if (mode == ExportMode.Export) {
var d = new SaveFileDialog() {
FileName = subject == ExportSubject.Selected ? $"Mitglied_{vm.SelectedMember?.MgNr}.elwig.zip" : $"Mitglieder_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip",
DefaultExt = ".elwig.zip",
DefaultExt = "elwig.zip",
Filter = "Elwig-Export-Datei (*.elwig.zip)|*.elwig.zip",
Title = $"{MemberList.Name} speichern unter - Elwig"
Title = $"{MemberList.Name} speichern unter - Elwig",
AddExtension = false,
};
if (d.ShowDialog() == true) {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
var members = await query
.OrderBy(m => m.MgNr)
.Include(m => m.BillingAddress)
.Include(m => m.TelephoneNumbers)
.Include(m => m.EmailAddresses)
.AsSplitQuery()
.ToListAsync();
var areaComs = await query
.SelectMany(m => m.AreaCommitments)
.Include(c => c.Rd)
.ToListAsync();
await ElwigData.Export(d.FileName, members, areaComs, filterNames);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
if (!d.FileName.EndsWith(".elwig.zip")) d.FileName += ".elwig.zip";
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
var members = await query
.OrderBy(m => m.MgNr)
.Include(m => m.BillingAddress)
.Include(m => m.TelephoneNumbers)
.Include(m => m.EmailAddresses)
.Include(m => m.DefaultWbKg!.Gl)
.AsSplitQuery()
.ToListAsync();
var areaComs = await query
.SelectMany(m => m.AreaCommitments)
.Include(c => c.Rd)
.Include(c => c.Kg.Gl)
.ToListAsync();
var wbKgs = members
.Where(m => m.DefaultWbKg != null)
.Select(m => m.DefaultWbKg!)
.Union(areaComs.Select(c => c.Kg))
.Distinct()
.OrderBy(k => k.KgNr)
.ToList();
await ElwigData.Export(d.FileName, members, areaComs, wbKgs, filterNames);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
} else if (mode == ExportMode.Upload && App.Config.SyncUrl != null) {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
var filename = $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip";
var path = Path.Combine(App.TempPath, filename);
var members = await query
.OrderBy(m => m.MgNr)
.Include(m => m.BillingAddress)
.Include(m => m.TelephoneNumbers)
.Include(m => m.EmailAddresses)
.AsSplitQuery()
.ToListAsync();
var areaComs = await query
.SelectMany(m => m.AreaCommitments)
.Include(c => c.Rd)
.ToListAsync();
if (members.Count == 0) {
MessageBox.Show("Es wurden keine Mitglieder zum Hochladen ausgewählt!", "Mitglieder hochladen",
MessageBoxButton.OK, MessageBoxImage.Error);
} else {
await ElwigData.Export(path, members, areaComs, filterNames);
await Utils.UploadExportData(path, App.Config.SyncUrl, App.Config.SyncUsername, App.Config.SyncPassword);
MessageBox.Show($"Hochladen von {members.Count:N0} Mitgliedern erfolgreich!", "Mitglieder hochgeladen",
MessageBoxButton.OK, MessageBoxImage.Information);
}
} catch (HttpRequestException exc) {
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Mitglieder hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
} catch (TaskCanceledException exc) {
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Mitglieder hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
await SyncService.Upload(App.Config.SyncUrl, App.Config.SyncUrl, App.Config.SyncPassword, query, filterNames);
});
Mouse.OverrideCursor = null;
} else {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
var data = await MemberListData.FromQuery(query, filterNames, filterNames.Where(f => f.StartsWith("Flächenbindung")).Select(f => f.Split(' ')[^1]));
using var doc = new MemberList(string.Join(" / ", filterNames), data);
await Utils.ExportDocument(doc, mode);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
var data = await MemberListData.FromQuery(query, filterNames, filterNames.Where(f => f.StartsWith("Flächenbindung")).Select(f => f.Split(' ')[^1]));
using var doc = new MemberList(string.Join(" / ", filterNames), data);
await Utils.ExportDocument(doc, mode);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
}
public static async Task<int> UpdateMember(this MemberAdminViewModel vm, int? oldMgNr) {
var newMgNr = (int)vm.MgNr!;
var m = new Member {
MgNr = oldMgNr ?? newMgNr,
PredecessorMgNr = vm.PredecessorMgNr,
IsJuridicalPerson = vm.IsJuridicalPerson,
Prefix = vm.IsJuridicalPerson || string.IsNullOrWhiteSpace(vm.Prefix) ? null : vm.Prefix,
GivenName = vm.IsJuridicalPerson || string.IsNullOrWhiteSpace(vm.GivenName) ? null : vm.GivenName,
Name = vm.Name!,
Suffix = vm.IsJuridicalPerson || string.IsNullOrWhiteSpace(vm.Suffix) ? null : vm.Suffix,
ForTheAttentionOf = !vm.IsJuridicalPerson || string.IsNullOrWhiteSpace(vm.ForTheAttentionOf) ? null : vm.ForTheAttentionOf,
Birthday = string.IsNullOrEmpty(vm.Birthday) ? null : string.Join("-", vm.Birthday!.Split(".").Reverse()),
IsDeceased = vm.IsDeceased,
CountryNum = 40, // Austria AT AUT
PostalDestId = vm.Ort!.Id,
Address = vm.Address!,
using (var ctx = new AppDbContext()) {
var m = new Member {
MgNr = oldMgNr ?? newMgNr,
PredecessorMgNr = vm.PredecessorMgNr,
IsJuridicalPerson = vm.IsJuridicalPerson,
Prefix = vm.IsJuridicalPerson || string.IsNullOrWhiteSpace(vm.Prefix) ? null : vm.Prefix,
GivenName = vm.IsJuridicalPerson || string.IsNullOrWhiteSpace(vm.GivenName) ? null : vm.GivenName,
Name = vm.Name!,
Suffix = vm.IsJuridicalPerson || string.IsNullOrWhiteSpace(vm.Suffix) ? null : vm.Suffix,
ForTheAttentionOf = !vm.IsJuridicalPerson || string.IsNullOrWhiteSpace(vm.ForTheAttentionOf) ? null : vm.ForTheAttentionOf,
Birthday = string.IsNullOrEmpty(vm.Birthday) ? null : string.Join("-", vm.Birthday!.Split(".").Reverse()),
IsDeceased = vm.IsDeceased,
CountryNum = 40, // Austria AT AUT
PostalDestId = vm.Ort!.Id,
Address = vm.Address!,
Iban = string.IsNullOrEmpty(vm.Iban) ? null : vm.Iban?.Replace(" ", ""),
Bic = string.IsNullOrEmpty(vm.Bic) ? null : vm.Bic,
Iban = string.IsNullOrEmpty(vm.Iban) ? null : vm.Iban?.Replace(" ", ""),
Bic = string.IsNullOrEmpty(vm.Bic) ? null : vm.Bic,
UstIdNr = string.IsNullOrEmpty(vm.UstIdNr) ? null : vm.UstIdNr,
LfbisNr = string.IsNullOrEmpty(vm.LfbisNr) ? null : vm.LfbisNr,
IsBuchführend = vm.IsBuchführend,
IsOrganic = vm.IsOrganic,
UstIdNr = string.IsNullOrEmpty(vm.UstIdNr) ? null : vm.UstIdNr,
LfbisNr = string.IsNullOrEmpty(vm.LfbisNr) ? null : vm.LfbisNr,
IsBuchführend = vm.IsBuchführend,
IsOrganic = vm.IsOrganic,
EntryDateString = string.IsNullOrEmpty(vm.EntryDate) ? null : string.Join("-", vm.EntryDate.Split(".").Reverse()),
ExitDateString = string.IsNullOrEmpty(vm.ExitDate) ? null : string.Join("-", vm.ExitDate.Split(".").Reverse()),
BusinessShares = (int)vm.BusinessShares!,
AccountingNr = string.IsNullOrEmpty(vm.AccountingNr) ? null : vm.AccountingNr,
IsActive = vm.IsActive,
IsVollLieferant = vm.IsVollLieferant,
IsFunktionär = vm.IsFunktionär,
ZwstId = vm.Branch?.ZwstId,
DefaultKgNr = vm.DefaultKg?.KgNr,
Comment = string.IsNullOrEmpty(vm.Comment) ? null : vm.Comment,
ContactViaPost = vm.ContactViaPost,
ContactViaEmail = vm.ContactViaEmail,
};
EntryDateString = string.IsNullOrEmpty(vm.EntryDate) ? null : string.Join("-", vm.EntryDate.Split(".").Reverse()),
ExitDateString = string.IsNullOrEmpty(vm.ExitDate) ? null : string.Join("-", vm.ExitDate.Split(".").Reverse()),
BusinessShares = (int)vm.BusinessShares!,
AccountingNr = string.IsNullOrEmpty(vm.AccountingNr) ? null : vm.AccountingNr,
IsActive = vm.IsActive,
IsVollLieferant = vm.IsVollLieferant,
IsFunktionär = vm.IsFunktionär,
ZwstId = vm.Branch?.ZwstId,
DefaultKgNr = vm.DefaultKg?.KgNr,
Comment = string.IsNullOrEmpty(vm.Comment) ? null : vm.Comment,
ContactViaPost = vm.ContactViaPost,
ContactViaEmail = vm.ContactViaEmail,
};
return await Task.Run(async () => {
using var ctx = new AppDbContext();
if (oldMgNr != null) {
ctx.Update(m);
} else {
@@ -622,7 +691,7 @@ namespace Elwig.Services {
KgNr = c.KgNr,
GstNr = c.GstNr,
RdNr = c.RdNr,
YearFrom = vm.MaintainAreaComYearTo ? c.YearFrom : year,
YearFrom = vm.MaintainAreaComYearFrom ? c.YearFrom : year,
YearTo = c.YearTo,
}));
@@ -648,15 +717,14 @@ namespace Elwig.Services {
if (newMgNr != m.MgNr) {
await ctx.Database.ExecuteSqlAsync($"UPDATE member SET mgnr = {newMgNr} WHERE mgnr = {oldMgNr}");
}
}
App.HintContextChange();
return newMgNr;
return newMgNr;
});
}
public static async Task DeleteMember(int mgnr, bool deletePaymentData, bool deleteDeliveries, bool deleteAreaComs) {
using (var ctx = new AppDbContext()) {
await Task.Run(async () => {
using var ctx = new AppDbContext();
var l = (await ctx.Members.FindAsync(mgnr))!;
if (deletePaymentData) {
ctx.RemoveRange(l.Credits);
@@ -669,8 +737,7 @@ namespace Elwig.Services {
}
ctx.Remove(l);
await ctx.SaveChangesAsync();
}
App.HintContextChange();
});
}
}
}

View File

@@ -0,0 +1,387 @@
using Elwig.Documents;
using Elwig.Helpers;
using Elwig.Helpers.Billing;
using Elwig.Helpers.Export;
using Elwig.Models.Dtos;
using Elwig.Models.Entities;
using Elwig.ViewModels;
using Microsoft.EntityFrameworkCore;
using Microsoft.Win32;
using System;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
namespace Elwig.Services {
public static class PaymentVariantService {
private static readonly JsonSerializerOptions JsonOpt = new() { WriteIndented = true };
public static void ClearInputs(this PaymentVariantsViewModel vm) {
vm.IsPaymentVariantSelected = false;
vm.EditText = "Bearbeiten";
vm.SaveIsEnabled = false;
vm.DeleteIsEnabled = false;
vm.CalculateIsEnabled = false;
vm.CommitIsEnabled = false;
vm.CommitVisibility = Visibility.Visible;
vm.RevertIsEnabled = false;
vm.RevertVisibility = Visibility.Hidden;
vm.Arrow = "\xF0AF";
vm.ExportIsEnabled = false;
vm.IsReadOnly = true;
vm.DataIsReadOnly = true;
vm.BillingData = null;
vm.Name = "";
vm.Comment = "";
vm.Date = null;
vm.TransferDate = null;
vm.WeightModifier = null;
vm.ConsiderModifiers = false;
vm.ConsiderPenalties = false;
vm.ConsiderPenalty = false;
vm.ConsiderAuto = false;
vm.ConsiderCustom = false;
vm.IsEnabled = false;
vm.Data = "";
vm.StatusModifierSum = "-";
vm.StatusTotalSum = "-";
vm.StatusVatSum = "-";
vm.StatusDeductionSum = "-";
vm.StatusPaymentSum = "-";
}
public static void FillInputs(this PaymentVariantsViewModel vm, PaymentVar v) {
var locked = !v.TestVariant;
vm.IsPaymentVariantSelected = true;
vm.SaveIsEnabled = !locked;
vm.DeleteIsEnabled = !locked;
vm.CalculateIsEnabled = !locked;
vm.CommitIsEnabled = !locked && !vm.SeasonLocked;
vm.CommitVisibility = !locked ? Visibility.Visible : Visibility.Hidden;
vm.RevertIsEnabled = locked && !vm.SeasonLocked;
vm.RevertVisibility = locked ? Visibility.Visible : Visibility.Hidden;
vm.Arrow = locked ? "\xF0B0" : "\xF0AF";
vm.EditText = locked ? "Ansehen" : "Bearbeiten";
vm.ExportIsEnabled = locked;
vm.IsReadOnly = false;
vm.DataIsReadOnly = locked;
vm.Name = v.Name;
vm.Comment = v.Comment ?? "";
vm.Date = v.Date;
vm.TransferDate = v.TransferDate;
try {
vm.BillingData = BillingData.FromJson(v.Data);
vm.ConsiderModifiers = vm.BillingData.ConsiderDelieryModifiers;
vm.ConsiderPenalties = vm.BillingData.ConsiderContractPenalties;
vm.ConsiderPenalty = vm.BillingData.ConsiderTotalPenalty;
vm.ConsiderAuto = vm.BillingData.ConsiderAutoBusinessShares;
vm.ConsiderCustom = vm.BillingData.ConsiderCustomModifiers;
if (vm.BillingData.NetWeightModifier != 0) {
vm.WeightModifier = Math.Round(vm.BillingData.NetWeightModifier * 100.0, 8);
} else if (vm.BillingData.GrossWeightModifier != 0) {
vm.WeightModifier = Math.Round(vm.BillingData.GrossWeightModifier * 100.0, 8);
} else {
vm.WeightModifier = null;
}
vm.Data = JsonSerializer.Serialize(vm.BillingData.Data, JsonOpt);
} catch {
vm.BillingData = null;
vm.ConsiderModifiers = false;
vm.ConsiderPenalties = false;
vm.ConsiderPenalty = false;
vm.ConsiderAuto = false;
vm.ConsiderCustom = false;
vm.WeightModifier = null;
vm.Data = v.Data;
}
vm.IsEnabled = !locked;
vm.StatusModifierSum = "...";
vm.StatusTotalSum = "...";
vm.StatusVatSum = "...";
vm.StatusDeductionSum = "...";
vm.StatusPaymentSum = "...";
Utils.RunBackground("Variantendaten laden", async () => {
await vm.UpdateSums(v);
});
}
private static async Task UpdateSums(this PaymentVariantsViewModel vm, PaymentVar v) {
if (App.MainDispatcher == null)
return;
var modText = "-";
var totalText = "-";
var vatText = "-";
var deductionText = "-";
var paymentText = "-";
using var ctx = new AppDbContext();
var sym = v.Season.Currency.Symbol ?? v.Season.Currency.Code;
var modSum = await ctx.PaymentDeliveryParts
.Where(p => p.Year == v.Year && p.AvNr == v.AvNr)
.SumAsync(p => p.AmountValue - p.NetAmountValue);
modText = $"{v.Season.DecFromDb(modSum):N2} {sym}";
var totalSum = await ctx.MemberPayments
.Where(p => p.Year == v.Year && p.AvNr == v.AvNr)
.SumAsync(p => p.AmountValue);
totalText = $"{v.Season.DecFromDb(totalSum):N2} {sym}";
await App.MainDispatcher.BeginInvoke(() => {
if (vm.SelectedPaymentVariant != v)
return;
vm.StatusModifierSum = modText;
vm.StatusTotalSum = totalText;
});
var credits = ctx.Credits.Where(c => c.Year == v.Year && c.AvNr == v.AvNr);
if (!credits.Any()) {
long lastTotalSum = 0;
decimal vatSum = 0;
var currentPayments = await ctx.MemberPayments
.Where(p => p.Year == v.Year && p.AvNr == v.AvNr)
.ToDictionaryAsync(p => p.MgNr);
var lastV = await ctx.PaymentVariants
.Where(l => l.Year == v.Year && !l.TestVariant)
.OrderByDescending(l => l.TransferDateString ?? l.DateString)
.ThenByDescending(l => l.AvNr)
.FirstOrDefaultAsync();
if (lastV != null) {
var lastPayments = await ctx.MemberPayments
.Where(p => p.Year == v.Year && p.AvNr == lastV.AvNr)
.ToDictionaryAsync(p => p.MgNr);
lastTotalSum = lastPayments.Sum(e => e.Value.AmountValue);
foreach (int mgnr in currentPayments.Keys) {
var c = currentPayments[mgnr];
var l = lastPayments[mgnr];
vatSum += (c.Amount - l.Amount) * (decimal)(c.Member.IsBuchführend ? v.Season.VatNormal : v.Season.VatFlatrate);
}
} else {
vatSum = currentPayments.Sum(e => e.Value.Amount * (decimal)(e.Value.Member.IsBuchführend ? v.Season.VatNormal : v.Season.VatFlatrate));
}
vatText = $"~{vatSum:N2} {sym}";
deductionText = $"- {sym}";
paymentText = $"~{v.Season.DecFromDb(totalSum - lastTotalSum) + vatSum:N2} {sym}";
} else {
// all values in the credit table are stored with precision 2!
totalText = $"{Utils.DecFromDb(await credits.SumAsync(c => c.NetAmountValue), 2):N2} {sym}";
vatText = $"{Utils.DecFromDb(await credits.SumAsync(c => c.VatAmountValue), 2):N2} {sym}";
deductionText = $"{-Utils.DecFromDb(await credits.SumAsync(c => c.ModifiersValue ?? 0), 2):N2} {sym}";
paymentText = $"{Utils.DecFromDb(await credits.SumAsync(c => c.AmountValue), 2):N2} {sym}";
}
await App.MainDispatcher.BeginInvoke(() => {
if (vm.SelectedPaymentVariant != v)
return;
vm.StatusModifierSum = modText;
vm.StatusTotalSum = totalText;
vm.StatusVatSum = vatText;
vm.StatusDeductionSum = deductionText;
vm.StatusPaymentSum = paymentText;
});
}
public static async Task GenerateSummary(PaymentVar v, ExportMode mode) {
if (mode == ExportMode.SaveList) {
var d = new SaveFileDialog() {
FileName = $"Variantendaten-{v.Name.Trim().Replace(' ', '-')}.ods",
DefaultExt = "ods",
Filter = "OpenDocument Format Spreadsheet (*.ods)|*.ods",
Title = $"Variantendaten {v.Name} speichern unter - Elwig"
};
if (d.ShowDialog() == false)
return;
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
using var ctx = new AppDbContext();
var data = await PaymentVariantSummaryData.ForPaymentVariant(v, ctx.PaymentVariantSummaryRows);
using var ods = new OdsFile(d.FileName);
await ods.AddTable(data);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
} else {
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
using var ctx = new AppDbContext();
var data = await PaymentVariantSummaryData.ForPaymentVariant(v, ctx.PaymentVariantSummaryRows);
using var doc = new PaymentVariantSummary((await ctx.PaymentVariants.FindAsync(v.Year, v.AvNr))!, data);
await Utils.ExportDocument(doc, mode);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
}
public static async Task GenerateEbics(int year, int avnr) {
using var ctx = new AppDbContext();
var v = (await ctx.PaymentVariants.FindAsync(year, avnr))!;
var withoutIban = v.Credits.Count(c => c.Member.Iban == null);
if (withoutIban > 0) {
var r = MessageBox.Show($"Achtung: Für {withoutIban:N0} Mitglieder ist kein IBAN hinterlegt.\n\nDiese werden NICHT exportiert.",
"Mitglieder ohne IBAN", MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.Cancel);
if (r != MessageBoxResult.OK) return;
}
var withNegAmount = v.Credits.Count(c => c.Amount <= 0);
if (withNegAmount > 0) {
var r = MessageBox.Show($"Achtung: Es gibt {withNegAmount:N0} Traubengutschriften mit negativem Betrag.\n\nDiese werden NICHT exportiert.",
"Traubengutschriften mit negativem Betrag", MessageBoxButton.OKCancel, MessageBoxImage.Warning, MessageBoxResult.OK);
if (r != MessageBoxResult.OK) return;
}
var d = new SaveFileDialog() {
FileName = $"{App.Client.NameToken}-Überweisungsdaten-{v.Year}-{v.Name.Trim().Replace(' ', '-')}.{Ebics.FileExtension}",
DefaultExt = Ebics.FileExtension,
Filter = "EBICS-Datei (*.xml)|*.xml",
Title = $"Überweisungsdaten speichern unter - Elwig",
};
if (d.ShowDialog() == true) {
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
using var e = new Ebics(v, d.FileName, App.Client.ExportEbicsVersion, (Ebics.AddressMode)App.Client.ExportEbicsAddress);
await e.ExportAsync(Transaction.FromPaymentVariant(v));
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
}
public static async Task GenerateAccountingList(int year, int avnr, string name) {
var d = new SaveFileDialog() {
FileName = $"{App.Client.NameToken}-Buchungsliste-{year}-{name.Trim().Replace(' ', '-')}.ods",
DefaultExt = "ods",
Filter = "OpenDocument Format Spreadsheet (*.ods)|*.ods",
Title = $"Buchungsliste speichern unter - Elwig"
};
if (d.ShowDialog() == true) {
Mouse.OverrideCursor = Cursors.Wait;
await Task.Run(async () => {
try {
using var ctx = new AppDbContext();
var tbl = await CreditNoteData.ForPaymentVariant(ctx, year, avnr);
using var ods = new OdsFile(d.FileName);
await ods.AddTable(tbl);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
Mouse.OverrideCursor = null;
}
}
public static async Task<PaymentVar> CreatePaymentVariant(int year) {
return await Task.Run(async () => {
using var ctx = new AppDbContext();
var v = new PaymentVar {
Year = year,
AvNr = await ctx.NextAvNr(year),
Name = "Neue Auszahlungsvariante",
TestVariant = true,
DateString = $"{DateTime.Today:yyyy-MM-dd}",
Data = "{\"mode\": \"elwig\", \"version\": 1, \"payment\": {}, \"curves\": []}",
};
ctx.Add(v);
await ctx.SaveChangesAsync();
return v;
});
}
public static async Task<PaymentVar> Duplicate(this PaymentVar orig) {
return await Task.Run(async () => {
using var ctx = new AppDbContext();
var n = new PaymentVar {
Year = orig.Year,
AvNr = await ctx.NextAvNr(orig.Year),
Name = $"{orig.Name} (Kopie)",
TestVariant = true,
DateString = $"{DateTime.Today:yyyy-MM-dd}",
Data = orig.Data,
};
ctx.Add(n);
await ctx.SaveChangesAsync();
return n;
});
}
public static async Task<(int, int)> UpdatePaymentVariant(this PaymentVariantsViewModel vm, int? oldYear, int? oldAvNr) {
var year = oldYear ?? Utils.CurrentYear;
int avnr = 0;
await Task.Run(async () => {
var d = App.Config.Debug ? BillingData.FromJson(vm.Data ?? "{}") : vm.BillingData!;
d.ConsiderDelieryModifiers = vm.ConsiderModifiers;
d.ConsiderContractPenalties = vm.ConsiderPenalties;
d.ConsiderTotalPenalty = vm.ConsiderPenalty;
d.ConsiderAutoBusinessShares = vm.ConsiderAuto;
d.ConsiderCustomModifiers = vm.ConsiderCustom;
var modVal = vm.WeightModifier ?? 0;
d.NetWeightModifier = modVal > 0 ? modVal / 100.0 : 0;
d.GrossWeightModifier = modVal < 0 ? modVal / 100.0 : 0;
using var ctx = new AppDbContext();
var v = new PaymentVar {
Year = year,
AvNr = oldAvNr ?? await ctx.NextAvNr(year),
Name = vm.Name!,
DateString = $"{vm.Date!.Value:yyyy-MM-dd}",
TransferDate = vm.TransferDate,
TestVariant = vm.SelectedPaymentVariant?.TestVariant ?? true,
CalcTimeUnix = vm.SelectedPaymentVariant?.CalcTimeUnix,
Comment = string.IsNullOrEmpty(vm.Comment) ? null : vm.Comment,
Data = JsonSerializer.Serialize(d.Data),
};
avnr = v.AvNr;
ctx.Update(v);
await ctx.SaveChangesAsync();
});
vm.WeightModifierChanged = false;
return (year, avnr);
}
public static async Task DeletePaymentVariant(int year, int avnr) {
await Task.Run(async () => {
using var ctx = new AppDbContext();
var v = (await ctx.PaymentVariants.FindAsync(year, avnr))!;
ctx.Remove(v);
await ctx.SaveChangesAsync();
});
}
public static async Task Calculate(int year, int avnr) {
await Task.Run(async () => {
var b = new BillingVariant(year, avnr);
await b.Calculate();
});
}
public static async Task Commit(int year, int avnr) {
await Task.Run(async () => {
var b = new BillingVariant(year, avnr);
await b.Commit();
});
}
public static async Task Revert(int year, int avnr) {
await Task.Run(async () => {
var b = new BillingVariant(year, avnr);
await b.Revert();
});
}
}
}

View File

@@ -0,0 +1,277 @@
using Elwig.Helpers;
using Elwig.Helpers.Export;
using Elwig.Models.Entities;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows;
namespace Elwig.Services {
public static class SyncService {
public static readonly Expression<Func<Member, bool>> ChangedMembers = (m) => ((m.XTime == null && m.MTime > 1751328000) || m.MTime > m.XTime) && (m.ITime == null || m.MTime > m.ITime);
public static readonly Expression<Func<Delivery, bool>> ChangedDeliveries = (d) => ((d.XTime == null && d.MTime > 1751328000) || d.MTime > d.XTime) && (d.ITime == null || d.MTime > d.ITime);
public static async Task Upload(string url, string username, string password, IQueryable<Member> query, IEnumerable<string> filterNames) {
try {
var filename = $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip";
var path = Path.Combine(App.TempPath, filename);
var members = await query
.OrderBy(m => m.MgNr)
.Include(m => m.BillingAddress)
.Include(m => m.TelephoneNumbers)
.Include(m => m.EmailAddresses)
.Include(m => m.DefaultWbKg!.Gl)
.AsSplitQuery()
.ToListAsync();
var areaComs = await query
.SelectMany(m => m.AreaCommitments)
.OrderBy(c => c.MgNr).ThenBy(c => c.FbNr)
.Include(c => c.Rd)
.Include(c => c.Kg.Gl)
.ToListAsync();
var wbKgs = members
.Where(m => m.DefaultWbKg != null)
.Select(m => m.DefaultWbKg!)
.Union(areaComs.Select(c => c.Kg))
.Distinct()
.OrderBy(k => k.KgNr)
.ToList();
if (members.Count == 0) {
MessageBox.Show("Es wurden keine Mitglieder zum Hochladen ausgewählt!", "Mitglieder hochladen",
MessageBoxButton.OK, MessageBoxImage.Error);
} else {
var exportedAt = DateTime.Now;
await ElwigData.Export(path, members, areaComs, wbKgs, filterNames);
await Utils.UploadExportData(path, url, username, password);
await UpdateExportedAt(members, [], exportedAt);
MessageBox.Show($"Hochladen von {members.Count:N0} Mitgliedern erfolgreich!", "Mitglieder hochgeladen",
MessageBoxButton.OK, MessageBoxImage.Information);
}
} catch (HttpRequestException exc) {
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Mitglieder hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
} catch (TaskCanceledException exc) {
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Mitglieder hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
public static async Task Upload(string url, string username, string password, IQueryable<DeliveryPart> query, IEnumerable<string> filterNames) {
try {
var filename = $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip";
var path = Path.Combine(App.TempPath, filename);
var list = await query
.Select(p => p.Delivery)
.Distinct()
.Include(d => d.Parts).ThenInclude(p => p.PartModifiers).ThenInclude(m => m.Modifier)
.Include(d => d.Parts).ThenInclude(p => p.Rd)
.Include(d => d.Parts).ThenInclude(p => p.Kg!.Gl)
.AsSplitQuery()
.ToListAsync();
var wbKgs = list
.SelectMany(d => d.Parts)
.Where(p => p.Kg != null)
.Select(p => p.Kg!)
.DistinctBy(k => k.KgNr)
.OrderBy(k => k.KgNr)
.ToList();
if (list.Count == 0) {
MessageBox.Show("Es wurden keine Lieferungen zum Hochladen ausgewählt!", "Lieferungen hochladen",
MessageBoxButton.OK, MessageBoxImage.Error);
} else {
var exportedAt = DateTime.Now;
await ElwigData.Export(path, list, wbKgs, filterNames);
await Utils.UploadExportData(path, url, username, password);
await UpdateExportedAt([], list, exportedAt);
MessageBox.Show($"Hochladen von {list.Count:N0} Lieferungen erfolgreich!", "Lieferungen hochgeladen",
MessageBoxButton.OK, MessageBoxImage.Information);
}
} catch (HttpRequestException exc) {
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
} catch (TaskCanceledException exc) {
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
public static async Task UploadModified(string url, string username, string password) {
try {
var path = Path.Combine(App.TempPath, $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip");
List<Member> members;
List<AreaCom> areaComs;
List<Delivery> deliveries;
using (var ctx = new AppDbContext()) {
members = await ctx.Members
.Where(ChangedMembers)
.Include(m => m.BillingAddress)
.Include(m => m.TelephoneNumbers)
.Include(m => m.EmailAddresses)
.Include(m => m.DefaultWbKg!.Gl)
.OrderBy(m => m.MgNr)
.AsSplitQuery()
.ToListAsync();
areaComs = await ctx.Members
.Where(ChangedMembers)
.SelectMany(m => m.AreaCommitments)
.Include(c => c.Rd)
.Include(c => c.Kg.Gl)
.OrderBy(c => c.MgNr).ThenBy(c => c.FbNr)
.ToListAsync();
deliveries = await ctx.Deliveries
.Where(ChangedDeliveries)
.Include(d => d.Parts).ThenInclude(p => p.PartModifiers).ThenInclude(m => m.Modifier)
.Include(d => d.Parts).ThenInclude(p => p.Rd)
.Include(d => d.Parts).ThenInclude(p => p.Kg).ThenInclude(k => k!.Gl)
.OrderBy(d => d.DateString).ThenBy(d => d.TimeString).ThenBy(d => d.LsNr)
.AsSplitQuery()
.ToListAsync();
}
var wbKgs = members
.Where(m => m.DefaultWbKg != null)
.Select(m => m.DefaultWbKg!)
.Union(areaComs.Select(c => c.Kg))
.Union(deliveries.SelectMany(d => d.Parts)
.Where(p => p.Kg != null)
.Select(p => p.Kg!))
.DistinctBy(k => k.KgNr)
.OrderBy(k => k.KgNr)
.ToList();
if (members.Count == 0 && deliveries.Count == 0) {
MessageBox.Show("Es gibt keine geänderten Mitglieder oder Lieferungen, die hochgeladen werden könnten!", "Mitglieder und Lieferungen hochladen",
MessageBoxButton.OK, MessageBoxImage.Information);
} else {
var exportedAt = DateTime.Now;
await (new ElwigData.ElwigExport {
Members = (members, ["geändert seit letztem Export"]),
AreaComs = (areaComs, ["von exportierten Mitgliedern"]),
Deliveries = (deliveries, ["geändert seit letzem Export"]),
WbKgs = (wbKgs, ["von exportierten Mitgliedern, Flächenbindungen und Lieferungen"]),
}).Export(path);
await Utils.UploadExportData(path, url, username, password);
await UpdateExportedAt(members, deliveries, exportedAt);
MessageBox.Show($"Hochladen von {members.Count:N0} Mitgliedern und {deliveries.Count:N0} Lieferungen erfolgreich!", "Mitglieder und Lieferungen hochladen",
MessageBoxButton.OK, MessageBoxImage.Information);
}
} catch (HttpRequestException exc) {
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Mitglieder und Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
} catch (TaskCanceledException exc) {
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Mitglieder und Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Mitglieder und Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
public static async Task UploadBranchDeliveries(string url, string username, string password, int? year = null) {
try {
year ??= Utils.CurrentLastSeason;
var path = Path.Combine(App.TempPath, $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{App.ZwstId}.elwig.zip");
using var ctx = new AppDbContext();
var deliveries = await ctx.Deliveries
.Where(d => d.Year == year && d.ZwstId == App.ZwstId)
.Include(d => d.Parts).ThenInclude(p => p.PartModifiers).ThenInclude(m => m.Modifier)
.Include(d => d.Parts).ThenInclude(p => p.Rd)
.Include(d => d.Parts).ThenInclude(p => p.Kg).ThenInclude(k => k!.Gl)
.OrderBy(d => d.DateString).ThenBy(d => d.TimeString).ThenBy(d => d.LsNr)
.AsSplitQuery()
.ToListAsync();
var wbKgs = deliveries
.SelectMany(d => d.Parts)
.Where(p => p.Kg != null)
.Select(p => p.Kg!)
.DistinctBy(k => k.KgNr)
.ToList();
if (deliveries.Count == 0) {
MessageBox.Show("Es gibt keine Lieferungen, die hochgeladen werden können!", "Lieferungen hochladen",
MessageBoxButton.OK, MessageBoxImage.Error);
} else {
var exportedAt = DateTime.Now;
await ElwigData.Export(path, deliveries, wbKgs, [$"{year}", $"Zweigstelle {App.BranchName}"]);
await Utils.UploadExportData(path, url, username, password);
await UpdateExportedAt([], deliveries, exportedAt);
MessageBox.Show($"Hochladen von {deliveries.Count:N0} Lieferungen erfolgreich!", "Lieferungen hochladen",
MessageBoxButton.OK, MessageBoxImage.Information);
}
} catch (HttpRequestException exc) {
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
} catch (TaskCanceledException exc) {
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Lieferungen hochladen", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
public static async Task<List<(string Name, string Url)>> GetFilesToImport(string url, string username, string password) {
var data = await Utils.GetExportMetaData(url, username, password);
var files = data
.Select(f => new {
Name = f!["name"]!.AsValue().GetValue<string>(),
Timestamp = f!["timestamp"] != null && DateTime.TryParseExact(f!["timestamp"]!.AsValue().GetValue<string>(), "yyyy-MM-ddTHH:mm:ssK", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt) ? dt : (DateTime?)null,
ZwstId = f!["meta"]?["zwstid"]?.AsValue().GetValue<string>() ?? f!["zwstid"]?.AsValue().GetValue<string>(),
Device = f!["meta"]?["device"]?.AsValue().GetValue<string>(),
Url = f!["url"]!.AsValue().GetValue<string>(),
Size = f!["size"]!.AsValue().GetValue<long>(),
})
.Where(f => f.Timestamp >= new DateTime(Utils.CurrentLastSeason, 7, 1))
.ToList();
var imported = await ElwigData.GetImportedFiles();
return [.. files
.Where(f => f.Device != Environment.MachineName && !imported.Contains(f.Name))
.Select(f => (f.Name, f.Url))
];
}
public static async Task Download(string url, string username, string password) {
try {
var import = await GetFilesToImport(url, username, password);
var paths = new List<string>();
using (var client = Utils.GetHttpClient(username, password)) {
foreach (var f in import) {
var filename = Path.Combine(App.TempPath, f.Name);
using var stream = new FileStream(filename, FileMode.Create);
await client.DownloadAsync(f.Url, stream);
paths.Add(filename);
}
}
await ElwigData.Import(paths, ElwigData.ImportMode.FromBranches);
} catch (HttpRequestException exc) {
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Daten herunterladen", MessageBoxButton.OK, MessageBoxImage.Error);
} catch (TaskCanceledException exc) {
MessageBox.Show("Eventuell Internetverbindung prüfen!\n\n" + exc.Message, "Daten herunterladen", MessageBoxButton.OK, MessageBoxImage.Error);
} catch (Exception exc) {
MessageBox.Show(exc.Message, "Daten herunterladen", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private static async Task UpdateExportedAt(IEnumerable<Member> member, IEnumerable<Delivery> deliveries, DateTime dateTime) {
var timestamp = ((DateTimeOffset)dateTime.ToUniversalTime()).ToUnixTimeSeconds();
var mgnrs = string.Join(",", member.Select(m => $"{m.MgNr}").Append("0"));
var dids = string.Join(",", deliveries.Select(d => $"({d.Year},{d.DId})").Append("(0,0)"));
using (var cnx = await AppDbContext.ConnectAsync()) {
await cnx.ExecuteBatch($"""
BEGIN;
UPDATE client_parameter SET value = '0' WHERE param = 'ENABLE_TIME_TRIGGERS';
UPDATE member SET xtime = {timestamp} WHERE mgnr IN ({mgnrs});
UPDATE area_commitment SET xtime = {timestamp} WHERE mgnr IN ({mgnrs});
UPDATE delivery SET xtime = {timestamp} WHERE (year, did) IN ({dids});
UPDATE delivery_part SET xtime = {timestamp} WHERE (year, did) IN ({dids});
UPDATE client_parameter SET value = '1' WHERE param = 'ENABLE_TIME_TRIGGERS';
COMMIT;
""");
}
App.HintContextChange();
}
public static async Task<bool> ChangesAvailable(AppDbContext ctx, string url, string username, string password) {
return await ctx.Members.AnyAsync(ChangedMembers) || await ctx.Deliveries.AnyAsync(ChangedDeliveries) || (Utils.HasInternetConnectivity() && (await GetFilesToImport(url, username, password)).Count > 0);
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Elwig.ViewModels {
[ObservableProperty]
private string? _searchQuery = "";
public List<string> TextFilter {
get => [.. SearchQuery?.ToLower().Split(' ').ToList().FindAll(e => e.Length > 0)];
get => [.. SearchQuery?.ToLower().Split(' ').ToList().FindAll(e => e.Length > 0) ?? []];
set => SearchQuery = string.Join(' ', value.Where(e => e.Length > 0));
}

View File

@@ -18,7 +18,7 @@ namespace Elwig.ViewModels {
[ObservableProperty]
private string? _searchQuery = "";
public List<string> TextFilter => [.. SearchQuery?.ToLower().Split(' ').ToList().FindAll(e => e.Length > 0)];
public List<string> TextFilter => [.. SearchQuery?.ToLower().Split(' ').ToList().FindAll(e => e.Length > 0) ?? []];
[ObservableProperty]
private string? _lastScaleError;
@@ -169,10 +169,28 @@ namespace Elwig.ViewModels {
set => AcidString = $"{value:0.0}";
}
[ObservableProperty]
private bool _isLesewagen;
[ObservableProperty]
private bool? _isHandPicked;
public string? Unloading {
get => IsUnloadingDumper ? DeliveryPart.Dumper : IsUnloadingPumped ? DeliveryPart.Pumped : IsUnloadingBox ? DeliveryPart.Box : null;
set {
switch (value) {
case DeliveryPart.Dumper: IsUnloadingDumper = true; break;
case DeliveryPart.Pumped: IsUnloadingPumped = true; break;
case DeliveryPart.Box: IsUnloadingBox = true; break;
default: IsUnloadingOther = true; break;
}
}
}
[ObservableProperty]
private bool _isUnloadingDumper;
[ObservableProperty]
private bool _isUnloadingPumped;
[ObservableProperty]
private bool _isUnloadingBox;
[ObservableProperty]
private bool _isUnloadingOther;
[ObservableProperty]
private string _statusMembers = "-";
[ObservableProperty]

View File

@@ -10,7 +10,7 @@ namespace Elwig.ViewModels {
[ObservableProperty]
private string? _searchQuery = "";
public List<string> TextFilter => [.. SearchQuery?.ToLower().Split(' ').ToList().FindAll(e => e.Length > 0)];
public List<string> TextFilter => [.. SearchQuery?.ToLower().Split(' ').ToList().FindAll(e => e.Length > 0) ?? []];
[ObservableProperty]
private bool _filterOnlyUpcoming;

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