Compare commits

...
1142 Commits
Author SHA1 Message Date
lorenz.stechauner 859ef67407 [WIP] Bio Update
Test / Run tests (push) Successful in 3m7s
2026-09-29 16:07:59 +02:00
lorenz.stechauner e7c716e049 App: Add ShutdownNow() and ShutdownGracefully() 2026-09-29 16:07:53 +02:00
lorenz.stechauner c293fa0ac7 Helpers: Add Normalizer to normalize street names in addresses 2026-09-29 14:27:36 +02:00
lorenz.stechauner d2f1b4b94c BusinessDocument: Do not show billing address name when UseBillingAddress is false
Test / Run tests (push) Successful in 2m46s
2026-09-24 17:49:24 +02:00
lorenz.stechauner 70e2efad7c BusinessDocument: Add OrganicOperatorId to footer
Test / Run tests (push) Successful in 2m52s
2026-09-24 15:19:32 +02:00
lorenz.stechauner 4725ef7113 BusinessDocument: Add OrganicAuthorityCode and OrganicOperatorId to member Aside
Test / Run tests (push) Successful in 2m13s
2026-09-23 19:41:00 +02:00
lorenz.stechauner 38faf2c3f0 Entities/MemberHistory: Add CompanyHasSigned 2026-09-23 19:41:00 +02:00
lorenz.stechauner 9523e5c512 ClientParameters: Add ClientOrganicOperatorId 2026-09-23 19:40:39 +02:00
lorenz.stechauner e23b5e07dd Entities/WineCult: Add IsOrganic 2026-09-23 19:28:03 +02:00
lorenz.stechauner 0ed793d9be Entities/Member: Add OrganicOperatorId 2026-09-23 19:27:57 +02:00
lorenz.stechauner 569913fe8f Entities/Member: Add OrganicAuthorityCode 2026-09-22 11:21:05 +02:00
lorenz.stechauner d592fef352 BusinessDocument: Use more columns in Aside table
Test / Run tests (push) Successful in 2m5s
2026-09-22 11:19:41 +02:00
lorenz.stechauner f2b84053e6 DeliveryNote: Remove right padding for weight cells
Test / Run tests (push) Successful in 1m51s
2026-09-22 10:54:23 +02:00
lorenz.stechauner 7fef895491 DeliveryService: Add red/white distinction to tool tips
Test / Run tests (push) Successful in 1m53s
2026-09-16 17:28:38 +02:00
lorenz.stechauner 0e9121c462 TareCorrectionDialog: Allow actual tare to be null 2026-09-16 17:23:12 +02:00
lorenz.stechauner 15e191d74e Export/Database: Do not delete import source files 2026-09-16 17:12:52 +02:00
lorenz.stechauner abf06c558f Bump version to 1.1.2.2
Test / Run tests (push) Successful in 2m8s
Deploy / Build and Deploy (push) Successful in 2m55s
2026-09-14 17:10:23 +02:00
lorenz.stechauner 6436762e8d DeliveryAdminWindow: Show CorrectTareButton also for unloading type box
Test / Run tests (push) Successful in 2m11s
2026-09-14 16:40:16 +02:00
lorenz.stechauner 6441fa4f50 MemberAdminWindow: Add details in header and tool tips
Test / Run tests (push) Successful in 2m3s
2026-09-14 15:20:34 +02:00
lorenz.stechauner 6bed370eba Tests: Update dependencies
Test / Run tests (push) Successful in 2m13s
2026-09-14 14:53:39 +02:00
lorenz.stechauner 4cc33b56c8 Elwig: Update dependencies 2026-09-14 14:53:26 +02:00
lorenz.stechauner f4cc443521 DeliveryNote: Fix page breaks in delivery table
Test / Run tests (push) Successful in 2m12s
2026-09-14 14:45:39 +02:00
lorenz.stechauner 8ba051198f DeliveryAdminWindow: Add button to correct tare 2026-09-14 14:43:24 +02:00
lorenz.stechauner fea779bd95 Windows: Add filters to Titles in some AdministrationWindows
Test / Run tests (push) Successful in 2m21s
2026-09-12 22:59:57 +02:00
lorenz.stechauner e66780ccd0 DeliveryNote: Fix missing ':' for Comment and Acid 2026-09-12 22:48:28 +02:00
lorenz.stechauner cc01f92005 InteractionService: Add more detailed messages for SqliteException
Test / Run tests (push) Successful in 2m44s
2026-08-29 11:42:09 +02:00
lorenz.stechauner 0a93167ea7 Bump version to 1.1.2.1
Test / Run tests (push) Successful in 1m53s
Deploy / Build and Deploy (push) Successful in 1m51s
2026-08-27 11:33:48 +02:00
lorenz.stechauner 848a6d1ce0 MailWindow: Add Date header to email
Test / Run tests (push) Successful in 1m55s
2026-08-27 11:28:27 +02:00
lorenz.stechauner f0745eae4e Elwig: Fix LINQ queries containing DateTime.Today
Test / Run tests (push) Successful in 2m18s
2026-08-27 11:20:22 +02:00
lorenz.stechauner 8471ac9319 Bump version to 1.1.2.0
Test / Run tests (push) Successful in 2m34s
Deploy / Build and Deploy (push) Successful in 2m11s
2026-08-26 23:05:34 +02:00
lorenz.stechauner 7add96dfd5 Elwig: Update dependencies
Test / Run tests (push) Successful in 2m18s
2026-08-26 16:28:29 +02:00
lorenz.stechauner 2360402124 Tests: Update dependencies 2026-08-26 16:28:14 +02:00
lorenz.stechauner cadac4302d App: Add option to restore database when a DB error occurs at startup
Test / Run tests (push) Successful in 2m12s
2026-08-26 12:01:03 +02:00
lorenz.stechauner c1690bf104 DeliveryService: Fix GetFilterToday and add tests
Test / Run tests (push) Successful in 2m16s
2026-08-26 10:57:02 +02:00
lorenz.stechauner ca5b719630 Elwig: Add BackupService
Test / Run tests (push) Successful in 2m48s
2026-08-26 10:39:50 +02:00
lorenz.stechauner 163fe0fac8 Bump version to 1.1.1.2
Test / Run tests (push) Successful in 2m41s
Deploy / Build and Deploy (push) Successful in 1m52s
2026-08-25 20:36:03 +02:00
lorenz.stechauner edf3678154 Export/ElwigData: Fix detection of KGs without Großlage
Test / Run tests (push) Successful in 1m54s
2026-08-25 18:19:41 +02:00
lorenz.stechauner f450dc35b1 Weighing: Add option to opt-out of automatic datetime sync at startup 2026-08-25 18:19:31 +02:00
lorenz.stechauner 0c4611024f App: Add default weighing mode for Pamhagen
Test / Run tests (push) Successful in 1m57s
2026-08-25 16:31:33 +02:00
lorenz.stechauner 46e596ae23 Utils: Replace Utils.Today with DateTime.Today and adapt DeliveryService filters
Test / Run tests (push) Successful in 2m51s
2026-08-25 16:30:38 +02:00
lorenz.stechauner 3366cc25eb DeliveryAdminWindow: Change modifier handling for Matzen
Test / Run tests (push) Successful in 2m12s
2026-08-24 22:11:52 +02:00
lorenz.stechauner ff71e9fb7f DeliveryNote: Fix alignment of weights 2026-08-24 21:49:10 +02:00
lorenz.stechauner 7d5d7d9307 Bump version to 1.1.1.1
Test / Run tests (push) Successful in 2m20s
Deploy / Build and Deploy (push) Successful in 2m0s
2026-08-24 00:51:00 +02:00
lorenz.stechauner b468bbce31 MainWindow: Add individual commitments to Area Commitment export
Test / Run tests (push) Successful in 2m37s
2026-08-24 00:39:30 +02:00
lorenz.stechauner a79631c586 Bump version to 1.1.1.0
Test / Run tests (push) Successful in 3m10s
Deploy / Build and Deploy (push) Successful in 1m52s
2026-08-18 23:58:31 +02:00
lorenz.stechauner 20b6e11b7f Export/Bki: Honor vollablieferer flag for member
Test / Run tests (push) Successful in 2m44s
2026-08-18 16:26:29 +02:00
lorenz.stechauner 77e812f5e8 Bump version 1.1.0.3
Deploy / Build and Deploy (push) Successful in 2m1s
2026-08-17 23:53:32 +02:00
lorenz.stechauner 58bfb4f56d Tests: Update dependencies
Test / Run tests (push) Successful in 2m4s
2026-08-17 23:38:46 +02:00
lorenz.stechauner e6e140dc08 Elwig: Update dependencies 2026-08-17 23:38:43 +02:00
lorenz.stechauner eaddf0c3cb SyncService: Add distinction main/non-main device
Test / Run tests (push) Successful in 2m26s
2026-08-17 23:29:31 +02:00
lorenz.stechauner fc5ad82686 Printing: Print all copies as one single print job 2026-08-17 17:50:33 +02:00
lorenz.stechauner b76367f73a Bump version to 1.1.0.2
Test / Run tests (push) Successful in 1m56s
Deploy / Build and Deploy (push) Successful in 1m51s
2026-08-11 18:27:22 +02:00
lorenz.stechauner 76ea3513ac MainWindow: Delay email sending
Test / Run tests (push) Successful in 2m4s
2026-08-11 18:25:16 +02:00
lorenz.stechauner 07b197958d Printing: Try to duplex print with manual dialog
Test / Run tests (push) Successful in 2m12s
2026-08-11 18:09:51 +02:00
lorenz.stechauner 470c6e09e5 Printing: Change fit-to-printable-are to scale 1:1
Test / Run tests (push) Successful in 2m40s
2026-08-11 17:25:49 +02:00
lorenz.stechauner 20b19775b1 Printing: Use –FPDF_RenderPage instead of FPDF_RenderPageBitmap to allow vector graphics
Test / Run tests (push) Successful in 2m38s
2026-08-10 18:35:49 +02:00
lorenz.stechauner f13a836ebd Printing: Fix slight translocation to the bottom right
Test / Run tests (push) Successful in 3m2s
2026-08-10 13:10:10 +02:00
lorenz.stechauner 2bf926c84c Bump version to 1.1.0.1
Test / Run tests (push) Successful in 2m15s
Deploy / Build and Deploy (push) Successful in 1m59s
2026-08-05 17:17:27 +02:00
lorenz.stechauner a8cc3c9cb9 Elwig: Update dependencies
Test / Run tests (push) Successful in 1m52s
2026-08-05 10:33:47 +02:00
lorenz.stechauner 88616b2c0b MemberAdminWindow: Add icons to deliveries and area commitments buttons
Test / Run tests (push) Successful in 1m58s
2026-08-03 14:20:24 +02:00
lorenz.stechauner e6712732cd [#82] BaseDataWindow: Fixed error handling after first error to allow saving changes
Test / Run tests (push) Successful in 2m8s
2026-08-03 14:18:20 +02:00
lorenz.stechauner cfabf99c99 [#83] PaymentVariantsWindow: Automatically calculate when saving variant
Test / Run tests (push) Successful in 1m57s
2026-08-03 11:26:28 +02:00
lorenz.stechauner 668030fbff Tests: Update dependencies 2026-08-03 11:26:28 +02:00
lorenz.stechauner b445928c4d Elwig: Update dependencies 2026-08-03 11:26:28 +02:00
lorenz.stechauner 2a3999f300 Weighing: Set Date/Time of scales at startup 2026-08-03 11:26:23 +02:00
lorenz.stechauner 309f118739 Weighing: Differentiate between scale and terminal 2026-08-03 10:56:36 +02:00
lorenz.stechauner 0d8cdd5d6e Bump version to 1.1.0.0
Test / Run tests (push) Successful in 1m54s
Deploy / Build and Deploy (push) Successful in 1m42s
2026-07-09 01:43:38 +02:00
lorenz.stechauner 2350b55cea Tests: Update dependencies
Test / Run tests (push) Successful in 2m6s
2026-07-09 01:01:38 +02:00
lorenz.stechauner e8789aff37 Elwig: Update dependencies 2026-07-09 01:01:38 +02:00
lorenz.stechauner 918555f2b8 [#20] MemberAdminWindow: Add MemberBusinessSharesAdminWindow to manage member shares 2026-07-09 01:01:28 +02:00
lorenz.stechauner 1169ba5101 ChartWindow: Fix warnings and avoid null-forgiving operator
Test / Run tests (push) Successful in 2m10s
2026-07-06 19:11:39 +02:00
lorenz.stechauner 76da392f5f [#20][#80] MemberAdminWindow: Update business share status bar tool tip
Test / Run tests (push) Successful in 2m1s
2026-07-06 18:18:46 +02:00
lorenz.stechauner 000117dc67 CreditNoteData: Add AccountingNr of member
Test / Run tests (push) Successful in 3m6s
2026-07-06 11:03:38 +02:00
lorenz.stechauner 314a0d37a8 [#20] MemberDataSheet: Show members business share history 2026-07-03 12:02:44 +02:00
lorenz.stechauner 765edbdae6 [#20][#80] MemberAdminWindow: Add functionality to edit other business shares 2026-07-03 12:02:44 +02:00
lorenz.stechauner 9a0a422c8a [#20] Entities/MemberHistory: Add DeductYear to simplify billing 2026-07-03 12:02:37 +02:00
lorenz.stechauner a71c41af5c AppDbContext: Add Member.Branch AutoInclude to fix MemberDataSheet
Test / Run tests (push) Successful in 2m52s
2026-07-02 12:17:32 +02:00
lorenz.stechauner 62346a63a8 [#80] Elwig: Add client parameter to switch between simple and red/white business share mode
Test / Run tests (push) Successful in 2m20s
2026-07-01 17:09:55 +02:00
lorenz.stechauner feee6ad1ec [#20][#80] Elwig: Update member_history and add different types of shares
Test / Run tests (push) Successful in 2m0s
2026-07-01 16:22:01 +02:00
lorenz.stechauner 4ebe07f579 AppDbUpdater: Fix handling of foreign key errors
Test / Run tests (push) Successful in 3m4s
2026-07-01 00:36:04 +02:00
lorenz.stechauner beacba6bd9 Services: Add Utils.RunForeground() to factor Task.Run and try-catch-Blocks out
Test / Run tests (push) Successful in 2m2s
2026-06-30 02:32:24 +02:00
lorenz.stechauner 69efca1cc3 Tests: Use Assert.EnterMultipleScope instead of Assert.Multiple
Test / Run tests (push) Successful in 2m6s
2026-06-29 02:33:36 +02:00
lorenz.stechauner fcd0555e4d Elwig: Add InteractionService to centrally manage MessageBox and SaveFileDialogs
Test / Run tests (push) Successful in 2m50s
2026-06-29 02:28:34 +02:00
lorenz.stechauner b93e987685 Bump version to 1.0.5.6
Deploy / Build and Deploy (push) Successful in 1m58s
Test / Run tests (push) Successful in 2m24s
2026-06-25 13:07:10 +02:00
lorenz.stechauner 1ad97a78ff ClientParameters: Add client Seewinkel
Test / Run tests (push) Successful in 3m13s
2026-06-25 12:33:20 +02:00
lorenz.stechauner 1261be001c BaseDataWindow: Remove transactions inside *Save() functions to avoid nesting them 2026-06-25 12:32:46 +02:00
lorenz.stechauner 15de07a4c3 BillingData: Remove quality entry when using –FromGraphEntries
Test / Run tests (push) Successful in 2m34s
2026-06-24 15:51:37 +02:00
lorenz.stechauner 5cbad9a195 Bump version to 1.0.5.5
Test / Run tests (push) Successful in 6m43s
Deploy / Build and Deploy (push) Successful in 2m10s
2026-06-24 14:36:44 +02:00
lorenz.stechauner 4e027a9add Elwig: Update dependencies
Test / Run tests (push) Successful in 4m40s
2026-06-24 14:24:25 +02:00
lorenz.stechauner f32ff945ec BillingData: Preserve all settings when using FromGraphEntries() 2026-06-24 14:24:15 +02:00
lorenz.stechauner ba3f66591e BilllingVariant: Allow for credits with negative value 2026-06-24 13:20:46 +02:00
lorenz.stechauner 3f65b2350b PaymentVariantService: Fix crash in GenerateEbics()
Test / Run tests (push) Successful in 2m48s
2026-06-24 10:36:28 +02:00
lorenz.stechauner 7a6a77419b Bump version to 1.0.5.4
Test / Run tests (push) Successful in 2m17s
Deploy / Build and Deploy (push) Successful in 2m6s
2026-06-22 20:23:46 +02:00
lorenz.stechauner 7a993585c3 Elwig: Update dependencies
Test / Run tests (push) Successful in 2m3s
2026-06-22 20:08:18 +02:00
lorenz.stechauner d0ce377d92 AppDbContext: Fix FetchMembers when using for specific MgNr
Test / Run tests (push) Successful in 2m59s
2026-06-22 20:06:34 +02:00
lorenz.stechauner f8b3147c72 Tests: Update dependencies
Test / Run tests (push) Successful in 2m52s
2026-06-18 12:25:22 +02:00
lorenz.stechauner b4ab8349b4 Elwig: Update dependencies 2026-06-18 12:25:09 +02:00
lorenz.stechauner 5db5876905 Installer: Bundle and ship source code together with binaries
Test / Run tests (push) Successful in 3m14s
2026-05-01 15:10:19 +02:00
lorenz.stechauner 0617726139 Bump version to 1.0.5.3
Test / Run tests (push) Successful in 2m7s
Deploy / Build and Deploy (push) Successful in 2m13s
2026-04-29 11:42:05 +02:00
lorenz.stechauner 9dfe71d6d0 DeliveryConfirmation: Adapt spacing between rows to CreditNote
Test / Run tests (push) Successful in 2m52s
2026-04-29 11:37:09 +02:00
lorenz.stechauner 72155fc54e Documents: Fix error because of modifiers in CreditNote and DeliveryConfirmation 2026-04-29 11:29:16 +02:00
lorenz.stechauner 61bdb2f3b5 Bump version to 1.0.5.2
Test / Run tests (push) Successful in 2m15s
Deploy / Build and Deploy (push) Successful in 2m0s
2026-04-24 10:27:37 +02:00
lorenz.stechauner 2915f08a6b Elwig: Update dependencies
Test / Run tests (push) Successful in 3m7s
2026-04-24 09:49:06 +02:00
lorenz.stechauner 5c2fae1855 DeliveryAdminWindow: Fix attributes and cultivations in DeliveryPartList 2026-04-24 09:48:14 +02:00
lorenz.stechauner 95fabe3633 Bump version to 1.0.5.1
Test / Run tests (push) Successful in 2m25s
Deploy / Build and Deploy (push) Successful in 2m14s
2026-04-20 19:23:01 +02:00
lorenz.stechauner 7861a15e2e Test: Update dependencies
Test / Run tests (push) Successful in 2m20s
2026-04-20 15:12:29 +02:00
lorenz.stechauner 20fd5d5826 Elwig: Update dependencies 2026-04-20 15:12:29 +02:00
lorenz.stechauner 2b3c293730 Database: Adapt to changes for examples in WLNS 2026-04-20 15:12:22 +02:00
lorenz.stechauner a852dbb242 PaymentVariantSummary: Fix price display when null 2026-04-20 15:05:09 +02:00
lorenz.stechauner 7edf395497 CreditNote: Fix padding for Rbl. 2026-04-20 14:21:13 +02:00
lorenz.stechauner ea2b6db1fc Billing: Fix modifiers with absolute amounts 2026-04-20 14:20:43 +02:00
lorenz.stechauner cf48f005e2 DeliveryAdminWindow: Fix missing LostFocus for gradation inputs
Test / Run tests (push) Successful in 2m54s
2026-04-08 17:18:40 +02:00
lorenz.stechauner 4eac8cd629 DeliveryAdminWindow: Fix modifiers for bulk actions 2026-04-08 17:18:08 +02:00
lorenz.stechauner 360856585a Bump version to 1.0.5.0
Test / Run tests (push) Successful in 1m54s
Deploy / Build and Deploy (push) Successful in 2m14s
2026-04-08 00:47:15 +02:00
lorenz.stechauner cc6e31a006 Installer/Setup: Update to WiX 7 2026-04-08 00:31:00 +02:00
lorenz.stechauner ce1a55df86 Tests: Update dependencies
Test / Run tests (push) Successful in 2m1s
2026-04-07 23:57:52 +02:00
lorenz.stechauner 07d93dd384 Elwig: Update dependencies 2026-04-07 23:57:40 +02:00
lorenz.stechauner f6e9d429d5 [#77] Export/ElwigData: Fix import of AreaComContracts
Test / Run tests (push) Successful in 2m41s
2026-04-07 23:51:32 +02:00
lorenz.stechauner 7b78f9d6b9 [#79] Windows: Fix errors unconvered by E2E tests
Test / Run tests (push) Successful in 2m12s
2026-04-07 12:34:38 +02:00
lorenz.stechauner 22fbb0772f MailWindow: Fix MessageBox and ProgressBar
Test / Run tests (push) Successful in 2m11s
2026-04-07 12:16:50 +02:00
lorenz.stechauner 0a9c800116 BusinessDocument: Add DateFrom to be used for ShowDateAndLocation
Test / Run tests (push) Successful in 2m0s
2026-04-07 12:06:06 +02:00
lorenz.stechauner 278d79429b [#79] DeliveryAdminWindow: Cache modifiers 2026-04-07 11:15:56 +02:00
lorenz.stechauner e5e5e10cd7 ContextWindow: Add HasContextLoaded property 2026-04-07 11:15:34 +02:00
lorenz.stechauner d051a2bfcf [#79] AppDbContext: Use compiled queries 2026-04-07 11:15:14 +02:00
lorenz.stechauner 9c39a2f820 [#79] Entities: Remove EF proxies 2026-04-07 11:14:20 +02:00
lorenz.stechauner 4460de9975 [#77] Entities: Add AreaComContract to group area commitments together 2026-04-07 11:13:54 +02:00
lorenz.stechauner f96ebdcf60 DeliveryAdminWindow: Fix creation of new deliveries
Test / Run tests (push) Successful in 2m2s
2026-04-02 20:58:01 +02:00
lorenz.stechauner e593175e72 ContextWindow: Use Task.Run to load data outside main thread
Test / Run tests (push) Successful in 2m29s
2026-04-02 14:18:06 +02:00
lorenz.stechauner 1f4fe2129d Bump version to 1.0.4.1
Deploy / Build and Deploy (push) Successful in 1m49s
2026-03-27 12:29:28 +01:00
lorenz.stechauner cdb4b0a2bd Tests: Update dependencies
Test / Run tests (push) Successful in 1m54s
2026-03-27 12:19:39 +01:00
lorenz.stechauner ddfc86197d Elwig: Update dependencies 2026-03-27 12:19:31 +01:00
lorenz.stechauner ae0a082421 MailWindow: Fail silently when disposing documents
Test / Run tests (push) Successful in 1m49s
2026-03-27 12:16:41 +01:00
lorenz.stechauner 3c52156b7e Documents: Fix letterhead and other problems
Test / Run tests (push) Successful in 2m39s
2026-03-27 12:06:19 +01:00
lorenz.stechauner 982a6616e1 DeliveryNote: Set date to be mtime
Test / Run tests (push) Successful in 2m36s
2026-03-19 20:37:18 +01:00
lorenz.stechauner b1075d1e69 DeliveryAdminWindow: Ensure that bulk actions are not performed while editing or creating
Test / Run tests (push) Successful in 2m1s
2026-03-16 22:58:35 +01:00
lorenz.stechauner cc018ded10 ContextWindow: Use EnsureContextRenewed() correctly 2026-03-16 22:58:04 +01:00
lorenz.stechauner 0aefab5d63 DeliveryConfirmation: Fix text alignment
Test / Run tests (push) Successful in 2m3s
2026-03-16 19:47:33 +01:00
lorenz.stechauner 149f455256 Bump version to 1.0.4.0
Test / Run tests (push) Successful in 1m44s
Deploy / Build and Deploy (push) Successful in 2m3s
2026-03-16 19:14:32 +01:00
lorenz.stechauner f4fddd111f Tests: Update dependencies
Test / Run tests (push) Successful in 1m46s
2026-03-16 18:47:08 +01:00
lorenz.stechauner af73226c90 Elwig: Update dependencies 2026-03-16 18:46:59 +01:00
lorenz.stechauner 7e6a6138e2 [#78] DeliveryService: Add bulk actions for attributes and modifiers
Test / Run tests (push) Successful in 1m42s
2026-03-16 18:44:23 +01:00
lorenz.stechauner 8054a024f4 Documents: Replace Razor templates with iText
Test / Run tests (push) Successful in 2m24s
2026-03-16 18:40:30 +01:00
lorenz.stechauner d8c967b2f2 Printing: Replace WinziPrint with iText
Test / Run tests (push) Failing after 2m6s
2026-02-25 10:58:37 +01:00
lorenz.stechauner 1108427023 [#74] Weighing: Try to reconnect tcp scales on first error
Test / Run tests (push) Successful in 3m0s
2026-02-25 10:57:44 +01:00
lorenz.stechauner b58dee6d3f Bump version to 1.0.3.4
Deploy / Build and Deploy (push) Successful in 2m28s
2026-02-19 19:33:22 +01:00
lorenz.stechauner 0e2b004b0d Elwig: Update dependencies
Test / Run tests (push) Successful in 2m46s
2026-02-19 19:26:39 +01:00
lorenz.stechauner 19c3322ef2 Tests: Update dependencies 2026-02-19 19:26:29 +01:00
lorenz.stechauner 6f081811c4 MailWindow: Allow users to double click on avaiable/selected documents
Test / Run tests (push) Successful in 2m58s
2026-02-19 18:10:26 +01:00
lorenz.stechauner 432c511b85 Billing: Create credit notes only for members who receive a payment 2026-02-19 18:10:22 +01:00
lorenz.stechauner 7e22759c33 PaymentVariantsWindow: Never lock seasons
Test / Run tests (push) Successful in 2m27s
2026-02-19 16:12:54 +01:00
lorenz.stechauner a47904cf0b SyncService: Catch exceptions in ChangesAvailable()
Test / Run tests (push) Successful in 3m16s
2026-02-19 15:59:23 +01:00
lorenz.stechauner 6818491ae3 Export: Fix importing delivery parts with no kgnr 2026-02-19 15:58:51 +01:00
lorenz.stechauner 23db4de1ee Database: Fix xtime/mtime for delivery parts
Test / Run tests (push) Successful in 3m17s
2026-02-19 15:31:36 +01:00
lorenz.stechauner 9e5f709d42 DeliveryConfirmation: Show 'Davon abgewertet' below total sum 2026-02-19 15:26:09 +01:00
lorenz.stechauner 4cd7ef85a1 Printing: Replace PdfiumViewer with native pdfium.dll binary
Test / Run tests (push) Successful in 3m47s
2026-02-18 23:02:18 +01:00
lorenz.stechauner 2c0b000073 MainWindow: Fix Tooltip for 'Sorten-/Qual.aufschlüssel.' Button
Test / Run tests (push) Successful in 2m31s
2026-02-18 23:01:35 +01:00
lorenz.stechauner f1737af2a2 Bump version to 1.0.3.3
Test / Run tests (push) Successful in 2m26s
Deploy / Build and Deploy (push) Successful in 2m21s
2026-02-05 23:52:33 +01:00
thomas.hilscher 8f423e3e92 Pdfium: Use updated fork of PdfiumViewer
Test / Run tests (push) Successful in 2m46s
2026-02-05 23:21:28 +01:00
thomas.hilscher c8911c0acb Pdfium: Explicitly add WindowsForms after dotnet 10 upgrade
Test / Run tests (push) Successful in 3m5s
2026-02-05 22:50:06 +01:00
lorenz.stechauner baf598b76c Bump version to 1.0.3.2
Test / Run tests (push) Successful in 3m1s
Deploy / Build and Deploy (push) Successful in 2m3s
2026-02-04 16:34:52 +01:00
lorenz.stechauner 7adc9f89e4 Printing/Html: Try to hotfix null pointer error
Test / Run tests (push) Has been cancelled
2026-02-04 16:33:21 +01:00
lorenz.stechauner dde808df6a Bump version to 1.0.3.1
Test / Run tests (push) Successful in 2m28s
Deploy / Build and Deploy (push) Successful in 2m16s
2026-01-19 10:58:10 +01:00
lorenz.stechauner e63de0d867 Setup: Try to hotfix VC redist version incompatability
Deploy / Build and Deploy (push) Failing after 44s
2026-01-19 10:52:53 +01:00
lorenz.stechauner e9e90b1e98 Bump version to 1.0.3.0
Test / Run tests (push) Successful in 3m4s
Deploy / Build and Deploy (push) Successful in 2m8s
2026-01-16 14:43:52 +01:00
lorenz.stechauner 640dbf705e Printing/Pdf: Fix detection of WinziPrint.exe to isolate tests properly
Test / Run tests (push) Successful in 2m11s
2026-01-16 01:26:35 +01:00
lorenz.stechauner 42121fe7da Elwig: Update dependencies
Test / Run tests (push) Has been cancelled
2026-01-16 00:35:50 +01:00
lorenz.stechauner d45c3f867f Tests: Update dependencies 2026-01-16 00:35:38 +01:00
lorenz.stechauner a90be2644d [#50] MailWindow: Add button to cancel document generation
Test / Run tests (push) Has been cancelled
2026-01-16 00:22:11 +01:00
lorenz.stechauner 01739ba42e ClientParameters: Fix storing of MailSendPostal and MailSendEmail 2026-01-16 00:20:15 +01:00
lorenz.stechauner 7bea4d9ee0 BillingVariant: Fix NULL/0 error introduced in c7a2f2241d
Test / Run tests (push) Successful in 2m12s
2026-01-15 17:56:12 +01:00
lorenz.stechauner b6497fd422 PaymentVariantsWindow: Select last variant by default and ask user before deleting
Test / Run tests (push) Successful in 2m33s
2026-01-15 14:03:00 +01:00
lorenz.stechauner f141485537 Documents: Only show header and footer on BusinessDocuments 2026-01-15 14:01:21 +01:00
lorenz.stechauner b31603554a Document: Add IsPreview to indicate that a document may only be viewed by internal staff
Test / Run tests (push) Successful in 2m51s
2026-01-15 13:26:44 +01:00
lorenz.stechauner 90def81cc5 PaymentVariantSummary: Fix statistical sum for geb weight
Test / Run tests (push) Successful in 2m52s
2026-01-14 09:15:27 +01:00
lorenz.stechauner 87903227b5 [#66] Services: Add filters for import/export/upload 2026-01-03 20:49:00 +01:00
lorenz.stechauner 6d6776f0f9 DeliveryAdminWindow: Complete tooltip in search bar and warning when no season is found 2026-01-03 20:48:39 +01:00
lorenz.stechauner bf3cc2ea1e [#71] Weighing: Fix reconnection behaviour when COM port is connected/disconnected 2026-01-03 20:48:39 +01:00
lorenz.stechauner c1697dc4f3 [#66] MainWindow: Merge both sync buttons into a single one 2026-01-03 20:47:51 +01:00
lorenz.stechauner 3b335a568e AbbDbContext: Move SqliteConnection extensions to Extensions class 2026-01-03 20:47:45 +01:00
lorenz.stechauner beacdab54f [#66] Services: Add SyncService 2026-01-03 20:47:14 +01:00
lorenz.stechauner da05a49e10 DeliveryService: Add modifiers when splitting delivery parts
Test / Run tests (push) Successful in 2m31s
2025-12-18 18:12:14 +01:00
lorenz.stechauner 5cec5b3556 QueryWindow: Allow users to export query result to csv file
Test / Run tests (push) Successful in 1m47s
2025-12-16 16:55:52 +01:00
lorenz.stechauner 9af498287d Database: Add v_member view
Test / Run tests (push) Successful in 2m32s
2025-12-16 15:58:45 +01:00
lorenz.stechauner 452f246f24 [#73] DeliveryAdminWindow: Add Liefermengen Excel output 2025-12-15 00:03:45 +01:00
lorenz.stechauner e97c29db43 Dtos: Rename MemberDeliveryPerVarietyData to MemberDeliveryYieldsPerVarietyData 2025-12-14 23:14:40 +01:00
lorenz.stechauner 3419113dec DeliveryAdminWindow: Restrict handling of modifiers to specific clients
Test / Run tests (push) Successful in 2m19s
2025-12-13 08:30:10 +01:00
lorenz.stechauner bf6297f63b DeliveryPart: Add Unloading type instead of IsLesewagen
Test / Run tests (push) Successful in 1m54s
2025-12-12 12:35:48 +01:00
lorenz.stechauner f228ba3019 AppDbContext: Fix silent fail in ExecuteBatch() 2025-12-11 16:08:11 +01:00
lorenz.stechauner 495aa8a691 DeliveryNote: Add option to redact modifier value
Test / Run tests (push) Successful in 2m26s
2025-12-11 13:05:40 +01:00
lorenz.stechauner 811916a8b9 Controls/CheckComboBox: Apply AllItemsSelectedContent only when more than 1 items are selected 2025-12-11 13:05:40 +01:00
lorenz.stechauner 3b6333a6a2 Tests: Update dependencies
Test / Run tests (push) Successful in 2m22s
2025-11-29 12:34:33 +01:00
lorenz.stechauner 9b37330362 Elwig: Update dependencies 2025-11-29 12:34:33 +01:00
lorenz.stechauner 889a17b21c Upgrade to .NET 10 2025-11-29 12:34:33 +01:00
lorenz.stechauner ac6d559e5d Helpers/Utils: Fix mail log for single mails 2025-11-29 12:34:29 +01:00
lorenz.stechauner 6d80cca241 Bump version to 1.0.2.0
Test / Run tests (push) Successful in 1m44s
Deploy / Build and Deploy (push) Successful in 1m36s
2025-11-10 00:15:46 +01:00
lorenz.stechauner 9dc225d3e4 Export/VCard: Add Escape method
Test / Run tests (push) Successful in 1m54s
2025-11-10 00:13:53 +01:00
lorenz.stechauner 0d513f7bff Tests: Update dependencies
Test / Run tests (push) Successful in 2m3s
2025-11-09 23:49:42 +01:00
lorenz.stechauner b10c744bf9 Elwig: Update dependencies 2025-11-09 23:49:33 +01:00
lorenz.stechauner e6367da286 App: Add SerialPortWatcher
Test / Run tests (push) Has been cancelled
2025-11-09 23:47:38 +01:00
lorenz.stechauner 01f4480a08 MemberAdminWindow: Add feature to export .vcf files
Test / Run tests (push) Successful in 1m42s
2025-10-31 17:19:09 +01:00
lorenz.stechauner e9722c790c Bump version to 1.0.1.5
Test / Run tests (push) Successful in 1m45s
Deploy / Build and Deploy (push) Successful in 1m42s
2025-10-29 08:16:59 +01:00
lorenz.stechauner af98c32026 MailWindow: Fix crash when using Button in Leseabschluss
Test / Run tests (push) Has been cancelled
2025-10-29 08:14:37 +01:00
lorenz.stechauner 7300b30cf5 Bump version to 1.0.1.4
Test / Run tests (push) Successful in 1m44s
Deploy / Build and Deploy (push) Successful in 1m55s
2025-10-28 13:17:58 +01:00
lorenz.stechauner 428cd6ddc2 BillingVariant: Assume price to be 0 for undefined vaributes
Test / Run tests (push) Successful in 1m56s
2025-10-28 13:15:24 +01:00
lorenz.stechauner 2de8af878b MailWindow: Fix crash when generating docs for no-email and postal-where-no-email
Test / Run tests (push) Successful in 2m31s
2025-10-28 11:37:13 +01:00
lorenz.stechauner 34d95eab9d Bump version to 1.0.1.3
Test / Run tests (push) Successful in 1m47s
Deploy / Build and Deploy (push) Successful in 1m43s
2025-10-13 20:40:55 +02:00
lorenz.stechauner 548aeb2ce9 DeliveryAdminWindow: Show delivery (part) comments in list 2025-10-13 20:39:09 +02:00
lorenz.stechauner 7edd888aa2 DeliveryAncmtAdminWindow: Increase performace by aggregating AnnouncedWeight
Test / Run tests (push) Successful in 2m23s
2025-10-07 20:41:15 +02:00
lorenz.stechauner a0d4f19f30 DeliveryAdminWindow: Include attribute and cultivation in wine variety column
Test / Run tests (push) Successful in 2m24s
2025-10-05 21:21:03 +02:00
lorenz.stechauner 67ba342c28 App: Delay auto update check even more
Test / Run tests (push) Successful in 2m15s
2025-09-29 16:14:16 +02:00
lorenz.stechauner 1b69fcb16a Bump version to 1.0.1.2
Test / Run tests (push) Successful in 1m48s
Deploy / Build and Deploy (push) Successful in 1m40s
2025-09-25 22:51:35 +02:00
lorenz.stechauner c8a95422af Export/Database: Add version and meta.json entry to zip export
Test / Run tests (push) Successful in 2m13s
2025-09-25 21:08:11 +02:00
lorenz.stechauner 9d02f18bac ElwigData: Fail more gracefully when single files may not be processed
Test / Run tests (push) Successful in 1m47s
2025-09-25 12:08:43 +02:00
lorenz.stechauner f9ee2cb120 Bump version to 1.0.1.1
Test / Run tests (push) Successful in 1m43s
Deploy / Build and Deploy (push) Successful in 1m38s
2025-09-21 22:15:47 +02:00
lorenz.stechauner b27b89f599 DeliveryAdminWindow: Improve responsiveness by caching wine quality levels
Test / Run tests (push) Successful in 1m54s
2025-09-21 22:09:10 +02:00
lorenz.stechauner bfbd0a6a22 README: Add about sections 2025-09-21 15:15:57 +02:00
lorenz.stechauner e2de7a1f1c DeliveryAdminWindow: Unselect wine quality level on invalid gradation 2025-09-19 10:33:19 +02:00
lorenz.stechauner 3f769eb7d7 Bump version to 1.0.1.0
Test / Run tests (push) Successful in 1m58s
Deploy / Build and Deploy (push) Successful in 1m53s
2025-09-18 23:44:15 +02:00
lorenz.stechauner 8bc053053c App: Add auto update check on network change to active
Test / Run tests (push) Successful in 1m55s
2025-09-18 23:37:10 +02:00
lorenz.stechauner a0dcaf7b4f Elwig: Update database to include new BKI wine varieties
Test / Run tests (push) Successful in 2m43s
2025-09-18 23:29:38 +02:00
lorenz.stechauner 844fc5217a Utils: Add User-Agent to default http client
Test / Run tests (push) Successful in 1m53s
2025-09-17 15:09:28 +02:00
lorenz.stechauner 542afa5892 Bump version to 1.0.0.6
Test / Run tests (push) Successful in 1m38s
Deploy / Build and Deploy (push) Successful in 1m37s
2025-09-17 09:58:20 +02:00
lorenz.stechauner 9e02b15ff1 Elwig: Fix extensions in SaveFileDialog
Test / Run tests (push) Successful in 1m49s
2025-09-17 09:55:19 +02:00
lorenz.stechauner 463769b549 App: Fix auto selection of weighing mode
Test / Run tests (push) Successful in 2m17s
2025-09-17 09:33:34 +02:00
lorenz.stechauner 2c383d0c55 AboutWindow: Fix typo in description 2025-09-17 09:33:11 +02:00
lorenz.stechauner 5ba74c8368 Bump version to 1.0.0.5
Test / Run tests (push) Successful in 1m40s
Deploy / Build and Deploy (push) Successful in 1m36s
2025-09-15 23:14:49 +02:00
lorenz.stechauner 3c9b3c2db1 Windows: Add AboutWindow
Test / Run tests (push) Successful in 1m39s
2025-09-15 23:09:52 +02:00
lorenz.stechauner 98f8907817 Elwig: Update dependencies
Test / Run tests (push) Successful in 1m56s
2025-09-15 21:37:12 +02:00
lorenz.stechauner 44dcc5e19f Setup: Update dependencies 2025-09-15 21:36:57 +02:00
lorenz.stechauner d7012ebfa1 Config: Add option to specify weighing mode
Test / Run tests (push) Successful in 2m8s
2025-09-15 21:16:36 +02:00
lorenz.stechauner a9b5317e79 DeliveryAdminWindow: Make GerebeltGewogenInput required in Matzen
Test / Run tests (push) Successful in 1m49s
2025-09-15 11:28:57 +02:00
lorenz.stechauner f02598760f Export: Implement exporting and importing of sqlite3 and sql files
Test / Run tests (push) Successful in 2m15s
2025-09-15 10:53:27 +02:00
lorenz.stechauner 4b8cd2a0d7 Bump version to 1.0.0.4
Test / Run tests (push) Successful in 1m40s
Deploy / Build and Deploy (push) Successful in 1m45s
2025-09-01 22:03:46 +02:00
lorenz.stechauner 104798d4f2 DeliveryAdminWindow: Enable WineQualityLevelInput in Übernahme
Test / Run tests (push) Successful in 2m14s
2025-09-01 21:55:22 +02:00
lorenz.stechauner 4653a4f129 LogWindow: Improve loading time
Test / Run tests (push) Successful in 1m46s
2025-08-20 16:33:25 +02:00
lorenz.stechauner 07f9a0f522 Utils: Fix thread error when sending emails
Test / Run tests (push) Successful in 2m27s
2025-08-19 15:42:47 +02:00
lorenz.stechauner 7e21d9e2e4 Bump version to 1.0.0.3
Deploy / Build and Deploy (push) Successful in 1m42s
2025-08-11 23:01:15 +02:00
lorenz.stechauner 4e2eca295d Tests: Update dependencies
Test / Run tests (push) Successful in 1m45s
2025-08-11 22:50:12 +02:00
lorenz.stechauner 1f165055c1 Elwig: Update dependencies 2025-08-11 22:50:12 +02:00
lorenz.stechauner 7b953fa73e [#70] ElwigData: Sync WbKg 2025-08-11 22:50:04 +02:00
lorenz.stechauner d3157e4d48 ElwigData: Check zip file integrity
Test / Run tests (push) Successful in 1m59s
2025-08-06 12:31:19 +02:00
lorenz.stechauner 73fe4531cc [#69] Elwig: Add functionality to upload and replace database
Test / Run tests (push) Successful in 2m23s
2025-08-06 10:50:33 +02:00
lorenz.stechauner b6c03892b1 Helpers: Fix Issues for importing/exporting
Test / Run tests (push) Successful in 1m58s
2025-08-05 23:45:59 +02:00
lorenz.stechauner 91950ad9fd Bump version to 1.0.0.2
Test / Run tests (push) Successful in 1m55s
Deploy / Build and Deploy (push) Successful in 1m39s
2025-08-05 19:19:45 +02:00
lorenz.stechauner 77c3f388e7 Elwig: Add SQLitePCLRaw.bundle_e_sqlite3 explicitly
Test / Run tests (push) Successful in 1m52s
Deploy / Build and Deploy (push) Failing after 12s
2025-08-05 19:06:09 +02:00
lorenz.stechauner 6f3e1a8905 Bump version to 1.0.0.1
Test / Run tests (push) Successful in 1m54s
Deploy / Build and Deploy (push) Successful in 1m49s
2025-08-05 18:57:43 +02:00
lorenz.stechauner 466c8a322c Elwig: Update dependencies
Test / Run tests (push) Successful in 2m28s
2025-08-05 18:35:24 +02:00
lorenz.stechauner ab7c7404e2 ElwigData: Update error message for user 2025-08-05 18:33:33 +02:00
lorenz.stechauner ea3ea5ca83 Bump version to 1.0.0.0
Deploy / Build and Deploy (push) Successful in 2m29s
2025-07-30 17:37:25 +02:00
lorenz.stechauner 93e920f61a Add GNU GPLv3 as license 2025-07-29 23:53:53 +02:00
lorenz.stechauner 9b48242f0e Setup: Update dependencies 2025-07-29 23:43:42 +02:00
lorenz.stechauner 7153bfab6f Tests: Update dependencies 2025-07-29 23:43:27 +02:00
lorenz.stechauner 4b27ebf81b Elwig: Update dependencies 2025-07-29 23:43:17 +02:00
lorenz.stechauner c26e4a2dbd [#61] Windows: Use more red and green color to indicate type of WineVariety 2025-07-28 20:38:20 +02:00
lorenz.stechauner fad1e28c06 Documents: Add Bio-KSt. to footer 2025-07-23 17:07:10 +02:00
lorenz.stechauner c61c2e3fcd [#64] AreaComAdminWindow: Add dialog for modifying/deleting 2025-07-09 18:14:27 +02:00
lorenz.stechauner 267aa3d47c Dialogs: Rename AreaComDialog to AreaComTransferDialog 2025-07-09 16:56:08 +02:00
lorenz.stechauner 7e9a27c75d Services: Move App.HintContextChange() to Windows 2025-07-09 16:07:47 +02:00
lorenz.stechauner 53d82604a1 AreaComAdminWindow: Enhance visual appearance 2025-07-09 00:39:01 +02:00
lorenz.stechauner bb65a1f20e [#62] CreditNote: Add longer custom text to bottom 2025-07-08 16:06:12 +02:00
lorenz.stechauner c6748055fd [#61] Windows: Use red and green color to indicate type of WineVariety 2025-07-08 15:27:13 +02:00
lorenz.stechauner e6746f76b1 Windows: Small fixes 2025-07-08 15:27:13 +02:00
lorenz.stechauner e9f389b885 DeliveryService: Fix spacing in tooltip 2025-07-08 15:27:13 +02:00
lorenz.stechauner 919ab3356d [#57] MailWindow: Use Task.Run() 2025-07-08 15:27:13 +02:00
lorenz.stechauner 39060f6a1e [#57] PaymentAdjustmentWindow: Use Task.Run() 2025-07-08 15:26:57 +02:00
lorenz.stechauner 070dda1bde [#57] ChartWindow: Use Task.Run() 2025-07-07 23:53:08 +02:00
lorenz.stechauner b6bfc60ab8 [#57] DeliveryScheduleAdminWindow: Use Task.Run() 2025-07-07 23:39:55 +02:00
lorenz.stechauner 8aaa22fcb4 [#57] DeliveryAncmtAdminWindow: Use Task.Run() 2025-07-07 23:36:08 +02:00
lorenz.stechauner 64832e9a53 [#57] PaymentVariantsWindow: Use Task.Run() 2025-07-07 23:31:03 +02:00
lorenz.stechauner 7c9844e277 [#57] MainWindow: Use Task.Run() 2025-07-07 22:45:28 +02:00
lorenz.stechauner 19d2208fc3 [#57] AreaComAdminWindow: Use Task.Run() 2025-07-07 22:45:28 +02:00
lorenz.stechauner 38d0ff969d BaseDataWindow: Use Cursors.Wait instead of Cursors.AppStarting 2025-07-07 22:45:28 +02:00
lorenz.stechauner fb2b998d1b [#57] DeliveryAdminWindow: Use Task.Run() 2025-07-07 22:45:28 +02:00
lorenz.stechauner 10d5f49f63 [#57] MemberAdminWinodw: Use Task.Run() 2025-07-07 22:45:28 +02:00
lorenz.stechauner 41811925be PaymentVariantsWindow: Add ViewModel and Service 2025-07-07 22:44:30 +02:00
lorenz.stechauner 97300b6e30 [#65] MailLogWindow: Fix initial loading 2025-07-03 13:03:01 +02:00
lorenz.stechauner c6c8fd9b68 ChartWindow: Automatically calculate variant when saved 2025-07-03 13:03:01 +02:00
lorenz.stechauner 913129f155 PaymentVariantsWindow: Add estimates for uncommited variants 2025-07-03 13:02:57 +02:00
lorenz.stechauner 953532cae4 MemberService: Filter area commitments by CurrentLastSeason instead of CurrentYear 2025-06-24 13:31:58 +02:00
lorenz.stechauner 267797b55d AreaComDialog: Fix property name (MaintainYearFrom) 2025-06-24 13:30:04 +02:00
lorenz.stechauner 4234c7f994 Elwig: Add ITime/XTime to entities and allow to export/import CTime/MTime 2025-06-17 18:06:05 +02:00
lorenz.stechauner 3493ff6df1 Tests: Add UnitTests folder 2025-06-03 20:57:53 +02:00
lorenz.stechauner 623f55f5b0 App: Rename ExePath to InstallPath 2025-05-27 11:43:34 +02:00
lorenz.stechauner b580e1bf79 App: Use environment variables for paths 2025-05-26 15:18:32 +02:00
lorenz.stechauner b08ca63bed [#59] Documents: Move Documents install folder from ProgramData to Program Files 2025-05-26 15:12:49 +02:00
lorenz.stechauner 6a5f4cefa9 [#58] PaymentVariantSummary: Add 'attributlos gebunden' columns 2025-05-23 15:43:39 +02:00
lorenz.stechauner 0f3ce39f35 Elwig: Use 'Menge' instead of 'Gewicht' where applicable 2025-05-20 15:55:11 +02:00
lorenz.stechauner 336aef5c70 CreditNoteDeliveryData: Include WeighingModifier only if delivery modifiers are considered 2025-05-20 13:09:29 +02:00
lorenz.stechauner 0dff3986b7 PaymentVariantsWindow: Add note, that modifiers include Rebelzuschlag 2025-05-20 12:07:28 +02:00
lorenz.stechauner 318fb5dc7b Bump version to 0.13.9 2025-05-05 10:41:18 +02:00
lorenz.stechauner 41c5288fc5 Elwig: Update dependencies 2025-05-05 10:36:30 +02:00
lorenz.stechauner e50e7337e6 Helpers/Utils: Automatically change URL to sync.elwig.at when applicable 2025-05-05 10:36:30 +02:00
lorenz.stechauner ffe85d471c README: Add text 2025-04-24 16:05:41 +02:00
lorenz.stechauner d1c07ee92a Installer/Setup: Update to WiX 6 2025-04-24 14:35:40 +02:00
lorenz.stechauner 4af2fa256e Tests: Update dependencies 2025-04-24 14:31:36 +02:00
lorenz.stechauner bf0db37872 Elwig: Update dependencies 2025-04-24 14:31:27 +02:00
lorenz.stechauner 3161351a30 Bump version to 0.13.8 2025-02-21 12:05:34 +01:00
lorenz.stechauner aa98909c0a DeliveryAdminWindow: Fix Handlese/Gerebelt Gewogen input behaviour 2025-02-20 17:06:40 +01:00
lorenz.stechauner 775bb08e95 Elwig: Update dependencies 2025-02-20 16:08:52 +01:00
lorenz.stechauner fe0a7dab2a Tests: Update dependencies 2025-02-20 16:08:42 +01:00
lorenz.stechauner 138dae715e DeliveryAdminWindow: Allow Gr.Inzersdorf to select LesewagenInput 2025-02-20 15:56:26 +01:00
lorenz.stechauner 28af7f8dd3 CHANGELOG: Fix unser anchor names 2025-01-22 16:19:24 +01:00
lorenz.stechauner 7cc2f75e7c Bump version to 0.13.7 2025-01-21 11:59:54 +01:00
lorenz.stechauner c7a2f2241d Billing: Handle negative credit amount in following credits 2025-01-21 11:58:08 +01:00
lorenz.stechauner bd4ebb8c35 PaymentVariantsWindow: Allow user to change date 2025-01-21 11:03:54 +01:00
lorenz.stechauner 6d88c5645c PaymentVariantsWindow: Warn user about negative credit exports 2025-01-21 11:01:56 +01:00
lorenz.stechauner 5d017cc8ea MailLogWindow: Fix crash when opening 2025-01-19 17:00:31 +01:00
lorenz.stechauner 0b8a1b321f BillingData: Fix collapsing tested with permutations 2025-01-18 12:19:33 +01:00
lorenz.stechauner 95927c3f1a ChartWindow: Remove old commented-out code 2025-01-16 09:09:28 +01:00
lorenz.stechauner f7297d313a Bump version to 0.13.6 2025-01-14 23:43:20 +01:00
lorenz.stechauner 80fec4473a Elwig: Update dependencies 2025-01-14 22:43:53 +01:00
lorenz.stechauner 95ccb2627c Tests: Update dependencies 2025-01-14 22:43:04 +01:00
lorenz.stechauner 20e3e2a76b BillingData: Fix collapsing with cultivations/defaults 2025-01-14 22:05:48 +01:00
lorenz.stechauner b83df45e8f Bump version to 0.13.5 2025-01-02 17:41:51 +01:00
lorenz.stechauner c24b1ca2b9 DeliveryAdminWindow: Add WineLocalityStatistics 2025-01-02 17:25:46 +01:00
lorenz.stechauner 5e53d864b1 MemberAdminWindow: Add filters for active and non-active members 2025-01-02 14:08:46 +01:00
lorenz.stechauner 633b560a67 Tests: Update dependencies 2025-01-02 14:04:50 +01:00
lorenz.stechauner c9e483ba9d Elwig: Update dependencies 2025-01-02 14:04:40 +01:00
lorenz.stechauner c484d27520 Bump version to 0.13.4 2024-11-25 19:23:20 +01:00
lorenz.stechauner 6c7f10cb26 Tests: Update dependencies 2024-11-25 19:19:31 +01:00
lorenz.stechauner 4a10e94d71 Elwig: Update dependencies 2024-11-25 19:19:28 +01:00
lorenz.stechauner 338f9fe092 AreaComUnderDeliveryData: Also include inactive members with active area commitments 2024-11-25 12:38:04 +01:00
lorenz.stechauner 3b97c2243a AreaComUnderDeliveryData: Fix file name 2024-11-25 12:34:43 +01:00
lorenz.stechauner 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
lorenz.stechauner a99a23fd08 Tests: Update dependencies 2024-11-17 16:58:09 +01:00
lorenz.stechauner 70e01849be Setup: Update dependencies 2024-11-17 16:57:53 +01:00
lorenz.stechauner c3a2f983d5 Elwig: Update dependencies 2024-11-17 16:57:40 +01:00
lorenz.stechauner ebf0c20a90 Bump version to 0.13.3 2024-11-13 23:35:29 +01:00
lorenz.stechauner 2ee0d56dcc Windows: Add MailLogWindow 2024-11-13 18:37:50 +01:00
lorenz.stechauner 0a9731af09 MailWindow: Fix small bugs and persist all settings 2024-11-13 18:07:03 +01:00
lorenz.stechauner 6718ad4c8d AreaComAdminWindow: Add filters for season 2024-11-10 16:35:44 +01:00
lorenz.stechauner a1d84dd988 MemberAdminWindow: Add more filters for AreaComs 2024-11-10 16:35:03 +01:00
lorenz.stechauner f4fa549130 Windows: Add icons on Buttons and MenuItems 2024-11-10 10:58:44 +01:00
lorenz.stechauner c5453c2fe6 MainWindow: Add 'Flächenbindungen' and 'Liefermenge/Ertrag' button 2024-11-10 10:58:35 +01:00
lorenz.stechauner 54deccf021 MainWindow: Add statistics table in Leseabschluss 2024-11-09 09:49:19 +01:00
lorenz.stechauner f5d3a04cb1 Bump version to 0.13.2 2024-10-13 19:08:20 +02:00
lorenz.stechauner 0675c45617 Elwig: Use ExecuteSql/FromSql instead of ExecuteSqlRaw/FromSqlRaw where possible 2024-10-13 18:24:40 +02:00
lorenz.stechauner d1f67dc57d BaseDataWindow: Fix WineAttr/WineCult Id updates in payment variant data 2024-10-13 18:18:31 +02:00
lorenz.stechauner 3cbffdbf27 Documents: Add DeliveryDepreciationList 2024-10-13 11:54:18 +02:00
lorenz.stechauner e247925472 Controls: Fix blurry borders when system scaling is enabled 2024-10-12 14:25:24 +02:00
lorenz.stechauner a1b3cff624 [#11] Tests: Add DeliveryServiceTest 2024-10-07 23:43:22 +02:00
lorenz.stechauner 65498dd18f App: Fix BranchLocation shortening 2024-10-05 19:31:59 +02:00
lorenz.stechauner 27dc4f648f [#11] Tests: Add MemberServiceTest 2024-10-05 19:31:51 +02:00
lorenz.stechauner 86f7f693a0 Export/Bki: Format house nr that excel interprets it correctly 2024-10-02 11:08:14 +02:00
lorenz.stechauner 8680e51052 Weighing: Fix scale L320 for Baden 2024-10-02 10:45:04 +02:00
lorenz.stechauner 1d97f3c422 Bump version to 0.13.1 2024-09-29 22:25:39 +02:00
lorenz.stechauner b6ae1f5675 Elwig: Update dependencies 2024-09-29 22:24:31 +02:00
lorenz.stechauner c185437b9a DeliveryService: Do not delete own delivery in SplitDeliveryToLsNr() 2024-09-29 22:12:19 +02:00
lorenz.stechauner a2315e84bd Windows: Ask user if they really want to send an email 2024-09-29 09:43:17 +02:00
lorenz.stechauner 6ba1973087 MemberAdminWindow: Fix 'Save as PDF' option for DeliveryConfirmation and CreditNote 2024-09-29 09:16:39 +02:00
lorenz.stechauner c62947dacd DeliveryAdminWindow: Add DeliverySplittingDialog 2024-09-28 19:54:50 +02:00
lorenz.stechauner f29a609d3b Bump version to 0.13.0 2024-09-25 22:56:48 +02:00
lorenz.stechauner 8a61747538 DeliveryAdminWindow: Fix extraction error 2024-09-25 22:51:06 +02:00
lorenz.stechauner 4a7c95e250 MailWindow: Make more user safe 2024-09-25 22:30:26 +02:00
lorenz.stechauner 4fa5b8f6d4 Billing: Include all deliveries when calculating under delivery for area commitments 2024-09-25 15:32:26 +02:00
lorenz.stechauner 579ed53487 Windows: Prettify formatting for numbers 2024-09-25 12:19:25 +02:00
lorenz.stechauner 1fc736ce16 Bump version to 0.12.0 2024-09-24 23:10:17 +02:00
lorenz.stechauner ce39797d8b [#56] Windows: Fail gracefully while saving 2024-09-24 20:10:26 +02:00
lorenz.stechauner 94a6dd5312 Printing: Replace PDFtoPrinter with PdfiumViewer 2024-09-24 20:10:26 +02:00
lorenz.stechauner b67857ae22 [#56] AppDbContext: Turn off connection pooling 2024-09-24 20:10:26 +02:00
lorenz.stechauner a48ea8e7e2 Printing/Pdf: Small fixes 2024-09-24 20:10:21 +02:00
lorenz.stechauner 0f87446906 Bump version to 0.11.4 2024-09-22 23:33:45 +02:00
lorenz.stechauner e0fcaf1f53 Services: Fix spacing in tool tip grids 2024-09-22 23:22:23 +02:00
lorenz.stechauner 26e549caa6 Bump version to 0.11.3 2024-09-22 21:05:42 +02:00
lorenz.stechauner 1d187c25f3 MemberAdminWindow: Revert caching of MembersDeliveries 2024-09-22 21:01:48 +02:00
lorenz.stechauner accbb9df08 Bump version to 0.11.2 2024-09-22 20:49:14 +02:00
lorenz.stechauner 7791d02979 Services: Extract GenerateToolTipData() from GenerateToolTip() 2024-09-22 20:47:06 +02:00
lorenz.stechauner dcec6f03fe DeliveryAdminWindow: Fix 'Collection was modified' error 2024-09-22 12:35:37 +02:00
lorenz.stechauner 526e951029 Elwig: Add LogWindow 2024-09-22 12:16:14 +02:00
lorenz.stechauner 3cde360aaa DeliveryAncmtAdminWindow: Replace 'Traubenanmeldung' with 'Anmeldung' 2024-09-22 10:29:48 +02:00
lorenz.stechauner 66be5a3e2c MemberAdminWindow: Cache MembersDeliveries 2024-09-21 23:29:10 +02:00
lorenz.stechauner 5c12dba125 DeliveryService: Fix duplicate LsNr error 2024-09-21 23:04:30 +02:00
lorenz.stechauner 165770fa37 DeliveryAncmtList: Add DefaultKg of member 2024-09-21 23:04:30 +02:00
lorenz.stechauner 648c406ad2 Services: Make FillInputs synchronous 2024-09-21 23:04:26 +02:00
lorenz.stechauner 9fa9d9fbec DeliveryAdminWindow: Minor layout changes 2024-09-21 21:37:57 +02:00
lorenz.stechauner 5dc725620b Bump version to 0.11.1 2024-09-19 12:35:25 +02:00
lorenz.stechauner 27d8a5cfb6 DeliveryAncmtAdminWindow: Add more filters and tooltip for weight 2024-09-19 11:56:45 +02:00
lorenz.stechauner 642fb3a625 DeliveryService: Small fixes 2024-09-19 11:54:57 +02:00
lorenz.stechauner 3f7cd2a6ff DeliveryAncmtAdminWindow: Focus MgNrInput when creating new ancmt 2024-09-19 09:34:44 +02:00
lorenz.stechauner 21a1b11d68 AreaCom: Make YearFrom nullable 2024-09-18 18:26:16 +02:00
lorenz.stechauner d8beb03b96 Tests: Update dependencies 2024-09-17 23:18:51 +02:00
lorenz.stechauner eee90c784b Elwig: Update dependencies 2024-09-17 23:18:41 +02:00
lorenz.stechauner 74200083ab DeliveryAncmtAdminWindow: Mark cancelled schedules with Strikethrough 2024-09-17 23:08:22 +02:00
lorenz.stechauner 871bc299bd Utils: Fix SplitName() for double names 2024-09-17 19:18:43 +02:00
lorenz.stechauner a18b58f438 DeliveryAncmtAdminWindow: Make delivery schedule list bigger 2024-09-17 19:07:59 +02:00
lorenz.stechauner cadb5515ea Bump version to 0.11.0 2024-09-16 11:17:23 +02:00
lorenz.stechauner d8a10152b3 DeliveryScheduleAdminWindow: Add Attribute, Cultivation and IsCancelled 2024-09-16 10:10:31 +02:00
lorenz.stechauner f09c43c1bd App: Make HintContextChange() synchronous by using MainDispatcher 2024-09-12 11:40:32 +02:00
lorenz.stechauner 5c08f61963 MailWindow: Add feature to address members with ancmts on specific day 2024-09-08 15:55:10 +02:00
lorenz.stechauner 7a15050575 Bump version to 0.10.8 2024-09-05 23:32:23 +02:00
lorenz.stechauner 8d9172f91e AppDbContext: Move all calls to App.HintContextChange() outside of any AppDbContext block 2024-09-05 17:21:00 +02:00
lorenz.stechauner 7437187630 DeliveryAncmtAdminWindow: Add date column 2024-09-05 15:28:55 +02:00
lorenz.stechauner a38cdaa8af DeliveryAdminWindow: Slightly increase default width 2024-09-04 18:18:19 +02:00
lorenz.stechauner a5638135a3 DeliveryAncmtAdminWindow: Add option to search in all delivery schedules and init mgnr with selected 2024-09-04 18:11:56 +02:00
lorenz.stechauner a04c7d538e MemberDataSheet: Add VAT to 'Buchführend' 2024-09-04 17:32:34 +02:00
lorenz.stechauner 26235f8c0a BusinessDocument: Add ':' in aside block 2024-09-04 17:23:22 +02:00
lorenz.stechauner f43d9c020c DeliveryAncmtList: Center 'Anmldg.' column 2024-09-04 17:18:36 +02:00
lorenz.stechauner 22514715c1 DeliveryService: Small text fix 2024-09-04 17:16:14 +02:00
lorenz.stechauner 5eda25ed14 Bump version to 0.10.7 2024-09-02 23:42:00 +02:00
lorenz.stechauner 543185d48e DeliveryAdminWindow: Add weight filter 2024-09-02 23:28:55 +02:00
lorenz.stechauner 141086673f DeliveryAncmtList: Add status to indicate late ancmts 2024-09-02 23:10:37 +02:00
lorenz.stechauner 6627ab6d12 App: Try to fix auto context renewal 2 2024-09-01 11:44:38 +02:00
lorenz.stechauner 7ef3faa39e Bump version to 0.10.6 2024-08-30 22:35:10 +02:00
lorenz.stechauner 8c8c0a8c2b App: Try to fix auto context renewal 2024-08-30 22:26:49 +02:00
lorenz.stechauner 78a72c641f MemberAdminWindow: Fix checking and unchecking of ContactEmailInput 2024-08-30 21:40:01 +02:00
lorenz.stechauner e18bc58b6c DeliveryAncmtAdminWindow: Fix bug when pressing Enter in weight input 2024-08-30 21:33:58 +02:00
lorenz.stechauner 21f68caf4c DeliveryAncmtAdminWindow: Increase window width 2024-08-30 21:33:16 +02:00
lorenz.stechauner 2ef10b4bb2 DeliveryAdminWindow: Scroll DeliveryPartList down when adding new delivery parts 2024-08-30 21:31:44 +02:00
lorenz.stechauner 5321be46c7 [#33] ChartWindow: Add CheckBox to indicate which graph (gebunden, normal) is currently selected 2024-08-28 11:14:51 +02:00
lorenz.stechauner 0f24f9da08 [#33] ChartWindow: Fix scaling bug 2024-08-28 10:20:21 +02:00
lorenz.stechauner 8ce8492c74 MainWindow: Use 'Waage' instead of 'Waagen' 2024-08-26 23:00:24 +02:00
lorenz.stechauner ee1315929c AreaComAdminWindow: Fix window title 2024-08-26 22:12:37 +02:00
lorenz.stechauner 9930e6173c Dtos: Rewrite SQL queries to be more efficient 2024-08-24 16:37:41 +02:00
lorenz.stechauner b5c1bfb08f Bump version to 0.10.5 2024-08-24 14:00:49 +02:00
lorenz.stechauner cd2b482b5a Weighing: Add SetDateAndTime() 2024-08-24 13:54:59 +02:00
lorenz.stechauner bec1b165bf MemberAdminWindow: Allow user to keep AreaCom YearTo when transfering 2024-08-24 13:35:14 +02:00
lorenz.stechauner 2ae2564647 DeliveryAncmtAdminWinodw: Do not show warning windows 2024-08-23 14:49:05 +02:00
lorenz.stechauner adbe418b7c DeliveryAncmtAdminWindow: Change status bar spacing 2024-08-23 08:53:48 +02:00
lorenz.stechauner 94db0723c5 Bump version to 0.10.4 2024-08-22 13:36:38 +02:00
lorenz.stechauner f54677d429 DeliveryAncmtAdminWindow: Display stats in status bar 2024-08-22 13:32:08 +02:00
lorenz.stechauner 49e4b65c27 MemberAdminWindow: Display currently shown member count and business shares 2024-08-22 12:29:41 +02:00
lorenz.stechauner ada5085cae Validator: Add some syntax sugar 2024-08-22 11:04:24 +02:00
lorenz.stechauner 85931e62e8 DeliveryAncmtAdminWindow: Show weekday also in ComboBox 2024-08-21 16:57:21 +02:00
lorenz.stechauner 39956cbcbd Bump version to 0.10.3 2024-08-21 00:01:47 +02:00
lorenz.stechauner 84d8d0cecb DeliveryScheduleAdminWindow: Fix AncmtToDateInput input bug 2024-08-20 23:39:08 +02:00
lorenz.stechauner fe7f9d675b DeliveryAncmtAdminWindow: Add day of week to delivery schedule list 2024-08-20 23:31:37 +02:00
lorenz.stechauner 9d1ee8638c Bump version to 0.10.2 2024-08-16 21:31:43 +02:00
lorenz.stechauner cd40075702 ElwigData: Allow legacy field family_name to be used and other fixes 2024-08-16 21:24:04 +02:00
lorenz.stechauner 204dfe8745 MemberService: Include area commitments when uploading export data 2024-08-16 21:23:32 +02:00
lorenz.stechauner f97dfc3c72 Bump version to 0.10.1 2024-08-14 12:02:36 +02:00
lorenz.stechauner d3839c288a MemberList: Allow filtering area commitments 2024-08-14 11:57:19 +02:00
lorenz.stechauner 2764a0ca21 Dtos: Fix error when given_name is null 2024-08-14 11:57:04 +02:00
lorenz.stechauner 48970de652 Billing: Fix error when no deliveries exist in season 2024-08-14 11:56:25 +02:00
lorenz.stechauner 367c3ac357 Bump version to 0.10.0 2024-08-13 15:23:58 +02:00
lorenz.stechauner 4d89a17e80 Billing: Allow users to add custom member modifiers before VAT 2024-08-13 14:43:12 +02:00
lorenz.stechauner f52c11b91e PaymentVariantSummary: Subtract member modifiers from sum 2024-08-12 15:44:04 +02:00
lorenz.stechauner f48c6a02cb [#54] Member: Add IsJuridicalPerson 2024-08-12 15:18:34 +02:00
lorenz.stechauner 025ff08d84 [#14] Documents: Add DeliveryAncmtList 2024-08-10 15:45:37 +02:00
lorenz.stechauner b091bd0ec3 [#14] Windows: Add DeliveryAncmtWindow 2024-08-09 22:11:47 +02:00
lorenz.stechauner 804a17911c [#14] Windows: Add DeliveryScheduleAdminWindow 2024-08-09 17:45:14 +02:00
lorenz.stechauner 170cfda37e Windows: Minor cleanups 2024-08-09 17:44:48 +02:00
lorenz.stechauner 2d737e2780 [#14] Models: Add DeliveryAncmt 2024-08-09 17:44:21 +02:00
lorenz.stechauner 2333077aa5 Billing: Include predecessors in Treuebonus for WGM 2024-08-08 19:09:38 +02:00
lorenz.stechauner 9127cd3f03 MemberAdminWindow: Fix area commitment transfer for new members 2024-08-08 17:35:58 +02:00
lorenz.stechauner 036a0dc978 App: Use Version class 2024-08-02 20:48:53 +02:00
lorenz.stechauner 7749f6ab45 Bump version to 0.9.3 2024-08-02 12:22:52 +02:00
lorenz.stechauner cf05a0c658 DeliveryJournalData: Add delivery and member branch to excel export 2024-08-02 11:42:27 +02:00
lorenz.stechauner 5567d9f25a Bump version to 0.9.2 2024-08-01 16:06:13 +02:00
lorenz.stechauner 4403754ada MemberAdminWindow: Fix error when saving telephone numbers 2024-08-01 15:57:49 +02:00
lorenz.stechauner 8db6007264 MainWindow: Fix closing behaviour when other windows are open 2024-08-01 13:49:04 +02:00
lorenz.stechauner 944270744a Bump version to 0.9.1 2024-08-01 09:12:33 +02:00
lorenz.stechauner 10ee1d6548 [#3] MemberService: Export area commitments with members 2024-07-30 17:00:29 +02:00
lorenz.stechauner db4de5b5fe [#3] Windows: Add option to export selected member or delivery only 2024-07-30 14:24:53 +02:00
lorenz.stechauner f69d2809f3 MailWindow: Allow users to send emails without documents 2024-07-30 12:57:07 +02:00
lorenz.stechauner 1c2e0baa68 [#3] Services: Use .elwig.zip as export extension everywhere 2024-07-30 12:57:04 +02:00
lorenz.stechauner 39f93da0ba SysTecITScale: Add error code 20 for negative weight 2024-07-30 00:07:12 +02:00
lorenz.stechauner 91717f8efb Services: Add also messages for TaskCanceledException 2024-07-29 23:35:03 +02:00
lorenz.stechauner 7786fb421a DeliveryAdminWindow: Fix scale button ordering 2024-07-29 23:12:00 +02:00
lorenz.stechauner 12d2aeecaf Bump version to 0.9.0 2024-07-28 22:13:17 +02:00
lorenz.stechauner 1bc0d67d26 [#3] Services: Add 'Check internet connection' message 2024-07-28 09:35:01 +02:00
lorenz.stechauner 38315cd928 [#3] Services: Update 'upload successful' message 2024-07-28 00:27:21 +02:00
lorenz.stechauner 53d3affefe DeliveryConfirmation: Use checkboxes for 'gerebelt' 2024-07-27 21:55:20 +02:00
lorenz.stechauner 4c3f0c40fa DeliveryAdminWindow: Handle members without default KG correctly 2024-07-27 20:46:13 +02:00
lorenz.stechauner 662862090e [#3] ElwigData: Fix import error when no deliveries are present 2024-07-27 19:44:22 +02:00
lorenz.stechauner a5164e286f [#3] Services: Use .elwig.zip as export extension 2024-07-27 19:44:22 +02:00
lorenz.stechauner ae1e985656 [#3] MainWindow: Add functionality to import data from files 2024-07-27 19:44:22 +02:00
lorenz.stechauner 36c1bd35a7 [#3] MemberAdminWindow: Add Export item in menu 2024-07-27 19:44:22 +02:00
lorenz.stechauner 4aa3362029 DeliveryConfirmation: Use 'gerebelt' instead of 'bto./nto.' 2024-07-27 19:44:15 +02:00
lorenz.stechauner cc97004b30 [#3] ElwigData: Fix importing of duplicate data 2024-07-27 16:57:23 +02:00
lorenz.stechauner 935b31f6e3 DeliveryAdminWindow: Force user to input gerebelt gewogen input 2024-07-26 19:47:47 +02:00
lorenz.stechauner f09753ccc2 Remove byte order marks 2024-07-26 19:44:41 +02:00
lorenz.stechauner 53c7cb2ec0 Workflows: Check for byte order mark 2024-07-26 19:44:37 +02:00
lorenz.stechauner 80c3ec1b9c Windows: Use Indeterminate event in ThreeState CheckBoxes 2024-07-26 16:15:51 +02:00
lorenz.stechauner b49c9c65b1 [#3] Elwig: Add user-friendly sync method 2024-07-26 14:58:15 +02:00
lorenz.stechauner b6afb94246 MemberListData: Include contact information like phone numbers and email addresses 2024-07-24 19:28:34 +02:00
lorenz.stechauner 8e9f2f4e90 WeighingTests: Add Haugsdorf and Sitzendorf 2024-07-24 16:42:38 +02:00
lorenz.stechauner d741ba92dc Printing/Pdf: Set CreateNoWindow to true 2024-07-23 20:22:59 +02:00
lorenz.stechauner 84f772a32f MailWindow: Fix crash when no season is present 2024-07-23 20:22:59 +02:00
lorenz.stechauner fd0ed97305 Weighing: Do not ignore gross and tare weight and show it on DeliveryNote 2024-07-23 20:22:59 +02:00
lorenz.stechauner 1141331608 MemberAdminWindow: Add filters for E-Mail, Tel.-Nr., and contact options 2024-07-23 20:22:59 +02:00
lorenz.stechauner f235d5b380 DeliveryAdminWindow: Fix ModifierInput_SelectionChanged event 2024-07-23 20:22:59 +02:00
lorenz.stechauner 30116f7848 DeliveryAdminWindow: Improve status bar widths 2024-07-23 20:22:59 +02:00
lorenz.stechauner abe7699a5b [#26] MemberAdminWindow: Rearrange status bar items 2024-07-23 20:22:59 +02:00
lorenz.stechauner bb77a4e79a [#26] AreaComAdminWindow: Overhaul status bar and search filters 2024-07-23 20:22:59 +02:00
lorenz.stechauner 46ea0f29ff [#26] MemberAdminWindow: Add tooltip to area commitment status bar 2024-07-23 20:22:59 +02:00
lorenz.stechauner 4229fbbef6 [#26] AreaComService: Add GenerateToolTip() 2024-07-23 20:22:59 +02:00
lorenz.stechauner 8dde1cb3f4 [#10] ViewModels: Unify FilterMember property 2024-07-23 20:22:59 +02:00
lorenz.stechauner c314321039 Utils: Change CurrentLastSeason to switch in july 2024-07-23 20:22:59 +02:00
lorenz.stechauner cf1e975d8e [#10] MemberAdminWindow: Add tooltip for delivieries 2024-07-23 20:22:59 +02:00
lorenz.stechauner 60359935a4 [#10] DeliveryAdminWindow: Move generation of tooltips to DeliveryService 2024-07-23 20:22:59 +02:00
lorenz.stechauner 49e988f71a [#10] AreaComAdminWindow: Implement MVVM 2024-07-23 20:22:59 +02:00
lorenz.stechauner 2a8de18772 [#10] DeliveryAdminWindow: Implement MVVM 2024-07-23 20:22:59 +02:00
lorenz.stechauner af80e827b7 [#10] MemberAdminWindow: Implement MVVM 2024-07-23 20:22:59 +02:00
lorenz.stechauner 9a2fa3ee3d Bump version to 0.8.9 2024-07-23 20:22:52 +02:00
lorenz.stechauner ba9e1d7201 Export/Ods: Escape strings in XML 2024-07-23 20:13:04 +02:00
lorenz.stechauner 49f03c0a3c CheckComboBox: Fix SelectedItems property 2024-07-23 11:08:42 +02:00
lorenz.stechauner 37adf92e80 Bump version to 0.8.8 2024-07-22 09:48:53 +02:00
lorenz.stechauner 4c75dbe4aa GassnerScale: Hot fix record parsing 2024-07-22 09:48:18 +02:00
lorenz.stechauner 5c31ad8851 Bump version to 0.8.7 2024-07-22 00:19:11 +02:00
lorenz.stechauner 3ac9536e76 App: Small changes in auto updater 2024-07-21 15:52:57 +02:00
lorenz.stechauner 7246852181 E2ETests: Refactor to use .FindElement(By.Something()) 2024-07-19 14:34:12 +02:00
lorenz.stechauner dd48a24c58 Setup: Update dependencies 2024-07-19 13:43:48 +02:00
lorenz.stechauner ffef1fd6e4 Elwig: Update dependencies 2024-07-19 13:43:40 +02:00
lorenz.stechauner 01d658f51d Tests: Fix flaky tests when extracting table 2024-07-19 10:49:21 +02:00
lorenz.stechauner 5a36e84b1f MemberAdminWindow: Make first email address required when contact via email is enabled 2024-07-19 10:35:14 +02:00
lorenz.stechauner 5b2f617a68 Tests: Change IBAN and LfbisNr to be valid 2024-07-19 00:41:49 +02:00
lorenz.stechauner dd5049faae E2ETests: Add DeliveryAdminWindowReceiptTest 2024-07-19 00:16:33 +02:00
lorenz.stechauner ffe0ff5508 WeighingTests: Move all scale handlers into ScaleHandlers.cs 2024-07-18 23:46:05 +02:00
lorenz.stechauner 34178105a7 E2ETests: Use ElwigTestDB.sqlite3 instead of default 2024-07-08 13:05:21 +02:00
lorenz.stechauner 6b48a1090c E2ETests: Refactor initial class structure 2024-07-07 14:35:54 +02:00
thomas.hilscher ddd821e478 Tests: Add E2ETests 2024-07-07 01:22:43 +02:00
lorenz.stechauner 658a1f4dc1 DeliveryAdminWindow: Only show active modifiers in receipt mode 2024-07-06 18:50:02 +02:00
lorenz.stechauner daf83c4bbc CheckComboBox: Add setter for SelectedItems 2024-07-03 12:25:07 +02:00
lorenz.stechauner 26d75ea3cd Controls: Use nameof(Property) instead of string 2024-07-02 11:02:07 +02:00
lorenz.stechauner 86937485e4 DocumentTests: Fix Test Date 2024-07-02 01:19:12 +02:00
lorenz.stechauner e9de54415a UpdateDialog: Fix cancellation and buttons 2024-07-01 12:04:13 +02:00
lorenz.stechauner 62f63ef63d UpdateDialog: Add Link to Changelog 2024-07-01 11:07:21 +02:00
lorenz.stechauner 44656e0022 Workflows: Only test on push on a branch 2024-07-01 10:03:15 +02:00
lorenz.stechauner 80df16999f Bump version to 0.8.6 2024-07-01 09:59:51 +02:00
thomas.hilscher d317ccc1e0 [#51] ChartWindow: Select 73 Oechsle per default 2024-06-30 22:04:01 +02:00
lorenz.stechauner 6195363335 PaymentVariantsWindow: Rearrange button grid 2024-06-28 23:39:49 +02:00
lorenz.stechauner 6a92eb76a0 PaymentAdjustmentWindow: Minor layout changes 2024-06-28 23:38:04 +02:00
lorenz.stechauner f9d6da7bc8 [#49] PaymentVariantSummary: Add section heading 2024-06-28 23:09:07 +02:00
lorenz.stechauner 9478f2a1ab [#49] PaymentVariantSummary: Add modifier statistics 2024-06-28 21:07:55 +02:00
lorenz.stechauner 157d0b75a2 [#52] BaseDataWindow: Better group penalties in season tab 2024-06-28 17:32:40 +02:00
lorenz.stechauner 255bcbe3ad CreditNote: Show business share penalty correctly 2024-06-28 15:43:05 +02:00
lorenz.stechauner bce2eea3ac MemberAdminWindow: Add menu item for generating CreditNotes per member 2024-06-28 13:48:22 +02:00
lorenz.stechauner 91c60018f1 [#48] Dtos/CreditNoteData: Include custom member modifiers 2024-06-28 13:05:08 +02:00
lorenz.stechauner 5037818997 [#48] Billing: Add custom modifiers for members 2024-06-27 13:40:52 +02:00
lorenz.stechauner 5c76b8ec52 PaymentAdjustmentWindow: Improve DataGrid and add status bar 2024-06-26 19:04:43 +02:00
lorenz.stechauner 9d9bb099e1 Workflows: Improve deploy and test workflow 2024-06-25 23:09:43 +02:00
lorenz.stechauner f4172235be Changelog: Add v0.7.0, v0.7.1, and v0.7.2 2024-06-25 12:10:11 +02:00
lorenz.stechauner 299c65ab1a Changelog: Reformat 2024-06-25 10:43:47 +02:00
lorenz.stechauner ba8034ad75 Changelog: Add v0.6.5, v0.6.6, v0.6.7, and v0.6.8 2024-06-23 21:46:04 +02:00
lorenz.stechauner 7c63373a02 Changelog: Add v0.8.0 2024-06-23 21:01:46 +02:00
lorenz.stechauner 750ae53428 Test: Fix flaky DocumentTests by using arrays 2024-06-20 22:44:00 +02:00
lorenz.stechauner 1121c18dc5 Changelog: Add v0.8.2 and v0.8.1 2024-06-20 11:38:43 +02:00
lorenz.stechauner 96a3168d49 Changelog: Add description for v0.8.3 2024-06-19 19:11:54 +02:00
lorenz.stechauner e627f13264 Add Changelog and Readme 2024-06-19 17:27:20 +02:00
lorenz.stechauner 7ce8c3cabf MailWindow: Allow user to change document date 2024-06-17 19:28:21 +02:00
lorenz.stechauner 763f0197ca CreditNote: Remove calc time and add Variant.Date 2024-06-17 19:01:47 +02:00
lorenz.stechauner 43dddf2c07 PaymentAdjustmentWindow: Lock auto ajusting in old seasons 2024-06-17 18:58:39 +02:00
lorenz.stechauner 70129695ae Bump version to 0.8.5 2024-06-17 11:31:26 +02:00
lorenz.stechauner 51b9799b56 [#46] PaymentAdjustmentWindow: Persist parameters in ClientParameters 2024-06-17 11:25:35 +02:00
lorenz.stechauner c1903b1f36 [#46] PaymentAdjustmentWindow: Refine DataGrid 2024-06-17 11:03:32 +02:00
lorenz.stechauner 66eb177fbf [#46] Billing: Only adjust business shares for active members 2024-06-17 10:51:48 +02:00
lorenz.stechauner 87467bbe75 Export/Ebics: Remove blank line for not shown <Ctry/> 2024-06-17 10:47:14 +02:00
lorenz.stechauner abf465f821 OverUnderDeliveryData: Also show inactive members 2024-06-17 10:24:58 +02:00
lorenz.stechauner 792c18365e MailWindow: Send DeliveryConfirmations and CreditNotes also to inactive members 2024-06-17 10:17:28 +02:00
lorenz.stechauner 5c46a00752 [#46] Windows: Add PaymentAdjustmentWindow 2024-06-17 01:41:32 +02:00
lorenz.stechauner 9d9f929843 [#46] MemberHistory: Add Type to PK 2024-06-17 01:19:25 +02:00
lorenz.stechauner b76c5ea874 [#46] CreditNote: Show number of added business shares 2024-06-16 23:54:53 +02:00
lorenz.stechauner 86e69e9ff8 [#48] PaymentVariantsWindow: Add checkbox for custom member modifiers 2024-06-16 18:31:02 +02:00
lorenz.stechauner 050e4f5b6f ControlUtils: Allow RenewItemsSource for ListBox to reselect all items 2024-06-14 17:02:25 +02:00
lorenz.stechauner 01f055ee17 Test: Fix DocumentTests again? 2024-06-14 12:15:05 +02:00
lorenz.stechauner da9df5cbeb Export/Ebics: Warn user if no client IBAN is set 2024-06-14 12:11:49 +02:00
lorenz.stechauner cc0aa6046f Export/Ebics: Also export Ctry in address line mode 2024-06-13 10:00:49 +02:00
lorenz.stechauner 5cb7d2cbb0 Export/Ebics: Escape address properly 2024-06-13 01:48:37 +02:00
lorenz.stechauner 6c1e50ad02 Bump version to 0.8.4 2024-06-13 01:45:53 +02:00
lorenz.stechauner 46551fb142 Tests: Implement tests for Austrian Ebics 2024-06-13 01:43:23 +02:00
lorenz.stechauner b404839ad1 Tests: Fix DocumentTests? 2024-06-13 01:39:12 +02:00
lorenz.stechauner ab926421b0 Export/Ebics: Implement version customization 2024-06-13 01:19:36 +02:00
lorenz.stechauner bbd8b67afd MainWindow: Unify gaps between buttons 2024-06-12 18:55:50 +02:00
lorenz.stechauner 70f8276808 MainWindow: Fix crash on closing, when other window is in editing or creating mode 2024-06-12 17:05:57 +02:00
lorenz.stechauner 4483eb6a69 [#37] Controls: Implement CheckComboBox and remove xctk 2024-06-12 16:29:57 +02:00
lorenz.stechauner 6fc38b9db0 Bump version to 0.8.3 2024-06-11 12:43:13 +02:00
lorenz.stechauner 5a4ff26f31 [#16] DeleteMemberDialog: Highlight invalid/not-checked inputs 2024-06-11 12:34:07 +02:00
lorenz.stechauner 012352c562 DeliveryAdminWindow: Fix ModifierInput source 2024-06-11 11:55:47 +02:00
lorenz.stechauner 81f286f504 BaseDataWindow: Set SeasonPenaltyPerBs inputs to readonly when applicable 2024-06-11 11:21:21 +02:00
lorenz.stechauner 324a63cf9a DeliveryAdminWindow: Fix modifier bug 2024-06-11 01:22:43 +02:00
lorenz.stechauner ca0497e396 [#44] BaseDataWindow: Fix conversion between season precisions 2024-06-10 23:01:42 +02:00
lorenz.stechauner 08f551a394 PaymentVariantSummary: Show net/gross modifier in statistics 2024-06-10 22:48:44 +02:00
lorenz.stechauner 3460b9378c [#44] BaseDataWindow: Add button to add/delete seasons 2024-06-10 20:43:38 +02:00
lorenz.stechauner a06921d4ec Windows: Minor UI text changes 2024-06-10 17:38:27 +02:00
lorenz.stechauner f2c2d3270b ChartWindow: Round PriceInput.Text 2024-06-10 16:30:21 +02:00
lorenz.stechauner 6927d44b82 AreaComWindow: Add style for UnitTextBox 2024-06-10 16:26:25 +02:00
lorenz.stechauner cc0843f183 [#45] MemberAdminWindow: Add dialog for area commitment cancellation/transfer 2024-06-10 16:22:28 +02:00
lorenz.stechauner 5039c1252a MailWindow: Fix crash on area commitment filter 2024-06-10 12:43:42 +02:00
lorenz.stechauner 6e4f3b799d MailWindow: Fix crash when selecting custom members 2024-06-10 11:53:53 +02:00
lorenz.stechauner 4c9a151f77 DeliveryConfirmation: Do not print sender by default 2024-06-10 11:49:20 +02:00
lorenz.stechauner 03cacf604b Dialogs: Refactor XAML 2024-06-10 10:10:45 +02:00
lorenz.stechauner c12d111c57 [#16] Dialogs: Add DeleteMemberDialog 2024-06-08 18:22:55 +02:00
lorenz.stechauner 302870fb58 [#47] Billing: Add penalty per business share 2024-06-08 16:13:01 +02:00
lorenz.stechauner f756220d75 BaseDataWindow: Fix 'Schilling crash' 2024-06-07 09:51:23 +02:00
lorenz.stechauner 293c8967be Installer: Update URLs in config.ini 2024-06-07 00:23:34 +02:00
lorenz.stechauner 601ac548fe Tests: Update dependencies 2024-06-03 18:30:42 +02:00
lorenz.stechauner 4e477c38e0 Elwig: Update dependencies 2024-06-03 18:30:34 +02:00
lorenz.stechauner 46fc0db6ba IntegerUpDown: Fix xml spacing 2024-06-03 17:13:37 +02:00
lorenz.stechauner a531e948c1 [#37] IntegerUpDown: Add middle line 2024-06-03 17:07:40 +02:00
lorenz.stechauner cc4ec6c5db [#37] Controls: Implement IntegerUpDown 2024-06-03 16:59:45 +02:00
lorenz.stechauner ff375e3caf curl: Add '--fail' argument to return a meaningful status code 2024-05-15 00:24:42 +02:00
lorenz.stechauner 5b952c4eb1 Workflows: Update upload URL 2024-05-14 23:00:29 +02:00
lorenz.stechauner 48c441b787 Bump version to 0.8.2 2024-05-14 22:51:36 +02:00
lorenz.stechauner be246d6f06 Tests: Fix old url in fetch-resources.bat 2024-05-14 22:51:00 +02:00
lorenz.stechauner 2b10e52ab0 Installer: Fix url for PDFtoPrinter once again 2024-05-14 22:45:26 +02:00
lorenz.stechauner e3fd705f52 App: Show scale errors always in debug mode 2024-05-13 22:34:19 +02:00
lorenz.stechauner 81e18ac553 Config: Add 'required' option to scales 2024-05-13 11:33:27 +02:00
lorenz.stechauner f95f0f0ef3 BaseDataWindow: Fix crash when CurrentLastSeason does not exist 2024-05-12 22:34:53 +02:00
lorenz.stechauner 8eba40a8c1 Bump version to 0.8.1 2024-05-12 22:01:31 +02:00
lorenz.stechauner 69aa75a50a PaymentVariantSummary: Use '–' instead of '~' 2024-05-09 23:42:08 +02:00
lorenz.stechauner 13340c0979 Tests: Remove unused code 2024-05-09 19:40:59 +02:00
lorenz.stechauner 4bd378e048 [#32] PaymentVariantSummary: Add summary header 2024-05-09 19:12:27 +02:00
lorenz.stechauner 602c237fa0 MemberDataSheet: Fix spacing 2024-05-09 12:07:16 +02:00
lorenz.stechauner 5e6d0ebf17 [#32] PaymentVariantsWindow: Add export options for Summary 2024-05-09 11:15:32 +02:00
lorenz.stechauner b064643010 [#32] Tests: Add PaymentVariantSummaryTest 2024-05-08 23:24:05 +02:00
lorenz.stechauner 3526234432 [#32] Dtos/PaymentVariantSummaryData: Allow Data to be exported as Ods 2024-05-08 16:48:22 +02:00
lorenz.stechauner b03f81d4f2 PaymentVariantsWindow: Add Menu and StatusBar 2024-05-08 11:53:41 +02:00
lorenz.stechauner f123bb44c5 PaymentVariantWindow: Fix sum calculation and crash on export 2024-05-07 12:53:25 +02:00
lorenz.stechauner 30536819e7 [#32] Documents: Add PaymentVariantSummary 2024-05-07 12:32:53 +02:00
thomas.hilscher 384f7c9ec0 Migrate Installer and Setup to Wix 5 2024-05-03 21:27:27 +02:00
thomas.hilscher d102a1cb7a Add german localization to Wix Setup 2024-05-03 18:56:44 +02:00
lorenz.stechauner 6906584ef0 App: Fix errors on startup 2024-05-03 21:22:20 +02:00
lorenz.stechauner c0c0b4a26a Models: Move IAddress from Helpers to Models 2024-05-03 14:57:31 +02:00
lorenz.stechauner 0ce8e488f9 ChartWindow: Replace deprecated property names 2024-05-03 10:31:12 +02:00
lorenz.stechauner eb4562dceb MemberAdminWindow: Save season instead of bool for cancelling or transfering area commitments 2024-05-03 10:24:01 +02:00
thomas.hilscher 35e5a1dfff Revert wix extensions to v4.0.5 2024-05-01 12:35:35 +02:00
lorenz.stechauner 179c8bd4f7 Bump version to 0.8.0 2024-05-01 12:08:26 +02:00
lorenz.stechauner fd17d294b9 Update dependencies 2024-05-01 10:19:57 +02:00
lorenz.stechauner 2a4e8d69d0 MainWindow: Add WeightBreakdownButton 2024-04-30 23:34:31 +02:00
lorenz.stechauner 8bf8362480 App: Replace DataTemplate/ControlTemplate with TemplateSelector 2024-04-30 13:02:45 +02:00
lorenz.stechauner c4d68d11bc Controls: Rewrite UnitTextBox as extension of TextBox instead of UserControl 2024-04-28 20:07:09 +02:00
lorenz.stechauner 21fe5bc094 Update dependencies 2024-04-18 16:13:09 +02:00
lorenz.stechauner e7bfc69842 [#3] Elwig: Add feature to sync deliveries 2024-04-15 15:22:04 +02:00
lorenz.stechauner f53371ab19 Helpers: Collapse extensions into one single file 2024-04-15 13:04:49 +02:00
lorenz.stechauner 5f8688f0cd Small fixes 2024-04-15 11:36:55 +02:00
lorenz.stechauner fa00eaaefc Tests: Remove Console.WriteLine 2024-04-13 18:00:54 +02:00
lorenz.stechauner 443e111594 Weighing: Add Gassner scale and update tests 2024-04-13 18:00:17 +02:00
lorenz.stechauner c6905bbb42 Elwig: Update dependencies 2024-04-06 17:37:41 +02:00
lorenz.stechauner c360e6b6a7 MainWindow: Add feedback for users when App.CheckForUpdates finds no updates 2024-04-06 17:35:30 +02:00
lorenz.stechauner 9062d55b20 BaseDataWindow: Fix Parameter in FillInputs() 2024-03-31 20:43:53 +02:00
lorenz.stechauner a9f38a3ccb Windows: Remove SeasonFinishWindow and TestWindow 2024-03-31 17:11:10 +02:00
lorenz.stechauner cbc0d0ebff Tests: Rename class 2024-03-30 15:21:15 +01:00
lorenz.stechauner 27b5d653e6 WineQualityStatistics: Add and fix grouping by KMW 2024-03-30 15:21:02 +01:00
lorenz.stechauner 869f652afc WineQualityStatistics: Add KMW mode 2024-03-30 13:12:54 +01:00
lorenz.stechauner 80e91ad776 Tests: Add CreditNoteTest 2024-03-30 12:42:22 +01:00
lorenz.stechauner bce709efe4 Tests: Remove Console.WriteLine()s 2024-03-30 11:28:28 +01:00
lorenz.stechauner 12eb53cb44 Tests: Add DeliveryConfirmationTest 2024-03-30 11:16:51 +01:00
lorenz.stechauner 657910ff48 Tests: Add WineQualityStatisticsTest 2024-03-30 11:09:39 +01:00
lorenz.stechauner 5c3cf41d3d Tests: Add DeliveryJournalTest 2024-03-30 10:59:42 +01:00
lorenz.stechauner b8851fb241 Tests: Rename LetterheadTest 2024-03-30 09:57:45 +01:00
lorenz.stechauner 66898714bb Tests: Add more DocumentsTests 2024-03-30 09:51:48 +01:00
lorenz.stechauner 1047bc6e8f BaseDataWindow: Fix data renewal 2024-03-30 08:49:11 +01:00
lorenz.stechauner 1419c834ac BaseDataWindow: Fix season modifier crash 2024-03-30 08:39:57 +01:00
lorenz.stechauner eddea88e77 Documents: Use border thickness of 0.5pt 2024-03-28 20:34:59 +01:00
lorenz.stechauner 7274d793c4 Bump version to 0.7.2 2024-03-28 17:57:59 +01:00
lorenz.stechauner 79d9e5d242 MemberList: Fix member DTO name refactoring bug 2024-03-28 17:18:46 +01:00
lorenz.stechauner b2f52072f8 Windows: Add feature to save pdf in menu 2024-03-28 16:54:51 +01:00
lorenz.stechauner 9aa6cba1ff Documents: Add variable for border thickness 2024-03-28 15:56:46 +01:00
lorenz.stechauner d501cfaf72 WineQualityStatistics: Small changes 2024-03-28 15:45:58 +01:00
lorenz.stechauner c2b6486ede Helpers: Add enum ExportMode 2024-03-28 13:41:05 +01:00
lorenz.stechauner 82ea5920f2 [#30] WineQualityStatistics: Add number of deliveries 2024-03-28 13:18:32 +01:00
lorenz.stechauner d011c69812 DeliveryJournalData: Fix modifier ordering 2024-03-28 12:48:23 +01:00
lorenz.stechauner 26a9902a13 [#30] Documents: Add WineQualityStatistics 2024-03-28 12:27:48 +01:00
lorenz.stechauner 57662534f3 Dtos: Unify member names in DataTables 2024-03-27 16:41:08 +01:00
lorenz.stechauner d87f3ce6a6 DeliveryAdminWindow: Include more data when refreshing list 2024-03-27 16:24:29 +01:00
lorenz.stechauner 7f21b7b231 [#13][#31] DeliveryAdminWindow: Rework menu and add DeliveryJournal export 2024-03-27 16:10:28 +01:00
lorenz.stechauner cac0959fe7 Models: Add IDelivery for DeliveryParts 2024-03-27 16:04:47 +01:00
lorenz.stechauner a04f887b4d DeliveryNote: Add Name property 2024-03-27 15:40:14 +01:00
lorenz.stechauner 5c42ef8104 MemberAdminWindow: Restructure some doc.Show()/.Print() calls 2024-03-27 11:03:19 +01:00
lorenz.stechauner cf2ec3bdc4 DeliveryAdminWindow: Improve filters even more 2024-03-26 17:32:05 +01:00
lorenz.stechauner b31b5f6164 DeliveryAdminWindow: Show modifiers in DataGrid 2024-03-26 17:18:33 +01:00
lorenz.stechauner 85f48f1d2a Windows: Add Strg+F tooltip to SearchInput 2024-03-26 16:58:36 +01:00
lorenz.stechauner e3cb20366c [#12] DeliveryAdminWindow: Improve filters 2024-03-26 16:55:40 +01:00
lorenz.stechauner 56ac79b4dd DeliveryJournal: Small changes in header 2024-03-26 13:10:09 +01:00
lorenz.stechauner 175d006d5b MemberAdminWindow: Add try/catch block in delete 2024-03-26 13:09:34 +01:00
lorenz.stechauner f4ef75ac40 [#36] MemberAdminWindow: Add MemberList 2024-03-26 12:53:31 +01:00
lorenz.stechauner 5795c5e8ba Ods: Add support for bool types 2024-03-25 17:19:24 +01:00
lorenz.stechauner 04351a906f DeliveryJournal: Add Name Property 2024-03-25 16:58:02 +01:00
lorenz.stechauner 3f9c4cb1f6 Utils: Add ActiveAreaCommitments() 2024-03-25 16:22:58 +01:00
lorenz.stechauner c6e83ffff4 Ods: Add support for DateOnly, TimeOnly and DateTime 2024-03-25 14:03:21 +01:00
lorenz.stechauner dd408ca40e [#31] AdministrationWindow: Add shortcuts 2024-03-24 21:53:18 +01:00
lorenz.stechauner 555ce228d4 [#31] ContextWindow: Add shortcuts for Ctrl+R and F5 2024-03-24 20:28:19 +01:00
lorenz.stechauner 9d80c5913f DataTable: Add Subtitle 2024-03-24 13:50:20 +01:00
lorenz.stechauner e435e5da8d [#31] MemberAdminWindow: Rework menu and add more features 2024-03-23 21:50:29 +01:00
lorenz.stechauner 7b48385992 Utils: Add SendEmail() 2024-03-23 21:49:29 +01:00
lorenz.stechauner 48f0ddd232 ClientParameters: Add ORDERING_MEMBERLIST 2024-03-23 21:49:17 +01:00
lorenz.stechauner c9bb075910 App: Do not use FileSystemWatcher any more 2024-03-23 13:49:24 +01:00
lorenz.stechauner ee1f4081f4 BaseDataWindow: Add transaction for saving 2024-03-21 11:37:00 +01:00
lorenz.stechauner b6e37c0c67 [#43] BaseDataWindow: Fix editing 2024-03-21 11:29:35 +01:00
lorenz.stechauner 87da56b7a9 App: Improve auto update behaviour 2024-03-21 10:23:44 +01:00
lorenz.stechauner afc143e1e4 ControlUtils: Fix ScrollIntoView() 2024-03-19 22:32:23 +01:00
lorenz.stechauner 545033daf5 [#41] MemberAdminWindow: Add feature to cancel area commitments on member non-active 2024-03-19 18:52:57 +01:00
lorenz.stechauner 1b822a88f3 [#41] MemberAdminWindow: Add feature to transfer area commitments from predecessor 2024-03-19 18:27:19 +01:00
lorenz.stechauner a9bad4dd3f DeliveryAdminWindow: Fix editing of delivery 2024-03-19 15:36:51 +01:00
lorenz.stechauner 1806b02039 [#43] App: Use FileSystemWatcher to renew contexts on demand 2024-03-19 13:17:06 +01:00
lorenz.stechauner 98688168b8 [#43] DeliveryAdminWindow: Do not use Context from WintextWindow any more 2024-03-19 11:48:26 +01:00
lorenz.stechauner 2f3524db9d ControlUtils: Cleanup SelectItem() method and use accordingly 2024-03-18 17:55:27 +01:00
lorenz.stechauner 51e345f1fd AdministrationWindow: Use GetHashCode() to compare default/original values 2024-03-18 16:10:31 +01:00
lorenz.stechauner 729d2fd76c [#43] BaseDataWindow: Do not use Context from ContextWindow any more 2024-03-18 10:28:35 +01:00
lorenz.stechauner 5715c41a2e SeasonFinishWindow: Remove async from OnRenewContext() to avoid warnings 2024-03-16 13:44:08 +01:00
lorenz.stechauner 9353581a56 ActionCommand: Small fixes to avoid warnings 2024-03-16 13:42:08 +01:00
lorenz.stechauner a72803f749 WineQualLevel: Implement GetHashCode() to avoid warning 2024-03-16 13:41:31 +01:00
lorenz.stechauner 9d1ce4138c BillingTest: Remove warnings 2024-03-16 13:36:40 +01:00
lorenz.stechauner 4afd2d8242 AppDbContext: Cleanup UpdateDeliveryPartModifiers() 2024-03-16 12:47:31 +01:00
lorenz.stechauner b7d33e6d89 PaymentVariantsWindow: Use async for UpdateSums() 2024-03-15 17:25:42 +01:00
lorenz.stechauner 711bab5d33 [#43] PaymentVariantWindow: Do not use Context from ContextWindow any more 2024-03-15 17:20:07 +01:00
lorenz.stechauner d9f9ab2391 Models: Use nameof() in InverseProperty 2024-03-15 16:41:40 +01:00
lorenz.stechauner ebb196b094 [#43] OriginHierarchyWindow: Do not use Context from ContextWindow any more 2024-03-15 16:35:19 +01:00
lorenz.stechauner 298e423de8 [#43] Billing: Do only use AppDbContext short-lived 2024-03-15 15:05:15 +01:00
lorenz.stechauner e2e46bc52a [#43] SeasonFinishWindow: Do not use Context from ContextWindow any more 2024-03-15 14:26:21 +01:00
lorenz.stechauner a8e3eb6c1c [#43] MailWindow: Do not use Context from ContextWindow any more 2024-03-11 11:09:53 +01:00
lorenz.stechauner 4e2c087260 [#43] ChartWindow: Do not use Context from ContextWindow any more 2024-03-11 10:32:54 +01:00
lorenz.stechauner 0c63e315bb [#43] AdministrationWindow: Do not use Context from ContextWindow any more 2024-03-11 10:14:39 +01:00
lorenz.stechauner f3cdac8a61 [#43] AreaComAdminWindow: Do not use Context from ContextWindow any more 2024-03-10 21:31:59 +01:00
lorenz.stechauner acc159ed9c [#43] MemberAdminWindow: Context cleanup 2024-03-10 20:39:36 +01:00
lorenz.stechauner 1eba3d9d20 [#43] MemberAdminWindow: Do not use Context from ContextWindow any more 2024-03-10 13:05:15 +01:00
lorenz.stechauner 239b8a9091 QueryWindow: Minor improvements 2024-03-12 18:23:38 +01:00
lorenz.stechauner 2a3a69d96f Bump version to 0.7.1 2024-03-11 14:54:25 +01:00
lorenz.stechauner 3f09717922 App: Header cleanup 2024-03-11 10:22:09 +01:00
lorenz.stechauner f242b077bd AppDbContext: Fix NextMgNr() for gaps greater than 1000 2024-03-10 13:06:49 +01:00
lorenz.stechauner 61c8d1ee97 MemberAdminWindow: Remove Rundschreiben capabilities 2024-03-10 00:26:18 +01:00
lorenz.stechauner a5df03aa2c MailWindow: Move Location field to first page 2024-03-10 00:05:15 +01:00
lorenz.stechauner c0f4a484ab Printing/Pdf: Update WinziPrint version to 0.2.9 2024-03-09 22:57:36 +01:00
lorenz.stechauner 6a5507060a MailWindow: Return cleanly if WinziPrint fails 2 2024-03-09 21:23:58 +01:00
lorenz.stechauner c70772b47d MailWindow: Return cleanly if WinziPrint fails 2024-03-09 21:17:47 +01:00
lorenz.stechauner d0fe264af4 [#40] BillingTest: Fix tests 2024-03-09 21:11:20 +01:00
lorenz.stechauner dc83e64db6 [#40] Billing: Add Rebelzuschlag 2024-03-09 20:24:49 +01:00
lorenz.stechauner 34ebc8fa34 MailWindow: Add feature to change location 2024-03-09 15:46:32 +01:00
lorenz.stechauner 0629f4eb1b MailWindow: Wrap text in FooterInputs 2024-03-09 15:11:52 +01:00
lorenz.stechauner 746d0f10de MailWindow: Order members by billing address if applicable 2024-03-09 15:08:51 +01:00
lorenz.stechauner 58c7eec6f8 [#40] DeliveryConfirmation: Fix last row cell count 2024-03-08 16:33:32 +01:00
lorenz.stechauner e1201bc6b8 [#40] DeliveryConfirmation: Add column indication gross/net weight 2024-03-08 15:18:47 +01:00
lorenz.stechauner ccd4a58007 BillingData: Compact data even more 2024-03-07 10:42:26 +01:00
lorenz.stechauner b5d060aca6 Bump version to 0.7.0 2024-03-06 09:20:06 +01:00
lorenz.stechauner 271e085fdf App: Fix Version comparison in auto updater 2024-03-06 09:19:53 +01:00
lorenz.stechauner e7375c7f9f app.manifest: Remove weird assemblyIdentity 2024-03-05 23:17:57 +01:00
lorenz.stechauner ea6621ee57 AreaComAdminWindow: Fix GstNrInput validation by removing CheckGstNr 2024-03-05 23:06:34 +01:00
lorenz.stechauner d6f1ce01fb Utils: Fix spacing 2024-03-05 17:29:58 +01:00
lorenz.stechauner 5a488369be Printing/Pdf: Wait for WinziPrint to be ready 2024-03-05 17:18:59 +01:00
lorenz.stechauner d944aabc06 Elwig: Update NuGet packages 2024-03-05 16:37:41 +01:00
lorenz.stechauner 74da1ba46f [#15] MailWindow: Add email sending feature 2024-03-05 16:32:21 +01:00
lorenz.stechauner 0812c6a8f9 App: Remove unused import 2024-03-05 16:07:05 +01:00
lorenz.stechauner d3c232d550 Document: Rename DoubleSided to DoublePaged 2024-03-05 12:19:38 +01:00
lorenz.stechauner 95850c1d81 [#15] MailWindow: Add feature to print 2024-03-05 12:18:02 +01:00
lorenz.stechauner 234710887e MemberAdminWindow: Update member delete box text 2024-03-05 11:42:52 +01:00
lorenz.stechauner b6269f8131 [#16] MessageBox: Update visual style to look like current windows style 2024-03-05 11:10:09 +01:00
lorenz.stechauner a5a6915db1 UpdateDialog: Swap buttons 2024-03-05 10:58:34 +01:00
lorenz.stechauner 77cf47e154 App: Remove IsPrintingReady 2024-03-04 21:43:13 +01:00
lorenz.stechauner e9d0eec3bd Printing/Pdf: Increase init delay to 2 seconds 2024-03-04 21:35:48 +01:00
lorenz.stechauner 7e1843a1b3 [#8] Add auto update checker 2024-03-04 21:19:08 +01:00
lorenz.stechauner ac4026571e Printing/Pdf: Wait 1 sec for process to initialize 2024-03-02 20:12:36 +01:00
lorenz.stechauner fb28ce5006 workflows/test: Add installer to PATH at first position 2024-03-02 20:05:35 +01:00
lorenz.stechauner 46c97089e7 [#19] Printing/Pdf: Use WinziPrint's daemon function to allow parallel usage 2024-03-02 19:55:51 +01:00
lorenz.stechauner 376af72700 MailWindow: Add try/catch block around document creation 2024-03-02 18:57:03 +01:00
lorenz.stechauner 9139557cc4 Printing/Pdf: Update WinziPrint version to 0.2.3 2024-03-02 18:49:32 +01:00
lorenz.stechauner 37e10136f4 MailWindow: Trim folder name when previewing email docs 2024-02-29 22:35:33 +01:00
lorenz.stechauner a275385b5c MailWindow: Make first page more responsive 2024-02-29 22:31:59 +01:00
lorenz.stechauner 060acc56c3 MailWindow: Include all payment variants 2024-02-29 18:02:44 +01:00
lorenz.stechauner 55c447621b Windows: Get rid of more warnings 2024-02-29 16:14:13 +01:00
lorenz.stechauner 247367d1bf Dtos: Get rid of more warnings 2024-02-29 16:13:46 +01:00
lorenz.stechauner e693f83152 Document: Overwrite any other file in SaveTo() 2024-02-29 16:13:28 +01:00
lorenz.stechauner f922388db9 Entities: Use 'required' and '= null!' to get rid of warnings 2024-02-29 15:48:09 +01:00
lorenz.stechauner 53a25b3be4 ContextWindow: Context has not to be Disposed
https://stackoverflow.com/questions/15666824/entity-framework-and-calling-context-dispose

https://blog.jongallant.com/2012/10/do-i-have-to-call-dispose-on-dbcontext/
2024-02-29 13:03:32 +01:00
lorenz.stechauner cc72a8365e AreaComWindow: Fix wine cultivation null crash 2024-02-29 13:03:00 +01:00
lorenz.stechauner cc5396711d MailWindow: Use PDF-Dokument instead of PDF-Datei 2024-02-29 12:39:58 +01:00
lorenz.stechauner ccb83911b1 Member: Use upper case Eszett in Administrative name 2024-02-29 12:38:48 +01:00
lorenz.stechauner 20772d09ae [#15] PaymentVariantsWindow: Use MailWindow 2024-02-29 11:32:38 +01:00
lorenz.stechauner 624c9a6b34 [#15] MailWindow: Small quality fixes 2024-02-29 11:19:51 +01:00
lorenz.stechauner 09a739d135 [#15] DeliveryConfirmationsWindow: Replace with MailWindow 2024-02-29 11:14:18 +01:00
lorenz.stechauner e5c462b43f [#15] MailWindow: Add Rundschreiben-Funktion 2024-02-29 10:48:48 +01:00
lorenz.stechauner 92c3ed991b AppDbUpdater: Switch foreign keys off when heavily altering tables 2024-02-29 10:26:58 +01:00
lorenz.stechauner 614e0010fd AppDbUpdater: Do not turn off foreign keys per default 2024-02-29 02:10:57 +01:00
lorenz.stechauner 3b94875a7f ContextWindow: Dispose context after creating new one 2024-02-29 02:04:54 +01:00
lorenz.stechauner d897e44f3b AppDbUpdater: Actually check foreign key violations after updating 2024-02-29 02:03:41 +01:00
lorenz.stechauner 546a9f23c1 [#34] AppDbUpdater: Fix migration for area commitments 2024-02-28 17:57:35 +01:00
lorenz.stechauner 3a0f2e9556 MemberAdminWindow: Cleanup deletion of telnr and email addresses 2024-02-28 15:08:28 +01:00
lorenz.stechauner e9f6f22bc8 MemberAdminWindow: Fix crash when editing telnr or email 2024-02-28 14:44:42 +01:00
lorenz.stechauner c5b1867de8 SeasonFinishWindow: Fix typo in 'nachzeichnen' 2024-02-26 14:33:44 +01:00
lorenz.stechauner 4673877d36 MemberDataSheet: Never show area commitments on first page 2024-02-26 10:31:29 +01:00
lorenz.stechauner 665e16d78f MemberAdminWindow+DeliveryAdminWindow: Add button to jumpt to member (predecessor) 2024-02-25 19:44:18 +01:00
lorenz.stechauner 7181d744fc AreaComAdminWindow: Add - Keine Angabe - to wine cultivation list 2024-02-25 18:44:18 +01:00
lorenz.stechauner 0a42d4776a App: Rename FocusPaymentVariantsWindow to FocusPaymentVariants 2024-02-24 16:33:22 +01:00
lorenz.stechauner efe91192bc DeliveryAdminWindow: Add cooldown of one second to weighing buttons 2024-02-23 23:53:07 +01:00
lorenz.stechauner 06a095a199 [#35] Installer: Fix WIX version detection 2024-02-23 18:31:20 +01:00
lorenz.stechauner 8031654e86 Billing: Use attribute only if applicable 2024-02-23 18:18:54 +01:00
lorenz.stechauner 424bd87c94 [#34] Billing: Fix price calculation for attributes without area commitment use 2024-02-23 16:12:31 +01:00
lorenz.stechauner 190ef82872 [#34] DeliveryAdminWindow: Show cultivation beside attribute 2024-02-23 12:54:09 +01:00
lorenz.stechauner 7b1a3b4f8b [#34] DeliveryNote: Make Attribute column smaller 2024-02-23 12:42:50 +01:00
lorenz.stechauner e6cab7993f BaseDataWindow: Attributes: add description to Max. Ertrag 2024-02-22 11:13:37 +01:00
lorenz.stechauner 25a0722f96 Migrate: Honor attribute Huber 2024-02-22 11:12:49 +01:00
lorenz.stechauner 3324a9a238 MemberDataSheet: Fix bug where program crashes when no cultid is set 2024-02-22 10:52:45 +01:00
lorenz.stechauner 5a6317fcdb DeliveryAdminWindow: When in member mode, show only deliveries of current season 2024-02-22 10:51:42 +01:00
lorenz.stechauner 9fec79ef8c [#34] Billing: Collapse data more compactly 2024-02-20 23:14:00 +01:00
lorenz.stechauner 56fdf62c5c [#34] Third step of not using Bio as Attribute 2024-02-20 21:16:06 +01:00
lorenz.stechauner f8ee478a9e Utils: Code cleanup 2024-02-20 16:38:18 +01:00
lorenz.stechauner c82e8de724 [#34] Second step of not using Bio as Attribute 2024-02-20 16:36:12 +01:00
lorenz.stechauner 049927f90c Delivery: Use also 'netto'/'brutto' for 'gerebelt gewogen' 2024-02-19 22:27:00 +01:00
lorenz.stechauner abbb5a12a6 [#34] First step of not using Bio as Attribute 2024-02-19 22:14:47 +01:00
lorenz.stechauner 092c5788a4 Weighing: Fix Baden scale 2024-02-23 17:46:32 +01:00
lorenz.stechauner 96c9890b90 MainWindow: Ask user if all windows should be closed when closing 2024-02-23 16:45:58 +01:00
lorenz.stechauner 958fbaae50 PaymentVariantsWindow: Allow members to have no IBAN 2024-02-23 16:24:09 +01:00
lorenz.stechauner 04199376d2 [#39] ChartWindow: Add try/catch block around initialization 2024-02-22 09:22:04 +01:00
lorenz.stechauner 6e26bd8922 Bump version to 0.6.8 2024-02-22 00:28:30 +01:00
lorenz.stechauner ae7fdef2ea Weighing: Use App.MainDispatcher.BeginInvoke in DeliveryAdminWindow 2024-02-21 22:24:05 +01:00
lorenz.stechauner c0ff852f5e Weighing: Change Schember-Evt to Schember-Async 2024-02-21 22:09:36 +01:00
lorenz.stechauner 10b78dfb72 Weighing: Add SchemberEventScale 2024-02-21 18:33:36 +01:00
lorenz.stechauner d289a5d4bf Weighing: Update SysTecITScale spelling 2024-02-21 16:29:44 +01:00
lorenz.stechauner 9172222307 Bump version to 0.6.7 2024-02-21 15:16:24 +01:00
lorenz.stechauner 05a75a52cc Windows: Use App.HintContextChange() where applicable 2024-02-21 15:12:45 +01:00
lorenz.stechauner 8732141e6b MemberDataSheet: Show area com buckets of current year (regardless of season) 2024-02-21 15:10:27 +01:00
lorenz.stechauner 99ca12b276 Weighing: Restructure class structure 2024-02-21 12:57:55 +01:00
lorenz.stechauner 7ff069d068 ScaleTestMatzen: Use hard coded date instead of current time 2024-02-21 12:06:50 +01:00
lorenz.stechauner 583d5b4e3e ClientParameters: Add WG Weinland and Baden 2024-02-21 11:16:52 +01:00
lorenz.stechauner 3f2b5b684c Weighing: Remove unused scales 2024-02-21 11:00:30 +01:00
lorenz.stechauner 5db14c09ad UtilsTest: Add Scale from Gr.Inzersdorf 2024-02-21 10:50:59 +01:00
lorenz.stechauner 791eaddf58 Bump version to 0.6.6 2024-02-18 23:21:29 +01:00
lorenz.stechauner 5cb29aa75f AppDbContext: Do not use Min() to avoid errors when no members/FBs are present 2024-02-18 23:19:56 +01:00
lorenz.stechauner 3c0fea30f5 DeliveryAdminWindow: Allow users to create deliveries in current year before march/july 2024-02-18 23:19:06 +01:00
lorenz.stechauner f2df121435 SystecScale: Remove .ToString() 2024-02-18 22:24:10 +01:00
lorenz.stechauner 7f4cfdc1b5 ScaleTestMatzen: Add more tests 2024-02-18 20:47:12 +01:00
lorenz.stechauner f4eb6456be Tests: Add WeighingTests 2024-02-18 17:31:10 +01:00
lorenz.stechauner f13fb3aaf0 UtilsTest: Add Test_CalcCrc16Modbus 2024-02-18 17:29:50 +01:00
lorenz.stechauner 9a39879804 Elwig: Bump version to 0.6.5 2024-02-15 07:32:42 +01:00
lorenz.stechauner 11be424c38 AppDbUpdater: Update db version to 16 2024-02-15 07:30:21 +01:00
lorenz.stechauner 1b9064a97c Tests: Small fixes 2024-02-13 12:44:43 +01:00
lorenz.stechauner 805f782c83 Tests: Add tests for documents 2024-02-13 12:38:37 +01:00
lorenz.stechauner 912206f52d Tests: Update DatabaseSetup 2024-02-13 11:29:23 +01:00
lorenz.stechauner 825bd6f304 Export/Ebics: Escape client and member names 2024-02-12 19:45:46 +01:00
lorenz.stechauner 9ecad6aa79 Weighing: Add ICommandScale and IEventScale 2024-02-10 18:43:45 +01:00
lorenz.stechauner 68f1a2c091 MemberDataSheet: Fix bug where no spaces are in billing address PLZ 2024-02-10 18:18:27 +01:00
lorenz.stechauner 59cd69ddaf MemberAdminWindow: Allow search filter to be 2 characters long (instead of 3) 2024-02-10 18:15:13 +01:00
lorenz.stechauner 7c23f9bdae [#6] Workflows: Add workflow to build and deploy the installer 2023-11-19 20:37:01 +01:00
lorenz.stechauner 6d53e35399 Workflows: Add workflow for running tests on push 2024-02-06 20:05:57 +01:00
lorenz.stechauner 42eb68d431 BillingTest: Implement Test_02 and Test_03 2024-02-03 00:34:11 +01:00
lorenz.stechauner 0591d91f49 Billing: Fix BIO billing and update method parameters 2024-02-03 00:32:42 +01:00
lorenz.stechauner befe6a753b Export/Ebics: Add Tests to validate against schemas and fix issues 2024-02-01 11:02:59 +01:00
lorenz.stechauner 4daa6deb26 Tests: Initialize App.Client 2024-01-31 16:22:27 +01:00
lorenz.stechauner c07a6b450c Tests: Insert members by default, and insert client_parameters 2024-01-31 16:20:32 +01:00
lorenz.stechauner 6fdd72e28b Tests: Add pain.001.001 xsd schemas 2024-01-31 16:19:48 +01:00
lorenz.stechauner 6af33c591f BillingTest: Add Ignore keyword to all unfinished tests 2024-01-31 14:50:48 +01:00
lorenz.stechauner f850fd08ff Tests: Move sql scripts from Resources/ to Resources/Sql/ 2024-01-31 14:39:38 +01:00
lorenz.stechauner b063b201e3 Elwig: Bump version to 0.6.4 2024-01-31 13:03:08 +01:00
lorenz.stechauner 60b624b009 PaymentVariantsWindow: Make buttons more user friendly 2024-01-31 13:01:43 +01:00
lorenz.stechauner 71a234ca60 CreditNote: Make credit sum table bigger and other small fixes 2024-01-31 12:57:48 +01:00
lorenz.stechauner 38abfb0edd CreditNote: Add switches to control which deductions are shown 2024-01-31 12:36:28 +01:00
lorenz.stechauner 05a037db70 BillingTest: Fix TearDown 2024-01-31 12:08:45 +01:00
lorenz.stechauner b9287f8260 Billing: Always call CalculateBuckets() when Calculate() is called to avoid user confusion 2024-01-31 12:07:06 +01:00
lorenz.stechauner 50ac757067 BillingTest: Add names for more test methods 2024-01-29 19:40:23 +01:00
lorenz.stechauner c6cd9d7c73 Elwig: Bump version to 0.6.3 2024-01-29 14:52:31 +01:00
lorenz.stechauner 1b28752f4c PaymentVariantsWindow: Add button to export Buchungsliste 2024-01-29 14:51:03 +01:00
lorenz.stechauner e0bdbee2ae AppDbUpdater: Add new VIEWs for penalties 2024-01-29 14:49:07 +01:00
lorenz.stechauner ff3bd5cea5 Dtos: Use collection initializer 2024-01-29 12:13:15 +01:00
lorenz.stechauner 116d88d3d6 Ods: Add support for decimals and add number grouping 2024-01-29 12:12:02 +01:00
lorenz.stechauner 6bcb2fb406 Dtos: Rename CreditNote and DeliveryConfirmation DTOs 2024-01-28 22:52:26 +01:00
lorenz.stechauner 8665c93702 CreditNote: Add footer text 2024-01-28 22:15:30 +01:00
lorenz.stechauner 62496a0770 BillingTest: Small refactoring 2024-01-28 20:32:10 +01:00
lorenz.stechauner 8678a02318 BillingTest: Use Assert.That(...) more often 2024-01-28 20:22:38 +01:00
lorenz.stechauner 9de7fad139 Tests: Add BillingTest.Test_01_NoActiveAreaCommitments() 2024-01-28 20:12:37 +01:00
lorenz.stechauner 85c8783f7e [#4] MemberAdminWindow: Add more input fields for email addresses 2024-01-28 16:22:54 +01:00
lorenz.stechauner 75e02751f0 [#12] DeliveryAdminWindow: Add abgewertet filter keyword 2024-01-28 11:01:58 +01:00
lorenz.stechauner ef1c3b25cf MemberListWindow: Remove window 2024-01-27 23:03:01 +01:00
lorenz.stechauner 255953a658 ChartWindow: Set limits to 120 Oe and 1.5 euro 2024-01-27 22:53:40 +01:00
lorenz.stechauner 9470b26aec ChartWindow: Define legend items as static 2024-01-27 22:47:49 +01:00
lorenz.stechauner 3a2bf81bd9 ChartWindow: Rework varibute selection 2024-01-27 22:41:31 +01:00
lorenz.stechauner d3aca196dd Elwig: Rename (wine) variant to variety 2024-01-27 11:38:08 +01:00
lorenz.stechauner 519e903d1c ChartWindow: Rename graph to curve for UI 2024-01-27 11:38:08 +01:00
lorenz.stechauner dd568b81e8 Billing: Rename attributeVariants and contracts to vaributes 2024-01-27 11:37:51 +01:00
lorenz.stechauner 31b0ae245d curl: Follow all redirects 2024-01-25 19:41:40 +01:00
lorenz.stechauner 46498ce337 Printing: Show message box when error in printing process occurs 2024-01-25 19:40:56 +01:00
lorenz.stechauner 0fff698a5d ChartWindow: Only display contracts delivered in current season 2024-01-25 12:42:46 +01:00
lorenz.stechauner a71c6685f0 BillingVariant: Calculate member modifiers only if delivery modifiers are considered 2024-01-25 12:27:53 +01:00
lorenz.stechauner ab41702f6c ChartWindow: Round PriceInput text 2024-01-25 01:26:55 +01:00
lorenz.stechauner 2bbf4dd1fd Billing: Prefer Attribute over gebunden status for price 2024-01-25 01:25:20 +01:00
lorenz.stechauner 8909b4a3a8 PaymentVariantsWindow: Disable calcualte button when save button is enabled 2024-01-25 01:00:29 +01:00
lorenz.stechauner f8d776c028 Bump version to 0.6.1 2024-01-25 00:42:02 +01:00
lorenz.stechauner 2154e253ad EditBillingData: Only show graphs with countract count > 0 2024-01-25 00:37:16 +01:00
thomas.hilscher df83430c35 ChartWindow: Add MessageBox when removing contract from other graph 2024-01-25 00:36:36 +01:00
lorenz.stechauner d59a713a8c ChartWindow: Margin fixes 2024-01-25 00:18:02 +01:00
lorenz.stechauner 5e48d8e8d1 PaymentVariantsWindow: Button IsEnabled fixes 2024-01-25 00:16:21 +01:00
lorenz.stechauner 4f95d3fe16 PaymentVariantsWindow: Initialize Quality object for newly addes payment variants 2024-01-24 23:51:25 +01:00
lorenz.stechauner ce3185842a BillingData: Implement GetQualtyGraphEntries 2024-01-24 23:41:53 +01:00
thomas.hilscher e1d19fd9e5 ChartWindow: Fix AbgewertetInput Unchecked 2024-01-23 01:28:24 +01:00
lorenz.stechauner 3931a4084c Billing: Fixes 2024-01-23 01:16:53 +01:00
lorenz.stechauner 1a492e4eff PaymentVariantsWindow: Fix locked json field 2024-01-23 00:56:23 +01:00
lorenz.stechauner 58a13eb3cc BillingData: Fix typo 2024-01-23 00:56:08 +01:00
lorenz.stechauner d5124829de EditBillingData: Fix conversion error 2024-01-23 00:47:33 +01:00
lorenz.stechauner 37658869e4 ChartWindow: Small fixes 2024-01-23 00:37:18 +01:00
lorenz.stechauner 24a43ff37d ChartWindow: Make GraphList bigger 2024-01-23 00:10:32 +01:00
lorenz.stechauner 16cf055834 GraphEntry: Update StringSimple 2024-01-23 00:02:47 +01:00
lorenz.stechauner ef0b913063 EditBillingData: Fix ids for virtual curves 2024-01-22 23:27:55 +01:00
lorenz.stechauner 05909919e2 Billing: Add functionality to collapse curves 2024-01-22 23:09:48 +01:00
thomas.hilscher 3642c5ac07 ChartWindow: Upgrade to Scottplot 5 2024-01-22 21:41:01 +01:00
lorenz.stechauner 6cee604448 Tests: Rename Helpers/ to HelperTests/ 2024-01-22 20:59:25 +01:00
lorenz.stechauner 89d20f4c42 ChartWindow: Make gebunden type fixed more user friendly 2024-01-21 12:48:40 +01:00
lorenz.stechauner 182b367811 ChartWindow: Fix saving bug 2024-01-21 01:20:50 +01:00
lorenz.stechauner a2bb09cfbd Billing: Build BillingData-Json in BillingData instead of anywhere else 2024-01-21 00:31:20 +01:00
thomas.hilscher b981b5f895 ChartWindow/Billing: Misc improvements 2024-01-20 19:24:26 +01:00
lorenz.stechauner 9dc2e8a59a Windows: Add Ctrl+P and Ctrl+Shift+P for delivery and member 2024-01-20 15:47:20 +01:00
lorenz.stechauner 1dc05e47cf DeliveryAdminWindow: Use Saison instead of Season in GUI string 2024-01-20 15:32:44 +01:00
lorenz.stechauner 21cc20ee63 Billing/GraphEntry: Use 73 Oe as MinX for gebunden graph 2024-01-20 12:27:01 +01:00
lorenz.stechauner 491c41b239 ChartWindow: Minor bugfixes and polishing 2024-01-20 12:02:50 +01:00
lorenz.stechauner 47658a72ae ChartWindow: Enhance ComboCheckBox 2024-01-20 02:57:22 +01:00
lorenz.stechauner 8b0a4d7979 EditBillingData: Use 140 as upper boundary 2024-01-20 02:43:15 +01:00
lorenz.stechauner 9ee7f6baf1 Billing/Graph: Remove ParseGraphData() 2024-01-20 02:35:59 +01:00
lorenz.stechauner ecbc9c2d82 BillingData: Add GetCurveValueAt(), extracted from PaymentBillingData 2024-01-20 02:33:05 +01:00
lorenz.stechauner bf90543ad8 ChartWindow: Change gebunden color to yellow 2024-01-20 02:09:03 +01:00
lorenz.stechauner 6a5676f916 ChartWindow: Load GraphEntries correctly from EditBillingData 2024-01-20 01:53:27 +01:00
lorenz.stechauner 75e9d756d2 BillingData: Upgrade GetSelection() 2024-01-20 00:48:23 +01:00
lorenz.stechauner ee161b149b BillingData: Extract GetData() from PaymentBillingData into GetSelection() 2024-01-20 00:31:50 +01:00
thomas.hilscher 0cb7b4bfc8 ChartWindow: Added second graph for gebunden 2024-01-19 23:54:16 +01:00
lorenz.stechauner 4a49a17b6a Tests: Update FetchResource target 2024-01-19 16:57:35 +01:00
lorenz.stechauner 741ccaacae Test: Update target once again 2024-01-19 15:50:50 +01:00
lorenz.stechauner 19f4300440 Tests: Add DependsOnTargets 2024-01-19 15:42:32 +01:00
lorenz.stechauner 954c7a8bdb Tests: Change Target to be executed before CoreBuild 2024-01-19 15:38:02 +01:00
lorenz.stechauner 626724fe87 [#29] DeliveryAdminWindow: Only show sums of filtered parts when filtering 2024-01-19 13:35:53 +01:00
lorenz.stechauner 42bf01656e Some Bugfixes 2024-01-19 00:22:49 +01:00
lorenz.stechauner 51293baaae App: Fix GroupSeparator bug 2024-01-18 23:48:42 +01:00
lorenz.stechauner 1d1398a9cd Config: Use Path.Combine() instead of GetAbsolutePath() 2024-01-18 22:07:31 +01:00
lorenz.stechauner 7d199282d0 Elwig: Upgrade publish profile to .Net8 2024-01-18 21:48:11 +01:00
lorenz.stechauner b56a5ed5c6 Setup: Update dependencies 2024-01-18 21:44:28 +01:00
lorenz.stechauner 201b63c2f1 Installer: Update dependencies 2024-01-18 21:42:34 +01:00
lorenz.stechauner b2bd0c9a21 Tests: Update dependencies 2024-01-18 21:42:07 +01:00
lorenz.stechauner 8502afdc9a Elwig: Update dependencies (except ScottPlot) 2024-01-18 21:38:50 +01:00
lorenz.stechauner cb541cb6e6 Billing: Add EditBillingData class 2024-01-18 21:30:42 +01:00
lorenz.stechauner 403e7723d2 Bump version to 0.6.0 2024-01-18 21:26:33 +01:00
lorenz.stechauner 8fbce03031 Tests: Adapt to new PaymentBillingData class usage 2024-01-18 21:23:10 +01:00
lorenz.stechauner b32a935150 DeliveryAdminWindow: Add filter for red/white 2024-01-18 20:02:16 +01:00
lorenz.stechauner 337bfa89d9 DeliveryAdminWindow: Fix filters for quality and attributes 2024-01-18 19:49:43 +01:00
lorenz.stechauner f886888ccc Billing: Split BillingData into BillingData and PaymentBillingData 2024-01-18 01:22:12 +01:00
thomas.hilscher 4dd036babd ChartWindow: wip 2024-01-17 22:33:20 +01:00
thomas.hilscher b6fd62f8ca ChartWindow: Use BillingData and Curve 2024-01-17 22:08:36 +01:00
lorenz.stechauner b52c09a176 Billing: Add possibility to automatically add business shares 2024-01-17 18:59:25 +01:00
lorenz.stechauner 668eb9a2d0 BillingData: Fix setter for ConsiderAutoBusinessShares 2024-01-17 17:37:08 +01:00
lorenz.stechauner 9eb013ce11 PaymentVariantsWindow: Add possibility to switch options on/off 2024-01-17 14:57:45 +01:00
lorenz.stechauner 38ad433b4e Fix ods export with doubles 2024-01-17 12:10:11 +01:00
lorenz.stechauner 0a60f01979 MemberDataSheet: Fix constructor 2024-01-14 21:41:53 +01:00
lorenz.stechauner a1ddef4666 MemberAdminWindow: Fix ToolTip for search bar 2024-01-14 21:40:23 +01:00
lorenz.stechauner 788d0efa4a CreditNote: Fix double-border for sum without Treuebonus 2024-01-14 10:15:29 +01:00
lorenz.stechauner 0f06d98d39 [#9] MemberAdminWindow: Add "Bio-Knopf" 2024-01-14 01:26:35 +01:00
lorenz.stechauner 228d17f8cb MemberAdminWindow: Enlarge comment field 2024-01-14 00:31:45 +01:00
lorenz.stechauner 1664024e64 [#2] MemberAdminWindow: Add search ToolTip 2024-01-13 20:38:51 +01:00
lorenz.stechauner 8fbfd33e8f DeliveryAdminWindow: Update search ToolTip 2024-01-13 20:38:49 +01:00
lorenz.stechauner 95853099bb [#2] MemberAdminWindow: Update search filters 2024-01-13 20:38:36 +01:00
lorenz.stechauner 8072febd5b DeliveryAdminWindow: Add branch name in title for übernahme mode 2024-01-13 19:40:00 +01:00
lorenz.stechauner 62d9641b28 Tests/DatabaseSetup: Add Insert.sql 2024-01-09 13:10:06 +01:00
lorenz.stechauner 3aabfbc603 PaymentVariantsWindow: Implement SeasonLock 2024-01-08 20:08:07 +01:00
lorenz.stechauner 09e55264bb BillingData: Implement WG Master parsing 2024-01-08 19:31:13 +01:00
lorenz.stechauner f894c3b212 PaymentVariantsWindow: Rework Buttons 2024-01-08 14:20:20 +01:00
lorenz.stechauner 09a7889044 AppDbContext: Add NextAvNr() 2024-01-08 14:18:15 +01:00
lorenz.stechauner d77aac43ec App: Add FocusBaseDataSeason() 2024-01-08 14:17:56 +01:00
lorenz.stechauner 062c7bed5e AppDbUpdater: Fix migration 10 to 11 by creating a new table 2024-01-08 11:21:01 +01:00
lorenz.stechauner b723161fa5 BillingVariant: Fix CalculateModifiers() by restricting modifier year 2024-01-08 02:40:32 +01:00
lorenz.stechauner debee3b4bf PaymentVariantsWindow: Fix once wrong, every time wrong error 2024-01-08 02:24:19 +01:00
lorenz.stechauner f15733f827 CreditNote: Fix styling 2024-01-07 23:43:56 +01:00
lorenz.stechauner 05b6e8ddd6 CreditNote: Add hint regarding calculations 2024-01-07 22:24:06 +01:00
lorenz.stechauner 5ae326074f Document: Fix GenerationProportion 2024-01-07 22:08:21 +01:00
lorenz.stechauner 60b99bf95b CreditNote: Split table into two tables 2024-01-07 21:47:25 +01:00
lorenz.stechauner 3562d304de Document: Remove break-before: avoid for tr.sum 2024-01-07 21:39:42 +01:00
lorenz.stechauner 9f73d13dbf Document: Add GenerationProportion and set to 1/8 2024-01-07 15:26:51 +01:00
lorenz.stechauner 5e665ffb50 PaymentVariantsWindow: Impelment BillingVariant 2024-01-07 15:21:32 +01:00
lorenz.stechauner 1e751c473a BillingData: Implement elwig json format parsing 2024-01-06 14:45:18 +01:00
lorenz.stechauner d67e434fed Use InvariantCulture instead of replacing ',' with '.' 2024-01-06 14:25:42 +01:00
lorenz.stechauner 3a89e16db3 Tests: Move tests into package 2024-01-05 18:03:28 +01:00
lorenz.stechauner 2556033a07 Tests: Add in-memory database for testing 2024-01-05 18:03:21 +01:00
lorenz.stechauner 3f6a94e773 AppDbContext: Add ExecuteEmbeddedScript() 2024-01-05 14:49:28 +01:00
lorenz.stechauner e75e2ddbda BillingData: Move schema to Resources/ 2024-01-05 13:45:37 +01:00
lorenz.stechauner 121ca10261 AppDbContext: Add ExecuteBatch() and ExecuteScalar() 2024-01-05 13:41:27 +01:00
lorenz.stechauner f28a1a2db9 AppDbUpdater: Extract sql commands as embedded resources 2024-01-05 13:17:29 +01:00
lorenz.stechauner ab61edc402 AppDbUpdater: Using async/await 2024-01-05 10:20:53 +01:00
lorenz.stechauner ba55692cbe Models: Update Payment models 2024-01-05 09:53:05 +01:00
lorenz.stechauner eb46955b3b Billing: Add BillingData and JSON schema validation 2024-01-05 00:41:20 +01:00
lorenz.stechauner 37bf8d0855 Documents: Add Property Name 2024-01-04 15:05:40 +01:00
lorenz.stechauner be87f31211 ChartWindow: Small fixes 2024-01-04 13:12:48 +01:00
lorenz.stechauner 4738cde9e4 Billing: Update EBICS exporter 2023-12-29 13:14:32 +01:00
lorenz.stechauner 6cf5e0d45e Documents: Change wording in documents 2023-12-23 23:05:25 +01:00
lorenz.stechauner 8e71e82efc CreditNote: Display under deliveries of area commitments 2023-12-23 21:27:26 +01:00
lorenz.stechauner 4f07d9b129 AreaComUnderDeliveryData: Fix typo 2023-12-23 14:49:43 +01:00
lorenz.stechauner 8555748202 CreditNote: Add FormatRow() 2023-12-23 14:07:10 +01:00
lorenz.stechauner a0914f4d54 BillingVariant: Fix retrieval of last used payment variant 2023-12-23 14:07:01 +01:00
lorenz.stechauner 2301251420 CreditNote: Small fixes 2023-12-23 14:06:57 +01:00
lorenz.stechauner 83f9b58d4d CreditNoteData: Fix SQL query for multiple payment variants in one year 2023-12-23 14:06:57 +01:00
lorenz.stechauner f8aef20b0d CreditNote: Add VAT display 2023-12-23 14:06:57 +01:00
lorenz.stechauner 0dcffc8677 PaymentVariantsWindow: Add commit and revert buttons 2023-12-23 14:06:57 +01:00
lorenz.stechauner bc578b212e BillingVariant: Add Commit() and Revert() functions 2023-12-23 14:06:52 +01:00
lorenz.stechauner 8368caf58a CreditNote: Add member modifier display 2023-12-22 20:18:48 +01:00
lorenz.stechauner c836b45920 Documents: Add .bold for tables 2023-12-22 19:48:13 +01:00
lorenz.stechauner b9a2893d80 [#17] CreditNote: Overhaul CreditNote 2023-12-22 15:34:03 +01:00
lorenz.stechauner 82f93746ab MemberDataSheet: Fix typo 2023-12-22 10:11:35 +01:00
lorenz.stechauner b79fcfb1ed MemberDataSheet: Make more compact 2023-12-22 00:56:57 +01:00
lorenz.stechauner ba241c98a9 MemberDataSheet: Make Bankverbindung section smaller 2023-12-22 00:15:42 +01:00
lorenz.stechauner f7cdd7a4c1 MemberDataSheet: Minor improvements 2023-12-21 23:24:14 +01:00
lorenz.stechauner 1bdb7183ed [#5][#17][#23] MemberDataSheet: Overhaul entire MemberDataSheet 2023-12-21 22:24:04 +01:00
lorenz.stechauner 4d5ad13e0c Utils: Add PhoneNrTypes 2023-12-21 22:21:38 +01:00
lorenz.stechauner 69e6b6a713 Member: Use current calendar year to retrieve ActiveAreaCommitments 2023-12-21 22:20:24 +01:00
lorenz.stechauner 781077e5e3 [#25] Documents: Fix page breaking 2023-12-20 01:39:26 +01:00
lorenz.stechauner 16d429e9e4 DeliveryConfirmation: Keep last two tables together on page 2023-12-19 22:04:34 +01:00
lorenz.stechauner 18600a44da Document: Add mailto: and http: hrefs to footer 2023-12-19 20:48:01 +01:00
lorenz.stechauner 9c46974bd7 [#23] DeliveryConfirmation: Add Sortenaufteilung table 2023-12-19 20:48:01 +01:00
lorenz.stechauner 480f99234c AppDbContext: Remove support for multiple attributes in buckets 2023-12-19 19:31:35 +01:00
lorenz.stechauner 8811ca25ce [#17][#23] BusinessDocument: Remove enum RowMode 2023-12-19 18:11:02 +01:00
lorenz.stechauner 161bf31a62 [#17][#23] Documents: Unify table stylings 2023-12-19 18:10:31 +01:00
lorenz.stechauner ae00fd2c31 [#22] DeliveryConfirmation: Fix wrong border 2023-12-18 21:01:15 +01:00
lorenz.stechauner 2c48c89cfa Installer/config.ini: Comment out branch 2023-12-18 12:20:05 +01:00
lorenz.stechauner de5e62de50 MemberAdminWindow: Rename ...Memberdata... to ...MemberDataSheet... 2023-12-02 17:30:04 +01:00
lorenz.stechauner 0eed426559 [#21] AdministrationWindow: Temporarily fix UnitTextBox/TextBox casting error 2023-12-02 14:00:22 +01:00
lorenz.stechauner 03a9a3793a Config: Use Microsoft's INI implementation instead of a 3rd party's 2023-12-02 13:04:50 +01:00
lorenz.stechauner 7528764ff3 Update project to .NET 8 2023-12-02 12:28:16 +01:00
lorenz.stechauner 3576a066fe AppDbContext: Use records instead of unnamed tuples for buckets 2023-11-30 17:29:01 +01:00
lorenz.stechauner 7efd34bc4d Bump version to 0.5.1 2023-11-30 02:02:47 +01:00
lorenz.stechauner 59553be571 MemberDataSheet: Fix bugs 2023-11-30 02:00:44 +01:00
lorenz.stechauner 8b9d62ea50 Billing: Calculate prices 2023-11-30 01:46:57 +01:00
thomas.hilscher 17d00a8524 MemberAdminWindow: Add member data sheet show button 2023-11-30 01:44:10 +01:00
thomas.hilscher 47950afb67 AreaComAdminWindow: Add min height and width to window 2023-11-30 01:44:10 +01:00
thomas.hilscher 08108f0c18 [#5] MemberDataSheet: Add generating a member data sheet 2023-11-30 01:44:10 +01:00
lorenz.stechauner 8a6086ba6d Add PaymentVariantsWindow 2023-11-29 22:48:43 +01:00
lorenz.stechauner b1c7b45523 DeliveryConfirmationsWindow: Remove Stichprobe Button 2023-11-29 13:18:42 +01:00
lorenz.stechauner 2bca2aed97 OriginHierarchyWindow: Add deactivation confirmation and status bar 2023-11-25 00:28:47 +01:00
lorenz.stechauner c25bfc9f1b Windows: Add OriginHierarchyWindow 2023-11-24 23:38:55 +01:00
lorenz.stechauner fb4dc613ae AppDbUpdater: Update to version 8 2023-11-24 13:24:24 +01:00
lorenz.stechauner 0413bc5afc AreaComAdminWindow: Fix bug by Tom 2023-11-17 00:38:29 +01:00
lorenz.stechauner f4ec95cdca AreaComAdminWindow: Allow users to create new reeds inline 2023-11-16 17:42:01 +01:00
lorenz.stechauner 077e03ab2f Ods: add mergeSubRowCells to AddTable() 2023-11-16 10:06:55 +01:00
lorenz.stechauner 9488c4c853 SeasonFinishWindow: first create tables, only then write to ods file 2023-11-16 10:05:40 +01:00
lorenz.stechauner e962d1e44b ControlUtils: small fixes 2023-11-16 09:42:47 +01:00
lorenz.stechauner df5007826d App: Fix embedded version parsing 2023-11-16 09:33:15 +01:00
lorenz.stechauner 6db2e0a4ec DeliveryAdminWindow: Add filter for branch when in Receipt mode 2023-11-16 09:32:25 +01:00
lorenz.stechauner 363005ad49 Ods: change colw to colXmm 2023-11-15 22:30:52 +01:00
lorenz.stechauner 22318e1b9a Dtos: merge under/over delivery with percent column 2023-11-15 22:16:13 +01:00
lorenz.stechauner 6e0da28ea3 TestWindow: Fix zip file creation 2023-11-15 22:11:01 +01:00
lorenz.stechauner e8169af6c8 Ods: fix numbers 2023-11-15 22:10:41 +01:00
lorenz.stechauner ed78c8facb Ods: small fixes 2023-11-15 18:39:44 +01:00
lorenz.stechauner b096163ed3 DataTable: Add units for columns 2023-11-15 18:29:14 +01:00
lorenz.stechauner 486655d071 Dtos: Add more DTOs 2023-11-15 16:40:17 +01:00
lorenz.stechauner d485f0fda1 Ods: Fix small issues 2023-11-13 22:40:12 +01:00
lorenz.stechauner 8509f04d4d Ods: Add settings.xml 2023-11-13 19:22:21 +01:00
lorenz.stechauner 3ee7a6e75e DeliveryConfirmationData: Fix ordering problem 2023-11-13 00:55:36 +01:00
lorenz.stechauner 8b96b65c8c Ods: move styles to autmatic-styles 2023-11-13 00:31:28 +01:00
lorenz.stechauner db8a449785 Helpers/Export: Add Dto file export 2023-11-13 00:22:21 +01:00
lorenz.stechauner 2cdde60644 Models: Add Dtos/ 2023-11-12 19:38:36 +01:00
lorenz.stechauner 32f229b0a5 Models: Add Entities/ folder 2023-11-10 13:48:25 +01:00
lorenz.stechauner 2f98df87a0 Setup/Installer: fix Elwig.ico/.png path 2023-11-08 18:36:23 +01:00
lorenz.stechauner f6ff2869c1 Bump version to 0.5.0 2023-11-08 18:23:34 +01:00
lorenz.stechauner 65ae2090ac SeasonFinishWindow: Add table in export csv for deliveries and area coms 2023-11-08 17:57:20 +01:00
lorenz.stechauner 73b3ddbcff SeasonFinishWindow: fix output csv 2023-11-08 16:43:12 +01:00
lorenz.stechauner 78d83691df AppDbUpdater: Fix v_area_commitment_bucket_strict 2023-11-08 15:10:55 +01:00
lorenz.stechauner c1b5631e24 AreaComAdminWindow: check active only by default 2023-11-08 15:08:26 +01:00
lorenz.stechauner f9a3d31a49 SeasonFinishWindow: Fix area commitment under delivery export 2023-11-08 15:02:42 +01:00
lorenz.stechauner 3c8501cbe2 DeliveryAdminWindow: capitalize headings in gradation tooltip 2023-11-07 13:37:29 +01:00
lorenz.stechauner 00eec47e13 DeliveryAdminWindow: let linq calculate aggregate functions in the database 2023-11-07 13:27:49 +01:00
lorenz.stechauner ff053e56cf DeliveryAdminWindow: add advanced tooltips for weight and gradation 2023-11-07 12:55:44 +01:00
lorenz.stechauner 493cfa3156 MemberAdminWindow: Add Bio checkbox 2023-11-06 22:10:39 +01:00
lorenz.stechauner 4fff89029d BaseDataWindow: implement area commitment type list 2023-11-06 19:35:48 +01:00
lorenz.stechauner 7e6aaf514b AreaComAdminWindow: move search bar closer to table 2023-11-06 17:37:08 +01:00
thomas.hilscher 053fbdbf30 Added search to AreaComAdminWindow 2023-11-05 17:42:25 +01:00
lorenz.stechauner a1ae44e135 BaseDataWindow: fix SeasonList Enabling/Disabling 2023-11-05 14:25:28 +01:00
lorenz.stechauner 7fe2ea76c3 BaseDataWindow: Add fields for season 2023-11-05 12:20:59 +01:00
lorenz.stechauner 6435a649b4 Controls: Add UnitTextBox 2023-11-04 21:59:59 +01:00
lorenz.stechauner 12226c3d0f SeasonFinishWindow: Fix Über-/Unterlieferungen export 2023-11-02 20:51:08 +01:00
lorenz.stechauner 1413f480cd Config: remove ' bei ...' suffix for branch locations 2023-11-02 20:40:29 +01:00
lorenz.stechauner f7a044ba79 BaseDataWindow: Implement branches, attributes and cultivations 2023-11-02 20:39:29 +01:00
lorenz.stechauner d9dbfe68c1 Config: remove ' an ...' suffix for branch locations 2023-11-02 20:36:19 +01:00
lorenz.stechauner 6b6373238a Billing: fix small bugs 2023-11-02 19:01:05 +01:00
lorenz.stechauner eebddf0527 Billing: Rename bins to buckets 2023-11-02 13:06:32 +01:00
lorenz.stechauner 9f67448b72 Billing: fix calculation 2023-11-02 11:57:38 +01:00
lorenz.stechauner 193b4688d3 Document: change progress portion of merging to 1/4 - 3/4 2023-11-02 00:28:23 +01:00
lorenz.stechauner 9cc365e779 AdministrationWindow: fix UTF-8 issue 2023-10-31 22:25:26 +01:00
lorenz.stechauner ad9f4d3a9a Models: Remove DeliveryPartAttr 2023-10-31 22:21:24 +01:00
lorenz.stechauner b1dad261d1 Add Resources/ directory 2023-10-29 13:31:49 +01:00
lorenz.stechauner 18c020e8a6 Update dependencies 2023-10-28 19:02:53 +02:00
lorenz.stechauner 3b0b84dcea AreaComAdminWindow: Fix crashing when not selecting a reed 2023-10-28 15:27:08 +02:00
lorenz.stechauner 11ea958059 Documents: restructure Documents/ directory 2023-10-26 23:27:28 +02:00
lorenz.stechauner 105854c389 QueryWindow: fix underscore bug 2023-10-25 16:18:33 +02:00
thomas.hilscher ba5d888e37 Rebuild WinziPrint.exe 2023-10-23 22:45:22 +02:00
lorenz.stechauner 8792404e0d Documents/Pdf: allow double-sided pages to be printed 2023-10-23 22:27:44 +02:00
lorenz.stechauner d5102c9cd7 DeliveryAdminWindow: Fix übernahme logic 2023-10-22 01:09:28 +02:00
lorenz.stechauner 25d681a1d8 DeliveryAdminWindow: Add Übernahme logic for Sitzendorf/Haugsdorf 2023-10-21 23:09:16 +02:00
lorenz.stechauner fa9f18da29 MainWindow: Show brach name 2023-10-21 23:08:03 +02:00
lorenz.stechauner e8bd81ccc7 Installer: Fix typo 2023-10-20 00:13:28 +02:00
lorenz.stechauner f26d55af68 Bump version to 0.4.3 2023-10-20 00:10:22 +02:00
lorenz.stechauner e46ddb9cdc DeliveryConfirmationsDialog: move to DeliveryConfirmationsWindow 2023-10-19 23:50:21 +02:00
lorenz.stechauner 3cf3ca71d6 Update fetch-resources.bat 2023-10-19 23:41:14 +02:00
lorenz.stechauner 1d4ab7b264 Update fetch-resources.bat 2023-10-19 23:32:47 +02:00
lorenz.stechauner 841f8bfb84 Documents/Pdf: fix progression 2023-10-19 23:25:55 +02:00
thomas.hilscher a075189bdd Added WinziPrint.exe to Wix Installer 2023-10-19 23:22:18 +02:00
lorenz.stechauner e52475a4bf Documents: fix WinziPrint args 2023-10-19 22:45:31 +02:00
lorenz.stechauner 8ce4911317 DeliveryConfirmationsDialog: small fixes 2023-10-19 22:35:31 +02:00
lorenz.stechauner c190ce1474 Documents/Pdf: Add progress tracker 2023-10-19 22:16:33 +02:00
lorenz.stechauner f19de3ae6e Billing: fix calc issue 2023-10-19 18:54:47 +02:00
lorenz.stechauner e56e209506 DeliveryConfirmation: Remove footer todo 2023-10-19 17:07:18 +02:00
lorenz.stechauner 28fb4f6fa2 DeliveryConfirmation: Cache deliveries of all members 2023-10-19 17:04:35 +02:00
lorenz.stechauner a832879b73 Documents: use WinziPrint instead of WeasyPrint 2023-10-19 16:38:03 +02:00
lorenz.stechauner a2f49e1b8b Bump version to 0.4.2 2023-10-19 01:10:13 +02:00
lorenz.stechauner 99c3474fb6 App: Fix wolkersdorf im weinviertel branch name issue 2023-10-19 01:09:26 +02:00
lorenz.stechauner 9924795888 DeliveryAdminWindow: make only non-predicate quality levels searchable 2023-10-19 01:09:03 +02:00
thomas.hilscher 733ab0d208 Add wine variety and attribute to AreaComAdminWindow datagrid 2023-10-19 00:18:28 +02:00
lorenz.stechauner 6e2ba56a7c SeasonFinishWindow: Implement Über-/Unterlieferungen 2023-10-19 00:09:38 +02:00
lorenz.stechauner 2507a2695f DeliveryConfirmation: Change stat table headings 2023-10-18 18:44:12 +02:00
lorenz.stechauner ddd83bf63d Bump version to 0.4.1 2023-10-18 01:31:22 +02:00
lorenz.stechauner 685793f0fb Billing: Implement feature 'avoid under deliveries' 2023-10-18 01:30:34 +02:00
lorenz.stechauner f6712704ee DeliveryConfirmationsDialog: Fix button width 2023-10-18 01:22:17 +02:00
lorenz.stechauner de500854c4 AppDbContext: Add v_area_commitment_bin 2023-10-17 23:48:16 +02:00
lorenz.stechauner d992b6a206 Document: Add member name to pdf viewer window title 2023-10-17 23:10:19 +02:00
lorenz.stechauner d2b96736bb DeliveryConfirmation: remove name from title 2023-10-17 23:02:28 +02:00
lorenz.stechauner 50b9f4e207 AppDbContext: add name for _ attribute 2023-10-17 22:58:22 +02:00
lorenz.stechauner 45fc0893b1 DeliveryConfirmation: fix tables 2023-10-17 22:56:21 +02:00
lorenz.stechauner c4dd56075d DeliveryConfirmationsDialog: Use Print-Tag 2023-10-17 20:53:40 +02:00
lorenz.stechauner f1084c716a DeliveryConfirmationsDialog: Add AllMemberInput checkbox 2023-10-17 20:50:16 +02:00
lorenz.stechauner 2f8e4ca812 Bump version to 0.4.0 2023-10-17 01:08:37 +02:00
lorenz.stechauner 450f5d8109 Dialogs: Add DeliveryConfirmationsDialog 2023-10-17 01:07:38 +02:00
lorenz.stechauner 62d6707d10 DeliveryConfirmation: no linebreaks in stat table 2023-10-17 00:58:01 +02:00
lorenz.stechauner 2bcf26cc8d MainWindow: Add version to header 2023-10-16 23:45:14 +02:00
lorenz.stechauner daddd069a3 MemberAdminWindow: Add more letterhead sorting options 2023-10-16 23:25:22 +02:00
lorenz.stechauner 3b3489b492 SeasonFinishWindow: enhnace button handling 2023-10-16 22:53:04 +02:00
lorenz.stechauner 505ee0ad24 SeasonFinishWindow: Change buttons 2023-10-16 22:46:18 +02:00
lorenz.stechauner 0b79fa192e Billing: Fix forced attribute calculation 2023-10-16 22:41:12 +02:00
lorenz.stechauner ff2968c989 DeliveryConfirmation: Add Statistics table 2023-10-16 22:23:42 +02:00
lorenz.stechauner 8c49cc8ef2 DeliveryConfirmation: align weight to bottom 2023-10-16 18:40:19 +02:00
lorenz.stechauner f0751499ea DeliveryConfirmation: Add user editable Text 2023-10-16 18:24:17 +02:00
lorenz.stechauner d4dd84394b SeasonFinishWindow: Add checkboxes for options 2023-10-15 23:14:40 +02:00
lorenz.stechauner 3b4340b5e8 Database: Change bins once again... 2023-10-15 22:46:19 +02:00
lorenz.stechauner 4db147e582 Billing: honor gebunden field 2023-10-15 15:13:59 +02:00
lorenz.stechauner c997acc95d TestWindow: Remove CalcBinsButton 2023-10-15 15:11:40 +02:00
lorenz.stechauner f8126c392e DeliveryPart: Add gebunden field 2023-10-15 13:40:29 +02:00
lorenz.stechauner b6d74d3c07 DeliveryConfirmation: enhance calculation 2023-10-15 13:12:40 +02:00
lorenz.stechauner 62bf425313 Billing: differentiate between flag FillLowerBins 2023-10-15 01:09:55 +02:00
lorenz.stechauner 5657a8f90a Windows: Add SeasonFinishWindow 2023-10-15 00:38:54 +02:00
lorenz.stechauner 56578a0a9d Documents: Add DeliveryConfirmation 2023-10-15 00:34:40 +02:00
lorenz.stechauner e71d9516ec Billing: small fixes 2023-10-15 00:11:42 +02:00
lorenz.stechauner 658c10c2a4 WineAttr: Add fill_lower_bins 2023-10-14 23:56:41 +02:00
lorenz.stechauner ff3defe52d Database: fix v_bucket update 2023-10-14 23:51:54 +02:00
lorenz.stechauner a489f13d99 Billing: set unused Bin names to NULL 2023-10-14 21:03:23 +02:00
lorenz.stechauner 20d4bbf7ac DeliveryJournal: correctly use .Include() 2023-10-14 17:31:13 +02:00
lorenz.stechauner 604094a603 MemberAdminWindow: Add Print Letterhead option for current Member 2023-10-14 14:28:41 +02:00
lorenz.stechauner 522fed818f Billing: Make bin calculate way more efficient 2023-10-14 14:20:44 +02:00
lorenz.stechauner 3930534273 MemberAdminWindow: also display GA from active only members 2023-10-12 13:37:25 +02:00
lorenz.stechauner 4d950b2597 Billing: Add feature to calculate member/delivery bins 2023-10-11 23:46:38 +02:00
lorenz.stechauner badf4ce955 DeliveryAdminWindow: add TODO for ToolTips 2023-10-11 10:02:31 +02:00
lorenz.stechauner 7d8c670ed2 DeliveryNote: Make stats changeable in BaseDataWindow 2023-10-10 19:19:29 +02:00
lorenz.stechauner 91a3786cd9 DeliveryNote: style expaneded table differently 2023-10-10 18:09:20 +02:00
lorenz.stechauner 73af12a64d Documents: remove script tags and comments 2023-10-10 16:34:10 +02:00
lorenz.stechauner 0e9bae4ec9 DeliveryNote: Rework according to Herbert 2023-10-10 16:24:16 +02:00
lorenz.stechauner 9df8056616 Bump version to 0.3.7 2023-10-06 01:22:20 +02:00
lorenz.stechauner 9e9195b9c0 MemberAdminWIndow: add status bar 2023-10-05 01:43:44 +02:00
lorenz.stechauner 1625f15f92 MemberAdminWindow: add gridsplitter 2023-10-04 23:28:45 +02:00
lorenz.stechauner 6a8bd9c932 DeliveryAdminWindow: Add member name row to DeliveryList 2023-10-04 10:06:51 +02:00
lorenz.stechauner 05da8eefac DeliveryAdminWindow: Add attribute list to DeliveryPartList 2023-10-04 10:01:07 +02:00
lorenz.stechauner 04badb658b Bump version to 0.3.6 2023-10-03 23:05:51 +02:00
lorenz.stechauner 8f2f5b28cf DeliveryAdminWindow: Allow to exclude varieties from filter 2023-10-03 22:58:13 +02:00
lorenz.stechauner d5e4e0a29d DeliveryAdminWindow: Update delivery filter 2023-10-03 22:32:49 +02:00
lorenz.stechauner 96570dffd4 DeliveryAdminWindow: fix attribute display bug 2023-10-03 19:38:33 +02:00
lorenz.stechauner e79f4baa2f DeiveryAdminWindow: Display percentage of weight in status bar 2023-10-01 09:37:17 +02:00
lorenz.stechauner b79ba14f9e DeliveryAdminWindow: Print Hotfix 2023-09-30 09:55:59 +02:00
397 changed files with 58683 additions and 7401 deletions

No files matched your search

+56
View File
@@ -0,0 +1,56 @@
name: Deploy
on:
push:
tags: ["v[0-9]+.[0-9]+.[0-9]+.[0-9]+"]
jobs:
deploy:
name: Build and Deploy
runs-on: windows-latest
permissions:
contents: write
steps:
- name: Set APP_VERSION variable from tag
shell: powershell
run: |
$APP_VERSION = $env:GITHUB_REF -replace '^refs/tags/v', ''
Add-Content -Path $env:GITHUB_ENV -Value "APP_VERSION=$APP_VERSION"
- name: Checkout repository
uses: actions/checkout@v4
- name: Check version in project
shell: powershell
run: |
Select-String Elwig/Elwig.csproj -Pattern "<Version>"
$res = Select-String Elwig/Elwig.csproj -Pattern "<Version>${{ env.APP_VERSION }}</Version>"
if ($res -eq $null) {
exit 1
}
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v1.1
- name: Setup NuGet
uses: nuget/setup-nuget@v1
- name: Restore NuGet packages
shell: powershell
run: $(& nuget restore Elwig.sln; $a=$lastexitcode) | findstr x*; exit $a
- name: Build Setup
shell: powershell
run: $(& msbuild -verbosity:quiet Setup/Setup.wixproj -property:Configuration=Release -property:Platform=x64; $a=$lastexitcode) | findstr x*; exit $a
- name: Rename artifact
shell: powershell
run: Move-Item Setup/bin/x64/Release/Elwig.exe Setup/bin/x64/Release/Elwig-${{ env.APP_VERSION }}.exe
- name: Create release
uses: akkuman/gitea-release-action@v1
with:
name: Elwig ${{ env.APP_VERSION }}
body: "**[Changelog](src/branch/main/CHANGELOG.md#v${{ env.APP_VERSION }})**"
files: |-
Setup/bin/x64/Release/Elwig-${{ env.APP_VERSION }}.exe
- name: Upload to website
shell: powershell
run: |
$content = [System.IO.File]::ReadAllBytes("Setup/bin/x64/Release/Elwig-${{ env.APP_VERSION }}.exe")
Invoke-WebRequest `
-Uri "https://elwig.at/files/Elwig-${{ env.APP_VERSION }}.exe" `
-Method PUT `
-Body $content `
-Headers @{ Authorization = "${{ secrets.API_AUTHORIZATION }}" } `
-ContentType "application/octet-stream"
+53
View File
@@ -0,0 +1,53 @@
name: Test
on:
push:
branches: ["**"]
paths: ["Elwig/**", "Tests/**", ".gitea/workflows/test.yaml"]
jobs:
test:
name: Run tests
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check for Byte order marks
shell: powershell
run: |
$pattern = [char]::ConvertFromUtf32(0xFEFF)
$files = git grep -IEl "^$pattern"
if ( $lastexitcode -ne 1 ) {
echo "Files with BOM found:"
echo $files
exit 1
} else {
echo "No files with BOM found"
exit 0
}
- name: Check for code smells
shell: powershell
run: |
git grep -IEn "\.(Single|First|Min|Max|Any)(OrDefault)?Async\([^)]|^using System.Data.Entity;"
if ( $lastexitcode -ne 1 ) {
exit 1
} else {
echo "No files with code smells found"
exit 0
}
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v1.1
- name: Setup NuGet
uses: nuget/setup-nuget@v1
- name: Restore NuGet packages
shell: powershell
run: $(& nuget restore Elwig.sln; $a=$lastexitcode) | findstr x*; exit $a
- name: Build Elwig
shell: powershell
run: $(& msbuild -verbosity:quiet Elwig/Elwig.csproj -property:Configuration=Debug; $a=$lastexitcode) | findstr x*; exit $a
- name: Build Tests
shell: powershell
run: $(& dotnet build Tests; $a=$lastexitcode) | findstr x*; exit $a
- name: Run Tests
shell: powershell
run: |
$env:PATH = "$(pwd)\Installer\Files;" + $env:PATH
$(& dotnet test Tests --filter "FullyQualifiedName!~E2ETests"; $a=$lastexitcode) | findstr x*; exit $a
+5
View File
@@ -3,3 +3,8 @@ bin/
*.user
.vs
.idea
Tests/Resources/Sql/Create.sql
*.exe
!WinziPrint.exe
*.sqlite3
*.zip
+1684
View File
File diff suppressed because it is too large. Load diff
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*" />
</dependentAssembly>
</dependency>
</assembly>
+68 -47
View File
@@ -1,12 +1,11 @@
<Application x:Class="Elwig.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Elwig"
xmlns:controls="clr-namespace:Elwig.Controls"
StartupUri="Windows\MainWindow.xaml"
xmlns:ui="http://schemas.modernwpf.com/2019">
<Application
x:Class="Elwig.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctrl="clr-namespace:Elwig.Controls">
<Application.Resources>
<controls:BoolToStringConverter x:Key="BoolToStarConverter" FalseValue="" TrueValue="*"/>
<ctrl:BoolToStringConverter x:Key="BoolToStarConverter" FalseValue="" TrueValue="*"/>
<ctrl:WidthToMarginConverter x:Key="WidthToMarginConverter"/>
<DataTemplate x:Key="PostalDestTemplate">
<StackPanel Orientation="Horizontal">
@@ -28,25 +27,25 @@
</StackPanel>
</DataTemplate>
<ControlTemplate x:Key="WineVarietyTemplateSimple">
<DataTemplate x:Key="WineVarietyTemplateCollapsed">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Name}" Foreground="{Binding Color}"/>
</StackPanel>
</ControlTemplate>
<ControlTemplate x:Key="WineVarietyTemplateExtended">
</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>
</ControlTemplate>
<DataTemplate x:Key="WineVarietyTemplate">
<Control x:Name="Control" Focusable="False" Template="{StaticResource WineVarietyTemplateExtended}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}">
<Setter TargetName="Control" Property="Template" Value="{StaticResource WineVarietyTemplateSimple}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
<DataTemplate 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">
@@ -55,6 +54,12 @@
<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">
@@ -62,45 +67,61 @@
</StackPanel>
</DataTemplate>
<ControlTemplate x:Key="WineQualityLevelTemplateSimple">
<DataTemplate x:Key="WineQualityLevelTemplateCollapsed">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</ControlTemplate>
<ControlTemplate x:Key="WineQualityLevelTemplateExtended">
</DataTemplate>
<DataTemplate x:Key="WineQualityLevelTemplateExpanded">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding IsPredicate, Converter={StaticResource BoolToStarConverter}}" MinWidth="6"/>
<TextBlock Text="{Binding Name}" MinWidth="100" Margin="0,0,10,0"/>
<TextBlock Text="{Binding MinKmwStr}"/>
</StackPanel>
</ControlTemplate>
<DataTemplate x:Key="WineQualityLevelTemplate">
<Control x:Name="Control" Focusable="False" Template="{StaticResource WineQualityLevelTemplateExtended}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}">
<Setter TargetName="Control" Property="Template" Value="{StaticResource WineQualityLevelTemplateSimple}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
<ControlTemplate x:Key="WineOriginTemplateSimple">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</ControlTemplate>
<ControlTemplate x:Key="WineOriginTemplateExtended">
<DataTemplate x:Key="WineOriginTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding HkIdLevel}" MinWidth="70" Margin="0,0,10,0"/>
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</ControlTemplate>
<DataTemplate x:Key="WineOriginTemplate">
<Control x:Name="Control" Focusable="False" Template="{StaticResource WineOriginTemplateExtended}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}">
<Setter TargetName="Control" Property="Template" Value="{StaticResource WineOriginTemplateSimple}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
<DataTemplate x:Key="WineOriginTemplateCollapsed">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="WineOriginTemplateExpanded">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding HkIdLevel}" MinWidth="70" Margin="0,0,10,0"/>
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="GemTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Gkz, StringFormat='{} ({0:00000})'}"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="WbKgTemplate">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock Width="250">
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding KgNr, StringFormat='{}({0:00000})'}"/>
</TextBlock>
<TextBlock Text="{Binding Gem.WbGem.Origin.Name}"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="WbKgGlTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Width="220">
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding KgNr, StringFormat='{}({0:00000})'}"/>
</TextBlock>
<TextBlock Text="{Binding WbKg.Gl.Name}"/>
</StackPanel>
</DataTemplate>
</Application.Resources>
</Application>
+387 -100
View File
@@ -1,39 +1,47 @@
using System;
using System.Data;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.IO;
using Elwig.Dialogs;
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.Services;
using Elwig.Windows;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Windows.Threading;
using System.Globalization;
using System.Threading;
using System.Windows.Markup;
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.Input;
using System.Windows.Threading;
namespace Elwig {
public partial class App : Application {
public static readonly string DataPath = @"C:\ProgramData\Elwig\";
public static readonly string ExePath = @"C:\Program Files\Elwig\";
public static App CurrentApp;
public static int NumWindows => CurrentApp.Windows.Count;
public static bool ForceShutdown { get; set; } = false;
private readonly DispatcherTimer _autoUpdateTimer = new() { Interval = TimeSpan.FromHours(1) };
private readonly DispatcherTimer _autoBackupTimer = new() { Interval = TimeSpan.FromMinutes(10) };
public readonly SerialPortWatcher SerialPortWatcher = new();
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 InstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Elwig");
public static readonly string TempPath = Path.Combine(Path.GetTempPath(), "Elwig");
public static readonly Config Config = new(DataPath + "config.ini");
public static int VersionMajor { get; private set; }
public static int VersionMinor { get; private set; }
public static int VersionPatch { get; private set; }
public static string Version {
get => $"{VersionMajor}.{VersionMinor}.{VersionPatch}";
private set {
var p = value.Split(".").Select(p => int.Parse(p.Trim())).ToArray();
VersionMajor = p.ElementAtOrDefault(0);
VersionMinor = p.ElementAtOrDefault(1);
VersionPatch = p.ElementAtOrDefault(2);
}
}
public static Config Config { get; private set; } = new(ConfigPath);
public static Version Version { get; private set; } = new();
public static int BranchNum { get; private set; }
public static string ZwstId { get; private set; }
public static string BranchName { get; private set; }
public static int? BranchPlz { get; private set; }
@@ -42,128 +50,407 @@ 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 ClientParameters Client { get; private set; }
public static bool IsPrintingReady => Documents.Html.IsReady && Documents.Pdf.IsReady;
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; }
private DateTime LastChanged;
private static DateTime CurrentLastWrite => File.GetLastWriteTime(Config.DatabaseFile);
private readonly DispatcherTimer ContextTimer = new() { Interval = TimeSpan.FromSeconds(2) };
public App() : base() {
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Directory.CreateDirectory(TempPath);
Directory.CreateDirectory(DataPath);
Directory.CreateDirectory(MailsPath);
MainDispatcher = Dispatcher;
Scales = Array.Empty<IScale>();
Scales = [];
CurrentApp = this;
Utils.OverrideCulture();
var args = Environment.GetCommandLineArgs();
if (args.Length >= 2) {
Config = new(Path.GetFullPath(args[1]));
}
ContextTimer.Tick += (sender, evt) => {
var ch = CurrentLastWrite;
if (ch > LastChanged) {
LastChanged = ch;
OnContextChanged();
}
};
}
protected override void OnStartup(StartupEventArgs evt) {
var locale = new CultureInfo("de-AT");
locale.NumberFormat.CurrencyGroupSeparator = "\u202f";
locale.NumberFormat.NumberGroupSeparator = "\u202f";
locale.NumberFormat.PercentGroupSeparator = "\u202f";
Thread.CurrentThread.CurrentCulture = locale;
Thread.CurrentThread.CurrentUICulture = locale;
CultureInfo.DefaultThreadCurrentCulture = locale;
CultureInfo.DefaultThreadCurrentUICulture = locale;
FrameworkElement.LanguageProperty.OverrideMetadata(
typeof(FrameworkElement),
new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag))
);
private static void OnContextChanged() {
MainDispatcher.BeginInvoke(HintContextChange);
}
Version = typeof(App).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "";
protected override async void OnStartup(StartupEventArgs evt) {
Version = new Version(typeof(App).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion.Split('+')[0] ?? "0.0.0.0");
try {
AppDbUpdater.CheckDb();
} catch (Exception e) {
MessageBox.Show($"Invalid Database:\n\n{e.Message}", "Invalid Database", MessageBoxButton.OK, MessageBoxImage.Error);
Shutdown();
await AppDbUpdater.CheckDb();
} catch (FileNotFoundException exc) {
InteractionService.ShowException("Datenbank nicht gefunden", "Die Datenbank konnte nicht gefunden werden", exc);
} catch (Exception exc) {
if (Config.UpdateUrl != null && Utils.HasInternetConnectivity()) {
await CheckForUpdates();
}
InteractionService.ShowException("Fehlerhafte Datenbank", "Schwerwiegender Fehler in der Datenbank, unbedingt den Programmierern melden!", exc);
if (InteractionService.AskQuestion("Backup wiederherstellen", "Soll die Datenbank aus einem Backup wiederhergestellt werden?\n\nAchtung: Das kann zu Datenverlust führen!", false))
await BackupService.RestoreDatabase();
ShutdownNow();
return;
}
Dictionary<string, (string, string, int?, string?, string?, string?, string?, string?)> branches = new();
LastChanged = CurrentLastWrite;
ContextTimer.Start();
Dictionary<string, (string, string, int?, string?, string?, string?, string?, string?)> branches = [];
using (var ctx = new AppDbContext()) {
branches = ctx.Branches.ToDictionary(b => b.Name.ToLower(), b => (b.ZwstId, b.Name, b.PostalDest?.AtPlz?.Plz, b.PostalDest?.AtPlz?.Ort.Name, b.Address, b.PhoneNr, b.FaxNr, b.MobileNr));
branches = await ctx.FetchBranches()
.ToDictionaryAsync(b => b.Name.ToLower(), b => (b.ZwstId, b.Name, b.PostalDest?.AtPlz?.Plz, b.PostalDest?.AtPlz?.Ort.Name, b.Address, b.PhoneNr, b.FaxNr, b.MobileNr));
try {
Client = new(ctx);
} catch (Exception e) {
MessageBox.Show($"Fehler beim Laden der Mandantendaten:\n\n{e.Message}", "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
Shutdown();
} catch (Exception exc) {
InteractionService.ShowException("Fehler", "Fehler beim Laden der Mandantendaten, unbedingt den Programmierern melden!", exc);
if (InteractionService.AskQuestion("Backup wiederherstellen", "Soll die Datenbank aus einem Backup wiederhergestellt werden?\n\nAchtung: Das kann zu Datenverlust führen!", false))
await BackupService.RestoreDatabase();
ShutdownNow();
return;
}
BranchNum = branches.Count;
}
Utils.RunBackground("HTML Initialization", () => Documents.Html.Init(PrintingReadyChanged));
Utils.RunBackground("PDF Initialization", () => Documents.Pdf.Init(PrintingReadyChanged));
Utils.RunBackground("Temp File Cleanup", () => {
Utils.CleanupTempFiles();
return Task.CompletedTask;
});
Utils.RunBackground("PDF Initialization", async () => Pdf.Init());
Utils.RunBackground("JSON Schema Initialization", BillingData.Init);
if (Config.BackupAuto) {
Utils.RunBackground("Automatisches Backup", async () => {
Directory.CreateDirectory(Config.BackupPath);
await Task.Delay(500);
await BackupService.TryDatabaseBackup();
});
_autoBackupTimer.Tick += new EventHandler((sender, evt) => {
Utils.RunBackground("Automatisches Backup", async () => {
await BackupService.TryDatabaseBackup();
});
});
_autoBackupTimer.Start();
}
if (Config.UpdateAuto && Config.UpdateUrl != null) {
if (Utils.HasInternetConnectivity()) {
Utils.RunBackground("Auto Updater", async () => {
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) {
var id = s[0];
try {
var type = s[1]?.ToLower();
var model = s[2];
var cnx = s[3];
var empty = s[4];
var filling = s[5];
int? limit = s[6] == null ? null : int.Parse(s[6]);
var log = s[7];
if (type == "systec") {
list.Add(new SystecScale(id, model, cnx, empty, filling, limit, log));
} else {
throw new ArgumentException($"Invalid scale type: \"{type}\"");
var scale = Scale.FromConfig(s);
if (s.SyncTime && scale is ICommandScale cmd) {
try {
await cmd.SetDateAndTime(DateTime.Now);
} catch { }
}
} catch (Exception e) {
list.Add(new InvalidScale(id));
MessageBox.Show($"Unable to create scale {s[0]}:\n\n{e.Message}", "Scale Error", MessageBoxButton.OK, MessageBoxImage.Error);
list.Add(scale);
} catch (Exception exc) {
list.Add(new InvalidScale(s.Id));
if (s.Required)
InteractionService.ShowException("Waagenfehler", $"Verbindung zu Waage {s.Id} konnte nicht hergestellt werden", exc);
}
}
Scales = list;
if (Config.Branch != null) {
if (!branches.ContainsKey(Config.Branch.ToLower())) {
MessageBox.Show("Invalid branch name in config!", "Invalid Branch Config", MessageBoxButton.OK, MessageBoxImage.Error);
Shutdown();
InteractionService.ShowError("Ungültige Zweigstelle", "Ungültige Zweigstelle in Konfigurationsdatei!");
ShutdownNow();
} else {
var entry = branches[Config.Branch.ToLower()];
ZwstId = entry.Item1;
BranchName = entry.Item2;
BranchPlz = entry.Item3;
BranchLocation = entry.Item4;
BranchAddress = entry.Item5;
BranchPhoneNr = entry.Item6;
BranchFaxNr = entry.Item7;
BranchMobileNr = entry.Item8;
SetBranch(branches[Config.Branch.ToLower()]);
}
} else if (branches.Count == 1) {
var entry = branches.First().Value;
ZwstId = entry.Item1;
BranchName = entry.Item2;
BranchPlz = entry.Item3;
BranchLocation = entry.Item4;
BranchAddress = entry.Item5;
BranchPhoneNr = entry.Item6;
BranchFaxNr = entry.Item7;
BranchMobileNr = entry.Item8;
SetBranch(branches.First().Value);
} else {
MessageBox.Show("Unable to determine local branch!", "Invalid Branch Config", MessageBoxButton.OK, MessageBoxImage.Error);
Shutdown();
InteractionService.ShowError("Ungültige Zweigstelle", "Erkennen der lokalen Zweigstelle nicht möglich!");
ShutdownNow();
}
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 || Client.IsPamhagen) {
Config.WeighingMode = WeighingMode.Gross;
}
}
base.OnStartup(evt);
var window = new MainWindow();
window.Show();
}
private void PrintingReadyChanged() {
Dispatcher.BeginInvoke(OnPrintingReadyChanged, new EventArgs());
public static void ShutdownNow() {
ForceShutdown = true;
Current.Shutdown();
}
protected void OnPrintingReadyChanged(EventArgs evt) {
foreach (Window w in Windows) {
foreach (var b in ControlUtils.FindAllChildren<Button>(w).Where(b => b.Tag?.ToString() == "Print")) {
b.IsEnabled = IsPrintingReady;
public async void ShutdownGracefully() {
Mouse.OverrideCursor = Cursors.AppStarting;
try {
await BackupService.TryDatabaseBackup();
} catch (Exception exc) {
InteractionService.ShowException("Automatisches Backup", exc);
}
SerialPortWatcher.Dispose();
foreach (var s in EventScales) {
s.Dispose();
}
Pdf.Cleanup();
Shutdown();
}
public static void SetBranch(Branch b) {
SetBranch((b.ZwstId, b.Name, b.PostalDest?.AtPlz?.Plz, b.PostalDest?.AtPlz?.Ort.Name, b.Address, b.PhoneNr, b.FaxNr, b.MobileNr));
}
private static void SetBranch((string, string, int?, string?, string?, string?, string?, string?) entry) {
ZwstId = entry.Item1;
BranchName = entry.Item2;
BranchPlz = entry.Item3;
BranchLocation = entry.Item4?
.Split(",")[0]
.Split(" in ")[0]
.Split(" im ")[0]
.Split(" an ")[0]
.Split(" am ")[0]
.Split(" bei ")[0]
.Split(" beim ")[0];
BranchAddress = entry.Item5;
BranchPhoneNr = entry.Item6;
BranchFaxNr = entry.Item7;
BranchMobileNr = entry.Item8;
}
public static void HintContextChange() {
if (CurrentApp == null) return;
var ch = CurrentLastWrite;
if (ch > CurrentApp.LastChanged)
CurrentApp.LastChanged = ch;
MainDispatcher.Invoke(() => {
foreach (Window w in CurrentApp.Windows) {
if (w is not ContextWindow c) continue;
MainDispatcher.Invoke(c.HintContextChange);
MainDispatcher.BeginInvoke(c.TryContextReload);
}
foreach (var i in ControlUtils.FindAllChildren<MenuItem>(w).Where(i => i.Tag?.ToString() == "Print")) {
i.IsEnabled = IsPrintingReady;
});
}
private void OnAutoUpdateTimer(object? sender, EventArgs? evt) {
foreach (Window w in CurrentApp.Windows) {
if (w is UpdateDialog) return;
}
if (Utils.HasInternetConnectivity()) {
Utils.RunBackground("Auto Updater", async () => await CheckForUpdates());
}
}
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);
InteractionService.ShowInformation($"Waage {s.Id}", $"Verbindung zu Waage {s.Id} wieder hergestellt!");
} catch (Exception exc) {
Scales[i] = new InvalidScale(s.Id);
InteractionService.ShowException("Waagenfehler", $"Verbindung zu Waage {s.Id} konnte nicht hergestellt werden", exc);
}
} else if (Scales[i] is IEventScale) {
InteractionService.ShowInformation($"Waage {s.Id}", $"Verbindung zu Waage {s.Id} wieder hergestellt!");
}
}
}
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) {
InteractionService.ShowWarning($"Waagen {s.Id}", $"Verbindung zu Waage {s.Id} unterbrochen!");
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);
if (latest != null && new Version(latest.Value.Version) > Version) {
await MainDispatcher.BeginInvoke(() => {
var d = new UpdateDialog(latest.Value.Version, latest.Value.Url, latest.Value.Size);
if (d.ShowDialog() == true) {
ShutdownNow();
}
});
} else if (showAlert) {
if (latest == null) {
InteractionService.ShowError("Nach Updates suchen", "Informationen konnten nicht abgerufen werden!");
} else {
InteractionService.ShowInformation("Nach Updates suchen", $"Elwig ist auf dem aktuellsten Stand! (Version: {latest.Value.Version})");
}
}
}
public static async Task ReplaceDatabase(string filename) {
try {
await Task.Run(async () => {
await Database.Import(filename);
});
InteractionService.ShowInformation("Datenbank ersetzen", "Das Ersetzen war erfolgreich!\n\nBitte starten Sie Elwig neu!");
ShutdownNow();
} catch (Exception exc) {
InteractionService.ShowException("Datenbank ersetzen", "Fehler beim Ersetzen", exc);
}
}
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))) {
if (t.WindowState == WindowState.Minimized)
t.WindowState = WindowState.Normal;
t.Activate();
return t;
}
}
var n = constructor();
n.Show();
return n;
}
public static DeliveryAdminWindow FocusReceipt() {
return FocusWindow<DeliveryAdminWindow>(() => new(true), w => w.ViewModel.IsReceipt);
}
public static MemberBusinessSharesAdminWindow FocusMemberBusinessSharesWindow(int mgnr) {
return FocusWindow<MemberBusinessSharesAdminWindow>(() => new(mgnr), w => w.ViewModel.FilterMember?.MgNr == mgnr);
}
public static DeliveryAdminWindow FocusMemberDeliveries(int mgnr) {
return FocusWindow<DeliveryAdminWindow>(() => new(mgnr), w => w.ViewModel.FilterMember?.MgNr == mgnr);
}
public static AreaComAdminWindow FocusMemberAreaComs(int mgnr) {
return FocusWindow<AreaComAdminWindow>(() => new(mgnr), w => w.ViewModel.FilterMember.MgNr == mgnr);
}
public static BaseDataWindow FocusBaseData() {
return FocusWindow<BaseDataWindow>(() => new());
}
public static BaseDataWindow FocusBaseDataAreaComType() {
var w = FocusBaseData();
w.AreaCommitmentTypes.Focus();
return w;
}
public static BaseDataWindow FocusBaseDataSeason(int year) {
var w = FocusBaseData();
w.Seasons.Focus();
ControlUtils.SelectItemWithPk(w.SeasonList, year);
return w;
}
public static OriginHierarchyWindow FocusOriginHierarchy() {
return FocusWindow<OriginHierarchyWindow>(() => new());
}
public static OriginHierarchyWindow FocusOriginHierarchyKg(int kgnr) {
var w = FocusOriginHierarchy();
w.FocusKgNr(kgnr);
return w;
}
public static DeliveryAncmtAdminWindow FocusDeliveryAncmt() {
return FocusWindow<DeliveryAncmtAdminWindow>(() => new());
}
public static DeliveryScheduleAdminWindow FocusDeliverySchedule() {
return FocusWindow<DeliveryScheduleAdminWindow>(() => new());
}
public static PaymentVariantsWindow FocusPaymentVariants(int year) {
return FocusWindow<PaymentVariantsWindow>(() => new(year), w => w.Year == year);
}
public static PaymentAdjustmentWindow FocusPaymentAdjustment(int year) {
return FocusWindow<PaymentAdjustmentWindow>(() => new(year), w => w.Year == year);
}
public static ChartWindow FocusChartWindow(int year, int avnr) {
return FocusWindow<ChartWindow>(() => new(year, avnr), w => w.Year == year && w.AvNr == avnr);
}
public static MemberAdminWindow FocusMember(int mgnr) {
var w = FocusWindow<MemberAdminWindow>(() => new());
w.FocusMember(mgnr);
return w;
}
public static MailWindow FocusMailWindow(int? year = null) {
return FocusWindow<MailWindow>(() => new(year), w => year == null || w.Year == year);
}
}
}
+3 -2
View File
@@ -1,16 +1,17 @@
using System;
using System.Windows.Data;
using System.Globalization;
namespace Elwig.Controls {
public class BoolToValueConverter<T> : IValueConverter {
public T FalseValue { get; set; }
public T TrueValue { get; set; }
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return (bool)value ? TrueValue : FalseValue;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return value?.Equals(TrueValue) ?? false;
}
}
+172
View File
@@ -0,0 +1,172 @@
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace Elwig.Controls {
public class CheckComboBox : ListBox {
public new static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(nameof(SelectedItems), typeof(IList), typeof(CheckComboBox), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemsChangedCallback));
public new IList SelectedItems {
get => (IList)GetValue(SelectedItemsProperty);
set => SetValue(SelectedItemsProperty, value);
}
public static readonly DependencyProperty DelimiterProperty = DependencyProperty.Register(nameof(Delimiter), typeof(string), typeof(CheckComboBox), new FrameworkPropertyMetadata(", "));
public string Delimiter {
get => (string)GetValue(DelimiterProperty);
set => SetValue(DelimiterProperty, value);
}
public static readonly DependencyProperty ListDisplayMemberPathProperty = DependencyProperty.Register(nameof(ListDisplayMemberPath), typeof(string), typeof(CheckComboBox), new FrameworkPropertyMetadata(null));
public string ListDisplayMemberPath {
get => (string)GetValue(ListDisplayMemberPathProperty);
set => SetValue(ListDisplayMemberPathProperty, value);
}
public static readonly DependencyProperty AllItemsSelectedContentProperty = DependencyProperty.Register(nameof(AllItemsSelectedContent), typeof(string), typeof(CheckComboBox), new FrameworkPropertyMetadata("All"));
public string AllItemsSelectedContent {
get => (string)GetValue(AllItemsSelectedContentProperty);
set => SetValue(AllItemsSelectedContentProperty, value);
}
public static readonly DependencyProperty IsSelectAllActiveProperty = DependencyProperty.Register(nameof(IsSelectAllActive), typeof(bool), typeof(CheckComboBox), new FrameworkPropertyMetadata(false));
public bool IsSelectAllActive {
get => (bool)GetValue(IsSelectAllActiveProperty);
set => SetValue(IsSelectAllActiveProperty, value);
}
public static readonly DependencyProperty SelectAllContentProperty = DependencyProperty.Register(nameof(SelectAllContent), typeof(string), typeof(CheckComboBox), new FrameworkPropertyMetadata("All"));
public string SelectAllContent {
get => (string)GetValue(SelectAllContentProperty);
set => SetValue(SelectAllContentProperty, value);
}
public static readonly DependencyProperty AllItemsSelectedProperty = DependencyProperty.Register(nameof(AllItemsSelected), typeof(bool?), typeof(CheckComboBox), new FrameworkPropertyMetadata(false));
public bool? AllItemsSelected {
get => (bool?)GetValue(AllItemsSelectedProperty);
set => SetValue(AllItemsSelectedProperty, value);
}
public static readonly DependencyProperty IsDropDownOpenProperty = DependencyProperty.Register(nameof(IsDropDownOpen), typeof(bool), typeof(CheckComboBox), new FrameworkPropertyMetadata(false));
public bool IsDropDownOpen {
get => (bool)GetValue(IsDropDownOpenProperty);
set => SetValue(IsDropDownOpenProperty, value);
}
public static readonly DependencyProperty MaxDropDownHeightProperty = DependencyProperty.Register(nameof(MaxDropDownHeight), typeof(double), typeof(CheckComboBox), new FrameworkPropertyMetadata(ComboBox.MaxDropDownHeightProperty.DefaultMetadata.DefaultValue));
public double MaxDropDownHeight {
get => (double)GetValue(MaxDropDownHeightProperty);
set => SetValue(MaxDropDownHeightProperty, value);
}
public new static readonly RoutedEvent SelectionChangedEvent = EventManager.RegisterRoutedEvent(nameof(SelectionChanged), RoutingStrategy.Bubble, typeof(SelectionChangedEventHandler), typeof(CheckComboBox));
public new event SelectionChangedEventHandler SelectionChanged {
add => AddHandler(SelectionChangedEvent, value);
remove => RemoveHandler(SelectionChangedEvent, value);
}
static CheckComboBox() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(CheckComboBox), new FrameworkPropertyMetadata(typeof(CheckComboBox)));
}
private bool _viewHandled;
private bool _modelHandled;
private TextBlock _textBox;
public CheckComboBox() {
SelectionMode = SelectionMode.Multiple;
SelectedItems = new ObservableCollection<object>();
}
public override void OnApplyTemplate() {
_textBox = (GetTemplateChild("TextBox") as TextBlock)!;
var button = GetTemplateChild("Button") as Button;
button!.Click += Button_MouseDown;
var item = GetTemplateChild("SelectAllItem") as ListBoxItem;
item!.PreviewMouseDown += SelectAllItem_MouseDown;
if (SelectedItems is INotifyCollectionChanged collection) {
collection.CollectionChanged += (s, e) => { SelectItems(); };
}
IsEnabledChanged += OnIsEnabledChanged;
base.SelectionChanged += OnSelectionChanged;
base.OnApplyTemplate();
}
private static void OnSelectedItemsChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs e) {
if (sender is CheckComboBox ccb)
ccb.OnSelectedItemsChanged();
}
private void OnSelectedItemsChanged() {
if (SelectedItems is INotifyCollectionChanged collection) {
collection.CollectionChanged += (s, e) => { SelectItems(); };
}
SelectItems();
}
private void Button_MouseDown(object sender, RoutedEventArgs evt) {
IsDropDownOpen = !IsDropDownOpen;
}
private void SelectAllItem_MouseDown(object sender, RoutedEventArgs evt) {
if (AllItemsSelected == false) {
SelectAll();
} else {
UnselectAll();
}
evt.Handled = true;
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs evt) {
SelectItemsReverse();
var dmp = !string.IsNullOrWhiteSpace(ListDisplayMemberPath) ? ListDisplayMemberPath : !string.IsNullOrWhiteSpace(DisplayMemberPath) ? DisplayMemberPath : null;
if (SelectedItems.Count == ItemsSource.Cast<object>().Count() && AllItemsSelectedContent != null) {
AllItemsSelected = true;
} else if (SelectedItems.Count == 0) {
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))
);
}
RaiseEvent(new SelectionChangedEventArgs(SelectionChangedEvent, evt.RemovedItems, evt.AddedItems));
}
private void OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs evt) {
if (!IsEnabled) IsDropDownOpen = false;
}
private void SelectItems() {
if (_viewHandled || _modelHandled)
return;
_viewHandled = true;
base.SelectedItems.Clear();
foreach (var item in SelectedItems)
base.SelectedItems.Add(item);
_viewHandled = false;
}
private void SelectItemsReverse() {
if (_modelHandled || _viewHandled)
return;
_modelHandled = true;
SelectedItems.Clear();
foreach (var item in base.SelectedItems)
SelectedItems.Add(item);
_modelHandled = false;
}
}
}
+118
View File
@@ -0,0 +1,118 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctrl="clr-namespace:Elwig.Controls">
<ctrl:VisibilityConverter x:Key="VisibilityConverter"/>
<Style TargetType="ctrl:CheckComboBox" BasedOn="{StaticResource {x:Type ListBox}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ctrl:CheckComboBox">
<Grid Style="{x:Null}">
<Button x:Name="Button" ClickMode="Press" BorderThickness="1"
BorderBrush="{TemplateBinding BorderBrush}"
IsEnabled="{Binding IsEnabled, RelativeSource={RelativeSource TemplatedParent}}">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="White"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}" SnapsToDevicePixels="True"
BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1">
<ContentPresenter HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Button.Style>
<Path x:Name="IconDropdown" Data="M 0,0 L 3,3 L 6,0" Stroke="#FF606060" StrokeThickness="1" Margin="0,0,5,0"
HorizontalAlignment="Right" VerticalAlignment="Center"/>
</Button>
<TextBlock x:Name="TextBox" Style="{x:Null}" Margin="6,0,18,0" IsHitTestVisible="False"
HorizontalAlignment="Stretch" VerticalAlignment="Center"/>
<Popup x:Name="Popup" Placement="Bottom" Focusable="True"
IsOpen="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
PopupAnimation="Slide" AllowsTransparency="True">
<Popup.Style>
<Style TargetType="{x:Type Popup}">
<Setter Property="StaysOpen" Value="False"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, ElementName=Button}" Value="True">
<Setter Property="StaysOpen" Value="True"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsMouseOver, ElementName=Border}" Value="True">
<Setter Property="StaysOpen" Value="True"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Popup.Style>
<Border x:Name="Border" Style="{x:Null}" BorderThickness="1" BorderBrush="Gray" Background="White" SnapsToDevicePixels="True"
MinWidth="{TemplateBinding ActualWidth}"
MaxHeight="{TemplateBinding MaxDropDownHeight}">
<DockPanel>
<ListBoxItem x:Name="SelectAllItem" Padding="2,1,2,1" DockPanel.Dock="Top"
Visibility="{TemplateBinding IsSelectAllActive, Converter={StaticResource VisibilityConverter}}">
<StackPanel Orientation="Horizontal">
<CheckBox VerticalAlignment="Center" Margin="0,0,5,0" IsThreeState="True"
IsChecked="{Binding AllItemsSelected, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"/>
<TextBlock Text="{TemplateBinding SelectAllContent}" VerticalAlignment="Center" Margin="0" SnapsToDevicePixels="True"/>
</StackPanel>
</ListBoxItem>
<ScrollViewer Style="{x:Null}">
<StackPanel Style="{x:Null}" IsItemsHost="True" SnapsToDevicePixels="True"
KeyboardNavigation.DirectionalNavigation="Contained"/>
</ScrollViewer>
</DockPanel>
</Border>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="IconDropdown" Property="Stroke" Value="#FFA0A0A0"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="IconDropdown" Property="Stroke" Value="Black"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Padding="2,1,2,1">
<StackPanel Orientation="Horizontal">
<CheckBox VerticalAlignment="Center" Margin="0,0,5,0"
IsChecked="{Binding IsSelected, Mode=TwoWay, RelativeSource={RelativeSource AncestorType=ListBoxItem}}"/>
<ContentPresenter VerticalAlignment="Center" Margin="0" SnapsToDevicePixels="True"/>
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="BorderThickness" Value="1"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="BorderBrush" Value="#FF70C0E7"/>
<Setter Property="Background" Value="#FFE5F3FB"/>
</Trigger>
</Style.Triggers>
</Style>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="Gray"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="BorderBrush" Value="#FF7EB4EA"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
+115
View File
@@ -0,0 +1,115 @@
using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
namespace Elwig.Controls {
public class IntegerUpDown : TextBox {
public static readonly DependencyProperty MinimumProperty = DependencyProperty.Register(nameof(Minimum), typeof(int?), typeof(IntegerUpDown), new FrameworkPropertyMetadata(null));
public int? Minimum {
get => (int?)GetValue(MinimumProperty);
set => SetValue(MinimumProperty, value);
}
public static readonly DependencyProperty MaximumProperty = DependencyProperty.Register(nameof(Maximum), typeof(int?), typeof(IntegerUpDown), new FrameworkPropertyMetadata(null));
public int? Maximum {
get => (int?)GetValue(MaximumProperty);
set => SetValue(MaximumProperty, value);
}
public int? Value {
get => int.TryParse(Text, out var res) ? res : null;
set => Text = $"{value}";
}
static IntegerUpDown() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(IntegerUpDown), new FrameworkPropertyMetadata(typeof(IntegerUpDown)));
}
public IntegerUpDown() {
TextChanged += IntegerUpDown_TextChanged;
LostFocus += IntegerUpDown_LostFocus;
KeyUp += IntegerUpDown_KeyUp;
}
public override void OnApplyTemplate() {
var incButton = GetTemplateChild("IncrementButton") as RepeatButton;
var decButton = GetTemplateChild("DecrementButton") as RepeatButton;
incButton!.Click += IncrementButton_Click;
decButton!.Click += DecrementButton_Click;
base.OnApplyTemplate();
UpdateButtons();
}
private void UpdateButtons() {
var incButton = GetTemplateChild("IncrementButton") as RepeatButton;
var decButton = GetTemplateChild("DecrementButton") as RepeatButton;
incButton?.IsEnabled = Maximum == null || Value < Maximum;
decButton?.IsEnabled = Minimum == null || Value > Minimum;
}
private void IntegerUpDown_TextChanged(object sender, TextChangedEventArgs evt) {
var idx = CaretIndex;
Text = new string([.. Text.Where(char.IsAsciiDigit).Take(4)]);
CaretIndex = idx;
evt.Handled = !((!Minimum.HasValue || Value >= Minimum) && (!Maximum.HasValue || Value <= Maximum));
if (idx >= 4) {
if (Value < Minimum) {
Value = Minimum;
} else if (Value > Maximum) {
Value = Maximum;
}
CaretIndex = 4;
}
UpdateButtons();
}
private void IntegerUpDown_LostFocus(object sender, RoutedEventArgs evt) {
if (Value < Minimum) {
Value = Minimum;
} else if (Value > Maximum) {
Value = Maximum;
}
}
private void IncrementButton_Click(object sender, RoutedEventArgs evt) {
Value = Math.Min((Value ?? 0) + 1, Maximum ?? int.MaxValue);
}
private void DecrementButton_Click(object sender, RoutedEventArgs evt) {
Value = Math.Max((Value ?? 0) - 1, Minimum ?? int.MinValue);
}
private void IntegerUpDown_KeyUp(object sender, KeyEventArgs evt) {
switch (evt.Key) {
case Key.Up:
case Key.Add:
case Key.OemPlus:
Value = Math.Min((Value ?? 0) + 1, Maximum ?? int.MaxValue);
evt.Handled = true;
CaretIndex = 4;
break;
case Key.Down:
case Key.Subtract:
case Key.OemMinus:
Value = Math.Max((Value ?? 0) - 1, Minimum ?? int.MinValue);
evt.Handled = true;
CaretIndex = 4;
break;
case Key.PageUp:
Value = Math.Min((Value ?? 0) + 10, Maximum ?? int.MaxValue);
evt.Handled = true;
CaretIndex = 4;
break;
case Key.PageDown:
Value = Math.Max((Value ?? 0) - 10, Minimum ?? int.MinValue);
evt.Handled = true;
CaretIndex = 4;
break;
}
}
}
}
+53
View File
@@ -0,0 +1,53 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctrl="clr-namespace:Elwig.Controls">
<Style TargetType="ctrl:IntegerUpDown" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ctrl:IntegerUpDown">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="18"/>
</Grid.ColumnDefinitions>
<Border x:Name="Border" BorderThickness="1,1,0,1" Grid.RowSpan="2"
BorderBrush="{Binding Path=BorderBrush, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}">
<ScrollViewer x:Name="PART_ContentHost" VerticalAlignment="Center"/>
</Border>
<RepeatButton x:Name="IncrementButton" Padding="0" Height="Auto" Width="Auto" BorderThickness="1,1,1,1"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="0" Grid.Column="1">
<Path x:Name="IconIncrement" Data="M 0,4 L 4,0 L 8,4 Z" Fill="#FF444444"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</RepeatButton>
<RepeatButton x:Name="DecrementButton" Padding="0" Height="Auto" Width="Auto" BorderThickness="1,0,1,1"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="1" Grid.Column="1">
<Path x:Name="IconDecrement" Data="M 0,0 L 4,4 L 8,0 Z" Fill="#FF444444"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</RepeatButton>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Border" Property="BorderBrush" Value="LightGray"/>
<Setter TargetName="IconIncrement" Property="Fill" Value="#FF888888"/>
<Setter TargetName="IconDecrement" Property="Fill" Value="#FF888888"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="TextAlignment" Value="Right"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="UseLayoutRounding" Value="True"/>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="Gray"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
+41
View File
@@ -0,0 +1,41 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Elwig.Controls {
public class UnitConverter : DependencyObject, IValueConverter {
public static readonly DependencyProperty UnitProperty = DependencyProperty.Register(nameof(Unit), typeof(string), typeof(UnitConverter), new FrameworkPropertyMetadata(null));
public string Unit {
get => (string)GetValue(UnitProperty);
set => SetValue(UnitProperty, value);
}
public static readonly DependencyProperty PrecisionProperty = DependencyProperty.Register(nameof(Precision), typeof(byte), typeof(UnitConverter), new FrameworkPropertyMetadata((byte)0));
public byte Precision {
get => (byte)GetValue(PrecisionProperty);
set => SetValue(PrecisionProperty, value);
}
public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture) {
if (value == null) {
return null;
}
var fmt = $"{{0:N{Precision}}}";
var unit = $"{(Unit != null ? " " : "")}{Unit}";
if (value is int i) {
return $"{string.Format(fmt, i)}{unit}";
} else if (value is decimal d) {
return $"{string.Format(fmt, d)}{unit}";
}
return Binding.DoNothing;
}
public object? ConvertBack(object? value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
}
+17
View File
@@ -0,0 +1,17 @@
using System.Windows;
using System.Windows.Controls;
namespace Elwig.Controls {
public class UnitTextBox : TextBox {
public static readonly DependencyProperty UnitProperty = DependencyProperty.Register(nameof(Unit), typeof(string), typeof(UnitTextBox), new FrameworkPropertyMetadata(""));
public string Unit {
get => (string)GetValue(UnitProperty);
set => SetValue(UnitProperty, value);
}
static UnitTextBox() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(UnitTextBox), new FrameworkPropertyMetadata(typeof(UnitTextBox)));
}
}
}
+42
View File
@@ -0,0 +1,42 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctrl="clr-namespace:Elwig.Controls">
<Style TargetType="ctrl:UnitTextBox" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ctrl:UnitTextBox">
<Border x:Name="Border"
BorderThickness="{Binding Path=BorderThickness, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
BorderBrush="{Binding Path=BorderBrush, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}">
<Grid Background="{Binding Path=Background, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}">
<ScrollViewer x:Name="PART_ContentHost" VerticalAlignment="Bottom">
<ScrollViewer.Margin>
<Binding ElementName="UnitBlock" Path="ActualWidth">
<Binding.Converter>
<ctrl:WidthToMarginConverter/>
</Binding.Converter>
</Binding>
</ScrollViewer.Margin>
</ScrollViewer>
<TextBlock x:Name="UnitBlock" Text="{Binding Path=Unit, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"
FontSize="10" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="3"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Border" Property="BorderBrush" Value="LightGray"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="TextAlignment" Value="Right"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="UseLayoutRounding" Value="True"/>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="Gray"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
+16
View File
@@ -0,0 +1,16 @@
using System;
using System.Windows;
using System.Windows.Data;
using System.Globalization;
namespace Elwig.Controls {
public class VisibilityConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return (bool)value ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return (Visibility)value == Visibility.Visible;
}
}
}
+16
View File
@@ -0,0 +1,16 @@
using System;
using System.Windows;
using System.Windows.Data;
using System.Globalization;
namespace Elwig.Controls {
public class WidthToMarginConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return new Thickness(0, 0, 2 + (double)value, 0);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return ((Thickness)value).Right - 2;
}
}
}
@@ -0,0 +1,15 @@
using System.Windows.Controls;
using System.Windows;
namespace Elwig.Controls {
public class WineOriginTemplateSelector : DataTemplateSelector {
public override DataTemplate SelectTemplate(object item, DependencyObject container) {
ContentPresenter presenter = (ContentPresenter)container;
if (presenter.TemplatedParent is ComboBox) {
return (DataTemplate)presenter.FindResource("WineOriginTemplateCollapsed");
} else {
return (DataTemplate)presenter.FindResource("WineOriginTemplateExpanded");
}
}
}
}
@@ -0,0 +1,15 @@
using System.Windows.Controls;
using System.Windows;
namespace Elwig.Controls {
public class WineQualityLevelTemplateSelector : DataTemplateSelector {
public override DataTemplate SelectTemplate(object item, DependencyObject container) {
ContentPresenter presenter = (ContentPresenter)container;
if (presenter.TemplatedParent is ComboBox) {
return (DataTemplate)presenter.FindResource("WineQualityLevelTemplateCollapsed");
} else {
return (DataTemplate)presenter.FindResource("WineQualityLevelTemplateExpanded");
}
}
}
}
@@ -0,0 +1,15 @@
using System.Windows.Controls;
using System.Windows;
namespace Elwig.Controls {
public class WineVarietyTemplateSelector : DataTemplateSelector {
public override DataTemplate SelectTemplate(object item, DependencyObject container) {
ContentPresenter presenter = (ContentPresenter)container;
if (presenter.TemplatedParent is ComboBox) {
return (DataTemplate)presenter.FindResource("WineVarietyTemplateCollapsed");
} else {
return (DataTemplate)presenter.FindResource("WineVarietyTemplateExpanded");
}
}
}
}
-33
View File
@@ -1,33 +0,0 @@
using Elwig.Helpers;
using System.Windows;
using System.Windows.Controls;
namespace Elwig.Dialogs {
public partial class AbwertenDialog : Window {
public int Weight;
public AbwertenDialog(string lsnr, string name, int weight) {
Weight = weight;
InitializeComponent();
TextLsNr.Text = lsnr;
TextMember.Text = name;
TextWeight.Text = $"{weight:N0}\u202fkg";
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
DialogResult = true;
Weight = int.Parse(WeightInput.Text);
Close();
}
private void UpdateButtons() {
ConfirmButton.IsEnabled = int.TryParse(WeightInput.Text, out var w) && w > 0 && w <= Weight;
}
private void WeightInput_TextChanged(object sender, TextChangedEventArgs evt) {
Validator.CheckInteger(WeightInput, true, 5);
UpdateButtons();
}
}
}
+71
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>
+67
View File
@@ -0,0 +1,67 @@
using Elwig.Helpers;
using System;
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, bool forceRetroactive = false) {
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";
forceRetroactive = forceRetroactive || yearFrom.HasValue && yearTo.HasValue && yearFrom.Value == yearTo.Value;
RetroactiveInput.IsEnabled = !forceRetroactive;
if (delete) {
QuestionBlock1.Visibility = Visibility.Hidden;
QuestionBlock2.Visibility = Visibility.Visible;
DescBlock1.Visibility = Visibility.Hidden;
DescBlock2.Visibility = Visibility.Visible;
}
SeasonInput.Minimum = yearFrom.HasValue ? yearFrom + 1 : null;
SeasonInput.Maximum = yearTo.HasValue ? yearTo : null;
if (forceRetroactive || (yearTo.HasValue && yearTo < Utils.CurrentYear) || (yearFrom.HasValue && yearFrom >= Utils.CurrentYear)) {
RetroactiveInput.IsChecked = true;
} else {
SeasonInput.Text = $"{Utils.CurrentYear}";
}
}
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 = $"{Math.Max(SeasonInput.Minimum ?? Utils.CurrentYear, Utils.CurrentYear)}";
DescBlock1.Visibility = QuestionBlock1.Visibility;
DescBlock2.Visibility = QuestionBlock2.Visibility;
}
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
DialogResult = true;
Close();
}
}
}
+74
View File
@@ -0,0 +1,74 @@
<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"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Dialogs"
xmlns:ctrl="clr-namespace:Elwig.Controls"
ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
WindowStartupLocation="CenterOwner"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="Flächenbindungen übertragen" Height="240" Width="450">
<Window.Resources>
<Style TargetType="Label">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="Padding" Value="2,4,2,4"/>
<Setter Property="Height" Value="25"/>
</Style>
<Style TargetType="TextBox">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="2"/>
<Setter Property="Height" Value="25"/>
<Setter Property="TextWrapping" Value="NoWrap"/>
</Style>
<Style TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="25"/>
</Style>
</Window.Resources>
<Grid>
<TextBlock x:Name="QuestionBlock1" TextAlignment="Center" Margin="0,10,0,0"
HorizontalAlignment="Center" VerticalAlignment="Top">
Sollen die aktiven Flächenbindungen des angegebenen Vorgängers<LineBreak/>
übernommen werden? (<Run Text="{Binding AreaComNum}"/> FB, <Run Text="{Binding Area}"/> m²)
</TextBlock>
<TextBlock x:Name="QuestionBlock2" TextAlignment="Center" Margin="0,10,0,0" Visibility="Hidden"
HorizontalAlignment="Center" VerticalAlignment="Top">
Sollen die aktiven Flächenbindungen gekündigt werden? (<Run Text="{Binding AreaComNum}"/> FB, <Run Text="{Binding Area}"/> m²)
</TextBlock>
<Label x:Name="SeasonLabel" Content="Saison:" Margin="0,50,100,0"
HorizontalAlignment="Center" VerticalAlignment="Top"/>
<ctrl:IntegerUpDown x:Name="SeasonInput" Width="56" Height="25" Margin="0,50,0,0" FontSize="14"
Minimum="1900" Maximum="9999"
HorizontalAlignment="Center" VerticalAlignment="Top"
TextChanged="SeasonInput_TextChanged"/>
<TextBlock x:Name="DescBlock1" Margin="0,90,0,0" TextAlignment="Center"
HorizontalAlignment="Center" VerticalAlignment="Top">
Die Flächenbindungen beim <Bold>Vorgänger</Bold> sind bis inkl. Saison <Bold><Run x:Name="CancelSeason1"/></Bold> gültig,<LineBreak/>
und werden beim <Bold>Nachfolger</Bold> ab <Run x:Name="DescBlock1Season" Text="inkl. Saison "/><Bold><Run x:Name="TransferSeason"/></Bold> übernommen.
</TextBlock>
<TextBlock x:Name="DescBlock2" Margin="0,70,0,0" TextAlignment="Center" Visibility="Hidden"
HorizontalAlignment="Center" VerticalAlignment="Top">
Die Flächenbindungen sind bis inklusive Saison <Bold><Run x:Name="CancelSeason2"/></Bold> gültig.
</TextBlock>
<TextBlock x:Name="InfoBlock" Margin="0,0,0,75" TextAlignment="Center"
HorizontalAlignment="Center" VerticalAlignment="Bottom">
Falls die Flächenbindungen später an ein neues Mitglied<LineBreak/>
übertragen werden sollen bitte <Italic>Nein</Italic> auswählen!
</TextBlock>
<TextBlock Text="Die Änderungen werden erst beim Speichern übernommen!"
HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,50"/>
<Button x:Name="ConfirmButton" Content="Ja" Margin="10,10,115,10" Grid.Column="1"
Click="ConfirmButton_Click"/>
<Button x:Name="CancelButton" Content="Nein" Margin="10,10,10,10" Grid.Column="1" IsCancel="True"/>
</Grid>
</Window>
@@ -0,0 +1,60 @@
using Elwig.Helpers;
using System.Windows;
using System.Windows.Controls;
namespace Elwig.Dialogs {
public partial class AreaComTransferDialog : Window {
public int CancelSeason => SuccessorSeason - 1;
public int SuccessorSeason { get; set; }
public string AreaComNum { get; set; }
public string Area { get; set; }
public AreaComTransferDialog(string name, int areaComNum, int area) {
SuccessorSeason = Utils.FollowingSeason;
AreaComNum = $"{areaComNum:N0}";
Area = $"{area:N0}";
InitializeComponent();
SeasonInput.Text = $"{SuccessorSeason}";
SeasonInput.Minimum = Utils.CurrentLastSeason;
Title = $"Aktive Flächenbindungen kündigen - {name}";
QuestionBlock1.Visibility = Visibility.Hidden;
QuestionBlock2.Visibility = Visibility.Visible;
DescBlock1.Visibility = Visibility.Hidden;
DescBlock2.Visibility = Visibility.Visible;
Height = 240;
SeasonInput.Margin = new(0, 40, 0, 0);
SeasonLabel.Margin = new(0, 40, 100, 0);
}
public AreaComTransferDialog(string name, string successorName, int areaComNum, int area) {
SuccessorSeason = Utils.FollowingSeason;
AreaComNum = $"{areaComNum:N0}";
Area = $"{area:N0}";
InitializeComponent();
SeasonInput.Text = $"{SuccessorSeason}";
SeasonInput.Minimum = Utils.CurrentLastSeason;
Title = $"Aktive Flächenbindungen übertragen - {name} - {successorName}";
InfoBlock.Visibility = Visibility.Hidden;
}
private void SeasonInput_TextChanged(object sender, TextChangedEventArgs evt) {
SuccessorSeason = (int)SeasonInput.Value!;
CancelSeason1.Text = $"{CancelSeason}";
CancelSeason2.Text = $"{CancelSeason}";
TransferSeason.Text = $"{SuccessorSeason}";
DescBlock1Season.Text = "inkl. Saison ";
}
private void CopyYearToInput_Changed(object sender, RoutedEventArgs evt) {
TransferSeason.Text = $"{SuccessorSeason}";
DescBlock1Season.Text = "inkl. Saison ";
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
DialogResult = true;
Close();
}
}
}
+64
View File
@@ -0,0 +1,64 @@
<Window x:Class="Elwig.Dialogs.DeleteMemberDialog"
AutomationProperties.AutomationId="DeleteMemberDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Dialogs"
ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
WindowStartupLocation="CenterOwner"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="Mitglied löschen" Height="280" Width="400">
<Window.Resources>
<Style TargetType="Label">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="Padding" Value="2,4,2,4"/>
<Setter Property="Height" Value="25"/>
</Style>
<Style TargetType="TextBox">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="2"/>
<Setter Property="Height" Value="25"/>
<Setter Property="TextWrapping" Value="NoWrap"/>
</Style>
<Style TargetType="CheckBox">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Top"/>
</Style>
<Style TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="25"/>
</Style>
</Window.Resources>
<Grid>
<TextBlock TextAlignment="Center" Margin="10,10,10,10" VerticalAlignment="Top">
Bei Bestätigung wird das Mitglied samt zugehöriger Daten<LineBreak/>
<Bold>unwiderruflich gelöscht!</Bold> Wenn möglich sollte stattdessen<LineBreak/>
der Status des Mitglieds auf <Italic>Inaktiv</Italic> gesetzt werden!
</TextBlock>
<Label Content="Name u. MgNr. wiederholen:" Margin="10,60,10,10" HorizontalAlignment="Center"/>
<TextBox x:Name="NameInput" Margin="10,85,10,10"
TextChanged="NameInput_TextChanged"/>
<Label Content="Beim Löschen müssen folgende Daten auch gelöscht werden:" Margin="10,120,10,10"/>
<CheckBox x:Name="AreaComInput" Content="Flächenbindungen" Margin="40,145,0,0"
Checked="CheckBox_Changed" Unchecked="CheckBox_Changed"
IsChecked="{Binding DeleteAreaComs}"/>
<CheckBox x:Name="DeliveryInput" Content="Lieferungen" Margin="40,165,0,0"
Checked="CheckBox_Changed" Unchecked="CheckBox_Changed"
IsChecked="{Binding DeleteDeliveries}"/>
<CheckBox x:Name="PaymentInput" Content="Auszahlungsdaten" Margin="40,185,0,0"
Checked="CheckBox_Changed" Unchecked="CheckBox_Changed"
IsChecked="{Binding DeletePaymentData}"/>
<Button x:Name="ConfirmButton" Content="Bestätigen" Margin="10,10,115,10" IsEnabled="False"
Click="ConfirmButton_Click"/>
<Button x:Name="CancelButton" Content="Abbrechen" Margin="10,10,10,10" IsCancel="True"/>
</Grid>
</Window>
+66
View File
@@ -0,0 +1,66 @@
using Elwig.Helpers;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace Elwig.Dialogs {
public partial class DeleteMemberDialog : Window {
protected string[] NameParts;
public bool DeleteAreaComs { get; set; }
public bool DeleteDeliveries { get; set; }
public bool DeletePaymentData { get; set; }
public DeleteMemberDialog(int mgnr, string name, int numAreaComs, int numDeliveries, int numCredits) {
NameParts = name.ToLower().Split(' ').Where(p => p.Length > 0).Append($"{mgnr}").ToArray();
InitializeComponent();
Title += " - " + name;
AreaComInput.IsEnabled = numAreaComs != 0;
AreaComInput.Content += $" ({numAreaComs:N0})";
DeliveryInput.IsEnabled = numDeliveries != 0;
DeliveryInput.Content += $" ({numDeliveries:N0})";
PaymentInput.IsEnabled = numCredits != 0;
PaymentInput.Content += $" ({numCredits:N0})";
}
private void NameInput_TextChanged(object sender, TextChangedEventArgs evt) {
Update();
}
private void CheckBox_Changed(object sender, RoutedEventArgs evt) {
Update();
}
private static void UpdateCheckBox(CheckBox cb) {
if (cb.IsEnabled && cb.IsChecked != true) {
ControlUtils.SetInputInvalid(cb);
} else {
ControlUtils.ClearInputState(cb);
}
}
private void Update() {
var t = NameInput.Text.ToLower().Split(' ');
var nameValid = NameParts.All(t.Contains);
UpdateCheckBox(AreaComInput);
UpdateCheckBox(DeliveryInput);
UpdateCheckBox(PaymentInput);
if (!nameValid) {
ControlUtils.SetInputInvalid(NameInput);
} else {
ControlUtils.ClearInputState(NameInput);
}
ConfirmButton.IsEnabled =
(!AreaComInput.IsEnabled || DeleteAreaComs) &&
(!DeliveryInput.IsEnabled || DeleteDeliveries) &&
(!PaymentInput.IsEnabled || DeletePaymentData) &&
nameValid;
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
DialogResult = true;
Close();
}
}
}
@@ -1,60 +0,0 @@
<Window x:Class="Elwig.Dialogs.DeliveryExtractionDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Dialogs"
mc:Ignorable="d"
ResizeMode="NoResize"
ShowInTaskbar="False"
Topmost="True"
WindowStartupLocation="CenterOwner"
FocusManager.FocusedElement="{Binding ElementName=WeightInput}"
Title="Teillieferung extrahieren" Height="210" Width="380">
<Window.Resources>
<Style TargetType="Label">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="Padding" Value="2,4,2,4"/>
<Setter Property="Height" Value="25"/>
</Style>
<Style TargetType="TextBox">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="2"/>
<Setter Property="Height" Value="25"/>
<Setter Property="TextWrapping" Value="NoWrap"/>
</Style>
<Style TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="25"/>
</Style>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Margin="10,10,0,10" TextWrapping="Wrap">
Was soll mit der Teillieferung <Run x:Name="TextLsNr" FontWeight="Bold" Text="20201010A000/1"/><LineBreak/>
von <Run x:Name="TextMember" FontWeight="Bold" Text="Max Mustermann"/> geschehen?
</TextBlock>
<RadioButton x:Name="NewDeliveryButton" Content="Neue Lieferung erstellen"
Margin="10,80,0,10" HorizontalAlignment="Left" VerticalAlignment="Top"
Checked="Selection_Changed" Unchecked="Selection_Changed"/>
<RadioButton x:Name="AddToDeliveryButton" Content="Zu Lieferung hinzufügen"
Margin="10,100,0,10" HorizontalAlignment="Left" VerticalAlignment="Top"
Checked="Selection_Changed" Unchecked="Selection_Changed"/>
<ListBox x:Name="DeliveryList" Grid.Column="1" Margin="10,10,10,45"
SelectionChanged="DeliveryList_SelectionChanged"/>
<Button x:Name="ConfirmButton" Content="Bestätigen" Margin="10,10,115,10" Grid.ColumnSpan="2" IsEnabled="False" IsDefault="True"
Click="ConfirmButton_Click"/>
<Button x:Name="CancelButton" Content="Abbrechen" Margin="10,10,10,10" Grid.ColumnSpan="2" IsCancel="True"/>
</Grid>
</Window>
@@ -1,37 +0,0 @@
using System.Collections.Generic;
using System.Windows;
namespace Elwig.Dialogs {
public partial class DeliveryExtractionDialog : Window {
public string? AddTo;
public DeliveryExtractionDialog(string lsnr, string name, bool single, IEnumerable<string> lsnrs) {
InitializeComponent();
TextLsNr.Text = lsnr;
TextMember.Text = name;
NewDeliveryButton.IsEnabled = !single;
DeliveryList.IsEnabled = false;
DeliveryList.ItemsSource = lsnrs;
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
DialogResult = true;
AddTo = NewDeliveryButton.IsChecked == true ? "new" : DeliveryList.SelectedItem as string;
Close();
}
private void UpdateButtons() {
ConfirmButton.IsEnabled = NewDeliveryButton.IsChecked == true || (AddToDeliveryButton.IsChecked == true && DeliveryList.SelectedItem != null);
DeliveryList.IsEnabled = AddToDeliveryButton.IsChecked == true;
}
private void Selection_Changed(object sender, RoutedEventArgs evt) {
UpdateButtons();
}
private void DeliveryList_SelectionChanged(object sender, RoutedEventArgs evt) {
UpdateButtons();
}
}
}
+104
View File
@@ -0,0 +1,104 @@
<local:ContextWindow
x:Class="Elwig.Dialogs.DeliverySplittingDialog"
AutomationProperties.AutomationId="DeliverySplittingDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Elwig.Windows"
xmlns:ctrl="clr-namespace:Elwig.Controls"
ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
WindowStartupLocation="CenterOwner"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="Lieferung abwerten oder aufteilen" Height="400" Width="600">
<Window.Resources>
<Style TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="25"/>
</Style>
</Window.Resources>
<Grid>
<RadioButton x:Name="DepreciateModeInput" GroupName="ModeInput" Content="Abwerten" Margin="15,10,10,10" IsChecked="True"
VerticalAlignment="Top" HorizontalAlignment="Left"
Checked="ModeInput_Changed"/>
<RadioButton x:Name="MemberModeInput" GroupName="ModeInput" Content="Auf Mitglied übertragen" Margin="15,30,10,10"
VerticalAlignment="Top" HorizontalAlignment="Left"
Checked="ModeInput_Changed"/>
<RadioButton x:Name="DeliveryModeInput" GroupName="ModeInput" Content="Zu anderer Lieferung hinzufügen" Margin="15,50,10,10"
VerticalAlignment="Top" HorizontalAlignment="Left"
Checked="ModeInput_Changed"/>
<TextBox x:Name="MgNrInput" FontSize="14" Padding="2" Visibility="Hidden"
Width="48" Margin="220,10,0,0" Height="25" TextAlignment="Right"
TextChanged="MgNrInput_TextChanged"
VerticalAlignment="Top" HorizontalAlignment="Left"/>
<ComboBox x:Name="MemberInput" FontSize="14" Visibility="Hidden"
Margin="273,10,40,10" IsEditable="True" Height="25"
ItemTemplate="{StaticResource MemberAdminNameTemplate}" TextSearch.TextPath="AdministrativeName"
SelectionChanged="MemberInput_SelectionChanged"
VerticalAlignment="Top" HorizontalAlignment="Stretch"/>
<Button x:Name="MemberReferenceButton" Height="25" Width="25" FontFamily="Segoe MDL2 Assets" Content="&#xEE35;" Padding="0"
Margin="10,10,10,10" VerticalAlignment="Top" HorizontalAlignment="Right" ToolTip="Zu Mitglied springen" FontSize="14" Visibility="Hidden"
Click="MemberReferenceButton_Click"/>
<ComboBox x:Name="DeliveryInput" FontSize="14" Visibility="Hidden"
Margin="220,10,10,10" Height="25"
TextSearch.TextPath="LsNr"
SelectionChanged="DeliveryInput_SelectionChanged"
VerticalAlignment="Top" HorizontalAlignment="Stretch">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding LsNr}" Width="100"/>
<TextBlock Text="{Binding Weight, StringFormat='{}{0:N0} kg'}" Width="80" TextAlignment="Right" Margin="0,0,10,0"/>
<TextBlock Text="{Binding Member.AdministrativeName}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock x:Name="InfoBlock" Margin="230,45,10,10" FontSize="14" TextAlignment="Right"
VerticalAlignment="Top" HorizontalAlignment="Stretch"
Text="Insgesamt 0 kg / 0 kg ausgewählt."/>
<ListBox x:Name="DeliveryPartList" Margin="10,75,10,40" ItemsSource="{Binding DeliveryParts, Mode=TwoWay}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Focusable" Value="False"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Part.DPNr}" Width="20" TextAlignment="Right"
VerticalAlignment="Center" Margin="0,0,5,0" FontSize="14"/>
<TextBlock Text="{Binding Part.SortId}" Width="40" TextAlignment="Center"
VerticalAlignment="Center" Margin="0,0,0,0" FontSize="14"/>
<TextBlock Text="{Binding Part.Kmw, StringFormat='{}{0:N1}°'}" Width="40" TextAlignment="Right" Padding="0,0,10,0"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding Part.QualId}" Width="30"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding Part.Weight, StringFormat='{}{0:N0} kg'}" Width="70" TextAlignment="Right"
VerticalAlignment="Center" Margin="0,0,10,0" FontSize="14"/>
<TextBlock Text="{Binding Part.Attribute.Name}" Width="60"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding Part.Cultivation.Name}" Width="50"
VerticalAlignment="Center"/>
<CheckBox IsChecked="{Binding SplitCompletely, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Content="Vollständig" Tag="{Binding Part.DPNr}"
VerticalAlignment="Center" Margin="20,0,5,0"
Checked="SplitCompletelyInput_Changed" Unchecked="SplitCompletelyInput_Changed"/>
<ctrl:UnitTextBox Text="{Binding SplitWeightString, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Unit="kg" Width="70" Height="25" IsEnabled="{Binding SplitWeightEnabled}" Tag="{Binding Part.DPNr}"
VerticalAlignment="Center" Margin="5,0,5,0" FontSize="14" Padding="2" Background="White"
TextChanged="SplitWeightInput_TextChanged"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button x:Name="ConfirmButton" Content="Bestätigen" Margin="10,10,115,10" IsEnabled="False"
Click="ConfirmButton_Click"/>
<Button x:Name="CancelButton" Content="Abbrechen" Margin="10,10,10,10" IsCancel="True"/>
</Grid>
</local:ContextWindow>
@@ -0,0 +1,158 @@
using Elwig.Helpers;
using Elwig.Models.Entities;
using Elwig.Windows;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace Elwig.Dialogs {
public partial class DeliverySplittingDialog : ContextWindow {
public class Row {
public DeliveryPart Part { get; set; }
public bool SplitCompletely { get; set; }
public string? SplitWeightString { get; set; }
public int? SplitWeight {
get => int.TryParse(SplitWeightString, out var v) ? v : null;
set => SplitWeightString = $"{value}";
}
public bool SplitWeightEnabled { get; set; }
}
private readonly Delivery _delivery;
public int? MgNr { get; set; }
public string? LsNr { get; set; }
public int[]? Weights => DeliveryParts.Select(r => r.SplitCompletely ? r.Part.Weight : r.SplitWeight ?? 0).ToArray();
public ObservableCollection<Row> DeliveryParts { get; set; }
public DeliverySplittingDialog(Delivery d) {
_delivery = d;
DeliveryParts = new(d.Parts.Select(p => new Row {
Part = p,
SplitCompletely = false,
SplitWeight = null,
SplitWeightEnabled = true,
}).ToList());
InitializeComponent();
}
protected override async Task OnRenewContext(AppDbContext ctx) {
ControlUtils.RenewItemsSource(MemberInput, await ctx.FetchMembers().ToListAsync());
ControlUtils.RenewItemsSource(DeliveryInput, await ctx.Deliveries
.Where(d => d.DateString == $"{_delivery.Date:yyyy-MM-dd}" && d.ZwstId == _delivery.ZwstId)
.OrderBy(d => d.LsNr)
.Include(d => d.Parts)
.ToListAsync());
if (DeliveryInput.SelectedItem == null)
ControlUtils.SelectItem(DeliveryInput, _delivery);
CheckValidity();
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
if (DepreciateModeInput.IsChecked == true) {
MgNr = null;
LsNr = null;
} else if (MemberModeInput.IsChecked == true) {
MgNr = ((Member)MemberInput.SelectedItem).MgNr;
LsNr = null;
} else if (DeliveryModeInput.IsChecked == true) {
MgNr = null;
LsNr = ((Delivery)DeliveryInput.SelectedItem).LsNr;
}
DialogResult = true;
Close();
}
private void ModeInput_Changed(object sender, RoutedEventArgs evt) {
if (!IsLoaded) return;
CheckValidity();
if (DepreciateModeInput.IsChecked == true) {
MgNrInput.Visibility = Visibility.Hidden;
MemberInput.Visibility = Visibility.Hidden;
MemberReferenceButton.Visibility = Visibility.Hidden;
DeliveryInput.Visibility = Visibility.Hidden;
} else if (MemberModeInput.IsChecked == true) {
MgNrInput.Visibility = Visibility.Visible;
MemberInput.Visibility = Visibility.Visible;
MemberReferenceButton.Visibility = Visibility.Visible;
DeliveryInput.Visibility = Visibility.Hidden;
} else if (DeliveryModeInput.IsChecked == true) {
MgNrInput.Visibility = Visibility.Hidden;
MemberInput.Visibility = Visibility.Hidden;
MemberReferenceButton.Visibility = Visibility.Hidden;
DeliveryInput.Visibility = Visibility.Visible;
}
}
private void CheckValidity() {
var weight = DeliveryParts.Sum(r => r.SplitCompletely ? r.Part.Weight : r.SplitWeight ?? 0);
var total = DeliveryParts.Sum(r => r.Part.Weight);
InfoBlock.Text = $"Insgesamt {weight:N0} kg / {total:N0} kg ausgewählt.";
ConfirmButton.IsEnabled = DeliveryParts.Any(r => r.SplitCompletely || r.SplitWeight > 0) && (
DepreciateModeInput.IsChecked == true ||
(MemberModeInput.IsChecked == true && MemberInput.SelectedItem != null && !DeliveryParts.All(r => r.SplitCompletely)) ||
(DeliveryModeInput.IsChecked == true && DeliveryInput.SelectedItem != null));
}
private void MgNrInput_TextChanged(object sender, TextChangedEventArgs evt) {
var res = Validator.CheckMgNr((TextBox)sender, true);
var text = MgNrInput.Text;
var caret = MgNrInput.CaretIndex;
ControlUtils.SelectItemWithPk(MemberInput, res.IsValid ? int.Parse(MgNrInput.Text) : null);
MgNrInput.Text = text;
MgNrInput.CaretIndex = caret;
}
private void MemberInput_SelectionChanged(object sender, SelectionChangedEventArgs evt) {
var m = MemberInput.SelectedItem as Member;
MgNrInput.Text = m?.MgNr.ToString();
CheckValidity();
}
private void MemberReferenceButton_Click(object sender, RoutedEventArgs evt) {
if (MemberInput.SelectedItem is not Member m) return;
App.FocusMember(m.MgNr);
}
private void DeliveryInput_SelectionChanged(object sender, SelectionChangedEventArgs evt) {
CheckValidity();
}
private void SplitCompletelyInput_Changed(object sender, RoutedEventArgs evt) {
var checkbox = (CheckBox)sender;
var dpnr = int.Parse(checkbox.Tag.ToString()!);
var row = DeliveryParts.First(d => d.Part.DPNr == dpnr);
if (checkbox.IsChecked == true) {
row.SplitWeightEnabled = false;
row.SplitWeight = row.Part.Weight;
} else if (checkbox.IsChecked == false) {
row.SplitWeightEnabled = true;
row.SplitWeight = null;
}
CollectionViewSource.GetDefaultView(DeliveryParts).Refresh();
CheckValidity();
}
private void SplitWeightInput_TextChanged(object sender, TextChangedEventArgs evt) {
var textbox = (TextBox)sender;
Validator.CheckInteger(textbox, false, 6);
var dpnr = int.Parse(textbox.Tag.ToString()!);
var row = DeliveryParts.First(d => d.Part.DPNr == dpnr);
var w = int.TryParse(textbox.Text, out var v) ? v : (int?)null;
if (w >= row.Part.Weight) {
row.SplitCompletely = true;
row.SplitWeightEnabled = false;
row.SplitWeight = row.Part.Weight;
CollectionViewSource.GetDefaultView(DeliveryParts).Refresh();
}
CheckValidity();
}
}
}
+1 -4
View File
@@ -4,10 +4,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Dialogs"
mc:Ignorable="d"
ResizeMode="NoResize"
ShowInTaskbar="False"
Topmost="True"
ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
WindowStartupLocation="CenterOwner"
FocusManager.FocusedElement="{Binding ElementName=PriceInput}"
Title="Linear wachsen" Height="140" Width="270">
@@ -1,5 +1,4 @@
using Elwig.Helpers;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
@@ -14,7 +13,7 @@ namespace Elwig.Dialogs {
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
DialogResult = true;
Price = double.Parse(PriceInput.Text.Replace("\u202f", ""));
Price = double.Parse(PriceInput.Text.Replace(Utils.GroupSeparator, ""));
Close();
}
+5 -9
View File
@@ -4,10 +4,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Dialogs"
mc:Ignorable="d"
ResizeMode="NoResize"
ShowInTaskbar="False"
Topmost="True"
xmlns:ctrl="clr-namespace:Elwig.Controls"
ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
WindowStartupLocation="CenterOwner"
FocusManager.FocusedElement="{Binding ElementName=WeightInput}"
Title="Handwiegung" Height="170" Width="400">
@@ -40,11 +38,9 @@
</Grid.ColumnDefinitions>
<Label Content="Gewicht:" Margin="10,20,10,10"/>
<Grid Grid.Column="1" Width="70" Height="25" Margin="0,20,10,10" HorizontalAlignment="Left" VerticalAlignment="Top">
<TextBox x:Name="WeightInput" TextAlignment="Right" Padding="2,2,17,2"
TextChanged="WeightInput_TextChanged"/>
<Label Content="kg" Margin="0,4,3,0" HorizontalAlignment="Right" FontSize="10" Padding="2,4,2,4"/>
</Grid>
<ctrl:UnitTextBox x:Name="WeightInput" Grid.Column="1" Width="62" Height="25" Unit="kg"
Margin="0,20,10,10" HorizontalAlignment="Left" VerticalAlignment="Top" FontSize="14" Padding="2"
TextChanged="WeightInput_TextChanged"/>
<Label Content="Grund:" Margin="10,50,10,10"/>
<TextBox x:Name="ReasonInput" Grid.Column="1" Margin="0,50,10,10"
+2 -2
View File
@@ -6,8 +6,8 @@ using System.Windows.Controls;
namespace Elwig.Dialogs {
public partial class ManualWeighingDialog : Window {
public int Weight = 0;
public string? Reason = null;
public int Weight { get; private set; }
public string? Reason { get; private set; }
public ManualWeighingDialog(string? reason = null) {
InitializeComponent();
+69
View File
@@ -0,0 +1,69 @@
<Window x:Class="Elwig.Dialogs.NewSeasonDialog"
AutomationProperties.AutomationId="NewSeasonDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Dialogs"
ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
WindowStartupLocation="CenterOwner"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="Saison anlegen" Height="180" Width="400">
<Window.Resources>
<Style TargetType="Label">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="Padding" Value="2,4,2,4"/>
<Setter Property="Height" Value="25"/>
</Style>
<Style TargetType="ComboBox">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Height" Value="25"/>
</Style>
<Style TargetType="CheckBox">
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Top"/>
</Style>
<Style TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="25"/>
</Style>
</Window.Resources>
<Grid>
<Label Content="Währung:" Margin="10,10,10,10"/>
<ComboBox x:Name="CurrencyInput" Width="150" Margin="130,10,10,10"
ItemsSource="{Binding Currencies}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Code}" Width="30"/>
<TextBlock Text="- "/>
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Label Content="Nachkommastellen:" Margin="10,40,10,10"/>
<ComboBox x:Name="PrecisionInput" Width="50" Margin="130,40,10,10">
<ComboBoxItem>2</ComboBoxItem>
<ComboBoxItem>3</ComboBoxItem>
<ComboBoxItem IsSelected="True">4</ComboBoxItem>
<ComboBoxItem>5</ComboBoxItem>
<ComboBoxItem>6</ComboBoxItem>
<ComboBoxItem>7</ComboBoxItem>
<ComboBoxItem>8</ComboBoxItem>
</ComboBox>
<CheckBox x:Name="CopyModifiersInput" Content="Zu-/Abschläge der letzten Saison übernehmen"
Margin="15,75,10,10" IsChecked="{Binding CopyModifiers}"/>
<Button x:Name="ConfirmButton" Content="Bestätigen" Margin="10,10,115,10" Grid.Column="1" IsDefault="True"
Click="ConfirmButton_Click"/>
<Button x:Name="CancelButton" Content="Abbrechen" Margin="10,10,10,10" Grid.Column="1" IsCancel="True"/>
</Grid>
</Window>
+30
View File
@@ -0,0 +1,30 @@
using Elwig.Helpers;
using Elwig.Models.Entities;
using System.Collections.Generic;
using System.Windows;
namespace Elwig.Dialogs {
public partial class NewSeasonDialog : Window {
public IEnumerable<Currency> Currencies { get; set; }
public int Year { get; set; }
public string CurrencyCode => (CurrencyInput.SelectedItem as Currency)!.Code;
public byte Precision => (byte)(PrecisionInput.SelectedIndex + 2);
public bool CopyModifiers { get; set; }
public NewSeasonDialog(Season? s, IEnumerable<Currency> currencies) {
Currencies = currencies;
CopyModifiers = s != null;
InitializeComponent();
CopyModifiersInput.IsEnabled = s != null;
ControlUtils.SelectItemWithPk(CurrencyInput, s?.CurrencyCode ?? "EUR");
PrecisionInput.SelectedIndex = (s?.Precision ?? 4) - 2;
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
DialogResult = true;
Close();
}
}
}
@@ -1,16 +1,14 @@
<Window x:Class="Elwig.Dialogs.AbwertenDialog"
<Window x:Class="Elwig.Dialogs.TareCorrectionDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Elwig.Dialogs"
mc:Ignorable="d"
ResizeMode="NoResize"
ShowInTaskbar="False"
Topmost="True"
xmlns:ctrl="clr-namespace:Elwig.Controls"
ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
WindowStartupLocation="CenterOwner"
FocusManager.FocusedElement="{Binding ElementName=WeightInput}"
Title="Teillieferung abwerten" Height="190" Width="400">
FocusManager.FocusedElement="{Binding ElementName=TargetTareInput}"
Title="Tara korrigieren" Height="170" Width="300">
<Window.Resources>
<Style TargetType="Label">
<Setter Property="HorizontalAlignment" Value="Left"/>
@@ -35,25 +33,23 @@
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="70"/>
<ColumnDefinition/>
<ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions>
<TextBlock Margin="10,10,10,10" Grid.ColumnSpan="2" TextWrapping="Wrap" TextAlignment="Center">
Welche Menge der Teillieferung <Run x:Name="TextLsNr" FontWeight="Bold" Text="20201010A000/1"/><LineBreak/>
von <Run x:Name="TextMember" FontWeight="Bold" Text="Max Mustermann"/><LineBreak/>
mit <Run x:Name="TextWeight" FontWeight="Bold" Text="1&#x202f;000&#x202f;kg"/> soll abgewertet werden?
</TextBlock>
<Label Content="Ist-Tara:" Margin="10,20,0,10" Grid.Column="1"/>
<ctrl:UnitTextBox x:Name="ActualTareInput" Grid.Column="2" Width="56" Height="25" Unit="kg" IsReadOnly="True"
Margin="0,20,10,10" HorizontalAlignment="Left" VerticalAlignment="Top" FontSize="14" Padding="2"
TextChanged="TareInput_TextChanged"/>
<Label Content="Gewicht:" Margin="10,70,10,10"/>
<Grid Grid.Column="1" Width="70" Height="25" Margin="0,70,10,10" HorizontalAlignment="Left" VerticalAlignment="Top">
<TextBox x:Name="WeightInput" TextAlignment="Right" Padding="2,2,17,2"
TextChanged="WeightInput_TextChanged"/>
<Label Content="kg" Margin="0,4,3,0" HorizontalAlignment="Right" FontSize="10" Padding="2,4,2,4"/>
</Grid>
<Label Content="Soll-Tara:" Margin="10,50,0,10" Grid.Column="1"/>
<ctrl:UnitTextBox x:Name="TargetTareInput" Grid.Column="2" Width="56" Height="25" Unit="kg"
Margin="0,50,10,10" HorizontalAlignment="Left" VerticalAlignment="Top" FontSize="14" Padding="2"
TextChanged="TareInput_TextChanged"/>
<Button x:Name="ConfirmButton" Content="Bestätigen" Margin="10,10,115,10" Grid.Column="1" IsEnabled="False" IsDefault="True"
<Button x:Name="ConfirmButton" Content="Bestätigen" Margin="10,10,115,10" Grid.ColumnSpan="3" IsEnabled="False" IsDefault="True"
Click="ConfirmButton_Click"/>
<Button x:Name="CancelButton" Content="Abbrechen" Margin="10,10,10,10" Grid.Column="1" IsCancel="True"/>
<Button x:Name="CancelButton" Content="Abbrechen" Margin="10,10,10,10" Grid.ColumnSpan="3" IsCancel="True"/>
</Grid>
</Window>
@@ -0,0 +1,38 @@
using Elwig.Helpers;
using System.Windows;
using System.Windows.Controls;
namespace Elwig.Dialogs {
public partial class TareCorrectionDialog : Window {
public int ActualTare { get; private set; }
public int TargetTare { get; private set; }
public TareCorrectionDialog(int? actualTare, int? targetTare = null) {
InitializeComponent();
ActualTare = actualTare ?? 0;
TargetTare = targetTare ?? actualTare ?? 0;
ActualTareInput.IsReadOnly = actualTare != null;
ActualTareInput.Text = ActualTare.ToString();
TargetTareInput.Text = TargetTare.ToString();
TargetTareInput.SelectAll();
}
private void ConfirmButton_Click(object sender, RoutedEventArgs evt) {
DialogResult = true;
ActualTare = int.Parse(ActualTareInput.Text);
TargetTare = int.Parse(TargetTareInput.Text);
Close();
}
private void UpdateButtons() {
ConfirmButton.IsEnabled = ActualTareInput.Text.Length > 0 && TargetTareInput.Text.Length > 0;
}
private void TareInput_TextChanged(object sender, TextChangedEventArgs evt) {
if (sender is not TextBox b) return;
Validator.CheckInteger(b, true, 4);
UpdateButtons();
}
}
}
+32
View File
@@ -0,0 +1,32 @@
<Window x:Class="Elwig.Dialogs.UpdateDialog"
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"
ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True"
WindowStartupLocation="CenterOwner"
Title="Neues Update verfügbar - Elwig" Height="190" Width="400"
Closed="OnClosed">
<Grid>
<TextBlock x:Name="Description" FontSize="14" Margin="0,0,0,40"
HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center">
Version <Run x:Name="VersionText" FontWeight="Bold">0.0.0</Run> von Elwig ist verfügbar!
(<Hyperlink NavigateUri="https://elwig.at/changelog" RequestNavigate="Hyperlink_RequestNavigate">Änderungen</Hyperlink>)<LineBreak/>
Soll das Update heruntergeladen und<LineBreak/>
installiert werden? (ca. <Run x:Name="SizeText">100</Run> MB)<LineBreak/>
<Run FontWeight="Bold">Achtung</Run>: Elwig wird dabei geschlossen!
</TextBlock>
<ProgressBar x:Name="ProgressBar" Margin="0,0,0,27" Visibility="Hidden"
HorizontalAlignment="Center" VerticalAlignment="Center"
Height="27" Width="300" SnapsToDevicePixels="True"/>
<Button x:Name="InstallButton" Content="Installieren" Margin="10,10,115,20"
FontSize="14" HorizontalAlignment="Center" VerticalAlignment="Bottom"
Width="100" Height="27"
Click="InstallButton_Click"/>
<Button x:Name="CancelButton" Content="Abbrechen" Margin="115,10,10,20" IsCancel="True"
FontSize="14" HorizontalAlignment="Center" VerticalAlignment="Bottom"
Width="100" Height="27"/>
</Grid>
</Window>
+68
View File
@@ -0,0 +1,68 @@
using Elwig.Helpers;
using Elwig.Services;
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Navigation;
namespace Elwig.Dialogs {
public partial class UpdateDialog : Window {
public string Version { get; private set; }
public string Url { get; private set; }
private readonly CancellationTokenSource Cancellation;
public UpdateDialog(string version, string url, long size) {
Version = version;
Url = url;
Cancellation = new();
InitializeComponent();
VersionText.Text = version;
SizeText.Text = $"{size / 1024 / 1024}";
}
private void OnClosed(object sender, EventArgs evt) {
Cancellation.Cancel();
}
private async void InstallButton_Click(object sender, RoutedEventArgs evt) {
Description.Visibility = Visibility.Hidden;
ProgressBar.Visibility = Visibility.Visible;
InstallButton.IsEnabled = false;
await Install();
Close();
}
public async Task Install() {
var fileName = Path.Combine(App.TempPath, $"Elwig-{Version}.exe");
try {
using var stream = new FileStream(fileName, FileMode.Create);
using var client = new HttpClient() {
Timeout = TimeSpan.FromSeconds(5),
};
await client.DownloadAsync(Url, stream, new Progress<double>(p => {
ProgressBar.Value = p * 100.0;
}), Cancellation.Token);
} catch (OperationCanceledException) {
File.Delete(fileName);
return;
} catch (Exception exc) {
InteractionService.ShowException(exc);
}
Process.Start(fileName);
DialogResult = true;
}
private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e) {
Process.Start(new ProcessStartInfo {
FileName = e.Uri.ToString(),
UseShellExecute = true,
});
}
}
}
+372
View File
@@ -0,0 +1,372 @@
using Elwig.Helpers;
using Elwig.Models;
using Elwig.Models.Entities;
using iText.Kernel.Colors;
using iText.Kernel.Geom;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Action;
using iText.Kernel.Pdf.Canvas;
using iText.Layout;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Properties;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Documents {
public class BusinessDocument : Document {
public Member Member;
public MemberHistoryPoint MemberShares;
public string? Location;
public bool IncludeSender = false;
public bool UseBillingAddress = false;
public bool ShowDateAndLocation = false;
public DateOnly? DateFrom;
protected Table? Aside;
public string Address {
get {
IAddress addr = (Member.BillingAddress != null && UseBillingAddress) ? Member.BillingAddress : Member;
var plz = addr.PostalDest.AtPlz;
return string.Join("\n", ((string?[])[addr is BillingAddr ? addr.FullName : null, Member.AdministrativeName, Member.ForTheAttentionOf, addr.Address, $"{plz?.Plz} {plz?.Ort.Name.Split(",")[0]}", addr.PostalDest.Country.Name]).Where(s => !string.IsNullOrWhiteSpace(s)));
}
}
protected Cell NewAsideCell(Paragraph text, int colspan = 1, bool isName = false, bool right = false, bool center = false) {
var cell = NewCell(text, colspan: colspan).SetPaddingsMM(0.25f, center ? 0 : right ? 1 : 0.5f, 0.25f, isName ? 1 : 0);
if (colspan == 26) {
cell.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetBackgroundColor(new DeviceRgb(0xe0, 0xe0, 0xe0))
.SetBorderTop(new SolidBorder(new DeviceRgb(0x80, 0x80, 0x80), BorderThickness))
.SetPaddingsMM(0.5f, 1, 0.5f, 1);
} else if (isName) {
cell.SetBorderLeft(new SolidBorder(new DeviceRgb(0x80, 0x80, 0x80), BorderThickness));
}
if (right) cell.SetTextAlignment(TextAlignment.RIGHT);
if (center) cell.SetTextAlignment(TextAlignment.CENTER);
return cell;
}
protected Cell NewAsideCell(string text, int colspan = 1, bool isName = false, bool right = false, bool center = false) {
return NewAsideCell(new KernedParagraph(text, 10), colspan, isName, right, center);
}
public BusinessDocument(string title, Member m, DateOnly? dateFrom, bool includeSender = false) :
base(title) {
Member = m;
MemberShares = new(m.Shares, m.SharesRed, m.SharesWhite, m.SharesDormant);
Location = App.BranchLocation;
IncludeSender = includeSender;
DateFrom = dateFrom;
}
protected override void BeforeRenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.BeforeRenderBody(doc, pdf);
Aside = new Table(ColsMM(2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5))
.SetWidth(UnitValue.CreatePointValue(65 * PtInMM)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetFont(NF).SetFontSize(10)
.SetBorder(new SolidBorder(new DeviceRgb(0x80, 0x80, 0x80), BorderThickness))
.AddCell(NewAsideCell("Mitglied", 26));
if (Member.IsOrganic) {
Aside
.AddCell(NewAsideCell("Mitgl.-Nr.:", 8, isName: true)).AddCell(NewAsideCell($"{Member.MgNr}", 4, right: true))
.AddCell(NewAsideCell("Bio-KSt.:", 6, isName: true)).AddCell(NewAsideCell(Member.OrganicAuthorityCode ?? "-", 8, right: true, center: Member.OrganicAuthorityCode == null))
.AddCell(NewAsideCell("Betr.-Nr.:", 6, isName: true)).AddCell(NewAsideCell(Member.LfbisNr ?? "-", 6, right: true, center: Member.LfbisNr == null))
.AddCell(NewAsideCell("OOC-ID:", 6, isName: true)).AddCell(NewAsideCell(Member.OrganicOperatorId ?? "-", 8, right: true, center: Member.OrganicOperatorId == null))
.AddCell(NewAsideCell("UID:", 6, isName: true)).AddCell(NewAsideCell(Member.UstIdNr ?? "-", 10, right: true, center: Member.UstIdNr == null))
.AddCell(NewAsideCell(new KernedParagraph("", 10).Add(Italic(Member.IsBuchführend ? "(buchführend)" : "(pauschaliert)")), 10, center: true));
} else {
Aside
.AddCell(NewAsideCell("Mitglieds-Nr.:", 12, isName: true)).AddCell(NewAsideCell($"{Member.MgNr}", 4, right: true)).AddCell(NewAsideCell("", 10))
.AddCell(NewAsideCell("Betriebs-Nr.:", 10, isName: true)).AddCell(NewAsideCell(Member.LfbisNr ?? "-", 6, right: true, center: Member.LfbisNr == null)).AddCell(NewAsideCell("", 10))
.AddCell(NewAsideCell("UID:", 6, isName: true)).AddCell(NewAsideCell(Member.UstIdNr ?? "-", 10, right: true, center: Member.UstIdNr == null))
.AddCell(NewAsideCell(new KernedParagraph("", 10).Add(Italic(Member.IsBuchführend ? "(buchführend)" : "(pauschaliert)")), 10, center: true));
}
}
protected void RenderAddress(Canvas canvas, Rectangle pageSize) {
canvas.Add(new Div()
.SetFixedPositionMM(25, 50, 80, 45, pageSize)
.SetFont(NF)
.Add(new KernedParagraph(IncludeSender ? $"{App.Client.Sender1}\n{App.Client.Sender2}" : "", 8).SetMargins(8, 0, 8, 0).SetHeight(16.0625f))
.Add(new KernedParagraph(Address, 12).SetMargin(0).SetHeight(12 * 5)));
}
protected static void RenderAside(Table aside, Canvas canvas, Rectangle pageSize) {
canvas.Add(new Div()
.SetFixedPositionMM(125, 50, 65, 50, pageSize)
.Add(aside));
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.RenderBody(doc, pdf);
var page = pdf.AddNewPage();
var pageSize = page.GetPageSize();
var pdfCanvas = new PdfCanvas(page.NewContentStreamBefore(), page.GetResources(), pdf);
using (var canvas = new Canvas(pdfCanvas, pageSize)) {
// header
var header = new Div()
.SetFixedPositionMM(25, 10, pageSize.GetWidth() / PtInMM - 45, 45, pageSize)
.SetTextAlignment(TextAlignment.CENTER);
header.Add(new KernedParagraph(App.Client.Name, 18).SetFont(BF).SetMarginsMM(8, 0, 0, 0));
if (App.Client.NameSuffix != null) header.Add(new KernedParagraph(App.Client.NameSuffix, 14).SetFont(BF).SetMargin(0));
header.Add(new KernedParagraph(App.Client.NameTypeFull, 12).SetFont(NF).SetMargin(0));
canvas.Add(header);
// address
RenderAddress(canvas, pageSize);
// aside
if (Aside != null) {
RenderAside(Aside, canvas, pageSize);
}
}
doc.Add(new KernedParagraph(ShowDateAndLocation ? $"{Location}, am {DateFrom ?? Date:dd.MM.yyyy}" : "", 12).SetTextAlignment(TextAlignment.RIGHT).SetVerticalAlignment(VerticalAlignment.MIDDLE).SetHeight(24).SetMargin(0));
doc.Add(new KernedParagraph(Title, 12).SetFont(BF).SetMargins(0, 0, 12, 0));
}
protected override Paragraph GetFooter() {
var c = App.Client;
var link1 = new Link(c.EmailAddress ?? "", PdfAction.CreateURI($"mailto:{Uri.EscapeDataString(c.Name)}%20<{c.EmailAddress}>"));
var link2 = new Link(c.Website ?? "", PdfAction.CreateURI($"http://{c.Website}/"));
link1.GetLinkAnnotation().SetBorder(new PdfAnnotationBorder(0, 0, 0));
link2.GetLinkAnnotation().SetBorder(new PdfAnnotationBorder(0, 0, 0));
return new KernedParagraph(10)
.AddAll(Utils.GenerateFooter("\n", " \u00b7 ")
.Item(c.NameFull).NextLine()
.Item(c.Address).Item($"{c.Plz} {c.Ort}").Item("Österreich").NextGroup()
.Item("Tel.", c.PhoneNr).Item("Fax", c.FaxNr).NextGroup()
.Item(c.EmailAddress != null ? link1 : null).NextLine()
.Item(c.Website != null ? link2 : null).NextGroup()
.Item("Betriebs-Nr.", c.LfbisNr).NextGroup()
.Item("Bio-KSt.", c.OrganicAuthorityCode).NextGroup()
.Item("OOC-ID", c.OrganicOperatorId).NextLine()
.Item("UID", c.UstIdNr).Item("BIC", c.Bic).Item("IBAN", c.Iban)
.RebalanceLines()
.ToLeafElements());
}
protected Cell NewWeightsHdr(Paragraph p) {
return NewCell(p)
.SetPaddingsMM(0.125f, 0, 0.125f, 0);
}
protected Cell NewWeightsHdr(string text) {
return NewCell(new KernedParagraph(text, 8).SetFont(IF).SetTextAlignment(TextAlignment.RIGHT))
.SetPaddingsMM(0.125f, 0, 0.125f, 0);
}
protected Cell NewWeightsTh(string name) {
return NewCell(new KernedParagraph(name, 10).SetFont(IF))
.SetPaddingsMM(0.125f, 0, 0.125f, 0);
}
protected Cell NewWeightsTd(string text, bool sum = false) {
return NewCell(new KernedParagraph(text, 10).SetFont(sum ? BF : NF).SetTextAlignment(TextAlignment.RIGHT))
.SetPaddingsMM(sum ? 0.25f : 0.125f, 0, sum ? 0.25f : 0.125f, 0)
.SetBorderTop(sum ? new SolidBorder(BorderThickness) : Border.NO_BORDER);
}
protected Table NewWeightsTable(List<MemberStat> stats) {
List<string> discrs = [""];
List<string> names = ["ohne Attr./Bewirt."];
List<string> bucketAttrs = [
.. stats
.Select(s => s.Discr)
.Distinct()
.Where(s => s.Length > 0)
.Order()
];
names.AddRange(bucketAttrs);
names.Add("Gesamt");
discrs.AddRange(bucketAttrs);
List<double> cols = [40];
cols.AddRange(names.Select(_ => 125.0 / names.Count));
var tbl = new Table(ColsMM([.. cols]))
.AddHeaderCell(NewWeightsHdr(new KernedParagraph(8).Add(BoldItalic("Sortenaufteilung ")).Add(Italic("[kg]"))));
foreach (var name in names)
tbl.AddHeaderCell(NewWeightsHdr(name));
foreach (var g in stats.GroupBy(b => b.Variety).OrderBy(b => b.Key)) {
var dict = g.ToDictionary(a => a.Discr, a => a.Weight);
var vals = discrs.Select(a => dict.GetValueOrDefault(a, 0)).ToList();
tbl.AddCell(NewWeightsTh(g.Key));
foreach (var v in vals)
tbl.AddCell(NewWeightsTd(v == 0 ? "-" : $"{v:N0}"));
tbl.AddCell(NewWeightsTd($"{dict.Values.Sum():N0}"));
}
var totalDict = stats.GroupBy(s => s.Discr).ToDictionary(g => g.Key, g => g.Sum(a => a.Weight));
var totals = discrs.Select(a => totalDict.TryGetValue(a, out int value) ? value : 0);
tbl.AddCell(NewWeightsTd("", true));
foreach (var v in totals)
tbl.AddCell(NewWeightsTd($"{v:N0}", true));
tbl.AddCell(NewWeightsTd($"{totalDict.Values.Sum():N0}", true));
return tbl;
}
protected Cell NewBucketHdr(Paragraph p, int rowspan = 1, bool left = false, bool unit = false) {
p.SetProperty(Property.NO_SOFT_WRAP_INLINE, true);
p.SetProperty(Property.OVERFLOW_X, OverflowPropertyValue.HIDDEN);
return NewCell(p, rowspan: rowspan)
.SetPaddingsMM(0.125f, unit ? 2 : 0, 0.125f, 0)
.SetTextAlignment(left ? TextAlignment.LEFT : TextAlignment.RIGHT).SetVerticalAlignment(VerticalAlignment.MIDDLE)
.SetFont(IF);
}
protected Cell NewBucketHdr(string text, int rowspan = 1, bool left = false, bool unit = false) {
return NewBucketHdr(new KernedParagraph(text, 8), rowspan, left, unit);
}
protected Cell NewBucketSubHdr(string text, int colspan, bool isTiny = false) {
var p = new KernedParagraph(text, 8);
p.SetProperty(Property.NO_SOFT_WRAP_INLINE, true);
p.SetProperty(Property.OVERFLOW_X, OverflowPropertyValue.HIDDEN);
return NewCell(p, colspan: colspan)
.SetBorderTop(!isTiny ? new SolidBorder(BorderThickness) : Border.NO_BORDER)
.SetPaddingsMM(isTiny ? 0 : 0.125f, 0, isTiny ? 0 : 0.125f, 0).SetVerticalAlignment(VerticalAlignment.MIDDLE)
.SetTextAlignment(TextAlignment.LEFT).SetFont(BI);
}
protected Cell NewBucketTh(string text, bool isTiny = false) {
var p = new KernedParagraph(text, isTiny ? 8 : 10);
p.SetProperty(Property.NO_SOFT_WRAP_INLINE, true);
p.SetProperty(Property.OVERFLOW_X, OverflowPropertyValue.HIDDEN);
return NewCell(p)
.SetPaddingsMM(isTiny ? 0 : 0.125f, 0, isTiny ? 0 : 0.125f, 0)
.SetTextAlignment(TextAlignment.LEFT).SetFont(IF);
}
protected Cell NewBucketTd(string text, bool bold = false, bool isTiny = false) {
var p = new KernedParagraph(text, isTiny ? 8 : 10);
p.SetProperty(Property.NO_SOFT_WRAP_INLINE, true);
p.SetProperty(Property.OVERFLOW_X, OverflowPropertyValue.HIDDEN);
return NewCell(p)
.SetPaddingsMM(isTiny ? 0 : 0.125f, 0, isTiny ? 0 : 0.125f, 0)
.SetTextAlignment(TextAlignment.RIGHT).SetFont(bold ? BF : NF);
}
protected Cell[] FormatRow(
int obligation, int right, int delivery, int? totalDelivery = null, int? payment = null, int? area = null,
bool isGa = false, bool showPayment = false, bool showArea = false, bool isTiny = false
) {
totalDelivery ??= delivery;
payment ??= delivery;
if (showArea) {
return [
NewBucketTd(area == null ? "" : $"{area:N0}", isTiny: isTiny),
NewBucketTd($"{obligation:N0}", isTiny: isTiny),
NewBucketTd($"{right:N0}", isTiny: isTiny),
];
}
return [
NewBucketTd(obligation == 0 ? "-" : $"{obligation:N0}", isTiny: isTiny),
NewBucketTd(right == 0 ? "-" : $"{right:N0}", isTiny: isTiny),
NewBucketTd(totalDelivery < obligation ? $"{obligation - totalDelivery:N0}" : "-", totalDelivery < obligation, isTiny: isTiny),
NewBucketTd(delivery <= right ? $"{right - delivery:N0}" : "-", isTiny: isTiny),
NewBucketTd(obligation == 0 && right == 0 ? "-" : (delivery > right ? $"{delivery - right:N0}" : "-"), delivery > right && isGa, isTiny: isTiny),
..(showPayment ? new List<Cell>() {
NewBucketTd(isGa ? "" : obligation == 0 && right == 0 ? "-" : $"{payment:N0}", isTiny: isTiny)
} : []),
NewBucketTd($"{totalDelivery:N0}", isTiny: isTiny),
];
}
protected Cell[] FormatRow(MemberBucket bucket, bool isGa = false, bool showPayment = false, bool showArea = false, bool isTiny = false) {
return FormatRow(bucket.Obligation, bucket.Right, bucket.Delivery, bucket.DeliveryTotal, bucket.Payment, bucket.Area, isGa, showPayment, showArea, isTiny);
}
protected Table NewBucketTable(
Season season, Dictionary<string, MemberBucket> buckets, int deliveredWeightRed, int deliveredWeightWhite,
bool includeDelivery = true, bool includePayment = false,
bool isTiny = false, IEnumerable<string>? filter = null
) {
includePayment = includePayment && includeDelivery;
var tbl = new Table(ColsMM(!includeDelivery ? [105, 20, 20, 20] : includePayment ? [45, 17, 17, 17, 19, 16, 17, 17] : [45, 20, 20, 20, 20, 20, 20]))
.SetBorder(Border.NO_BORDER).SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetFont(NF).SetFontSize(isTiny ? 8 : 10);
if (includeDelivery) {
tbl.AddHeaderCell(NewBucketHdr(new KernedParagraph(8)
.Add(BoldItalic($"Lese {season.Year}"))
.Add(Italic($" per {Date:dd.MM.yyyy} [kg]")), left: true))
.AddHeaderCell(NewBucketHdr("Lieferpflicht"))
.AddHeaderCell(NewBucketHdr("Lieferrecht"))
.AddHeaderCell(NewBucketHdr("Unterliefert"))
.AddHeaderCell(NewBucketHdr("Noch lieferbar"))
.AddHeaderCell(NewBucketHdr("Überliefert"));
if (includePayment) tbl.AddHeaderCell(NewBucketHdr("Zugeteilt"));
tbl.AddHeaderCell(NewBucketHdr("Geliefert"));
} else {
tbl.AddHeaderCell(NewBucketHdr(new KernedParagraph(8)
.Add(BoldItalic("Zusammengefasste Flächenbindungen"))
.Add(Italic($" per {Date:dd.MM.yyyy}")), 2, left: true))
.AddHeaderCell(NewBucketHdr("Fläche"))
.AddHeaderCell(NewBucketHdr("Lieferpflicht"))
.AddHeaderCell(NewBucketHdr("Lieferrecht"))
.AddHeaderCell(NewBucketHdr("[m²]", unit: true))
.AddHeaderCell(NewBucketHdr("[kg]", unit: true))
.AddHeaderCell(NewBucketHdr("[kg]", unit: true));
}
var mBuckets = buckets
.Where(b => ((!includeDelivery && b.Value.Area > 0) ||
(includeDelivery && (b.Value.Right > 0 || b.Value.Obligation > 0 || b.Value.Delivery > 0))) &&
(filter == null || filter.Contains(b.Key[..2])))
.ToList();
var fbVars = mBuckets
.Where(b => b.Value.Right > 0 || b.Value.Obligation > 0)
.Select(b => b.Key.Replace("_", ""))
.Order()
.ToArray();
var fbs = mBuckets
.Where(b => fbVars.Contains(b.Key) && b.Key.Length == 2)
.OrderBy(b => b.Value.Name);
var vtr = mBuckets
.Where(b => fbVars.Contains(b.Key) && b.Key.Length > 2)
.OrderBy(b => b.Value.Name);
var rem = mBuckets
.Where(b => !fbVars.Contains(b.Key))
.OrderBy(b => b.Value.Name);
if (MemberShares.Shares != 0 || (MemberShares.SharesRed == 0 && MemberShares.SharesWhite == 0)) {
tbl.AddCell(NewBucketTh("Gesamtlieferung lt. gez. GA", isTiny: isTiny));
tbl.AddCells(FormatRow(MemberShares.Shares * (season.MinKgPerShare ?? 0), MemberShares.Shares * (season.MaxKgPerShare ?? 0),
deliveredWeightRed + deliveredWeightWhite, isGa: true, showPayment: includePayment, showArea: !includeDelivery, isTiny: isTiny));
}
if (MemberShares.SharesRed != 0 || MemberShares.SharesWhite != 0) {
tbl.AddCell(NewBucketTh("Gesamtlieferung lt. gez. GA (rot)", isTiny: isTiny));
tbl.AddCells(FormatRow(MemberShares.SharesRed * (season.MinKgPerShareRed ?? season.MinKgPerShare ?? 0), MemberShares.SharesRed * (season.MaxKgPerShareRed ?? season.MaxKgPerShare ?? 0),
deliveredWeightRed, isGa: true, showPayment: includePayment, showArea: !includeDelivery, isTiny: isTiny));
tbl.AddCell(NewBucketTh("Gesamtlieferung lt. gez. GA (weiß)", isTiny: isTiny));
tbl.AddCells(FormatRow(MemberShares.SharesWhite * (season.MinKgPerShareWhite ?? season.MinKgPerShare ?? 0), MemberShares.SharesWhite * (season.MaxKgPerShareWhite ?? season.MaxKgPerShare ?? 0),
deliveredWeightWhite, isGa: true, showPayment: includePayment, showArea: !includeDelivery, isTiny: isTiny));
}
if (fbs.Any()) {
tbl.AddCell(NewBucketSubHdr("Flächenbindungen" + (vtr.Any() ? " (inkl. Verträge)" : "") + ":", includePayment ? 8 : 7, isTiny: isTiny));
foreach (var (id, b) in fbs) {
tbl.AddCell(NewBucketTh(b.Name, isTiny: isTiny)).AddCells(FormatRow(b, showPayment: includePayment, showArea: !includeDelivery, isTiny: isTiny));
}
}
if (vtr.Any()) {
tbl.AddCell(NewBucketSubHdr("Verträge:", includePayment ? 8 : 7, isTiny: isTiny));
foreach (var (id, b) in vtr) {
tbl.AddCell(NewBucketTh(b.Name, isTiny: isTiny)).AddCells(FormatRow(b, showPayment: includePayment, showArea: !includeDelivery, isTiny: isTiny));
}
}
return tbl;
}
}
}
-21
View File
@@ -1,21 +0,0 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.BusinessDocument>
@model Elwig.Documents.BusinessDocument
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\style-businessdocument.css" />
<div class="info-wrapper">
<div class="address-wrapper">
<div class="sender">
@if (Model.IncludeSender) {
<div>@Elwig.App.Client.Sender1</div>
<div>@Elwig.App.Client.Sender2</div>
}
</div>
<address>@Model.Address</address>
</div>
<aside>@Raw(Model.Aside)</aside>
@if (Model.ShowDateAndLocation) {
<div class="date">@Model.Location, am @($"{Model.Date:dd.MM.yyyy}")</div>
}
</div>
@RenderBody()
@@ -1,36 +0,0 @@
using Elwig.Helpers;
using Elwig.Models;
namespace Elwig.Documents {
public abstract class BusinessDocument : Document {
public bool ShowDateAndLocation = false;
public Member Member;
public bool IncludeSender = false;
public bool UseBillingAddress = false;
public string Aside;
public string? Location;
public BusinessDocument(string title, Member m, bool includeSender = false) : base(title) {
Member = m;
Location = App.BranchLocation;
IncludeSender = includeSender;
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>" +
$"<tr><th>Mitglieds-Nr.</th><td>{m.MgNr}</td></tr>" +
$"<tr><th>Betriebs-Nr.</th><td>{m.LfbisNr}</td></tr>" +
$"<tr><th>UID</th><td>{uid}</td></tr>" +
$"</tbody></table>";
}
public string Address {
get {
IAddress addr = (Member.BillingAddress != null && UseBillingAddress) ? Member.BillingAddress : Member;
var plz = addr.PostalDest.AtPlz;
return (addr is BillingAddr ? $"{addr.Name}\n" : "") + $"{Member.AdministrativeName}\n{addr.Address}\n{plz?.Plz} {plz?.Ort.Name.Split(",")[0]}\n{addr.PostalDest.Country.Name}";
}
}
}
}
-9
View File
@@ -1,9 +0,0 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.BusinessLetter>
@model Elwig.Documents.BusinessLetter
@{ Layout = "BusinessDocument"; }
<main>
<p>Sehr geehrtes Mitglied,</p>
<p>nein.</p>
<p>Mit freundlichen Grüßen<br/>Ihre Winzergenossenschaft</p>
</main>
-7
View File
@@ -1,7 +0,0 @@
using Elwig.Models;
namespace Elwig.Documents {
public class BusinessLetter : BusinessDocument {
public BusinessLetter(string title, Member m) : base(title, m) { }
}
}
+347
View File
@@ -0,0 +1,347 @@
using Elwig.Helpers;
using Elwig.Helpers.Billing;
using Elwig.Models.Dtos;
using Elwig.Models.Entities;
using iText.Kernel.Pdf;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Properties;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Elwig.Documents {
public class CreditNote : BusinessDocument {
public new static string Name => "Traubengutschrift";
public PaymentMember Payment;
public Credit? Credit;
public string? Text;
public string CurrencySymbol;
public int Precision;
public string? MemberModifier;
public List<(string Name, int Kg, decimal Amount)>? MemberUnderDeliveries;
public decimal MemberTotalUnderDelivery;
public int MemberAutoBusinessShares;
public decimal MemberAutoBusinessSharesAmount;
public PaymentCustom? CustomPayment;
protected bool ConsiderContractPenalties;
protected bool ConsiderTotalPenalty;
protected bool ConsiderAutoBusinessShares;
protected bool ConsiderCustomModifiers;
private CreditNoteDeliveryData? _data;
private Dictionary<string, UnderDelivery>? _underDeliveries;
public CreditNote(PaymentMember p, DateOnly? dateFrom, BillingData? billingData = null, CreditNoteDeliveryData? data = null, Dictionary<string, UnderDelivery>? underDeliveries = null) :
base($"{Name} {(p.Credit != null ? $"Nr. {p.Credit.Year}/{p.Credit.TgNr:000}" : p.Member.FullName)} – {p.Variant.Name}", p.Member, dateFrom) {
UseBillingAddress = true;
ShowDateAndLocation = true;
Payment = p;
Credit = p.Credit;
Text = App.Client.TextCreditNote;
DocumentId = $"Tr.-Gutschr. " + (Credit != null ? $"{Credit.Year}/{Credit.TgNr:000}" : Payment.MgNr);
IsPreview = Credit == null;
_data = data;
_underDeliveries = underDeliveries;
CurrencySymbol = Payment.Variant.Season.Currency.Symbol ?? Payment.Variant.Season.Currency.Code;
Precision = Payment.Variant.Season.Precision;
billingData ??= BillingData.FromJson(Payment.Variant.Data);
ConsiderContractPenalties = billingData.ConsiderContractPenalties;
ConsiderTotalPenalty = billingData.ConsiderTotalPenalty;
ConsiderAutoBusinessShares = billingData.ConsiderAutoBusinessShares;
ConsiderCustomModifiers = billingData.ConsiderCustomModifiers;
}
public static async Task<CreditNote> Initialize(int year, int avnr, int mgnr, DateOnly? dateFrom, BillingData? billingData = null, CreditNoteDeliveryData? data = null, Dictionary<string, UnderDelivery>? underDeliveries = null) {
using var ctx = new AppDbContext();
var p = await ctx.MemberPayments
.Where(p => p.Year == year && p.AvNr == avnr && p.MgNr == mgnr)
.SingleAsync();
return new CreditNote(p, dateFrom, billingData, data, underDeliveries);
}
protected override async Task LoadData(AppDbContext ctx) {
await base.LoadData(ctx);
var season = Payment.Variant.Season;
if (ConsiderCustomModifiers) {
CustomPayment = await ctx.CustomPayments.FindAsync(Payment.Year, Payment.MgNr);
}
_data ??= (await CreditNoteDeliveryData.ForPaymentVariant(ctx.CreditNoteDeliveryRows, ctx.PaymentVariants, Payment.Year, Payment.AvNr))[Member.MgNr];
_underDeliveries ??= await ctx.GetMemberUnderDelivery(Payment.Year, Member.MgNr);
var mod = App.Client.IsMatzen ? await ctx.FetchModifiers(season.Year).Where(m => m.Name.StartsWith("Treue")).FirstOrDefaultAsync() : null;
if (CustomPayment?.ModComment != null) {
MemberModifier = CustomPayment.ModComment;
} else if (mod != null) {
MemberModifier = $"{mod.Name} ({mod.PublicValueStr})";
} else {
MemberModifier = "Sonstige Zu-/Abschläge";
}
if (ConsiderTotalPenalty) {
var weights = _data.Rows.Select(r => new {r.Type, Weight = r.Buckets.Sum(b => b.Value)}).GroupBy(r => r.Type).ToDictionary(r => r.Key, g => g.Sum(r => r.Weight));
var red = weights.GetValueOrDefault("R", 0);
var white = weights.GetValueOrDefault("W", 0);
var total = red + white;
var underDeliveryTotal = total - Member.Shares * (season.MinKgPerShare ?? 0);
var underDeliveryRed = red - Member.SharesRed * (season.MinKgPerShareRed ?? season.MinKgPerShare ?? 0);
var underDeliveryWhite = white - Member.SharesWhite * (season.MinKgPerShareWhite ?? season.MinKgPerShare ?? 0);
MemberTotalUnderDelivery =
(underDeliveryTotal < 0 ? underDeliveryTotal * (season.PenaltyPerKg ?? 0) - (season.PenaltyAmount ?? 0) - (season.PenaltyPerShareAmount * Math.Floor(-(decimal)underDeliveryTotal / (season.MinKgPerShare ?? 0)) ?? 0) : 0) +
(underDeliveryRed < 0 ? underDeliveryRed * (season.PenaltyPerKg ?? 0) - (season.PenaltyAmount ?? 0) - (season.PenaltyPerShareAmount * Math.Floor(-(decimal)underDeliveryRed / (season.MinKgPerShareRed ?? season.MinKgPerShare ?? 0)) ?? 0) : 0) +
(underDeliveryWhite < 0 ? underDeliveryWhite * (season.PenaltyPerKg ?? 0) - (season.PenaltyAmount ?? 0) - (season.PenaltyPerShareAmount * Math.Floor(-(decimal)underDeliveryWhite / (season.MinKgPerShareWhite ?? season.MinKgPerShare ?? 0)) ?? 0) : 0);
if (total == 0)
MemberTotalUnderDelivery -= (season.PenaltyNone ?? 0) + (season.PenaltyPerShareNone * (Member.Shares + Member.SharesRed + Member.SharesWhite) ?? 0);
}
if (ConsiderAutoBusinessShares) {
MemberAutoBusinessShares = await ctx.MemberHistory
.Where(h => h.ToMgNr == Member.MgNr && h.DeductYear == season.Year)
.SumAsync(h => h.Shares);
MemberAutoBusinessSharesAmount = MemberAutoBusinessShares * (-season.BusinessShareValue ?? 0);
}
if (ConsiderContractPenalties) {
var varieties = await ctx.FetchWineVarieties().ToDictionaryAsync(v => v.SortId, v => v);
var attributes = await ctx.FetchWineAttributes().ToDictionaryAsync(a => a.AttrId, a => a);
var comTypes = await ctx.AreaCommitmentTypes.ToDictionaryAsync(t => t.VtrgId, t => t);
MemberUnderDeliveries = _underDeliveries?
.OrderBy(u => u.Key)
.Select(u => (
varieties[u.Key[..2]].Name + (u.Key.Length > 2 ? " " + attributes[u.Key[2..]].Name : ""),
u.Value.Diff,
u.Value.Diff * (comTypes[u.Key].PenaltyPerKg ?? 0)
- (comTypes[u.Key].PenaltyAmount ?? 0)
- ((u.Value.Weight == 0 ? comTypes[u.Key].PenaltyNone : null) ?? 0)))
.Where(u => u.Item3 != 0)
.ToList();
}
}
protected override void BeforeRenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.BeforeRenderBody(doc, pdf);
Aside?.AddCell(NewAsideCell("Gutschrift", 26))
.AddCell(NewAsideCell("Tr.-Gutschr.-Nr.:", 12, isName: true)).AddCell(NewAsideCell(Payment?.Credit != null ? $"{Payment.Credit.Year}/{Payment.Credit.TgNr:000}" : "-", 14))
.AddCell(NewAsideCell("Datum:", 12, isName: true)).AddCell(NewAsideCell($"{Payment?.Variant.Date:dd.MM.yyyy}", 14))
.AddCell(NewAsideCell("Überwiesen am:", 12, isName: true)).AddCell(NewAsideCell($"{Payment?.Variant.TransferDate:dd.MM.yyyy}", 14));
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
if (_data == null) throw new Exception("Call LoadData before RenderBody");
base.RenderBody(doc, pdf);
doc.Add(NewCreditTable(_data));
var div = new Table(ColsMM(60, 105))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE);
var hint = new KernedParagraph(8)
.Add(Italic("Hinweis:\n" +
$"Die Summe der Lieferungen und die Summe der anfal\u00adlenden Pönalen werden mit " +
$"{Payment?.Variant.Season.Precision} Nach\u00adkomma-stellen berechnent, " +
$"erst das Ergebnis wird kauf-männisch auf 2 Nach\u00adkomma\u00adstellen gerundet."))
.SetWidth(56 * PtInMM).SetMarginsMM(4, 2, 4, 2);
div.AddCell(new Cell(1, 2).SetPadding(0).SetBorder(Border.NO_BORDER).SetBorderTop(new SolidBorder(BorderThickness)));
div.AddCell(new Cell(3, 1).SetPadding(0).SetBorder(Border.NO_BORDER).Add(hint).SetKeepTogether(true));
var tbl1 = new Table(ColsMM(70, 5, 5, 25))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetKeepTogether(true);
var tbl2 = new Table(ColsMM(70, 5, 5, 25))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetKeepTogether(true);
var tbl3 = new Table(ColsMM(70, 5, 5, 25))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetKeepTogether(true);
var sum = _data.Rows.Sum(p => p.Amount);
if (Payment == null) {
tbl1.AddCells(FormatRow("Gesamt", sum, bold: true, noTopBorder: true));
} else {
var noBorder = true;
if (Payment.NetAmount != Payment.Amount) {
tbl1.AddCells(FormatRow("Zwischensumme", Payment.NetAmount, noTopBorder: noBorder));
noBorder = false;
tbl1.AddCells(FormatRow(MemberModifier ?? "", Payment.Amount - Payment.NetAmount, add: true));
}
if (Credit == null) {
tbl1.AddCells(FormatRow("Gesamtbetrag", Payment.Amount, bold: true, noTopBorder: noBorder));
// TODO Mock VAT
} else {
var hasPrev = Credit.PrevNetAmount != null;
tbl1.AddCells(FormatRow(hasPrev ? "Gesamtbetrag" : "Nettobetrag", Credit.NetAmount, bold: true, noTopBorder: noBorder));
if (hasPrev) {
tbl1.AddCells(FormatRow("Bisher berücksichtigt", -Credit.PrevNetAmount, add: true));
tbl1.AddCells(FormatRow("Nettobetrag", Credit.NetAmount - (Credit.PrevNetAmount ?? 0)));
}
tbl1.AddCells(FormatRow($"Mehrwertsteuer ({Credit.Vat * 100} %)", Credit.VatAmount, add: true));
tbl1.AddCells(FormatRow("Bruttobetrag", Credit.GrossAmount, bold: true));
}
}
decimal penalty = 0;
string? comment = null;
if (MemberUnderDeliveries != null && MemberUnderDeliveries.Count > 0) {
tbl2.AddCell(NewTd("Anfallende Pönalen durch Unterlieferungen:", colspan: 2).SetPaddingTopMM(5))
.AddCell(NewCell(colspan: 2));
foreach (var u in MemberUnderDeliveries) {
tbl2.AddCells(FormatRow($"{u.Name} ({u.Kg:N0} kg)", u.Amount, add: true, subCat: true));
penalty += u.Amount;
}
penalty = Math.Round(penalty, 2, MidpointRounding.AwayFromZero);
}
if (MemberTotalUnderDelivery != 0) {
tbl2.AddCells(FormatRow("Unterlieferung (GA)", MemberTotalUnderDelivery, add: true));
penalty += MemberTotalUnderDelivery;
}
if (MemberAutoBusinessSharesAmount != 0) {
tbl2.AddCells(FormatRow($"Nachzeichnung von GA ({MemberAutoBusinessShares})", MemberAutoBusinessSharesAmount, add: true));
penalty += MemberAutoBusinessSharesAmount;
}
if (CustomPayment?.Amount != null) {
comment = CustomPayment.Comment;
string text = (CustomPayment.Amount.Value < 0 ? "Weitere Abzüge" : "Weitere Zuschläge") + (comment != null ? "*" : "");
if (comment != null && comment!.Length <= 30) {
text = comment;
comment = null;
}
tbl2.AddCells(FormatRow(text, CustomPayment.Amount.Value, add: true));
penalty += CustomPayment.Amount.Value;
}
if (Credit == null) {
tbl3.AddCells(FormatRow("Auszahlungsbetrag", (Payment?.Amount + penalty) ?? (sum + penalty), bold: true));
} else {
var diff = Credit.Modifiers - penalty;
if (diff != 0) {
tbl3.AddCells(FormatRow(diff < 0 ? "Sonstige Abzüge" : "Sonstige Zuschläge", diff, add: true));
}
if (Credit.PrevModifiers != null && Credit.PrevModifiers != 0) {
tbl3.AddCells(FormatRow("Bereits berücksichtigte Abzüge", -Credit.PrevModifiers, add: true));
}
tbl3.AddCells(FormatRow("Auszahlungsbetrag", Credit.Amount, bold: true));
}
div.AddCell(new Cell().SetPadding(0).SetBorder(Border.NO_BORDER).Add(tbl1));
div.AddCell(new Cell().SetPadding(0).SetBorder(Border.NO_BORDER).Add(tbl2));
div.AddCell(new Cell().SetPadding(0).SetBorder(Border.NO_BORDER).Add(tbl3));
doc.Add(div);
if (comment != null) {
doc.Add(new KernedParagraph($"*{comment}", 12).SetMarginTopMM(10));
}
doc.Add(new KernedParagraph($"Überweisung erfolgt auf Konto {Utils.FormatIban(Member.Iban ?? "-")}.", 12).SetPaddingTopMM(10));
if (Text != null) {
doc.Add(new KernedParagraph(Text, 12).SetMarginTop(12).SetKeepTogether(true));
}
}
protected Cell[] FormatRow(string name, decimal? value, bool add = false, bool bold = false, bool subCat = false, bool noTopBorder = false) {
float textSize = subCat ? 8 : !add ? 12 : 10;
float numSize = subCat ? 8 : 12;
var border = !add && !noTopBorder;
var pad = !add && !noTopBorder ? 1 : 0.5f;
return [
NewTd($"{name}:", textSize, bold: bold, borderTop: border).SetPaddingTopMM(pad),
NewTd(value < 0 ? "–" : (add ? "+" : ""), numSize, right: true, bold: bold, borderTop: border).SetPaddingTopMM(pad),
NewTd(CurrencySymbol, numSize, bold: bold, borderTop: border).SetPaddingTopMM(pad),
NewTd($"{Math.Abs(value ?? 0):N2}", numSize, right: true, bold: bold, borderTop: border).SetPaddingTopMM(pad),
];
}
protected Table NewCreditTable(CreditNoteDeliveryData data) {
var tbl = new Table(ColsMM(25, 6, 36, 10, 10, 15, 12, 13, 5, 17, 16), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE);
tbl.AddHeaderCell(NewTh("Lieferschein-Nr.", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Pos.", rowspan: 2).SetPaddingLeft(0).SetPaddingRight(0))
.AddHeaderCell(NewTh("Sorte/Attribut/Bewirtschaftg.\nZu-/Abschlag", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Gradation", colspan: 2))
.AddHeaderCell(NewTh("Flächenbindung", colspan: 2))
.AddHeaderCell(NewTh("Preis"))
.AddHeaderCell(NewTh("Rbl.").SetPaddingLeft(0).SetPaddingRight(0))
.AddHeaderCell(NewTh("Zu-/Abschläge").SetPaddingLeft(0).SetPaddingRight(0))
.AddHeaderCell(NewTh("Betrag"))
.AddHeaderCell(NewTh("[°Oe]"))
.AddHeaderCell(NewTh("[°KMW]").SetPaddingLeft(0).SetPaddingRight(0))
.AddHeaderCell(NewTh("[kg]", colspan: 2))
.AddHeaderCell(NewTh($"[{CurrencySymbol}/kg]"))
.AddHeaderCell(NewTh("[%]").SetPaddingLeft(0).SetPaddingRight(0))
.AddHeaderCell(NewTh($"[{CurrencySymbol}]"))
.AddHeaderCell(NewTh($"[{CurrencySymbol}]"));
foreach (var p in data.Rows) {
var sub = new Table(ColsMM(25, 6, 36, 10, 10, 15, 12, 13, 5, 17, 16))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetKeepTogether(true);
var attr = p.Attribute != null || p.Cultivation != null || p.QualId == "WEI";
var rows = Math.Max(p.Buckets.Length, 1 + (attr ? 1 : 0) + p.Modifiers.Length);
for (int i = 0; i < rows; i++) {
if (i == 0) {
sub.AddCell(NewTd(p.LsNr))
.AddCell(NewTd($"{p.DPNr:N0}", center: true).SetPaddingLeft(0).SetPaddingRight(0))
.AddCell(NewTd(p.Variety))
.AddCell(NewTd($"{p.Gradation.Oe:N0}", center: true))
.AddCell(NewTd($"{p.Gradation.Kmw:N1}", center: true));
} else if (i == 1 && attr) {
var varibute = new KernedParagraph(8);
if (p.Attribute != null) varibute.Add(Normal(p.Attribute));
if (p.Attribute != null && p.Cultivation != null) varibute.Add(Normal(" / "));
if (p.Cultivation != null) varibute.Add(Normal(p.Cultivation));
if ((p.Attribute != null || p.Cultivation != null) && p.QualId == "WEI") varibute.Add(Normal(" / "));
if (p.QualId == "WEI") varibute.Add(Italic("abgew."));
sub.AddCell(NewCell(colspan: 2))
.AddCell(NewTd(varibute, colspan: 3).SetPaddingTop(0));
} else {
var idx = i - (rows - p.Modifiers.Length);
if (idx >= 0 && idx < p.Modifiers.Length) {
sub.AddCell(NewCell(colspan: 2))
.AddCell(NewTd(p.Modifiers[idx], 8, colspan: 3).SetPaddingTop(0).SetPaddingLeftMM(5));
} else {
sub.AddCell(NewCell(colspan: 5));
}
}
if (i < p.Buckets.Length) {
var bucket = p.Buckets[i];
var pad = i == 0 ? 0.5f : 0;
sub.AddCell(NewTd($"{bucket.Name}:", 8)
.SetPaddingTopMM(pad))
.AddCell(NewTd($"{bucket.Value:N0}", right: true)
.SetPaddingTopMM(pad))
.AddCell(NewTd($"{bucket.Price:N4}", right: true)
.SetPaddingTopMM(pad));
} else {
sub.AddCell(NewCell(colspan: 3));
}
if (i == p.Buckets.Length - 1) {
var rebelMod = p.WeighingModifier * 100;
var totalMod = p.TotalModifiers ?? 0;
var pad = i == 0 ? 0.5f : 0;
sub.AddCell(NewTd(rebelMod == 0 ? "-" : (Utils.GetSign(rebelMod) + $"{Math.Abs(rebelMod):0.0##}"), 6, center: true)
.SetPaddingTopMM(pad).SetPaddingLeft(0).SetPaddingRight(0))
.AddCell(NewTd(totalMod == 0 ? "-" : Utils.GetSign(totalMod) + $"{Math.Abs(totalMod):N2}", center: totalMod == 0, right: true)
.SetPaddingTopMM(pad))
.AddCell(NewTd($"{p.Amount:N2}", right: true)
.SetPaddingTopMM(pad));
} else {
sub.AddCell(NewCell(colspan: 3));
}
}
tbl.AddCell(new Cell(1, 12).SetPadding(0).SetBorder(Border.NO_BORDER).Add(sub));
}
return tbl;
}
}
}
-85
View File
@@ -1,85 +0,0 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.CreditNote>
@model Elwig.Documents.CreditNote
@{ Layout = "BusinessDocument"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\style-creditnote.css"/>
@{
var bucketNum = Model.BucketNames.Length;
}
<main>
<h1>@Model.Title</h1>
<table class="credit">
<colgroup>
<col style="width: 24mm;"/>
<col style="width: 6mm;"/>
<col style="width: 31mm;"/>
<col style="width: 15mm;"/>
<col style="width: 8mm;"/>
<col style="width: 10mm;"/>
<col style="width: 12mm;"/>
<col style="width: 10mm;"/>
<col style="width: 12mm;"/>
<col style="width: 15mm;"/>
<col style="width: 17mm;"/>
</colgroup>
<thead>
<tr>
<th rowspan="3" style="text-align: left;">Lieferschein-Nr.</th>
<th rowspan="3">Pos.</th>
<th rowspan="3" style="text-align: left;">Sorte</th>
<th rowspan="3" style="text-align: left;">Attribut(e)</th>
<th rowspan="2" colspan="2">Gradation</th>
<th colspan="2">Zu-/Abschläge</th>
<th colspan="2">@Raw(string.Join("<br/>", Model.BucketNames))</th>
<th rowspan="2">Betrag</th>
</tr>
<tr>
<th>Abs.</th>
<th>Rel.</th>
<th>Gewicht</th>
<th>Preis</th>
</tr>
<tr>
<th>[°Oe]</th>
<th>[°KMW]</th>
<th>[@Model.CurrencySymbol/kg]</th>
<th>[%]</th>
<th>[kg]</th>
<th>[@Model.CurrencySymbol/kg]</th>
<th>[@Model.CurrencySymbol]</th>
</tr>
</thead>
<tbody>
@{
string FormatRow(int? weight, decimal? amount) {
var w = weight == null || weight == 0 ? "-" : $"{weight:N0}";
return $"<td class='weight'>{w}</td><td class='amount'>{amount?.ToString("0." + string.Concat(Enumerable.Repeat('0', Model.Precision)))}</td>";
}
string? last = null;
}
@foreach (var part in Model.Parts) {
var pmt = part.Payment;
var abs = pmt?.ModAbs == null || pmt?.ModAbs == 0 ? "-" : pmt?.ModAbs.ToString("0." + string.Concat(Enumerable.Repeat('0', Model.Precision)));
var rel = pmt?.ModRel == null || pmt?.ModRel == 0 ? "-" : $"{pmt?.ModRel * 100:0.00##}";
<tr class="first @(bucketNum <= 1 ? "last" : "") @(last != null && last != part.SortId ? "new" : "")">
<td rowspan="@bucketNum" class="lsnr">@part.Delivery.LsNr</td>
<td rowspan="@bucketNum" class="dpnr">@part.DPNr</td>
<td rowspan="@bucketNum" class="variant">@part.Variant.Name</td>
<td rowspan="@bucketNum" class="attribute">@string.Join(" / ", part.PartAttributes.Select(a => a.AttrId))</td>
<td rowspan="@bucketNum" class="oe">@($"{part.Oe:N0}")</td>
<td rowspan="@bucketNum" class="kmw">@($"{part.Kmw:N1}")</td>
<td rowspan="@bucketNum" class="abs">@abs</td>
<td rowspan="@bucketNum" class="rel">@rel</td>
@Raw(FormatRow(pmt?.Buckets?.ElementAtOrDefault(0), pmt?.Prices?.ElementAtOrDefault(0)))
<td rowspan="@bucketNum" class="amount sum">@($"{pmt?.Amount:N2}")</td>
</tr>
@for (int i = 1; i < bucketNum; i++) {
<tr class="@(i == bucketNum - 1 ? "last" : "")">
@Raw(FormatRow(pmt?.Buckets?.ElementAtOrDefault(i), pmt?.Prices?.ElementAtOrDefault(i)))
</tr>
}
last = part.SortId;
}
</tbody>
</table>
</main>
-40
View File
@@ -1,40 +0,0 @@
using Elwig.Helpers;
using Elwig.Models;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Documents {
public class CreditNote : BusinessDocument {
public Credit Credit;
public string? Text;
public string CurrencySymbol;
public string[] BucketNames;
public int Precision;
public IEnumerable<DeliveryPart> Parts;
public CreditNote(Credit c, AppDbContext ctx) : base($"Traubengutschrift Nr. {c.TgId} – {c.Payment.Variant.Name}", c.Member) {
UseBillingAddress = true;
ShowDateAndLocation = true;
Credit = c;
Aside = Aside.Replace("</table>", "") +
$"<thead><tr><th colspan='2'>Gutschrift</th></tr></thead><tbody>" +
$"<tr><th>TG-Nr.</th><td>{c.TgId}</td></tr>" +
$"<tr><th>Überw. am</th><td>{c.Payment.Variant.TransferDate:dd.MM.yyyy}</td></tr>" +
$"<tr><th>Datum/Zeit</th><td>{c.ModifiedTimestamp:dd.MM.yyyy} / {c.ModifiedTimestamp:HH:mm}</td></tr>" +
$"</tbody></table>";
Text = App.Client.TextDeliveryNote;
DocumentId = $"Tr.-Gutschr. {c.TgId}";
CurrencySymbol = c.Payment.Variant.Season.Currency.Symbol ?? c.Payment.Variant.Season.Currency.Code;
BucketNames = c.Payment.Variant.BucketNames;
Precision = c.Payment.Variant.Season.Precision;
Parts = ctx.DeliveryParts.FromSql($"""
SELECT p.*
FROM v_delivery v
JOIN delivery_part p ON (p.year, p.did, p.dpnr) = (v.year, v.did, v.dpnr)
WHERE (v.year, v.mgnr) = ({c.Year}, {c.Member.MgNr})
ORDER BY sortid, LENGTH(attributes) DESC, attributes, kmw DESC, date, time, dpnr
""").ToList();
}
}}
+68
View File
@@ -0,0 +1,68 @@
using Elwig.Models.Dtos;
using iText.Kernel.Pdf;
using iText.Layout.Element;
using iText.Layout.Properties;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Documents {
public class DeliveryAncmtList : Document {
public new static string Name => "Anmeldeliste";
public string Filter;
public List<DeliveryAncmtListRow> Announcements;
public DeliveryAncmtList(string filter, IEnumerable<DeliveryAncmtListRow> announcements) :
base($"{Name} {filter}") {
Filter = filter;
Announcements = [.. announcements];
}
public DeliveryAncmtList(string filter, DeliveryAncmtListData data) :
this(filter, data.Rows) {
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.RenderBody(doc, pdf);
doc.Add(new KernedParagraph(Name, 24)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 2, 0));
doc.Add(new KernedParagraph(Filter, 14)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 2, 0));
doc.Add(NewAncmtTable(Announcements));
}
protected Table NewAncmtTable(List<DeliveryAncmtListRow> ancmts) {
var tbl = new Table(ColsMM(15, 12, 50, 25, 38, 11, 14), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE);
tbl.AddHeaderCell(NewTh("Datum", rowspan: 2))
.AddHeaderCell(NewTh("MgNr.", rowspan: 2))
.AddHeaderCell(NewTh("Mitglied", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Ort", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Sorte", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Anmldg.", rowspan: 2))
.AddHeaderCell(NewTh("Menge"))
.AddHeaderCell(NewTh("[kg]"));
foreach (var a in ancmts) {
tbl.AddCell(NewTd($"{a.Date:dd.MM.yyyy}", 8))
.AddCell(NewTd($"{a.MgNr}", right: true))
.AddCell(NewTd(a.AdministrativeName))
.AddCell(NewTd(a.DefaultKg, 8))
.AddCell(NewTd(a.Variety))
.AddCell(NewTd(a.Status ?? "-", 8, center: true))
.AddCell(NewTd($"{a.Weight:N0}", right: true));
}
tbl.AddCell(NewTd("Gesamt:", colspan: 2, bold: true, borderTop: true))
.AddCell(NewTd($"Anmeldungen: {ancmts.Count:N0}", colspan: 3, bold: true, borderTop: true))
.AddCell(NewTd($"{ancmts.Sum(a => a.Weight):N0}", colspan: 2, right: true, bold: true, borderTop: true));
return tbl;
}
}
}
+176
View File
@@ -0,0 +1,176 @@
using Elwig.Helpers;
using Elwig.Models.Dtos;
using Elwig.Models.Entities;
using iText.Kernel.Pdf;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Properties;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Elwig.Documents {
public class DeliveryConfirmation : BusinessDocument {
public new static string Name => "Anlieferungsbestätigung";
private readonly int _year;
public Season? Season;
public int MemberDeliveredWeightRed;
public int MemberDeliveredWeightWhite;
public DeliveryConfirmationDeliveryData? Data;
public string? Text = App.Client.TextDeliveryConfirmation;
public Dictionary<string, MemberBucket> MemberBuckets = [];
public List<MemberStat> MemberStats = [];
public DeliveryConfirmation(int year, Member m, DateOnly? dateFrom, DeliveryConfirmationDeliveryData? data = null) :
base($"{Name} {year}", m, dateFrom) {
_year = year;
ShowDateAndLocation = true;
UseBillingAddress = true;
DocumentId = $"Anlfrg.-Bstng. {_year}/{m.MgNr}";
Data = data;
}
protected override async Task LoadData(AppDbContext ctx) {
await base.LoadData(ctx);
Season = await ctx.FetchSeasons(_year).SingleOrDefaultAsync() ?? throw new ArgumentException("Invalid season");
var weights = await ctx.Deliveries
.Where(d => d.Year == Season.Year && d.MgNr == Member.MgNr)
.SelectMany(d => d.Parts)
.GroupBy(p => p.Variety.Type)
.ToDictionaryAsync(g => g.Key, g => g.Sum(p => p.Weight));
MemberDeliveredWeightRed = weights.GetValueOrDefault("R", 0);
MemberDeliveredWeightWhite = weights.GetValueOrDefault("W", 0);
MemberShares = await ctx.GetMemberHistory(Season.Year, Member.MgNr);
MemberBuckets = await ctx.GetMemberBuckets(Season.Year, Member.MgNr);
MemberStats = await AppDbContext.GetMemberStats(Season.Year, Member.MgNr);
Data ??= await DeliveryConfirmationDeliveryData.ForMember(ctx.DeliveryParts, Season.Year, Member);
}
protected override void BeforeRenderBody(iText.Layout.Document doc, PdfDocument pdf) {
if (Data == null) throw new Exception("Call LoadData before BeforeRenderBody");
base.BeforeRenderBody(doc, pdf);
var firstDay = Data.Rows.MinBy(r => r.Date)?.Date;
var lastDay = Data.Rows.MaxBy(r => r.Date)?.Date;
Aside?.AddCell(NewAsideCell("Saison", 26))
.AddCell(NewAsideCell("Lieferungen:", 12, isName: true)).AddCell(NewAsideCell($"{Data.Rows.DistinctBy(r => r.LsNr).Count():N0} (Teil-Lfrg.: {Data.RowNum:N0})", 14))
.AddCell(NewAsideCell("Zeitraum:", 8, isName: true)).AddCell(NewAsideCell(firstDay == null || lastDay == null ? "-" : firstDay == lastDay ? $"{firstDay:dd.MM.} (1 Tag)" : $"{firstDay:dd.MM.}\u2013{lastDay:dd.MM.} ({lastDay?.DayNumber - firstDay?.DayNumber + 1:N0} Tage)", 18, center: true))
.AddCell(NewAsideCell("Menge:", 12, isName: true)).AddCell(NewAsideCell($"{MemberStats.Sum(s => s.Weight):N0} kg", 14));
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
if (Season == null || Data == null) throw new Exception("Call LoadData before RenderBody");
base.RenderBody(doc, pdf);
doc.Add(NewDeliveryListTable(Data));
doc.Add(NewWeightsTable(MemberStats)
.SetMarginTopMM(10).SetKeepTogether(true));
doc.Add(NewBucketTable(Season, MemberBuckets, MemberDeliveredWeightRed, MemberDeliveredWeightWhite, includePayment: true)
.SetMarginTopMM(10).SetKeepTogether(true));
if (Text != null) {
doc.Add(new KernedParagraph(Text, 10)
.SetMarginTop(20).SetKeepTogether(true));
}
}
protected Table NewDeliveryListTable(DeliveryConfirmationDeliveryData data) {
var tbl = new Table(ColsMM(25, 7, 39, 14, 11, 11, 15, 12, 14, 3, 14), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE);
tbl.AddHeaderCell(NewTh("Lieferschein-Nr.", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Pos.", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Sorte/Attribut/Bewirtschaftung\nZu-/Abschlag", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Qual.", rowspan: 2))
.AddHeaderCell(NewTh("Gradation", colspan: 2))
.AddHeaderCell(NewTh("Flächenbindung", colspan: 2))
.AddHeaderCell(NewTh("Menge"))
.AddHeaderCell(NewTh("gerebelt", rowspan: 3, rotated: true))
.AddHeaderCell(NewTh("Davon\nabzuwerten"))
.AddHeaderCell(NewTh("[°Oe]"))
.AddHeaderCell(NewTh("[°KMW]"))
.AddHeaderCell(NewTh("[kg]", colspan: 2))
.AddHeaderCell(NewTh("[kg]"))
.AddHeaderCell(NewTh("[kg]"));
var lastVariety = "";
foreach (var p in data.Rows) {
var sub = new Table(ColsMM(25, 7, 39, 14, 11, 11, 15, 12, 14, 3, 14))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetKeepTogether(true);
if (lastVariety != "" && lastVariety != p.Variety) {
sub.SetBorderTop(new SolidBorder(BorderThickness));
}
var attr = p.Attribute != null || p.Cultivation != null;
var rows = Math.Max(p.Buckets.Length, 1 + (attr ? 1 : 0) + p.Modifiers.Length);
for (int i = 0; i < rows; i++) {
if (i == 0) {
sub.AddCell(NewTd(p.LsNr))
.AddCell(NewTd($"{p.DPNr:N0}", center: true))
.AddCell(NewTd(p.Variety))
.AddCell(NewTd(p.QualId, center: true))
.AddCell(NewTd($"{p.Gradation.Oe:N0}", center: true))
.AddCell(NewTd($"{p.Gradation.Kmw:N1}", center: true));
} else if (i == 1 && attr) {
sub.AddCell(NewCell(colspan: 2))
.AddCell(NewTd($"{p.Attribute}{(p.Attribute != null && p.Cultivation != null ? " / " : "")}{p.Cultivation}", 8, colspan: 2)
.SetPaddingTop(0))
.AddCell(NewCell(colspan: 2));
} else {
sub.AddCell(NewCell(colspan: 2));
var idx = i - (rows - p.Modifiers.Length);
if (idx >= 0 && idx < p.Modifiers.Length) {
sub.AddCell(NewTd(p.Modifiers[idx], 8, colspan: 2)
.SetPaddingTop(0).SetPaddingLeftMM(5));
} else {
sub.AddCell(NewCell(colspan: 2));
}
sub.AddCell(NewCell(colspan: 2));
}
if (i < p.Buckets.Length) {
var bucket = p.Buckets[i];
var pad = i == 0 ? 0.5f : 0;
sub.AddCell(NewTd($"{bucket.Name}:", 8).SetHeight(10).SetPaddingTopMM(pad));
sub.AddCell(NewTd($"{bucket.Value:N0}", right: true).SetPaddingTopMM(pad));
} else {
sub.AddCell(NewCell(colspan: 2));
}
if (i == p.Buckets.Length - 1) {
sub.AddCell(NewTd($"{p.Weight:N0}", right: true).SetPaddingTop(0));
sub.AddCell(NewTd(p.IsNetWeight ? "\u2611" : "\u2610", 7, right: true).SetFont(SF).SetPadding(0));
} else {
sub.AddCell(NewCell(colspan: 2));
}
if (i == 0) {
sub.AddCell(NewCell(rowspan: rows));
}
lastVariety = p.Variety;
}
tbl.AddCell(new Cell(1, 11).SetPadding(0).SetBorder(Border.NO_BORDER).Add(sub));
}
tbl.AddCell(NewTd("Gesamt:", 12, colspan: 7, bold: true, borderTop: true)
.SetPaddingsMM(1, 1, 0, 1))
.AddCell(NewTd($"{data.Rows.Sum(p => p.Weight):N0}", 12, colspan: 2, right: true, bold: true, borderTop: true)
.SetPaddingsMM(1, 1, 0, 1))
.AddCell(NewTd(colspan: 2, borderTop: true)
.SetPaddingsMM(1, 1, 0, 1))
.AddCell(NewTd("Davon abgewertet:", 10, colspan: 7)
.SetPaddingsMM(0, 1, 0.5f, 1))
.AddCell(NewTd($"{data.Rows.Where(p => p.IsDepreciated).Sum(p => p.Weight):N0}", 10, colspan: 2, right: true)
.SetPaddingsMM(0, 1, 0.5f, 1))
.AddCell(NewCell(colspan: 2)
.SetPaddingsMM(0, 1, 0.5f, 1));
return tbl;
}
}
}
+108
View File
@@ -0,0 +1,108 @@
using Elwig.Models.Dtos;
using iText.Kernel.Pdf;
using iText.Layout.Element;
using iText.Layout.Properties;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Documents {
public class DeliveryDepreciationList : Document {
public new static string Name => "Abwertungsliste";
public string Filter;
public List<DeliveryJournalRow> Deliveries;
public DeliveryDepreciationList(string filter, IEnumerable<DeliveryJournalRow> deliveries) :
base($"{Name} {filter}") {
Filter = filter;
Deliveries = [.. deliveries];
}
public DeliveryDepreciationList(string filter, DeliveryJournalData data) :
this(filter, data.Rows) {
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.RenderBody(doc, pdf);
doc.Add(new KernedParagraph(Name, 24)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 2, 0));
doc.Add(new KernedParagraph(Filter, 14)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 5, 0));
doc.Add(NewJournalTable(Deliveries));
}
protected Table NewJournalTable(List<DeliveryJournalRow> deliveries) {
var tbl = new Table(ColsMM(25, 6, 20, 12, 38, 18, 12, 10, 10, 14), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE);
tbl.AddHeaderCell(NewTh("Lieferschein-Nr.", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Pos.", rowspan: 2).SetPaddingRight(0))
.AddHeaderCell(NewTh("Datum", rowspan: 2))
.AddHeaderCell(NewTh("Zeit", rowspan: 2))
.AddHeaderCell(NewTh("Sorte", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Attr./Bewirt.", rowspan: 2, colspan: 2, left: true))
.AddHeaderCell(NewTh("Gradation", colspan: 2))
.AddHeaderCell(NewTh("Menge"))
.AddHeaderCell(NewTh("[°Oe]"))
.AddHeaderCell(NewTh("[°KMW]").SetPaddingLeft(0).SetPaddingRight(0))
.AddHeaderCell(NewTh("[kg]"));
int? lastMember = null;
foreach (var p in deliveries) {
if (lastMember != p.MgNr) {
var border = lastMember != null;
var memberDeliveries = deliveries.Where(d => d.MgNr == p.MgNr).ToList();
var memberKmw = Helpers.Utils.AggregateDeliveryPartsKmw(memberDeliveries);
var memberOe = Helpers.Utils.KmwToOe(memberKmw);
tbl.AddCell(NewTd($"{p.MgNr}, {p.AdministrativeName}", colspan: 5, borderTop: border)
.SetFont(BI))
.AddCell(NewTd("Teil-Lfrg.:", borderTop: border, bold: true))
.AddCell(NewTd($"{memberDeliveries.Count:N0}", right: true, borderTop: border, bold: true))
.AddCell(NewTd($"{memberOe:N0}", center: true, borderTop: border, bold: true))
.AddCell(NewTd($"{memberKmw:N1}", center: true, borderTop: border, bold: true))
.AddCell(NewTd($"{memberDeliveries.Sum(p => p.Weight):N0}", right: true, borderTop: border, bold: true));
}
tbl.AddCell(NewTd(p.LsNr))
.AddCell(NewTd($"{p.Pos:N0}", center: true).SetPaddingLeft(0).SetPaddingRight(0))
.AddCell(NewTd($"{p.Date:dd.MM.yyyy}", center: true))
.AddCell(NewTd($"{p.Time:HH:mm}", center: true).SetPadding(0))
.AddCell(NewTd(p.Variety))
.AddCell(NewTd($"{p.Attribute}{(p.Attribute != null && p.Cultivation != null ? " / " : "")}{p.Cultivation}", colspan: 2))
.AddCell(NewTd($"{p.Oe:N0}", center: true))
.AddCell(NewTd($"{p.Kmw:N1}", center: true).SetPaddingLeft(0).SetPaddingRight(0))
.AddCell(NewTd($"{p.Weight:N0}", right: true));
lastMember = p.MgNr;
}
var branches = deliveries.Select(d => d.DeliveryBranch).Distinct().Order().ToArray();
if (branches.Length > 1) {
foreach (var b in branches) {
var border = branches[0] == b;
var branchDeliveries = deliveries.Where(d => d.DeliveryBranch == b).ToList();
var branchKmw = Helpers.Utils.AggregateDeliveryPartsKmw(branchDeliveries);
var branchOe = Helpers.Utils.KmwToOe(branchKmw);
tbl.AddCell(NewTd($"{b}:", colspan: 2, bold: true, borderTop: border))
.AddCell(NewTd($"(Teil-)Lieferungen: {branchDeliveries.DistinctBy(p => p.LsNr).Count():N0} ({branchDeliveries.Count:N0})", colspan: 5, bold: true, borderTop: border))
.AddCell(NewTd($"{branchOe:N0}", center: true, bold: true, borderTop: border))
.AddCell(NewTd($"{branchOe:N1}", center: true, bold: true, borderTop: border))
.AddCell(NewTd($"{branchDeliveries.Sum(p => p.Weight):N0}", right: true, bold: true, borderTop: border));
}
}
var kmw = Helpers.Utils.AggregateDeliveryPartsKmw(deliveries);
var oe = Helpers.Utils.KmwToOe(kmw);
tbl.AddCell(NewTd("Gesamt:", colspan: 2, bold: true, borderTop: true))
.AddCell(NewTd($"(Teil-)Lieferungen: {deliveries.DistinctBy(p => p.LsNr).Count():N0} ({deliveries.Count:N0})", colspan: 5, bold: true, borderTop: true))
.AddCell(NewTd($"{oe:N0}", center: true, bold: true, borderTop: true))
.AddCell(NewTd($"{kmw:N1}", center: true, bold: true, borderTop: true))
.AddCell(NewTd($"{deliveries.Sum(p => p.Weight):N0}", right: true, bold: true, borderTop: true));
return tbl;
}
}
}
+94
View File
@@ -0,0 +1,94 @@
using Elwig.Models.Dtos;
using iText.Kernel.Pdf;
using iText.Layout.Element;
using iText.Layout.Properties;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Documents {
public class DeliveryJournal : Document {
public new static string Name => "Lieferjournal";
public string Filter;
public List<DeliveryJournalRow> Deliveries;
public DeliveryJournal(string filter, IEnumerable<DeliveryJournalRow> deliveries) :
base($"{Name} {filter}") {
Filter = filter;
Deliveries = [.. deliveries];
}
public DeliveryJournal(string filter, DeliveryJournalData data) :
this(filter, data.Rows) {
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.RenderBody(doc, pdf);
doc.Add(new KernedParagraph(Name, 24)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 2, 0));
doc.Add(new KernedParagraph(Filter, 14)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 5, 0));
doc.Add(NewJournalTable(Deliveries));
}
protected Table NewJournalTable(List<DeliveryJournalRow> deliveries) {
var tbl = new Table(ColsMM(25, 6, 15, 8, 11, 38, 28, 10, 10, 14), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE);
tbl.AddHeaderCell(NewTh("Lieferschein-Nr.", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Pos.", rowspan: 2).SetPaddingRight(0))
.AddHeaderCell(NewTh("Datum", rowspan: 2))
.AddHeaderCell(NewTh("Zeit", rowspan: 2))
.AddHeaderCell(NewTh("MgNr.", rowspan: 2))
.AddHeaderCell(NewTh("Mitglied", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Sorte", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Gradation", colspan: 2))
.AddHeaderCell(NewTh("Menge"))
.AddHeaderCell(NewTh("[°Oe]"))
.AddHeaderCell(NewTh("[°KMW]").SetPaddingLeft(0).SetPaddingRight(0))
.AddHeaderCell(NewTh("[kg]"));
foreach (var p in deliveries) {
tbl.AddCell(NewTd(p.LsNr))
.AddCell(NewTd($"{p.Pos:N0}", center: true).SetPaddingLeft(0).SetPaddingRight(0))
.AddCell(NewTd($"{p.Date:dd.MM.yyyy}", 8, center: true))
.AddCell(NewTd($"{p.Time:HH:mm}", 8, center: true).SetPadding(0))
.AddCell(NewTd($"{p.MgNr}", right: true))
.AddCell(NewTd(p.AdministrativeName, 8))
.AddCell(NewTd(p.Variety, 8))
.AddCell(NewTd($"{p.Oe:N0}", center: true))
.AddCell(NewTd($"{p.Kmw:N1}", center: true).SetPaddingLeft(0).SetPaddingRight(0))
.AddCell(NewTd($"{p.Weight:N0}", right: true));
}
var branches = deliveries.Select(d => d.DeliveryBranch).Distinct().Order().ToArray();
if (branches.Length > 1) {
foreach (var b in branches) {
var border = branches[0] == b;
var branchDeliveries = deliveries.Where(d => d.DeliveryBranch == b).ToList();
var branchKmw = Helpers.Utils.AggregateDeliveryPartsKmw(branchDeliveries);
var branchOe = Helpers.Utils.KmwToOe(branchKmw);
tbl.AddCell(NewTd($"{b}:", colspan: 2, bold: true, borderTop: border))
.AddCell(NewTd($"(Teil-)Lieferungen: {branchDeliveries.DistinctBy(p => p.LsNr).Count():N0} ({branchDeliveries.Count:N0})", colspan: 5, bold: true, borderTop: border))
.AddCell(NewTd($"{branchOe:N0}", center: true, bold: true, borderTop: border))
.AddCell(NewTd($"{branchOe:N1}", center: true, bold: true, borderTop: border))
.AddCell(NewTd($"{branchDeliveries.Sum(p => p.Weight):N0}", right: true, bold: true, borderTop: border));
}
}
var kmw = Helpers.Utils.AggregateDeliveryPartsKmw(deliveries);
var oe = Helpers.Utils.KmwToOe(kmw);
tbl.AddCell(NewTd("Gesamt:", colspan: 2, bold: true, borderTop: true))
.AddCell(NewTd($"(Teil-)Lieferungen: {deliveries.DistinctBy(p => p.LsNr).Count():N0} ({deliveries.Count:N0})", colspan: 5, bold: true, borderTop: true))
.AddCell(NewTd($"{oe:N0}", center: true, bold: true, borderTop: true))
.AddCell(NewTd($"{kmw:N1}", center: true, bold: true, borderTop: true))
.AddCell(NewTd($"{deliveries.Sum(p => p.Weight):N0}", right: true, bold: true, borderTop: true));
return tbl;
}
}
}
-67
View File
@@ -1,67 +0,0 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.DeliveryJournal>
@model Elwig.Documents.DeliveryJournal
@{ Layout = "Document"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\style-deliveryjournal.css"/>
<main>
<h1>Lieferjournal</h1>
<h2>@Model.Filter</h2>
<table class="journal">
<colgroup>
<col style="width: 25mm;"/>
<col style="width: 5mm;"/>
<col style="width: 17mm;"/>
<col style="width: 10mm;"/>
<col style="width: 8mm;"/>
<col style="width: 38mm;"/>
<col style="width: 28mm;"/>
<col style="width: 10mm;"/>
<col style="width: 10mm;"/>
<col style="width: 14mm;"/>
</colgroup>
<thead>
<tr>
<th rowspan="2" style="text-align: left;">Lieferschein-Nr.</th>
<th rowspan="2">Pos.</th>
<th rowspan="2">Datum</th>
<th rowspan="2">Zeit</th>
<th colspan="2" 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>
</tr>
<tr>
<th>[°Oe]</th>
<th>[°KMW]</th>
<th>[kg]</th>
</tr>
</thead>
<tbody>
@foreach (var p in Model.Deliveries) {
<tr>
<td>@p.Delivery.LsNr</td>
<td>@p.DPNr</td>
<td>@($"{p.Delivery.Date:dd.MM.yyyy}")</td>
<td>@($"{p.Delivery.Time:HH:mm}")</td>
<td class="mgnr">@p.Delivery.Member.MgNr</td>
<td>@p.Delivery.Member.AdministrativeName</td>
<td>@p.Variant.Name</td>
<td class="grad">@($"{p.Oe:N0}")</td>
<td class="grad">@($"{p.Kmw:N1}")</td>
<td class="weight">@($"{p.Weight:N0}")</td>
</tr>
}
<tr class="sum">
@{
var kmw = Elwig.Helpers.Utils.AggregateDeliveryPartsKmw(Model.Deliveries);
var oe = Elwig.Helpers.Utils.KmwToOe(kmw);
}
<td colspan="2">Gesamt:</td>
<td colspan="5">(Teil-)Lieferungen: @($"{Model.Deliveries.DistinctBy(p => p.Delivery).Count():N0}") (@($"{Model.Deliveries.Count():N0}"))</td>
<td class="grad">@($"{oe:N0}")</td>
<td class="grad">@($"{kmw:N1}")</td>
<td class="weight">@($"{Model.Deliveries.Sum(p => p.Weight):N0}")</td>
</tr>
</tbody>
</table>
</main>
-34
View File
@@ -1,34 +0,0 @@
using Elwig.Helpers;
using Elwig.Models;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Documents {
public class DeliveryJournal : Document {
public string Filter;
public IEnumerable<DeliveryPart> Deliveries;
public DeliveryJournal(string filter, IEnumerable<DeliveryPart> deliveries) : base($"Lieferjournal {filter}") {
Filter = filter;
Deliveries = deliveries;
}
public DeliveryJournal(string filter, IQueryable<DeliveryPart> deliveries) :
this(filter, deliveries
.Include(p => p.Delivery)
.Include(p => p.Delivery.Member)
.Include(p => p.Variant)
.ToList()) { }
public DeliveryJournal(AppDbContext ctx, DateOnly date) :
this(date.ToString("dd.MM.yyyy"), ctx.DeliveryParts
.Where(p => p.Delivery.DateString == date.ToString("yyy-MM-dd"))
.OrderBy(p => p.Delivery.DateString)
.ThenBy(p => p.Delivery.TimeString)
.ThenBy(p => p.Delivery.LsNr)
.ThenBy(p => p.DPNr)) { }
}
}
+253
View File
@@ -0,0 +1,253 @@
using Elwig.Helpers;
using Elwig.Models.Entities;
using iText.Kernel.Pdf;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Layout;
using iText.Layout.Properties;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Elwig.Documents {
public class DeliveryNote : BusinessDocument {
public new static string Name => "Traubenübernahmeschein";
public Delivery Delivery;
public string? Text;
public int MemberDeliveredWeightRed;
public int MemberDeliveredWeightWhite;
public Dictionary<string, MemberBucket> MemberBuckets = [];
// 0 - none
// 1 - GA only
// 2 - GA only and area commitments of varieties from delivery note
// 3 - full
public int DisplayStats = App.Client.ModeDeliveryNoteStats;
public DeliveryNote(Delivery d) :
base($"{Name} Nr. {d.LsNr}", d.Member, DateOnly.FromDateTime(d.ModifiedAt)) {
UseBillingAddress = true;
ShowDateAndLocation = true;
Delivery = d;
Text = App.Client.TextDeliveryNote;
DocumentId = d.LsNr;
}
public static async Task<DeliveryNote> Initialize(int year, int did) {
using var ctx = new AppDbContext();
await ctx.WineOrigins.LoadAsync();
var d = await ctx.Deliveries
.Where(d => d.Year == year && d.DId == did)
.Include(d => d.Parts).ThenInclude(p => p.PartModifiers)
.SingleAsync();
return new DeliveryNote(d);
}
public static async Task<DeliveryNote> Initialize(string lsnr) {
using var ctx = new AppDbContext();
await ctx.WineOrigins.LoadAsync();
var d = await ctx.Deliveries
.Where(d => d.LsNr == lsnr)
.Include(d => d.Parts).ThenInclude(p => p.PartModifiers)
.SingleAsync();
return new DeliveryNote(d);
}
protected override async Task LoadData(AppDbContext ctx) {
await base.LoadData(ctx);
var weights = await ctx.DeliveryParts
.Where(d => d.Year == Delivery.Year && d.Delivery.MgNr == Member.MgNr)
.GroupBy(p => p.Variety.Type)
.ToDictionaryAsync(g => g.Key, g => g.Sum(p => p.Weight));
MemberDeliveredWeightRed = weights.GetValueOrDefault("R", 0);
MemberDeliveredWeightWhite = weights.GetValueOrDefault("W", 0);
MemberShares = await ctx.GetMemberHistory(Delivery.Year, Member.MgNr);
MemberBuckets = await ctx.GetMemberBuckets(Delivery.Year, Member.MgNr) ?? [];
}
protected override void BeforeRenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.BeforeRenderBody(doc, pdf);
Aside?.AddCell(NewAsideCell("Lieferung", 26))
.AddCell(NewAsideCell("Lieferschein-Nr.:", 12, isName: true)).AddCell(NewAsideCell(Delivery.LsNr, 14))
.AddCell(NewAsideCell("Datum/Zeit:", 12, isName: true)).AddCell(NewAsideCell($"{Delivery.Date:dd.MM.yyyy} / {Delivery.Time:HH:mm}", 14))
.AddCell(NewAsideCell("Zweigstelle:", 12, isName: true)).AddCell(NewAsideCell(Delivery.Branch.Name, 14));
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.RenderBody(doc, pdf);
doc.Add(NewDeliveryTable());
if (Delivery.Comment != null) {
doc.Add(new KernedParagraph($"Anmerkung zur Lieferung: {Delivery.Comment}", 10).SetMarginsMM(5, 0, 0, 0));
}
if (DisplayStats > 0) {
doc.Add(NewBucketTable(Delivery.Season, MemberBuckets, MemberDeliveredWeightRed, MemberDeliveredWeightWhite, isTiny: true,
filter: DisplayStats > 2 ? null : DisplayStats == 1 ? [] : Delivery.Parts.Select(p => p.SortId).Distinct().ToList())
.SetKeepTogether(true)
.SetMarginsMM(5, 0, 0, 0));
}
var sig = new Div().SetWidth(165 * PtInMM);
if (Text != null)
sig.Add(new KernedParagraph(Regex.Replace(Text, @"\s+", " "), 10).SetTextAlignment(TextAlignment.JUSTIFIED).SetSpacingRatio(1));
sig.Add(new Table(ColsMM(15, 50, 35, 50, 15)).SetMarginsMM(18, 0, 2, 0)
.AddCell(NewTd())
.AddCell(NewTd("Genossenschaft", center: true, borderTop: true).SetPaddingTopMM(1))
.AddCell(NewTd())
.AddCell(NewTd("Mitglied", center: true, borderTop: true).SetPaddingTopMM(1))
.AddCell(NewTd()));
var renderer = sig.CreateRendererSubTree();
renderer.SetParent(doc.GetRenderer());
var layoutResult = renderer.Layout(new LayoutContext(new LayoutArea(1, pdf.GetDefaultPageSize())));
float sigHeight = layoutResult.GetOccupiedArea().GetBBox().GetHeight();
doc.Add(new Div().SetWidth(165 * PtInMM).SetHeight(sigHeight));
var size = pdf.GetDefaultPageSize();
doc.Add(sig.SetFixedPosition(
size.GetLeft() + doc.GetLeftMargin(),
size.GetBottom() + doc.GetBottomMargin(),
size.GetWidth() - doc.GetLeftMargin() - doc.GetRightMargin())); ;
}
protected Cell NewDeliveryMainTd(string? text, int rowspan = 1, int colspan = 1, bool center = false, bool right = false) {
return NewTd(text, 12, rowspan: rowspan, colspan: colspan, bold: true, center: center, right: right)
.SetPaddingTopMM(2);
}
protected Table NewDeliveryTable() {
var tbl = new Table(ColsMM(10, 21, 25, 20, 20, 30, 12.5, 12.5, 14), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorder(Border.NO_BORDER).SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE);
tbl.AddHeaderCell(NewTh("Pos.", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Sorte", colspan: 2, rowspan: 2, left: true))
.AddHeaderCell(NewTh("Attribut", colspan: 2, rowspan: 2, left: true))
.AddHeaderCell(NewTh("Qualitätsstufe", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Gradation", colspan: 2))
.AddHeaderCell(NewTh("Menge").SetPaddingRight(0))
.AddHeaderCell(NewTh("[°Oe]", 8))
.AddHeaderCell(NewTh("[°KMW]", 8))
.AddHeaderCell(NewTh("[kg]", 8).SetPaddingRight(0));
foreach (var part in Delivery.Parts.OrderBy(p => p.DPNr)) {
var sub = new Table(ColsMM(10, 21, 25, 20, 20, 30, 12.5, 12.5, 14), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetKeepTogether(true);
int rowspan = 1 + (part.Cultivation != null ? 1 : 0) + 1 + part.Modifiers.Count() + 1 + (part.Comment != null ? 1 : 0) + (part.Temperature != null || part.Acid != null ? 1 : 0);
sub.AddCell(NewDeliveryMainTd($"{part.DPNr:N0}", center: true))
.AddCell(NewDeliveryMainTd(part.Variety.Name, colspan: 2))
.AddCell(NewDeliveryMainTd(part.Attribute?.Name, colspan: 2))
.AddCell(NewDeliveryMainTd(part.Quality.Name))
.AddCell(NewDeliveryMainTd($"{part.Oe:N0}", center: true))
.AddCell(NewDeliveryMainTd($"{part.Kmw:N1}", center: true))
.AddCell(NewDeliveryMainTd($"{part.Weight:N0}", right: true).SetPaddingRight(0));
if (part.Cultivation != null) {
var cult = new KernedParagraph(8);
cult.Add(Italic("Bewirtschaftung:")).Add(Normal(" " + part.Cultivation.Name + (part.Cultivation.Description != null ? $" ({part.Cultivation.Description})" : "")));
if (part.Cultivation.IsOrganic) {
cult.Add(" \u2013 ").Add(Italic("Kontrollstelle:")).Add(Normal($" {Member.OrganicAuthorityCode ?? "?"}"));
}
sub.AddCell(NewTd())
.AddCell(NewTd(cult, colspan: 5))
.AddCell(NewTd(colspan: 3));
}
sub.AddCell(NewTd())
.AddCell(NewTd(new KernedParagraph(8).Add(Italic("Herkunft:")).Add(Normal(" " + part.OriginString.Replace("\n ", "\n\u00a0"))), colspan: 5))
.AddCell(NewTd(colspan: 3));
if (part.Modifiers.Any()) {
sub.AddCell(NewTd())
.AddCell(NewTd("Zu-/Abschläge:", 8, italic: true));
int i = 0, last = part.Modifiers.Count() - 1;
foreach (var mod in part.Modifiers) {
if (i > 0) sub.AddCell(NewCell(colspan: 2));
sub.AddCell(NewTd(mod.Name, 8, bold: true, colspan: 3).SetPaddingsMM(i == 0 ? 0.5f : 0, 1, i == last ? 0.5f : 0, 1))
.AddCell(NewTd(mod.PublicValueStr, 8).SetPaddingsMM(i == 0 ? 0.5f : 0, 1, i == last ? 0.5f : 0, 1))
.AddCell(NewTd(colspan: 3));
i++;
}
}
var weighing = new KernedParagraph(8);
if (part.IsManualWeighing) {
weighing.Add(Italic("Handwiegung " + (part.IsNetWeight ? "(gerebelt gewogen)" : "(nicht gerebelt gewogen)")));
if (part.WeighingReason != null) {
weighing.Add(Normal(", "))
.Add(Italic("Begründung:"))
.Add(Normal(" " + part.WeighingReason));
}
} else {
var info = part.WeighingInfo;
weighing.Add(Italic("Waage/Terminal:"))
.Add(Normal(" " + (info.ScaleId ?? "?") + "/" + (part.ScaleTerminalId ?? "?") + ", "))
.Add(Italic("ID:"))
.Add(Normal(" " + (info.Id ?? "?")));
if (info.Date != null || info.Time != null)
weighing.Add(Normal(" \u2013 "));
if (info.Time != null)
weighing.Add(Normal($"{info.Time:HH:mm}"));
if (info.Date != null)
weighing.Add(Normal($", {info.Date:dd.MM.yyyy}"));
if (info.Gross != null && info.Tare != null && info.Net != null) {
weighing.Add("\n")
.Add(Italic("Brutto:"))
.Add(Normal($" {info.Gross:N0} kg \u2013 "))
.Add(Italic("Tara:"))
.Add(Normal($" {info.Tare:N0} kg "));
if (info.TareCorrection != null && info.TareCorrection != 0)
weighing.Add(Bold($"(Korr.: {Utils.GetSign(info.TareCorrection.Value)}{Math.Abs(info.TareCorrection.Value)} kg)")).Add(" ");
weighing
.Add(Normal("\u2013 "))
.Add(Italic("Netto:"))
.Add(Normal($" {info.Net:N0} kg "));
if (info.TareCorrection != null && info.TareCorrection != 0)
weighing.Add(Normal($"({Utils.GetSign(-info.TareCorrection.Value)}{Math.Abs(info.TareCorrection.Value)} kg) "));
weighing
.Add(Normal("\u2013 "))
.Add(Italic(part.IsNetWeight ? "gerebelt gewogen" : "nicht gerebelt gewogen"));
} else {
weighing.Add(" ")
.Add(Italic(part.IsNetWeight ? "(gerebelt gewogen)" : "(nicht gerebelt gewogen)"));
}
}
sub.AddCell(NewCell())
.AddCell(NewCell(weighing, colspan: 5))
.AddCell(NewCell(colspan: 3));
if (part.Comment != null) {
sub.AddCell(NewCell())
.AddCell(NewCell(new KernedParagraph(8).Add(Italic("Anmerkung:")).Add(Normal(" " + part.Comment)), colspan: 5))
.AddCell(NewCell(colspan: 3));
}
if (part.Temperature != null || part.Acid != null) {
var p = new KernedParagraph(8);
if (part.Temperature != null)
p.Add(Italic("Temperatur:")).Add(Normal($" {part.Temperature:N1} °C"));
if (part.Temperature != null && part.Acid != null)
p.Add(Normal(", "));
if (part.Acid != null)
p.Add(Italic("Säure:")).Add(Normal($" {part.Acid:N1} g/l"));
sub.AddCell(NewCell())
.AddCell(NewCell(p, colspan: 5))
.AddCell(NewCell(colspan: 3));
}
tbl.AddCell(new Cell(1, 9).SetPadding(0).SetBorder(Border.NO_BORDER).Add(sub));
}
if (Delivery.Parts.Count > 1) {
tbl.AddCell(NewTd("Gesamt:", 12, bold: true, borderTop: true, colspan: 6).SetPaddingsMM(1, 1, 1, 0))
.AddCell(NewTd($"{Delivery.Oe:N0}", 12, bold: true, center: true, borderTop: true).SetPaddingsMM(1, 1, 1, 1))
.AddCell(NewTd($"{Delivery.Kmw:N1}", 12, bold: true, center: true, borderTop: true).SetPaddingsMM(1, 1, 1, 1))
.AddCell(NewTd($"{Delivery.Weight:N0}", 12, bold: true, right: true, borderTop: true).SetPaddingsMM(1, 0, 1, 1));
}
return tbl;
}
}
}
-179
View File
@@ -1,179 +0,0 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.DeliveryNote>
@model Elwig.Documents.DeliveryNote
@{ Layout = "BusinessDocument"; }
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\style-deliverynote.css" />
<main>
<h1>@Model.Title</h1>
@{
bool displayStats = true; // Model.Delivery.Year == Model.CurrentNextSeason
}
<script>
document.addEventListener("DOMContentLoaded", () => {
const hidden = document.getElementsByClassName("hidden")[0];
const table = document.getElementsByClassName("delivery")[0];
const stats = document.getElementById("delivery-stats");
const mm = px2mm(0, hidden.offsetTop + hidden.offsetHeight);
const heightTable = px2mm(table.offsetTop, hidden.offsetTop + hidden.offsetHeight);
if (mm >= heightA4 - heightFooter) {
if (heightTable + 10 >= heightMain) {
// force page break in table
const tblOff = px2mm(0, table.offsetTop);
let last = null;
for (const tr of table.getElementsByTagName("tr")) {
if (!tr.classList.contains("main")) continue;
const mm2 = tblOff + px2mm(0, tr.offsetTop);
if (mm2 >= heightA4 - heightFooter) {
last.classList.add("page-break");
break;
}
last = tr;
}
} else {
// force page break
const hr = document.createElement("hr");
hr.classList.add("page-break");
table.before(hr);
const p = document.createElement("p");
p.innerText = "Siehe nächste Seite."
hr.before(p);
}
}
});
</script>
<table class="delivery">
<colgroup>
<col style="width: 10.00mm;"/>
<col style="width: 21.25mm;"/>
<col style="width: 21.25mm;"/>
<col style="width: 21.25mm;"/>
<col style="width: 21.25mm;"/>
<col style="width: 30.00mm;"/>
<col style="width: 12.50mm;"/>
<col style="width: 12.50mm;"/>
<col style="width: 15.00mm;"/>
</colgroup>
<thead>
<tr>
<th class="main" rowspan="2" style="text-align: center;">Pos.</th>
<th class="main" rowspan="2" colspan="2">Sorte</th>
<th class="main" rowspan="2" colspan="2">Attribut(e)</th>
<th class="main" rowspan="2">Qualitätsstufe</th>
<th colspan="2">Gradation</th>
<th>Gewicht</th>
</tr>
<tr>
<th style="font-size: 8pt;">[°Oe]</th>
<th style="font-size: 8pt;">[°KMW]</th>
<th style="font-size: 8pt;">[kg]</th>
</tr>
</thead>
<tbody>
@foreach (var part in Model.Delivery.Parts.OrderBy(p => p.DPNr)) {
<tr class="main">
<td style="text-align: center;">@part.DPNr</td>
<td colspan="2">@part.Variant.Name</td>
<td colspan="2">@string.Join(" / ", part.Attributes)</td>
<td>@part.Quality.Name</td>
<td class="narrow" style="text-align: center;">@($"{part.Oe:N0}")</td>
<td class="narrow" style="text-align: center;">@($"{part.Kmw:N1}")</td>
<td class="narrow" style="text-align: right;">@($"{part.Weight:N0}")</td>
</tr>
<tr><td></td><td colspan="5" style="white-space: pre;"><i>Herkunft:</i> @part.OriginString</td></tr>
@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>
first = false;
}
}
<tr><td></td><td colspan="5">
@Raw(part.ManualWeighing ? "<i>Handwiegung</i>" : $"<i>Waage:</i> {part.ScaleId ?? "?"}, <i>ID:</i> {part.WeighingId ?? "?"}")
(@(part.IsGerebelt ? "gerebelt gewogen" : "nicht gerebelt gewogen"))@Raw(part.WeighingReason != null ? $", <i>Begründung:</i>" : "") @part.WeighingReason
</td></tr>
@if (part.Comment != null) {
<tr><td></td><td colspan="5"><i>Anmerkung:</i> @part.Comment</td></tr>
}
@if (part.Temperature != null || part.Acid != null) {
<tr><td></td><td colspan="5">@Raw(part.Temperature != null ? $"<i>Temperatur:</i> {part.Temperature:N1} °C" : "")@(part.Temperature != null && part.Acid != null ? ", " : "")@Raw(part.Acid != null ? $"<i>Säure:</i> {part.Acid:N1} g/l" : "")</td></tr>
}
}
@if (Model.Delivery.Parts.Count() > 1) {
<tr class="main sum">
<td colspan="6">Gesamt:</td>
<td style="text-align: center;">@($"{Model.Delivery.Oe:N0}")</td>
<td style="text-align: center;">@($"{Model.Delivery.Kmw:N1}")</td>
<td style="text-align: right;">@($"{Model.Delivery.Weight:N0}")</td>
</tr>
}
</tbody>
</table>
@if (Model.Delivery.Comment != null) {
<p class="comment">Amerkung zur Lieferung: @Model.Delivery.Comment</p>
}
@if (displayStats) {
<div id="delivery-stats">
<table class="delivery-stats">
<colgroup>
<col style="width: 45mm;"/>
<col style="width: 20mm;"/>
<col style="width: 20mm;"/>
<col style="width: 20mm;"/>
<col style="width: 20mm;"/>
<col style="width: 20mm;"/>
<col style="width: 20mm;"/>
</colgroup>
<thead>
<tr>
<th><b>Lese @Model.Delivery.Year</b> per @($"{Model.Date:dd.MM.yyyy}") [kg]</th>
<th>Lieferpflicht</th>
<th>Lieferrecht</th>
<th>Unterliefert</th>
<th>Noch zu liefern</th>
<th>Überliefert</th>
<th>Geliefert</th>
</tr>
</thead>
<tbody>
@{
string FormatRow(int obligation, int right, int sum) {
return $"<td>{obligation:N0}</td>" +
$"<td>{right:N0}</td>" +
$"<td>{(sum < obligation ? $"{obligation - sum:N0}" : "-")}</td>" +
$"<td>{(sum >= obligation && sum <= right ? $"{right - sum:N0}" : "-")}</td>" +
$"<td>{(sum > right ? $"{sum - right:N0}" : "-")}</td>" +
$"<td>{sum:N0}</td>";
}
var sortids = Model.Delivery.Parts.Select(p => p.SortId).ToList();
var buckets = Model.MemberBuckets.GroupBy(b => b.Item1[..2]).ToDictionary(g => g.Key, g => g.Count());
}
<tr>
<th>Gesamtlieferung</th>
@Raw(FormatRow(Model.Member.DeliveryObligation, Model.Member.DeliveryRight, Model.Member.Deliveries.Where(d => d.Year == Model.Delivery.Year).Sum(d => d.Weight)))
</tr>
@foreach (var (id, name, right, obligation, sum) in Model.MemberBuckets.OrderBy(b => b.Item1)) {
if (right > 0 || obligation > 0 || (sum > 0 && buckets[id[..2]] > 1 && !id.EndsWith('_'))) {
<tr class="@(sortids.Contains(id[..2]) ? "" : "optional")">
<th>@name</th>
@Raw(FormatRow(obligation, right, sum))
</tr>
}
}
</tbody>
</table>
</div>
}
</main>
@for (int i = 0; i < 2; i++) {
<div class="@(i == 0 ? "hidden" : "bottom")">
@if (Model.Text != null) {
<p class="comment">@Model.Text</p>
}
<div class="signatures">
<div>Genossenschaft</div>
<div>Mitglied</div>
</div>
</div>
}
-27
View File
@@ -1,27 +0,0 @@
using Elwig.Helpers;
using Elwig.Models;
using System.Collections.Generic;
namespace Elwig.Documents {
public class DeliveryNote : BusinessDocument {
public Delivery Delivery;
public string? Text;
public IEnumerable<(string, string, int, int, int)> MemberBuckets;
public DeliveryNote(Delivery d, AppDbContext ctx) : base($"Traubenübernahmeschein Nr. {d.LsNr}", d.Member) {
UseBillingAddress = true;
ShowDateAndLocation = true;
Delivery = d;
Aside = Aside.Replace("</table>", "") +
$"<thead><tr><th colspan='2'>Lieferung</th></tr></thead><tbody>" +
$"<tr><th>LS-Nr.</th><td>{d.LsNr}</td></tr>" +
$"<tr><th>Datum/Zeit</th><td>{d.Date:dd.MM.yyyy} / {d.Time:HH:mm}</td></tr>" +
$"<tr><th>Zweigstelle</th><td>{d.Branch.Name}</td></tr>" +
$"</tbody></table>";
Text = App.Client.TextDeliveryNote;
DocumentId = d.LsNr;
MemberBuckets = ctx.GetMemberBuckets(d.Member, d.Year).GetAwaiter().GetResult();
}
}
}
+511
View File
@@ -0,0 +1,511 @@
using Elwig.Helpers;
using Elwig.Helpers.Printing;
using iText.IO.Font;
using iText.Kernel.Font;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas;
using iText.Kernel.Pdf.Event;
using iText.Kernel.Pdf.Xobject;
using iText.Kernel.Utils;
using iText.Layout;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Properties;
using MimeKit;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Elwig.Documents {
public class Document : IDisposable {
public static string Name => "Dokument";
public const float PtInMM = 2.8346456693f;
public const float BorderThickness = 0.5f;
protected PdfFont NF = null!;
protected PdfFont BF = null!;
protected PdfFont IF = null!;
protected PdfFont BI = null!;
protected PdfFont SF = null!;
//protected readonly PdfFont NF = PdfFontFactory.CreateFont(StandardFonts.TIMES_ROMAN);
//protected readonly PdfFont BF = PdfFontFactory.CreateFont(StandardFonts.TIMES_BOLD);
//protected readonly PdfFont IF = PdfFontFactory.CreateFont(StandardFonts.TIMES_ITALIC);
//protected readonly PdfFont BI = PdfFontFactory.CreateFont(StandardFonts.TIMES_BOLDITALIC);
protected TempFile? _pdfFile = null;
protected string? _pdfPath;
protected string? PdfPath => _pdfPath ?? _pdfFile?.FilePath;
public int? TotalPages { get; private set; }
public int? Pages => TotalPages / (IsDoublePaged ? 2 : 1);
public bool ShowFoldMarks = App.Config.Debug;
public bool IsDoublePaged = false;
public bool IsPreview = false;
private iText.Layout.Document? _doc;
public string? DocumentId;
public string Title;
public string Author;
public DateOnly Date;
public Document(string title) {
Title = title;
Author = App.Client.NameFull;
Date = DateOnly.FromDateTime(DateTime.Today);
}
~Document() {
Dispose();
}
public void Dispose() {
_pdfFile?.Dispose();
_pdfFile = null;
GC.SuppressFinalize(this);
}
public static Document Merge(IEnumerable<Document> docs) {
return new MergedDocument(docs);
}
public static Document FromPdf(string path) {
return new RawPdfDocument(path);
}
private class RawPdfDocument : Document {
public RawPdfDocument(string pdfPath) :
base(Path.GetFileNameWithoutExtension(pdfPath)) {
_pdfPath = pdfPath;
}
}
public int Render(string path) {
using (var writer = new PdfWriter(path)) {
Render(writer);
}
using var reader = new PdfReader(path);
using var pdf = new PdfDocument(reader);
return pdf.GetNumberOfPages();
}
private void Render(PdfWriter writer) {
NF = PdfFontFactory.CreateFont(@"C:\Windows\Fonts\times.ttf", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_NOT_EMBEDDED);
BF = PdfFontFactory.CreateFont(@"C:\Windows\Fonts\timesbd.ttf", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_NOT_EMBEDDED);
IF = PdfFontFactory.CreateFont(@"C:\Windows\Fonts\timesi.ttf", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_NOT_EMBEDDED);
BI = PdfFontFactory.CreateFont(@"C:\Windows\Fonts\timesbi.ttf", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_NOT_EMBEDDED);
SF = PdfFontFactory.CreateFont(@"C:\Windows\Fonts\seguisym.ttf", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_NOT_EMBEDDED);
NF.SetSubset(true);
BF.SetSubset(true);
IF.SetSubset(true);
BI.SetSubset(true);
SF.SetSubset(true);
writer.SetCompressionLevel(CompressionConstants.BEST_COMPRESSION);
writer.SetSmartMode(true);
using var pdf = new PdfDocument(writer);
pdf.GetDocumentInfo()
.SetTitle(Title)
.SetAuthor(Author)
.SetCreator($"Elwig {App.Version}");
var handler = new EventHandler(this);
pdf.AddEventHandler(PdfDocumentEvent.START_PAGE, handler);
pdf.AddEventHandler(PdfDocumentEvent.END_PAGE, handler);
pdf.AddEventHandler(PdfDocumentEvent.START_DOCUMENT_CLOSING, handler);
_doc = new iText.Layout.Document(pdf, iText.Kernel.Geom.PageSize.A4);
try {
_doc.SetFont(NF).SetFontSize(12);
BeforeRenderBody(_doc, pdf);
RenderBody(_doc, pdf);
} finally {
_doc.Close();
}
}
protected virtual async Task LoadData(AppDbContext ctx) { }
protected virtual void BeforeRenderBody(iText.Layout.Document doc, PdfDocument pdf) { }
protected virtual void RenderBody(iText.Layout.Document doc, PdfDocument pdf) { }
protected virtual Paragraph GetFooter() {
return new KernedParagraph(App.Client.NameFull, 10);
}
public async Task Generate(AppDbContext ctx, CancellationToken? cancelToken = null, IProgress<double>? progress = null) {
if (_pdfFile != null)
return;
progress?.Report(0.0);
if (this is RawPdfDocument) {
// nothing to do
} else if (this is MergedDocument m) {
using var tmpPdf = new TempFile("pdf");
var pdf = new TempFile("pdf");
try {
var pageNums = new List<int>();
using var writer = new PdfWriter(pdf.FilePath);
using var mergedPdf = new PdfDocument(writer);
var merger = new PdfMerger(mergedPdf);
(PdfPage Page, int InsertIndex, int DocIndex)? letterhead = null;
int p = mergedPdf.GetNumberOfPages();
for (int i = 0; i < m.Documents.Count; i++) {
if (cancelToken?.IsCancellationRequested ?? false)
throw new OperationCanceledException("Dokumentenerzeugung abgebrochen!");
var doc = m.Documents[i];
int p0 = p;
if (letterhead != null && doc is Letterhead) {
if (p0 <= letterhead.Value.InsertIndex) {
mergedPdf.AddPage(letterhead.Value.Page);
mergedPdf.AddNewPage();
} else {
mergedPdf.AddPage(letterhead.Value.InsertIndex + 1, letterhead.Value.Page);
mergedPdf.AddNewPage(letterhead.Value.InsertIndex + 2);
}
pageNums[letterhead.Value.DocIndex] = 1;
letterhead = null;
p += 2;
}
if (doc is RawPdfDocument) {
using var reader = new PdfReader(doc.PdfPath);
using var src = new PdfDocument(reader);
merger.Merge(src, 1, src.GetNumberOfPages());
p += src.GetNumberOfPages();
} else {
await doc.LoadData(ctx);
int pageNum = doc.Render(tmpPdf.FilePath);
if (IsDoublePaged && doc is Letterhead) {
using var reader = new PdfReader(tmpPdf.FilePath);
using var src = new PdfDocument(reader);
letterhead = (src.GetPage(1).CopyTo(mergedPdf), p0, i);
} else {
using var reader = new PdfReader(tmpPdf.FilePath);
using var src = new PdfDocument(reader);
merger.Merge(src, 1, pageNum);
p += pageNum;
}
}
int p1 = p;
pageNums.Add(p1 - p0);
if (IsDoublePaged && doc is not Letterhead && p % 2 != 0) {
if (letterhead != null) {
mergedPdf.AddPage(letterhead.Value.Page);
letterhead = null;
} else {
mergedPdf.AddNewPage();
}
p++;
}
progress?.Report(100.0 * (i + 1) / (m.Documents.Count + 1));
}
if (letterhead != null) {
if (p <= letterhead.Value.InsertIndex) {
mergedPdf.AddPage(letterhead.Value.Page);
mergedPdf.AddNewPage();
} else {
mergedPdf.AddPage(letterhead.Value.InsertIndex + 1, letterhead.Value.Page);
mergedPdf.AddNewPage(letterhead.Value.InsertIndex + 2);
}
pageNums[letterhead.Value.DocIndex] = 1;
p += 2;
}
TotalPages = p;
} catch {
pdf.Dispose();
throw;
}
_pdfFile = pdf;
} else {
if (cancelToken?.IsCancellationRequested ?? false)
throw new OperationCanceledException("Dokumentenerzeugung abgebrochen!");
var pdf = new TempFile("pdf");
try {
await LoadData(ctx);
TotalPages = Render(pdf.FilePath);
} catch {
pdf.Dispose();
throw;
}
_pdfFile = pdf;
}
progress?.Report(100.0);
}
public void SaveTo(string pdfPath) {
if (PdfPath == null) throw new InvalidOperationException("Pdf file has not been generated yet");
File.Copy(PdfPath, pdfPath, true);
}
public async Task Print(int copies = 1) {
if (PdfPath == null) throw new InvalidOperationException("Pdf file has not been generated yet");
Pdf.Print(PdfPath, copies, IsDoublePaged);
}
public void Show() {
if (_pdfFile == null) throw new InvalidOperationException("Pdf file has not been generated yet");
Pdf.Show(_pdfFile.NewReference(), Title + (this is BusinessDocument b ? $" - {b.Member.FullName}" : ""));
}
public MimePart AsEmailAttachment(string filename) {
if (PdfPath == null) throw new InvalidOperationException("Pdf file has not been generated yet");
return new("application", "pdf") {
Content = new MimeContent(File.OpenRead(PdfPath)),
ContentDisposition = new ContentDisposition(ContentDisposition.Attachment),
ContentTransferEncoding = ContentEncoding.Base64,
FileName = filename
};
}
private class MergedDocument : Document {
public List<Document> Documents;
public MergedDocument(IEnumerable<Document> docs) :
base("Mehrere Dokumente") {
Documents = [.. docs];
IsDoublePaged = docs.Any(x => x.IsDoublePaged);
}
}
protected static Cell NewCell(Paragraph? p = null, int rowspan = 1, int colspan = 1, bool overflow = false) {
var cell = new Cell(rowspan, colspan)
.SetBorder(Border.NO_BORDER)
.SetPaddingsMM(0.5f, 1, 0.5f, 1);
if (p != null) {
p.SetProperty(Property.NO_SOFT_WRAP_INLINE, true);
if (!overflow) p.SetProperty(Property.OVERFLOW_X, OverflowPropertyValue.HIDDEN);
cell.Add(p);
}
return cell;
}
protected Cell NewTh(string? text, float fontSize = 8, int rowspan = 1, int colspan = 1, bool left = false, bool rotated = false) {
var p = new KernedParagraph(text ?? "", fontSize);
if (rotated) p.SetRotationAngle(0.5 * Math.PI);
var cell = NewCell(p, rowspan: rowspan, colspan: colspan)
.SetTextAlignment(left ? TextAlignment.LEFT : TextAlignment.CENTER)
.SetVerticalAlignment(VerticalAlignment.MIDDLE)
.SetFont(IF);
if (rotated || !left) cell.SetPadding(0);
return cell;
}
protected Cell NewTd(string? text = null, float fontSize = 10, int rowspan = 1, int colspan = 1, bool center = false, bool right = false, bool bold = false, bool italic = false, bool borderTop = false, bool overflow = false) {
return NewTd(new KernedParagraph(text ?? "", fontSize), rowspan: rowspan, colspan: colspan, center: center, right: right, bold: bold, italic: italic, borderTop: borderTop, overflow: overflow);
}
protected Cell NewTd(KernedParagraph p, int rowspan = 1, int colspan = 1, bool center = false, bool right = false, bool bold = false, bool italic = false, bool borderTop = false, bool overflow = false) {
return NewCell(p, rowspan: rowspan, colspan: colspan, overflow: overflow)
.SetTextAlignment(center ? TextAlignment.CENTER : right ? TextAlignment.RIGHT : TextAlignment.LEFT)
.SetVerticalAlignment(VerticalAlignment.MIDDLE)
.SetBorderTop(borderTop ? new SolidBorder(BorderThickness) : Border.NO_BORDER)
.SetFont(bold ? (italic ? BI : BF) : (italic ? IF : NF));
}
public static float[] ColsMM(params double[] widths) {
return [.. widths.Select(w => (float)w * PtInMM)];
}
public Text Normal(string? text, float? fontSize = null) {
var t = new Text(text ?? "").SetFont(NF);
if (fontSize != null) t.SetFontSize(fontSize.Value);
return t;
}
public Text Bold(string? text, float? fontSize = null) {
var t = new Text(text ?? "").SetFont(BF);
if (fontSize != null) t.SetFontSize(fontSize.Value);
return t;
}
public Text Italic(string? text, float? fontSize = null) {
var t = new Text(text ?? "").SetFont(IF);
if (fontSize != null) t.SetFontSize(fontSize.Value);
return t;
}
public Text BoldItalic(string? text, float? fontSize = null) {
var t = new Text(text ?? "").SetFont(BI);
if (fontSize != null) t.SetFontSize(fontSize.Value);
return t;
}
private class EventHandler : AbstractPdfDocumentEventHandler {
private const float _fontSize = 10;
private const float _placeholderWidth = 50 * PtInMM;
private readonly Document _doc;
private readonly List<PdfFormXObject> _pageNumPlaceholders;
public int NumberOfPages { get; private set; }
public EventHandler(Document doc) {
_doc = doc;
_pageNumPlaceholders = [];
}
protected override void OnAcceptedEvent(AbstractPdfDocumentEvent evt) {
if (evt.GetType() == PdfDocumentEvent.START_PAGE) {
OnPageStart((PdfDocumentEvent)evt);
} else if (evt.GetType() == PdfDocumentEvent.END_PAGE) {
OnPageEnd((PdfDocumentEvent)evt);
} else if (evt.GetType() == PdfDocumentEvent.START_DOCUMENT_CLOSING) {
OnDocumentClose((PdfDocumentEvent)evt);
}
}
private void OnPageStart(PdfDocumentEvent evt) {
var pdf = evt.GetDocument();
var page = evt.GetPage();
var pageNum = pdf.GetPageNumber(page);
if (pageNum == 1) {
// first page
if (_doc is BusinessDocument) {
_doc._doc?.SetMarginsMM(90, 20, 35, 25);
} else {
_doc._doc?.SetMarginsMM(20, 20, 35, 25);
}
} else if (_doc.IsDoublePaged && (pageNum % 2) == 0) {
// left page (= swapped)
_doc._doc?.SetMarginsMM(20, 25, 25, 20);
} else {
// right page
_doc._doc?.SetMarginsMM(20, 20, 25, 25);
}
}
private void OnPageEnd(PdfDocumentEvent evt) {
if (_doc is Letterhead) return;
var pdf = evt.GetDocument();
var page = evt.GetPage();
var pageNum = pdf.GetPageNumber(page);
var pageSize = page.GetPageSize();
float leftX1 = pageSize.GetLeft() + 25 * PtInMM;
float leftX2 = pageSize.GetLeft() + 20 * PtInMM;
float rightX1 = pageSize.GetRight() - 20 * PtInMM;
float rightX2 = pageSize.GetRight() - 25 * PtInMM;
float footerY = pageSize.GetBottom() + 25 * PtInMM;
float y1 = footerY + _fontSize;
float y2 = footerY - _fontSize;
var pdfCanvas = new PdfCanvas(page.NewContentStreamAfter(), page.GetResources(), pdf);
using var canvas = new Canvas(pdfCanvas, pageSize);
var placeholder = new PdfFormXObject(new iText.Kernel.Geom.Rectangle(0, 0, _placeholderWidth, _fontSize));
_pageNumPlaceholders.Add(placeholder);
var c = App.Client;
var dateP = new KernedParagraph($"{_doc.Date:dddd, d. MMMM yyyy}", _fontSize).SetFont(_doc.NF);
var centerP = new KernedParagraph(_doc.DocumentId ?? "", _fontSize).SetFont(_doc.IF);
var pageNumP = new KernedParagraph(_fontSize).Add(new Image(placeholder)).SetFont(_doc.NF);
if (pageNum == 1) {
// first page
canvas.ShowTextAligned(dateP, leftX1, y1, TextAlignment.LEFT, VerticalAlignment.BOTTOM);
canvas.ShowTextAligned(centerP, (leftX1 + rightX1) / 2, y1, TextAlignment.CENTER, VerticalAlignment.BOTTOM);
canvas.ShowTextAligned(pageNumP, rightX1, y1, TextAlignment.RIGHT, VerticalAlignment.BOTTOM);
var footer = _doc.GetFooter();
using var footerCanvas = new Canvas(page, pageSize);
footerCanvas.Add(new Table(1).AddCell(new Cell().Add(footer).SetBorder(Border.NO_BORDER).SetPaddingsMM(1, 0, 0, 0))
.SetFixedPositionMM(25, 0, 165).SetHeightMM(25)
.SetFont(_doc.NF).SetFontSize(10)
.SetTextAlignment(TextAlignment.CENTER)
.SetBorder(Border.NO_BORDER)
.SetBorderTop(new SolidBorder(BorderThickness)));
} else if (_doc.IsDoublePaged && (pageNum % 2 == 0)) {
// left page (= swapped)
canvas.ShowTextAligned(pageNumP, leftX2, y2, TextAlignment.LEFT, VerticalAlignment.TOP);
canvas.ShowTextAligned(centerP, (leftX2 + rightX2) / 2, y2, TextAlignment.CENTER, VerticalAlignment.TOP);
canvas.ShowTextAligned(dateP, rightX2, y2, TextAlignment.RIGHT, VerticalAlignment.TOP);
} else {
// right page
canvas.ShowTextAligned(dateP, leftX1, y2, TextAlignment.LEFT, VerticalAlignment.TOP);
canvas.ShowTextAligned(centerP, (leftX1 + rightX1) / 2, y2, TextAlignment.CENTER, VerticalAlignment.TOP);
canvas.ShowTextAligned(pageNumP, rightX1, y2, TextAlignment.RIGHT, VerticalAlignment.TOP);
}
if (_doc.ShowFoldMarks) {
var m1 = pageSize.GetTop() - 105 * PtInMM;
var m2 = pageSize.GetTop() - 148.5 * PtInMM;
var m3 = pageSize.GetTop() - 210 * PtInMM;
pdfCanvas.SetLineWidth(BorderThickness);
pdfCanvas.MoveTo(0, m1);
pdfCanvas.LineTo(10 * PtInMM, m1);
pdfCanvas.MoveTo(pageSize.GetRight(), m1);
pdfCanvas.LineTo(pageSize.GetRight() - 10 * PtInMM, m1);
pdfCanvas.MoveTo(0, m2);
pdfCanvas.LineTo(7 * PtInMM, m2);
pdfCanvas.MoveTo(pageSize.GetRight(), m2);
pdfCanvas.LineTo(pageSize.GetRight() - 7 * PtInMM, m2);
pdfCanvas.MoveTo(0, m3);
pdfCanvas.LineTo(10 * PtInMM, m3);
pdfCanvas.MoveTo(pageSize.GetRight(), m3);
pdfCanvas.LineTo(pageSize.GetRight() - 10 * PtInMM, m3);
pdfCanvas.ClosePathStroke();
}
if (NumberOfPages > 0) {
// FillPlaceholders() was already called
FillPlaceholder(pdf, pageNum);
}
}
private void OnDocumentClose(PdfDocumentEvent evt) {
var pdf = evt.GetDocument();
var page = evt.GetPage();
var pageNum = pdf.GetPageNumber(page);
// ...
FillPlaceholders(pdf);
}
private void FillPlaceholders(PdfDocument pdf) {
NumberOfPages = pdf.GetNumberOfPages();
for (int i = 0; i < _pageNumPlaceholders.Count; i++)
FillPlaceholder(pdf, i + 1);
}
private void FillPlaceholder(PdfDocument pdf, int pageNum) {
var placeholder = _pageNumPlaceholders[pageNum - 1];
using var canvas = new Canvas(placeholder, pdf);
if (_doc.IsDoublePaged && (pageNum % 2 == 0)) {
// left page (= swapped)
var p = new KernedParagraph(_fontSize).SetFont(_doc.NF);
if (_doc.IsPreview) p.Add(_doc.Bold("(vorläufig) "));
p.Add(_doc.Normal($"Seite {pageNum:N0} von {NumberOfPages:N0} "));
canvas.ShowTextAligned(p, 0, 0, TextAlignment.LEFT);
} else {
// right page
var p = new KernedParagraph(_fontSize).SetFont(_doc.NF)
.Add(_doc.Normal($"Seite {pageNum:N0} von {NumberOfPages:N0}"));
if (_doc.IsPreview) p.Add(_doc.Bold(" (vorläufig)"));
canvas.ShowTextAligned(p, _placeholderWidth, 0, TextAlignment.RIGHT);
}
}
}
}
}
-43
View File
@@ -1,43 +0,0 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.Document>
@model Elwig.Documents.Document
<!DOCTYPE html>
<html lang="de-AT">
<head>
<title>@Model.Title</title>
<meta name="author" value="@Model.Author"/>
<meta charset="UTF-8"/>
<script>
const heightA4 = 297, widhtA4 = 210, heightFooter = 35, heightHeader = 25;
const heightMain = heightA4 - heightFooter - heightHeader;
function px2mm(px1, px2) {
return (px2 - px1 + 1) * 2.54 / 96 * window.devicePixelRatio * 10;
}
</script>
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\style.css"/>
<link rel="stylesheet" href="file:///@Raw(Model.DataPath)\resources\style-page.css"/>
</head>
<body>
@if (Model.ShowFoldMarks) {
<div class="m1"></div>
<div class="m2"></div>
<div class="m3"></div>
<div class="m1 r"></div>
<div class="m2 r"></div>
<div class="m3 r"></div>
}
<div class="footer-wrapper">
<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>
</div>
<footer>@Raw(Model.Footer)</footer>
</div>
<header>@Raw(Model.Header)</header>
<div class="spacing"></div>
<div class="main-wrapper">
@RenderBody()
</div>
</body>
</html>
-142
View File
@@ -1,142 +0,0 @@
using System;
using System.Threading.Tasks;
using System.IO;
using Elwig.Helpers;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Linq;
namespace Elwig.Documents {
public abstract partial class Document : IDisposable {
private TempFile? _pdfFile = null;
private string? _renderedHtml = null;
public bool ShowFoldMarks = App.Config.Debug;
public string DataPath;
public int CurrentNextSeason;
public string? DocumentId;
public string Title;
public string Author;
public string Header;
public string Footer;
public DateTime Date;
public Document(string title) {
var c = App.Client;
DataPath = App.DataPath;
CurrentNextSeason = Utils.CurrentNextSeason;
Title = title;
Author = c.NameFull;
Header = $"<div class='name'>{c.Name}</div><div class='suffix'>{c.NameSuffix}</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).Item(c.Website).Item("Betriebs-Nr.", c.LfbisNr).Item("UID", c.UstIdNr).NextLine()
.Item("BIC", c.Bic).Item("IBAN", c.Iban)
.ToString();
Date = DateTime.Today;
}
[GeneratedRegex(@"</body>.*?</footer>\s*</div>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled)]
private static partial Regex GeneratedDocumentHeaderRegex();
private static readonly Regex DocumentHeaderRegex = GeneratedDocumentHeaderRegex();
[GeneratedRegex(@"<style>.*?/style>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled)]
private static partial Regex GeneratedHtmlStyleRegex();
private static readonly Regex HtmlStyleRegex = GeneratedHtmlStyleRegex();
[GeneratedRegex(@"<link[^>]*>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled)]
private static partial Regex GeneratedHtmlLinkRegex();
private static readonly Regex HtmlLinkRegex = GeneratedHtmlLinkRegex();
~Document() {
Dispose();
}
public void Dispose() {
_pdfFile?.Dispose();
_pdfFile = null;
GC.SuppressFinalize(this);
}
public static async Task<Document> Merge(IEnumerable<Document> docs) {
string html = "";
var styles = new List<string>();
foreach (var d in docs) {
var h = await d.Render();
var s = HtmlStyleRegex.Matches(h).Select(m => m.Value).ToList();
var l = HtmlLinkRegex.Matches(h).Select(m => m.Value).ToList();
if (s.All(styles.Contains)) {
h = HtmlStyleRegex.Replace(h, "");
} else {
styles.AddRange(s);
}
if (l.All(styles.Contains)) {
h = HtmlLinkRegex.Replace(h, "");
} else {
styles.AddRange(l);
}
html += h;
}
html = DocumentHeaderRegex.Replace(html, "<div class='document-break'/>");
return new InternalDocument("Mehrere Dokumente") {
_renderedHtml = html,
};
}
private async Task<string> Render() {
if (_renderedHtml != null)
return _renderedHtml;
string name;
if (this is BusinessLetter) {
name = "BusinessLetter";
} else if (this is DeliveryNote) {
name = "DeliveryNote";
} else if (this is CreditNote) {
name = "CreditNote";
} else if (this is DeliveryJournal) {
name = "DeliveryJournal";
} else if (this is Letterhead) {
name = "Letterhead";
} else {
throw new InvalidOperationException("Invalid document object");
}
return await Render(name);
}
private async Task<string> Render(string name) {
_renderedHtml = await Html.CompileRenderAsync(name, this);
return _renderedHtml;
}
public async Task Generate() {
var pdf = new TempFile("pdf");
using (var tmpHtml = new TempFile("html")) {
await File.WriteAllTextAsync(tmpHtml.FilePath, await Render(), Utils.UTF8);
await Pdf.Convert(tmpHtml.FilePath, pdf.FilePath);
}
_pdfFile = pdf;
}
public void SaveTo(string pdfPath) {
if (_pdfFile == null) throw new InvalidOperationException("Pdf file has not been generated yet");
File.Copy(_pdfFile.FilePath, pdfPath);
}
public async Task Print(int copies = 1) {
if (_pdfFile == null) throw new InvalidOperationException("Pdf file has not been generated yet");
await Pdf.Print(_pdfFile.FilePath, copies);
}
public void Show() {
if (_pdfFile == null) throw new InvalidOperationException("Pdf file has not been generated yet");
Pdf.Show(_pdfFile.NewReference(), Title);
}
private class InternalDocument : Document {
public InternalDocument(string title) : base(title) { }
}
}
}
+93
View File
@@ -0,0 +1,93 @@
using iText.Kernel.Geom;
using iText.Layout;
using iText.Layout.Element;
using iText.Layout.Properties;
namespace Elwig.Documents {
public static class Extensions {
public static T SetFixedPositionMM<T>(this BlockElement<T> element, float left, float top, float width, Rectangle pageSize) where T : IElement {
element.SetVerticalAlignment(VerticalAlignment.TOP);
return element.SetFixedPosition(left * Document.PtInMM, pageSize.GetTop() - top * Document.PtInMM, width * Document.PtInMM);
}
public static T SetFixedPositionMM<T>(this BlockElement<T> element, float left, float top, float width, float height, Rectangle pageSize) where T : IElement {
element.SetHeight(height * Document.PtInMM);
return element.SetFixedPosition(left * Document.PtInMM, pageSize.GetTop() - (top + height) * Document.PtInMM, width * Document.PtInMM);
}
public static T SetFixedPositionMM<T>(this ElementPropertyContainer<T> element, float left, float bottom, float width) where T : IPropertyContainer {
return element.SetFixedPosition(left * Document.PtInMM, bottom * Document.PtInMM, width * Document.PtInMM);
}
public static T SetHeightMM<T>(this BlockElement<T> element, float height) where T : IElement {
return element.SetHeight(height * Document.PtInMM);
}
public static T SetPaddingTopMM<T>(this BlockElement<T> element, float top) where T : IElement {
return element.SetPaddingTop(top * Document.PtInMM);
}
public static T SetPaddingRightMM<T>(this BlockElement<T> element, float right) where T : IElement {
return element.SetPaddingRight(right * Document.PtInMM);
}
public static T SetPaddingBottomMM<T>(this BlockElement<T> element, float bottom) where T : IElement {
return element.SetPaddingBottom(bottom * Document.PtInMM);
}
public static T SetPaddingLeftMM<T>(this BlockElement<T> element, float left) where T : IElement {
return element.SetPaddingLeft(left * Document.PtInMM);
}
public static T SetPaddingsMM<T>(this BlockElement<T> element, float top, float right, float bottom, float left) where T : IElement {
return element.SetPaddings(top * Document.PtInMM, right * Document.PtInMM, bottom * Document.PtInMM, left * Document.PtInMM);
}
public static T SetMarginTopMM<T>(this BlockElement<T> element, float top) where T : IElement {
return element.SetMarginTop(top * Document.PtInMM);
}
public static T SetMarginRightMM<T>(this BlockElement<T> element, float right) where T : IElement {
return element.SetMarginRight(right * Document.PtInMM);
}
public static T SetMarginBottomMM<T>(this BlockElement<T> element, float bottom) where T : IElement {
return element.SetMarginBottom(bottom * Document.PtInMM);
}
public static T SetMarginLeftMM<T>(this BlockElement<T> element, float left) where T : IElement {
return element.SetMarginLeft(left * Document.PtInMM);
}
public static T SetMarginsMM<T>(this BlockElement<T> element, float top, float right, float bottom, float left) where T : IElement {
return element.SetMargins(top * Document.PtInMM, right * Document.PtInMM, bottom * Document.PtInMM, left * Document.PtInMM);
}
public static void SetTopMarginMM(this iText.Layout.Document element, float top) {
element.SetTopMargin(top * Document.PtInMM);
}
public static void SetRightMarginMM(this iText.Layout.Document element, float right) {
element.SetRightMargin(right * Document.PtInMM);
}
public static void SetBottomMarginMM(this iText.Layout.Document element, float bottom) {
element.SetBottomMargin(bottom * Document.PtInMM);
}
public static void SetLeftMarginMM(this iText.Layout.Document element, float left) {
element.SetLeftMargin(left * Document.PtInMM);
}
public static void SetMarginsMM(this iText.Layout.Document element, float top, float right, float bottom, float left) {
element.SetMargins(top * Document.PtInMM, right * Document.PtInMM, bottom * Document.PtInMM, left * Document.PtInMM);
}
public static Table AddCells(this Table table, params Cell[] cells) {
foreach (var cell in cells)
table.AddCell(cell);
return table;
}
}
}
-34
View File
@@ -1,34 +0,0 @@
using RazorLight;
using System;
using System.Threading.Tasks;
namespace Elwig.Documents {
public static class Html {
private static RazorLightEngine? Engine = null;
public static bool IsReady => Engine != null;
public static async Task Init(Action evtHandler) {
var e = new RazorLightEngineBuilder()
.UseFileSystemProject(App.DataPath + "resources")
.UseMemoryCachingProvider()
.Build();
await e.CompileTemplateAsync("Document");
await e.CompileTemplateAsync("BusinessDocument");
await e.CompileTemplateAsync("BusinessLetter");
await e.CompileTemplateAsync("DeliveryNote");
await e.CompileTemplateAsync("CreditNote");
await e.CompileTemplateAsync("DeliveryJournal");
await e.CompileTemplateAsync("Letterhead");
Engine = e;
evtHandler();
}
public static async Task<string> CompileRenderAsync(string key, object model) {
if (Engine == null) throw new InvalidOperationException("The razor engine has not been initialized yet");
return await Engine.CompileRenderAsync(key, model);
}
}
}
+82
View File
@@ -0,0 +1,82 @@
using iText.IO.Font;
using iText.IO.Font.Otf;
using iText.Kernel.Font;
using iText.Layout.Element;
using iText.Layout.Properties;
using iText.Layout.Renderer;
using System;
namespace Elwig.Documents {
public class KernedParagraph : Paragraph {
public KernedParagraph(float fontSize) :
base() {
SetFontKerning(FontKerning.YES);
SetFixedLeading(fontSize);
SetFontSize(fontSize);
}
public KernedParagraph(string text, float fontSize) :
base(text) {
SetFontKerning(FontKerning.YES);
SetFixedLeading(fontSize);
SetFontSize(fontSize);
}
public KernedParagraph(Text text, float fontSize) :
base(text) {
SetFontKerning(FontKerning.YES);
SetFixedLeading(fontSize);
SetFontSize(fontSize);
}
public override KernedParagraph Add(ILeafElement element) {
if (element is Text t) {
t.SetFontKerning(FontKerning.YES);
t.SetNextRenderer(new KerningTextRenderer(t));
}
base.Add(element);
return this;
}
public override Paragraph Add(IBlockElement element) {
base.Add(element);
return this;
}
public class KerningTextRenderer(Text textElement) : TextRenderer(textElement) {
public override IRenderer GetNextRenderer() {
return new KerningTextRenderer((Text)modelElement);
}
public override void ApplyOtf() {
PdfFont font;
try {
font = GetPropertyAsFont(Property.FONT);
} catch (InvalidCastException) {
return;
}
if (strToBeConverted != null) {
SetProcessedGlyphLineAndFont(TextPreprocessingUtil.ReplaceSpecialWhitespaceGlyphs(font.CreateGlyphLine(strToBeConverted), font), font);
}
if (otfFeaturesApplied || text.GetStart() >= text.GetEnd()) {
return;
}
if (GetProperty(Property.FONT_KERNING, (FontKerning?)FontKerning.NO) == FontKerning.YES) {
ApplyKerning(font.GetFontProgram(), text);
}
otfFeaturesApplied = true;
}
private static void ApplyKerning(FontProgram font, GlyphLine text) {
for (int i = 1; i < text.Size(); i++) {
var kerning = font.GetKerning(text.Get(i - 1), text.Get(i));
if (kerning != 0) {
text.Set(i - 1, new Glyph(text.Get(i - 1), 0, 0, kerning, 0, 0));
}
}
}
}
}
}
+21
View File
@@ -0,0 +1,21 @@
using Elwig.Models.Entities;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas;
using iText.Layout;
namespace Elwig.Documents {
public class Letterhead : BusinessDocument {
public Letterhead(Member m) :
base($"Briefkopf {m.FullName}", m, null, includeSender: true) {
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
// do not render anything except this
var page = pdf.AddNewPage();
var pageSize = page.GetPageSize();
var pdfCanvas = new PdfCanvas(page.NewContentStreamBefore(), page.GetResources(), pdf);
using var canvas = new Canvas(pdfCanvas, pageSize);
RenderAddress(canvas, pageSize);
}
}
}
-9
View File
@@ -1,9 +0,0 @@
@using RazorLight
@inherits TemplatePage<Elwig.Documents.Letterhead>
@model Elwig.Documents.Letterhead
@{ Layout = "BusinessDocument"; }
<style>
header, .footer-wrapper {
visibility: hidden;
}
</style>
-9
View File
@@ -1,9 +0,0 @@
using Elwig.Models;
namespace Elwig.Documents {
public class Letterhead : BusinessDocument {
public Letterhead(Member m) : base($"Briefkopf {m.Name}", m, true) {
Aside = "";
}
}
}
+366
View File
@@ -0,0 +1,366 @@
using Elwig.Helpers;
using Elwig.Models;
using Elwig.Models.Entities;
using iText.Kernel.Colors;
using iText.Kernel.Pdf;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Properties;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Elwig.Documents {
public class MemberDataSheet : BusinessDocument {
public new static string Name => "Stammdatenblatt";
public Season? Season;
public int MemberDeliveredWeightRed;
public int MemberDeliveredWeightWhite;
public List<MemberHistory> MemberHistory = [];
public Dictionary<string, MemberBucket> MemberBuckets = [];
public List<AreaCom> ActiveAreaCommitments = [];
public MemberDataSheet(Member m) :
base($"{Name} {m.AdministrativeName}", m, DateOnly.FromDateTime(m.ModifiedAt)) {
ShowDateAndLocation = true;
DocumentId = $"{Name} {m.MgNr}";
}
public static async Task<MemberDataSheet> Initialize(int mgnr) {
using var ctx = new AppDbContext();
return new MemberDataSheet(await ctx.FetchMembers(mgnr, includeContactInfo: true).SingleAsync());
}
protected override async Task LoadData(AppDbContext ctx) {
await base.LoadData(ctx);
Season = await ctx.FetchSeasons().FirstOrDefaultAsync() ?? throw new ArgumentException("Invalid season");
MemberHistory = await ctx.MemberHistory
.Where(h => h.FromMgNr == Member.MgNr || h.ToMgNr == Member.MgNr)
.OrderBy(h => h.DateNoticeString).ThenBy(h => h.HistNr)
.ToListAsync();
MemberBuckets = await ctx.GetMemberBuckets(Utils.CurrentYear, Member.MgNr);
ActiveAreaCommitments = await Member.ActiveAreaCommitments(ctx)
.Include(c => c.Contract).ThenInclude(c => c.Revisions)
.ToListAsync();
var weights = await ctx.Deliveries
.Where(d => d.Year == Season.Year && d.MgNr == Member.MgNr)
.SelectMany(d => d.Parts)
.GroupBy(p => p.Variety.Type)
.ToDictionaryAsync(g => g.Key, g => g.Sum(p => p.Weight));
MemberDeliveredWeightRed = weights.GetValueOrDefault("R", 0);
MemberDeliveredWeightWhite = weights.GetValueOrDefault("W", 0);
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
if (Season == null) throw new Exception("Call LoadData before RenderBody");
base.RenderBody(doc, pdf);
doc.Add(NewMemberData(Season).SetMarginBottomMM(5));
doc.Add(NewBucketTable(Season, MemberBuckets, MemberDeliveredWeightRed, MemberDeliveredWeightWhite, includeDelivery: false));
if (ActiveAreaCommitments.Count != 0) {
bool firstOnPage = false;
if (pdf.GetNumberOfPages() == 1) {
doc.Add(new AreaBreak(AreaBreakType.NEXT_PAGE));
firstOnPage = true;
}
doc.Add(new KernedParagraph(12).Add(Bold($"Flächenbindungen per {Date:dd.MM.yyyy}")).SetMargins(firstOnPage ? 0 : 24, 0, 6, 0));
doc.Add(NewAreaComTable(ActiveAreaCommitments));
}
if (App.Client.EnableMemberHistory) {
bool firstOnPage = false;
if (pdf.GetNumberOfPages() == 1) {
doc.Add(new AreaBreak(AreaBreakType.NEXT_PAGE));
firstOnPage = true;
}
doc.Add(new KernedParagraph(12).Add(Bold($"Verlauf der Geschäftsanteile per {Date:dd.MM.yyyy}")).SetMargins(firstOnPage ? 0 : 24, 0, 6, 0));
doc.Add(NewMemberBusinessSharesTable(MemberHistory));
}
}
protected Cell NewDataHdr(string title, int colspan) {
return NewTd(title, 10, colspan: colspan, bold: true, italic: true, center: true, borderTop: true)
.SetBackgroundColor(new DeviceRgb(0xe0, 0xe0, 0xe0));
}
protected Cell NewDataTh(string text, float fontSize = 10, int colspan = 1) {
return NewTd(text, fontSize, colspan: colspan, italic: true)
.SetPaddingRightMM(0);
}
protected Table NewMemberData(Season season) {
var tbl = new Table(ColsMM(30.0, 51.5, 20.0, 12.0, 18.0, 31.5))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetBorder(new SolidBorder(BorderThickness));
tbl.AddCell(NewDataHdr("Persönliche Daten", 6));
if (Member.IsJuridicalPerson) {
tbl
.AddCell(NewDataTh("Name", 8, colspan: 3))
.AddCell(NewDataTh("Zu Handen", 8, colspan: 3))
.AddCell(NewTd(Member.Name, 12, colspan: 3))
.AddCell(NewTd(Member.ForTheAttentionOf, 12, colspan: 3));
} else {
tbl
.AddCell(NewDataTh("Titel (vorangestellt)", 8))
.AddCell(NewDataTh("Vorname", 8))
.AddCell(NewDataTh("Nachname", 8, colspan: 3))
.AddCell(NewDataTh("Titel (nachgestellt)", 8))
.AddCell(NewTd(Member.Prefix, 12))
.AddCell(NewTd($"{Member.GivenName} {Member.MiddleName}", 12))
.AddCell(NewTd(Member.Name, 12, colspan: 3))
.AddCell(NewTd(Member.Suffix, 12));
}
tbl
.AddCell(NewDataTh("Mitglieds-Nr.:")).AddCell(NewTd($"{Member.MgNr}"))
.AddCell(NewDataTh(Member.IsJuridicalPerson ? "Gründungsjahr/-tag:" : "Geburtsjahr/-tag:", colspan: 2))
.AddCell(NewTd(string.Join('.', Member.Birthday?.Split('-')?.Reverse() ?? []), colspan: 2))
.AddCell(NewDataTh("Adresse:")).AddCell(NewTd(Member.Address, colspan: 5))
.AddCell(NewDataTh("PLZ/Ort:"))
.AddCell(NewTd($"{Member.PostalDest.AtPlz?.Plz} {Member.PostalDest.AtPlz?.Dest} ({Member.PostalDest.AtPlz?.Ort.Name})", colspan: 5))
.AddCell(NewDataHdr("Rechnungsadresse (optional)", colspan: 6))
.AddCell(NewDataTh("Name:")).AddCell(NewTd(Member.BillingAddress?.FullName, colspan: 5))
.AddCell(NewDataTh("Adresse:")).AddCell(NewTd(Member.BillingAddress?.Address, colspan: 5))
.AddCell(NewDataTh("PLZ/Ort:"))
.AddCell(NewTd(Member.BillingAddress != null ? $"{Member.BillingAddress.PostalDest.AtPlz?.Plz} {Member.BillingAddress.PostalDest.AtPlz?.Dest} ({Member.BillingAddress.PostalDest.AtPlz?.Ort.Name})" : "", colspan: 5));
tbl.AddCell(NewDataHdr("Kontaktdaten", colspan: 3))
.AddCell(NewDataHdr("Bankverbindung", colspan: 3).SetBorderLeft(new SolidBorder(BorderThickness)));
List<string?[]> subTbl1 = [
.. Member.EmailAddresses.Select(a => new[] { "E-Mail-Adresse", a.Address }),
.. Member.TelephoneNumbers.Select(n => new[] { Utils.PhoneNrTypeToString(n.Type), n.Number, n.Comment }),
["Tel.-Nr./E-Mail-Adr.", null],
];
List<string?[]> subTbl2 = [
["IBAN", Member.Iban != null ? Utils.FormatIban(Member.Iban) : null],
["BIC", Member.Bic],
];
for (int i = 0; i < Math.Max(subTbl1.Count, subTbl2.Count); i++) {
tbl.AddCell(NewDataTh(i < subTbl1.Count ? subTbl1[i][0] + ":" : ""));
if (i < subTbl1.Count && subTbl1[i].Length >= 3 && subTbl1[i][2] != null) {
tbl.AddCell(NewTd(subTbl1[i][1])).AddCell(NewTd($"({subTbl1[i][2]})"));
} else {
tbl.AddCell(NewTd(i < subTbl1.Count ? subTbl1[i][1] : "", colspan: 2));
}
tbl.AddCell(NewDataTh(i < subTbl2.Count ? subTbl2[i][0] + ":" : "").SetBorderLeft(new SolidBorder(BorderThickness)))
.AddCell(NewTd(i < subTbl2.Count ? subTbl2[i][1] : "", colspan: 2));
}
var shares = (Member.Shares != 0 || (Member.SharesRed == 0 && Member.SharesWhite == 0) ? $"{Member.Shares:N0}" : "") +
(Member.SharesRed != 0 || Member.SharesWhite != 0 ? (Member.Shares != 0 ? " / " : "") + $"{Member.SharesRed:N0} (rot) / {Member.SharesWhite:N0} (weiß)" : "") +
(Member.SharesDormant != 0 ? $" / {Member.SharesDormant:N0} (ruhend)" : "");
tbl.AddCell(NewDataHdr("Betrieb", colspan: 6))
.AddCell(NewDataTh("Betriebs-Nr.:")).AddCell(NewTd(Member.LfbisNr))
.AddCell(NewDataTh("UID:", colspan: 2)).AddCell(NewTd(Member.UstIdNr, colspan: 2))
.AddCell(NewDataTh("Stammgemeinde:")).AddCell(NewTd(Member.DefaultKg?.Name))
.AddCell(NewDataTh("Buchführend:", colspan: 2)).AddCell(NewTd(new KernedParagraph(Member.IsBuchführend ? "Ja " : "Nein ", 10)
.Add(Normal($"({(Member.IsBuchführend ? season.VatNormal : season.VatFlatrate) * 100:N0}% USt.)", 8)), colspan: 2))
.AddCell(NewDataTh("(Katastralgemeinde mit dem größten Anteil an Weinbauflächen)", 8, colspan: 2))
.AddCell(NewDataTh("Bio:", colspan: 2)).AddCell(NewTd(new KernedParagraph(Member.IsOrganic ? "Ja" : "Nein", 10)
.Add(Normal(Member.OrganicAuthorityCode != null || Member.OrganicOperatorId != null ? $" ({Member.OrganicAuthorityCode}{(Member.OrganicAuthorityCode != null && Member.OrganicOperatorId != null ? ", " : "")}{Member.OrganicOperatorId})" : "", 8)), colspan: 2))
.AddCell(NewDataHdr("Genossenschaft", colspan: 6))
.AddCell(NewDataTh("Status:")).AddCell(NewTd(new KernedParagraph(Member.IsActive ? "Aktiv " : "Nicht aktiv ", 10)
.Add(Normal("(" + (Member.ExitDate != null ? $"{Member.EntryDate:dd.MM.yyyy}\u2013{Member.ExitDate:dd.MM.yyyy}" : $"seit {Member.EntryDate:dd.MM.yyyy}") + ")", 8))))
.AddCell(NewDataTh("Geschäftsanteile:", colspan: 2)).AddCell(NewTd(shares, colspan: 2))
.AddCell(NewDataTh("Stamm-Zweigstelle:")).AddCell(NewTd(Member.Branch?.Name))
.AddCell(NewDataTh("Volllieferant:", colspan: 2)).AddCell(NewTd(Member.IsVollLieferant ? "Ja" : "Nein", colspan: 2))
.AddCell(NewDataTh("Zusendungen per\u2026")).AddCell(NewTd(new KernedParagraph(10)
.Add(Italic("Post:")).Add(Normal(Member.ContactViaPost ? " Ja \u2013 " : " Nein \u2013 "))
.Add(Italic("E-Mail:")).Add(Normal(Member.ContactViaEmail ? " Ja" : " Nein"))))
.AddCell(NewDataTh("Funktionär:", colspan: 2)).AddCell(NewTd(Member.IsFunktionär ? "Ja" : "Nein", colspan: 2));
return tbl;
}
protected Table NewAreaComTable(IEnumerable<AreaCom> activeAreaComs) {
var areaComs = activeAreaComs.GroupBy(a => a.AreaComType).Select(group => new {
Type = group.Key,
AreaComs = group.OrderBy(c => c.Contract.Kg.AtKg.Name).ToList(),
Size = group.Sum(c => c.Area)
}).OrderByDescending(a => a.Size).ToList();
var tbl = new Table(ColsMM(40, 30, 35, 15, 25, 20), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetBorder(Border.NO_BORDER)
.SetFontSize(10);
tbl.AddHeaderCell(NewTh("Katastralgemeinde", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Ried", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Parzelle(n)", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Fläche"))
.AddHeaderCell(NewTh("Bewirt.", rowspan: 2))
.AddHeaderCell(NewTh("Laufzeit", rowspan: 2))
.AddHeaderCell(NewTh("[m²]"));
var lastContract = "";
foreach (var contractType in areaComs) {
tbl.AddCell(NewCell(new KernedParagraph(10).Add(BoldItalic($"{contractType.Type.WineVar.Name} {(contractType.Type.WineAttr != null ? "(" + contractType.Type.WineAttr + ")" : "")}")), colspan: 3)
.SetBorderTop(contractType.Type.DisplayName != lastContract && lastContract != "" ? new SolidBorder(BorderThickness) : Border.NO_BORDER))
.AddCell(NewCell(new KernedParagraph(10).Add(Bold($"{contractType.Size:N0}")).SetTextAlignment(TextAlignment.RIGHT))
.SetBorderTop(contractType.Type.DisplayName != lastContract && lastContract != "" ? new SolidBorder(BorderThickness) : Border.NO_BORDER))
.AddCell(NewCell(colspan: 2)
.SetBorderTop(contractType.Type.DisplayName != lastContract && lastContract != "" ? new SolidBorder(BorderThickness) : Border.NO_BORDER));
foreach (var areaCom in contractType.AreaComs) {
var c = areaCom.Contract;
tbl.AddCell(NewTd(new KernedParagraph(10).Add(Normal($"{c.Kg.AtKg.Name} ")).Add(Normal($"({c.Kg.AtKg.KgNr:00000})", 8))))
.AddCell(NewTd(c.Rd?.Name))
.AddCell(NewTd(Regex.Replace(areaCom.GstNr.Replace(",", ", ").Replace("-", "\u2013"), @"\s+", " "), 10))
.AddCell(NewTd($"{areaCom.Area:N0}", right: true))
.AddCell(NewTd(areaCom.WineCult?.Name, center: true))
.AddCell(NewTd(c.YearTo == null ? (c.YearFrom == null ? "unbefristet" : $"ab {c.YearFrom}") : (c.YearFrom == null ? $"bis {c.YearTo}" : $"{c.YearFrom}–{c.YearTo}"), center: true));
lastContract = contractType.Type.DisplayName;
}
}
tbl.AddCell(NewTd("Gesamt:", 12, colspan: 2, bold: true, borderTop: true).SetPaddingsMM(1, 1, 1, 1));
tbl.AddCell(NewTd($"{activeAreaComs.Sum(a => a.Area):N0}", 12, colspan: 2, right: true, bold: true, borderTop: true).SetPaddingsMM(1, 1, 1, 1));
tbl.AddCell(NewTd(colspan: 2, borderTop: true).SetPaddingsMM(1, 1, 1, 1));
return tbl;
}
protected Cell[] NewBusinessSharesCells(MemberHistoryPoint p, bool isDiff = true) {
if (App.Client.HasRedWhite) {
return [
NewTd(isDiff ? (p.SharesRed == 0 ? "" : p.SharesRed < 0 ? $"–{-p.SharesRed:N0}" : $"+{p.SharesRed:N0}") : $"{p.SharesRed:N0}",
right: true, bold: !isDiff, borderTop: !isDiff),
NewTd(isDiff ? (p.SharesWhite == 0 ? "" : p.SharesWhite < 0 ? $"–{-p.SharesWhite:N0}" : $"+{p.SharesWhite:N0}") : $"{p.SharesWhite:N0}",
right: true, bold: !isDiff, borderTop: !isDiff),
NewTd(isDiff ? (p.SharesDormant == 0 ? "" : p.SharesDormant < 0 ? $"–{-p.SharesDormant:N0}" : $"+{p.SharesDormant:N0}") : $"{p.SharesDormant:N0}",
right: true, bold: !isDiff, borderTop: !isDiff),
];
} else {
return [
NewTd(isDiff ? (p.Shares == 0 ? "" : p.Shares < 0 ? $"–{-p.Shares:N0}" : $"+{p.Shares:N0}") : $"{p.Shares:N0}",
right: true, bold: !isDiff, borderTop: !isDiff),
NewTd(isDiff ? (p.SharesDormant == 0 ? "" : p.SharesDormant < 0 ? $"–{-p.SharesDormant:N0}" : $"+{p.SharesDormant:N0}") : $"{p.SharesDormant:N0}",
right : true, bold: !isDiff, borderTop: !isDiff),
];
}
}
protected Table NewMemberBusinessSharesTable(IEnumerable<MemberHistory> history) {
var tbl = new Table(ColsMM(18, 93, App.Client.HasRedWhite ? 18 : 12, App.Client.HasRedWhite ? 12 : 18, 12, 12), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetBorder(Border.NO_BORDER)
.SetFontSize(10);
var tw = App.Client.HasRedWhite ? 1 : 2;
tbl.AddHeaderCell(NewTh("Meldung am", rowspan: 2))
.AddHeaderCell(NewTh("Text", rowspan: 2, colspan: tw, left: true))
.AddHeaderCell(NewTh("Wirksam ab", rowspan: 2))
.AddHeaderCell(NewTh("Geschäftsanteile", colspan: App.Client.HasRedWhite ? 3 : 2));
if (App.Client.HasRedWhite) {
tbl.AddHeaderCell(NewTh("rot"))
.AddHeaderCell(NewTh("weiß"))
.AddHeaderCell(NewTh("ruhend"));
} else {
tbl.AddHeaderCell(NewTh("normal"))
.AddHeaderCell(NewTh("ruhend"));
}
if (!history.Any() || Member.EntryDate <= history.First().DateNotice) {
tbl.AddCell(NewTd($"{Member.EntryDate:dd.MM.yyyy}"))
.AddCell(NewTd("Eintritt", colspan: tw, bold: true))
.AddCell(NewTd($"{Member.EntryDate:dd.MM.yyyy}"))
.AddCells(NewBusinessSharesCells(new()));
}
var recorded = history.Aggregate(new MemberHistoryPoint(), (s, h) => new(
s.Shares + (h.ToMgNr == Member.MgNr && h.ToType == BusinessShareType.Normal ? h.Shares : 0) - (h.FromMgNr == Member.MgNr && h.FromType == BusinessShareType.Normal ? h.Shares : 0),
s.SharesRed + (h.ToMgNr == Member.MgNr && h.ToType == BusinessShareType.Red ? h.Shares : 0) - (h.FromMgNr == Member.MgNr && h.FromType == BusinessShareType.Red ? h.Shares : 0),
s.SharesWhite + (h.ToMgNr == Member.MgNr && h.ToType == BusinessShareType.White ? h.Shares : 0) - (h.FromMgNr == Member.MgNr && h.FromType == BusinessShareType.White ? h.Shares : 0),
s.SharesDormant + (h.ToMgNr == Member.MgNr && h.ToType == BusinessShareType.Dormant ? h.Shares : 0) - (h.FromMgNr == Member.MgNr && h.FromType == BusinessShareType.Dormant ? h.Shares : 0)));
var cur = new MemberHistoryPoint(Member.Shares - recorded.Shares, Member.SharesRed - recorded.SharesRed, Member.SharesWhite - recorded.SharesWhite, Member.SharesDormant - recorded.SharesDormant);
if (cur.Shares != 0 || cur.SharesRed != 0 || cur.SharesWhite != 0 || cur.SharesDormant != 0) {
tbl.AddCell(NewTd())
.AddCell(NewTd("Nicht in Software erfasst", colspan: tw, italic: true))
.AddCell(NewTd())
.AddCells(NewBusinessSharesCells(cur));
if (history.Any()) {
tbl.AddCell(NewTd(borderTop: true))
.AddCell(NewTd($"Bis Lese {history.First().DateNotice.Year - 1}", borderTop: true, colspan: tw, bold: true))
.AddCell(NewTd(borderTop: true))
.AddCells(NewBusinessSharesCells(cur, isDiff: false));
}
}
DateOnly? lastDate = null;
foreach (var row in history) {
int s = 0, r = 0, w = 0, d = 0;
if (row.FromMgNr == Member.MgNr) {
switch (row.FromType) {
case BusinessShareType.Normal: s -= row.Shares; break;
case BusinessShareType.Red: r -= row.Shares; break;
case BusinessShareType.White: w -= row.Shares; break;
case BusinessShareType.Dormant: d -= row.Shares; break;
}
}
if (row.ToMgNr == Member.MgNr) {
switch (row.ToType) {
case BusinessShareType.Normal: s += row.Shares; break;
case BusinessShareType.Red: r += row.Shares; break;
case BusinessShareType.White: w += row.Shares; break;
case BusinessShareType.Dormant: d += row.Shares; break;
}
}
if (lastDate < Member.EntryDate && row.DateNotice <= Member.EntryDate) {
tbl.AddCell(NewTd($"{Member.EntryDate:dd.MM.yyyy}"))
.AddCell(NewTd("Eintritt", colspan: tw, bold: true))
.AddCell(NewTd($"{Member.EntryDate:dd.MM.yyyy}"))
.AddCells(NewBusinessSharesCells(new()));
}
if (lastDate != null && row.DateNotice.Year != lastDate?.Year) {
tbl.AddCell(NewTd(borderTop: true))
.AddCell(NewTd(lastDate?.Year == row.DateNotice.Year - 1 ? $"Lese {lastDate?.Year}" : $"Lese {lastDate?.Year}–{row.DateNotice.Year - 1}", borderTop: true, colspan: tw, bold: true))
.AddCell(NewTd(borderTop: true))
.AddCells(NewBusinessSharesCells(cur, isDiff: false));
}
if (lastDate < Member.ExitDate && row.DateNotice <= Member.ExitDate) {
tbl.AddCell(NewTd($"{Member.ExitDate:dd.MM.yyyy}"))
.AddCell(NewTd("Austritt", colspan: tw, bold: true))
.AddCell(NewTd($"31.12.{Member.ExitDate.Value.Year + 1}")) // FIXME
.AddCells(NewBusinessSharesCells(new()));
}
cur = new(cur.Shares + s, cur.SharesRed + r, cur.SharesWhite + w, cur.SharesDormant + d);
tbl.AddCell(NewTd($"{row.DateNotice:dd.MM.yyyy}"))
.AddCell(NewTd(row.FormatReasonPhrase(Member.MgNr), colspan: tw))
.AddCell(NewTd($"{row.DateEffective:dd.MM.yyyy}"))
.AddCells(NewBusinessSharesCells(new(s, r, w, d)));
lastDate = row.DateNotice;
}
if (Member.EntryDate != null && history.Any() && Member.EntryDate > lastDate) {
tbl.AddCell(NewTd($"{Member.EntryDate:dd.MM.yyyy}"))
.AddCell(NewTd("Eintritt", colspan: tw, bold: true))
.AddCell(NewTd($"{Member.EntryDate:dd.MM.yyyy}"))
.AddCells(NewBusinessSharesCells(new()));
}
if (Member.ExitDate != null && (!history.Any() || Member.ExitDate > lastDate)) {
tbl.AddCell(NewTd($"{Member.ExitDate:dd.MM.yyyy}"))
.AddCell(NewTd("Austritt", colspan: tw, bold: true))
.AddCell(NewTd($"31.12.{Member.ExitDate.Value.Year + 1}")) // FIXME
.AddCells(NewBusinessSharesCells(new()));
}
tbl.AddCell(NewTd(borderTop: true))
.AddCell(NewTd(lastDate == null ? "Aktuell" : $"Ab Lese {lastDate?.Year}", borderTop: true, colspan: tw, bold: true))
.AddCell(NewTd(borderTop: true))
.AddCells(NewBusinessSharesCells(cur, isDiff: false));
return tbl;
}
}
}
+111
View File
@@ -0,0 +1,111 @@
using Elwig.Models.Dtos;
using iText.Kernel.Colors;
using iText.Kernel.Pdf;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Properties;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Documents {
public class MemberList : Document {
public new static string Name => "Mitgliederliste";
public string Filter;
public List<MemberListRow> Members;
public string[] AreaComFilters;
public bool FilterAreaComs => AreaComFilters.Length > 0;
public MemberList(string filter, IEnumerable<MemberListRow> members) :
base(Name) {
Filter = filter;
Members = [.. members];
AreaComFilters = [..members
.SelectMany(m => m.AreaCommitmentsFiltered)
.Select(c => c.VtrgId)
.Distinct()
.Order()];
}
public MemberList(string filter, MemberListData data) :
this(filter, data.Rows) {
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.RenderBody(doc, pdf);
doc.Add(new KernedParagraph(Name, 24)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 2, 0));
doc.Add(new KernedParagraph(Filter, 14)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 2, 0));
doc.Add(NewMemberTable(Members));
}
protected Table NewMemberTable(List<MemberListRow> members) {
var tbl = new Table(AreaComFilters.Length > 1 ? ColsMM(8, 38, 36, 8, 18, 12, 5, 16, 12, 12) : ColsMM(8, 42, 40, 8, 20, 12, 5, 18, 12), true)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE);
var headerSpan = FilterAreaComs ? 3 : 2;
tbl.AddHeaderCell(NewTh("Nr.", rowspan: headerSpan))
.AddHeaderCell(NewTh("Name", rowspan: headerSpan, left: true))
.AddHeaderCell(NewTh("Adresse", rowspan: headerSpan, left: true))
.AddHeaderCell(NewTh("PLZ", rowspan: headerSpan))
.AddHeaderCell(NewTh("Ort", rowspan: headerSpan, left: true))
.AddHeaderCell(NewTh("Betr.-Nr.", rowspan: headerSpan))
.AddHeaderCell(NewTh("GA", rowspan: headerSpan).SetPaddingLeft(0).SetPaddingRight(0))
.AddHeaderCell(NewTh("Stamm-KG", rowspan: headerSpan, left: true))
.AddHeaderCell(NewTh("Geb. Fl.", colspan: FilterAreaComs ? AreaComFilters.Length : 1));
if (FilterAreaComs) {
foreach (var vtrgId in AreaComFilters) {
tbl.AddHeaderCell(NewTh(vtrgId));
}
}
for (int i = 0; i < (FilterAreaComs ? AreaComFilters.Length : 1); i++) {
tbl.AddHeaderCell(NewTh("[m²]"));
}
string? lastBranch = members.Select(m => m.Branch).Distinct().Count() == 1 ? null : "";
foreach (var m in members) {
if (lastBranch != null && m.Branch != lastBranch) {
tbl.AddCell(NewCell(colspan: 8 + Math.Max(AreaComFilters.Length, 1)).SetHeightMM(5).SetKeepWithNext(true));
tbl.AddCell(NewCell(new KernedParagraph(m.Branch ?? "", 16).SetFont(BF), colspan: 8 + Math.Max(AreaComFilters.Length, 1))
.SetPaddingsMM(1, 2, 1, 2)
.SetBorder(new SolidBorder(BorderThickness))
.SetBackgroundColor(new DeviceRgb(0xe0, 0xe0, 0xe0)));
lastBranch = m.Branch;
}
tbl.AddCell(NewTd($"{m.MgNr}", 8, rowspan: m.BillingName != null ? 2 : 1, right: true).SetVerticalAlignment(VerticalAlignment.TOP))
.AddCell(NewTd($"{m.AdminName1} {m.Name2}", 8))
.AddCell(NewTd(m.Address, 8))
.AddCell(NewTd($"{m.Plz}", 8))
.AddCell(NewTd(m.Locality, 6))
.AddCell(NewTd(m.LfbisNr ?? "", 8))
.AddCell(NewTd($"{m.SharesActive:N0}", 8, right: true).SetPaddingLeft(0).SetPaddingRight(0))
.AddCell(NewTd(m.DefaultKg ?? "", 6));
if (AreaComFilters.Length > 0) {
foreach (var v in AreaComFilters) {
tbl.AddCell(NewTd($"{m.AreaCommitmentsFiltered.FirstOrDefault(c => c.VtrgId == v).Area:N0}", 8, right: true));
}
} else {
tbl.AddCell(NewTd($"{m.AreaCommitment:N0}", 8, right: true));
}
if (m.BillingName != null) {
tbl.AddCell(NewTd(m.BillingName, 8))
.AddCell(NewTd(m.BillingAddress ?? "", 8))
.AddCell(NewTd($"{m.BillingPlz}", 8))
.AddCell(NewTd(m.BillingLocality ?? "", 6))
.AddCell(NewTd(colspan: 3 + Math.Max(AreaComFilters.Length, 1)));
}
}
return tbl;
}
}
}
+316
View File
@@ -0,0 +1,316 @@
using Elwig.Helpers;
using Elwig.Helpers.Billing;
using Elwig.Models.Dtos;
using Elwig.Models.Entities;
using iText.Kernel.Colors;
using iText.Kernel.Pdf;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Properties;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Elwig.Documents {
public class PaymentVariantSummary : Document {
public new static string Name => "Auszahlungsvariante";
public PaymentVariantSummaryData? Data;
public PaymentVar Variant;
public BillingData BillingData;
public string CurrencySymbol;
public int MemberNum;
public int DeliveryNum;
public int DeliveryPartNum;
public List<ModifierStat>? ModifierStat;
public Dictionary<string, Modifier>? Modifiers;
private List<Credit> _credits = [];
private List<PaymentDeliveryPart> _parts = [];
public PaymentVariantSummary(PaymentVar v, PaymentVariantSummaryData? data = null) :
base($"{Name} {v.Year} - {v.Name}") {
Variant = v;
BillingData = BillingData.FromJson(v.Data);
Data = data;
CurrencySymbol = v.Season.Currency.Symbol ?? v.Season.Currency.Code;
}
public static async Task<PaymentVariantSummary> Initialize(int year, int avnr, PaymentVariantSummaryData? data = null) {
using var ctx = new AppDbContext();
var v = await ctx.PaymentVariants
.Where(v => v.Year == year && v.AvNr == avnr)
.SingleAsync();
return new PaymentVariantSummary(v, data);
}
protected override async Task LoadData(AppDbContext ctx) {
_credits = await ctx.Credits.Where(c => c.Year == Variant.Year && c.AvNr == Variant.AvNr).ToListAsync();
_parts = await ctx.PaymentDeliveryParts.Where(p => p.Year == Variant.Year && p.AvNr == Variant.AvNr).ToListAsync();
MemberNum = _credits.Count;
IsPreview = MemberNum == 0;
DeliveryNum = await ctx.Deliveries.Where(d => d.Year == Variant.Year).CountAsync();
DeliveryPartNum = await ctx.DeliveryParts.Where(d => d.Year == Variant.Year).CountAsync();
Data ??= await PaymentVariantSummaryData.ForPaymentVariant(Variant, ctx.PaymentVariantSummaryRows);
ModifierStat = await AppDbContext.GetModifierStats(Variant.Year, Variant.AvNr);
Modifiers = await ctx.FetchModifiers(Variant.Year).ToDictionaryAsync(m => m.ModId);
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
if (Data == null || Modifiers == null || ModifierStat == null) throw new Exception("Call LoadData before RenderBody");
base.RenderBody(doc, pdf);
doc.Add(new KernedParagraph($"{Name} Lese {Variant.Year}", 24)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 2, 0));
doc.Add(new KernedParagraph(Variant.Name, 14)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginsMM(0, 0, 10, 0));
doc.Add(NewVariantStatTable(Data).SetMarginBottomMM(10));
doc.Add(NewModifierStatTable(Modifiers, ModifierStat));
doc.Add(new AreaBreak(AreaBreakType.NEXT_PAGE));
doc.Add(NewPriceTable(Data));
}
protected Cell NewSectionHdr(string text, int colspan = 1, bool borderLeft = false) {
return NewTd(text, 10, colspan: colspan, bold: true, italic: true, center: true, borderTop: true)
.SetBackgroundColor(new DeviceRgb(0xe0, 0xe0, 0xe0))
.SetPaddingsMM(0.5f, 1, 0.5f, 1)
.SetBorderLeft(borderLeft ? new SolidBorder(BorderThickness) : Border.NO_BORDER);
}
protected Cell NewSectionTh(string? text = null, float fontSize = 10, int colspan = 1, bool borderTop = false, bool borderLeft = false, bool overflow = false) {
return NewTd(text, fontSize, colspan: colspan, italic: true, borderTop: borderTop, overflow: overflow)
.SetPaddingRightMM(0)
.SetBorderLeft(borderLeft ? new SolidBorder(BorderThickness) : Border.NO_BORDER);
}
protected Table NewVariantStatTable(PaymentVariantSummaryData data) {
var tbl = new Table(ColsMM(20, 30, 4.5, 4.5, 23.5, 47.5, 15, 20))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetBorder(new SolidBorder(BorderThickness));
//var sum1 = _parts.Sum(p => p.NetAmount);
//var sum2 = _credits.Sum(p => p.); //Variant.MemberPayments.Sum(p => p.Amount);
var deliveryModifiers = _parts.Sum(p => p.Amount - p.NetAmount);
var memberModifiers = _credits.Sum(c => c.Payment.Amount - c.Payment.NetAmount);
var sum2 = _credits.Sum(p => p.NetAmount);
var sum1 = sum2 - deliveryModifiers - memberModifiers;
var payed = -_credits.Sum(p => p.PrevNetAmount ?? 0m);
var netSum = _credits.Sum(p => p.NetAmount) - _credits.Sum(p => p.PrevNetAmount ?? 0m);
var vat = _credits.Sum(p => p.VatAmount);
var grossSum = _credits.Sum(p => p.GrossAmount);
var totalMods = _credits.Sum(p => p.Modifiers ?? 0m);
var considered = -_credits.Sum(p => p.PrevModifiers ?? 0m);
var totalSum = _credits.Sum(p => p.Amount);
var weiRows = data.Rows.Where(r => r.QualityLevel == "Wein");
var minWei = weiRows.Min(r => r.Ungeb.MinPrice);
var maxWei = weiRows.Max(r => r.Ungeb.MaxPrice);
var quwRows = data.Rows.Where(r => r.QualityLevel != "Wein");
var minPrice = quwRows.Min(r => r.Ungeb.MinPrice);
var maxPrice = quwRows.Max(r => r.Ungeb.MaxPrice);
var gebRows = data.Rows
.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();
tbl.AddCell(NewSectionHdr("Allgemein", colspan: 5))
.AddCell(NewSectionHdr("Berücksichtigt", colspan: 3, borderLeft: true))
.AddCell(NewSectionTh("Name:"))
.AddCell(NewTd(Variant.Name, colspan: 4))
.AddCell(NewSectionTh("Zu-/Abschläge bei Lieferungen:", colspan: 2, borderLeft: true))
.AddCell(NewTd(BillingData.ConsiderDelieryModifiers ? "Ja" : "Nein", center: true))
.AddCell(NewSectionTh("Beschr.:"))
.AddCell(NewTd(Variant.Comment, colspan: 4))
.AddCell(NewSectionTh("Pönalen bei Unterlieferungen (FB):", colspan: 2, borderLeft: true))
.AddCell(NewTd(BillingData.ConsiderContractPenalties ? "Ja" : "Nein", center: true))
.AddCell(NewSectionTh("Rebel-Zuschl.:", overflow: true))
.AddCell(NewTd($"{Utils.GetSign(BillingData.NetWeightModifier)}{Math.Abs(BillingData.NetWeightModifier) * 100:N2} % / {Utils.GetSign(BillingData.GrossWeightModifier)}{Math.Abs(BillingData.GrossWeightModifier) * 100:N2} %", colspan: 4, center: true))
.AddCell(NewSectionTh("Strafen bei Unterlieferungen (GA):", colspan: 2, borderLeft: true))
.AddCell(NewTd(BillingData.ConsiderTotalPenalty ? "Ja" : "Nein", center: true))
.AddCell(NewSectionTh("Datum/Überw.:", overflow: true))
.AddCell(NewTd($"{Variant.Date:dd.MM.yyyy} / {Variant.TransferDate:dd.MM.yyyy}", colspan: 4, center: true))
.AddCell(NewSectionTh("(Automatische) Nachzeichnung von GA:", colspan: 2, borderLeft: true))
.AddCell(NewTd(BillingData.ConsiderAutoBusinessShares ? "Ja" : "Nein", center: true))
.AddCell(NewSectionTh("Berechnung:"))
.AddCell(NewTd($"{Variant.CalcTime:dd.MM.yyyy, HH:mm:ss}", colspan: 4, center: true))
.AddCell(NewSectionTh("Benutzerdef. Zu-/Abschläge pro Mitglied:", colspan: 2, borderLeft: true))
.AddCell(NewTd(BillingData.ConsiderCustomModifiers ? "Ja" : "Nein", center: true))
.AddCell(NewSectionHdr("Beträge", colspan: 5))
.AddCell(NewSectionHdr("Statistik", colspan: 3, borderLeft: true))
.AddCell(NewSectionTh("Zwischensumme:", colspan: 2))
.AddCell(NewTd())
.AddCell(NewTd(CurrencySymbol))
.AddCell(NewTd($"{sum1:N2}", right: true))
.AddCell(NewSectionTh("Lieferanten:", borderLeft: true))
.AddCell(NewTd($"{MemberNum:N0}", colspan: 2, right: true))
.AddCell(NewSectionTh("Zu-/Abschläge (Mitglieder):", colspan: 2))
.AddCell(NewTd(Utils.GetSign(memberModifiers), right: true))
.AddCell(NewTd(CurrencySymbol))
.AddCell(NewTd($"{Math.Abs(memberModifiers):N2}", right: true))
.AddCell(NewSectionTh("Lieferungen:", borderLeft: true))
.AddCell(NewTd($"{DeliveryNum:N0}", colspan: 2, right: true))
.AddCell(NewSectionTh("Zu-/Abschläge (Lieferungen):", colspan: 2))
.AddCell(NewTd(Utils.GetSign(deliveryModifiers), right: true))
.AddCell(NewTd(CurrencySymbol))
.AddCell(NewTd($"{Math.Abs(deliveryModifiers):N2}", right: true))
.AddCell(NewSectionTh("Teillieferungen:", borderLeft: true))
.AddCell(NewTd($"{DeliveryPartNum:N0}", colspan: 2, right: true))
.AddCell(NewSectionTh("Gesamtsumme:", colspan: 2))
.AddCell(NewTd(borderTop: true))
.AddCell(NewTd(CurrencySymbol, borderTop: true))
.AddCell(NewTd($"{sum2:N2}", right: true, borderTop: true))
.AddCell(NewSectionTh(borderLeft: true))
.AddCell(NewTd(colspan: 2))
.AddCell(NewSectionTh("Bisher ausgezahlt:", colspan: 2))
.AddCell(NewTd(Utils.GetSign(payed), right: true))
.AddCell(NewTd(CurrencySymbol))
.AddCell(NewTd($"{Math.Abs(payed):N2}", right: true))
.AddCell(NewSectionTh("Preis (abgewertet):", borderTop: true, borderLeft: true))
.AddCell(NewTd(minWei == null && maxWei == null ? "-" : (minWei != maxWei ? $"{minWei:N4}\u2013{maxWei:N4}" : $"{minWei:N4}") + $" {CurrencySymbol}/kg", colspan: 2, center: true, borderTop: true))
.AddCell(NewSectionTh("Nettosumme:", colspan: 2))
.AddCell(NewTd(borderTop: true))
.AddCell(NewTd(CurrencySymbol, borderTop: true))
.AddCell(NewTd($"{netSum:N2}", right: true, borderTop: true))
.AddCell(NewSectionTh("Preis (ungeb., nicht abgew.):", borderLeft: true))
.AddCell(NewTd(minPrice == null && maxPrice == null ? "-" : (minPrice != maxPrice ? $"{minPrice:N4}–{maxPrice:N4}" : $"{minPrice:N4}") + $" {CurrencySymbol}/kg", colspan: 2, center: true))
.AddCell(NewSectionTh("Mehrwertsteuer:", colspan: 2))
.AddCell(NewTd(Utils.GetSign(vat), right: true))
.AddCell(NewTd(CurrencySymbol))
.AddCell(NewTd($"{Math.Abs(vat):N2}", right: true))
.AddCell(NewSectionTh("Gebunden-Zuschlag:", borderLeft: true))
.AddCell(NewTd(minGeb == null && maxGeb == null ? "-" : minGeb != maxGeb ? $"{minGeb:N4}\u2013{maxGeb:N4} {CurrencySymbol}/kg" : minGeb == 0 ? "-" : $"{minGeb:N4} {CurrencySymbol}/kg", colspan: 2, center: true))
.AddCell(NewSectionTh("Bruttosumme:", colspan: 2))
.AddCell(NewTd(borderTop: true))
.AddCell(NewTd(CurrencySymbol, borderTop: true))
.AddCell(NewTd($"{grossSum:N2}", right: true, borderTop: true))
.AddCell(NewSectionTh(borderLeft: true))
.AddCell(NewTd(colspan: 2))
.AddCell(NewSectionTh("Abzüge (Strafen/Pönalen, GA, \u2026):", colspan: 2))
.AddCell(NewTd(Utils.GetSign(totalMods)))
.AddCell(NewTd(CurrencySymbol))
.AddCell(NewTd($"{Math.Abs(totalMods):N2}", right: true))
.AddCell(NewSectionTh("Menge (ungebunden):", borderLeft: true, borderTop: true))
.AddCell(NewTd($"{data.Rows.Sum(r => r.Ungeb.Weight):N0} kg", colspan: 2, right: true, borderTop: true))
.AddCell(NewSectionTh("Bereits berücksichtigte Abzüge:", colspan: 2))
.AddCell(NewTd(Utils.GetSign(considered)))
.AddCell(NewTd(CurrencySymbol))
.AddCell(NewTd($"{Math.Abs(considered):N2}", right: true))
.AddCell(NewSectionTh("Menge (gebunden):", borderLeft: true))
.AddCell(NewTd($"{data.Rows.Sum(r => r.Geb.Weight + r.LowGeb.Weight):N0} kg", colspan: 2, right: true))
.AddCell(NewSectionTh("Auszahlungsbetrag:", colspan: 2))
.AddCell(NewTd(borderTop: true))
.AddCell(NewTd(CurrencySymbol, borderTop: true))
.AddCell(NewTd($"{totalSum:N2}", right: true, borderTop: true))
.AddCell(NewSectionTh("Gesamtmenge:", borderLeft: true))
.AddCell(NewTd($"{data.Rows.Sum(r => r.Ungeb.Weight + r.LowGeb.Weight + r.Geb.Weight):N0} kg", colspan: 2, right: true, borderTop: true));
return tbl;
}
protected Table NewModifierStatTable(Dictionary<string, Modifier> modifiers, IEnumerable<ModifierStat> modStat) {
var tbl = new Table(ColsMM(35, 30, 25, 25, 25, 25))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetBorder(new SolidBorder(BorderThickness));
tbl.AddCell(NewSectionHdr("Statistik Zu-/Abschläge", colspan: 6))
.AddCell(NewTh("Name", rowspan: 2))
.AddCell(NewTh("Zu-/Abschlag", rowspan: 2))
.AddCell(NewTh("Lieferungen"))
.AddCell(NewTh("Minimum"))
.AddCell(NewTh("Maximum"))
.AddCell(NewTh("Betrag"))
.AddCell(NewTh("[#]"))
.AddCell(NewTh($"[{CurrencySymbol}]"))
.AddCell(NewTh($"[{CurrencySymbol}]"))
.AddCell(NewTh($"[{CurrencySymbol}]"));
foreach (var m in modStat) {
var mod = modifiers[m.ModId];
tbl.AddCell(NewTd(mod.Name, italic: true))
.AddCell(NewTd(mod.ValueStr, right: true))
.AddCell(NewTd($"{m.Count:N0}", right: true))
.AddCell(NewTd($"{m.Min:N2}", right: true))
.AddCell(NewTd($"{m.Max:N2}", right: true))
.AddCell(NewTd($"{m.Sum:N2}", right: true));
}
return tbl;
}
protected Table NewPriceTable(PaymentVariantSummaryData data) {
var tbl = new Table(ColsMM(25, 19, 18, 15, 18, 15, 18, 15, 22))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE);
tbl.AddHeaderCell(NewTh("Sorte/Attr./Bewirt.\nQualitätsstufe", rowspan: 2, left: true))
.AddHeaderCell(NewTh("Gradation"))
.AddHeaderCell(NewTh("ungebunden", colspan: 2))
.AddHeaderCell(NewTh("attributlos gebunden", colspan: 2))
.AddHeaderCell(NewTh("gebunden", colspan: 2))
.AddHeaderCell(NewTh("Gesamt"))
.AddHeaderCell(NewTh(true ? "[°Oe]" : "[°KMW]"))
.AddHeaderCell(NewTh("[kg]"))
.AddHeaderCell(NewTh($"[{CurrencySymbol}/kg]"))
.AddHeaderCell(NewTh("[kg]"))
.AddHeaderCell(NewTh($"[{CurrencySymbol}/kg]"))
.AddHeaderCell(NewTh("[kg]"))
.AddHeaderCell(NewTh($"[{CurrencySymbol}/kg]"))
.AddHeaderCell(NewTh($"[{CurrencySymbol}]"));
string? lastHdr = null;
foreach (var row in data.Rows) {
var hdr = $"{row.Variety}{(row.Attribute != null ? " / " : "")}{row.Attribute}{(row.Cultivation != null ? " / " : "")}{row.Cultivation}";
if (lastHdr != hdr) {
var rows = data.Rows
.Where(r => r.Variety == row.Variety && r.Attribute == row.Attribute && r.Cultivation == row.Cultivation)
.ToList();
var border = lastHdr != null;
tbl.AddCell(NewTd(hdr, colspan: 2, bold: true, italic: true, borderTop: border))
.AddCell(NewTd($"{rows.Sum(r => r.Ungeb.Weight):N0}", right: true, bold: true, borderTop: border))
.AddCell(NewTd(borderTop: border))
.AddCell(NewTd($"{rows.Sum(r => r.LowGeb.Weight):N0}", right: true, bold: true, borderTop: border))
.AddCell(NewTd(borderTop: border))
.AddCell(NewTd($"{rows.Sum(r => r.Geb.Weight):N0}", right: true, bold: true, borderTop: border))
.AddCell(NewTd(borderTop: border))
.AddCell(NewTd($"{rows.Sum(r => r.Amount):N2}", right: true, bold: true, borderTop: border));
}
tbl.AddCell(NewTd(row.QualityLevel))
.AddCell(NewTd($"{row.Oe:N0}", center: true))
.AddCell(NewTd(row.Ungeb.Weight != 0 ? $"{row.Ungeb.Weight:N0}" : "-", right: true))
.AddCell(NewTd(row.Ungeb.MaxPrice != null ? $"{row.Ungeb.MaxPrice:N4}" : "-", right: true))
.AddCell(NewTd(row.LowGeb.Weight != 0 ? $"{row.LowGeb.Weight:N0}" : "-", right: true))
.AddCell(NewTd(row.LowGeb.MaxPrice != null ? $"{row.LowGeb.MaxPrice:N4}" : "-", right: true))
.AddCell(NewTd(row.Geb.Weight != 0 ? $"{row.Geb.Weight:N0}" : "-", right: true))
.AddCell(NewTd(row.Geb.MaxPrice != null ? $"{row.Geb.MaxPrice:N4}" : "-", right: true))
.AddCell(NewTd($"{row.Amount:N2}", right: true));
lastHdr = hdr;
}
return tbl;
}
}
}
-68
View File
@@ -1,68 +0,0 @@
using System.Threading.Tasks;
using Elwig.Helpers;
using Elwig.Windows;
using System.Diagnostics;
using Balbarak.WeasyPrint;
using System;
using System.IO;
namespace Elwig.Documents {
public static class Pdf {
private static readonly string PdfToPrinter = App.ExePath + "PDFtoPrinter.exe";
private static readonly FilesManager WeasyPrintManager = new();
private static string? WeasyPrintPython = null;
private static string? WeasyPrintDir => WeasyPrintManager.FolderPath;
public static bool IsReady => WeasyPrintPython != null && WeasyPrintDir != null;
public static async Task Init(Action evtHandler) {
if (!WeasyPrintManager.IsFilesExsited()) {
await WeasyPrintManager.InitFilesAsync();
}
WeasyPrintPython = Path.Combine(WeasyPrintManager.FolderPath, "python.exe");
evtHandler();
}
public static async Task Convert(string htmlPath, string pdfPath) {
var p = new Process() { StartInfo = new() {
FileName = WeasyPrintPython,
CreateNoWindow = true,
WorkingDirectory = WeasyPrintDir,
RedirectStandardError = true,
} };
p.StartInfo.EnvironmentVariables["PATH"] = "Scripts;gtk3;" + Environment.GetEnvironmentVariable("PATH");
p.StartInfo.ArgumentList.Add("scripts/weasyprint.exe");
p.StartInfo.ArgumentList.Add("-e");
p.StartInfo.ArgumentList.Add("utf8");
p.StartInfo.ArgumentList.Add(htmlPath);
p.StartInfo.ArgumentList.Add(pdfPath);
p.Start();
await p.WaitForExitAsync();
var stderr = await p.StandardError.ReadToEndAsync();
if (p.ExitCode != 0) throw new Exception(stderr);
}
public static void Show(TempFile file, string title) {
App.MainDispatcher.BeginInvoke(() => {
var w = new DocumentViewerWindow(title, file);
w.Show();
});
}
public static void Show(string path, string title) {
App.MainDispatcher.BeginInvoke(() => {
var w = new DocumentViewerWindow(title, path);
w.Show();
});
}
public static async Task Print(string path, int copies = 1) {
var p = new Process() { StartInfo = new() { FileName = PdfToPrinter } };
p.StartInfo.ArgumentList.Add(path);
p.StartInfo.ArgumentList.Add("/s");
p.StartInfo.ArgumentList.Add($"copies={copies}");
p.Start();
await p.WaitForExitAsync();
}
}
}
+125
View File
@@ -0,0 +1,125 @@
using Elwig.Helpers;
using Elwig.Models.Dtos;
using iText.Kernel.Colors;
using iText.Kernel.Pdf;
using iText.Layout.Borders;
using iText.Layout.Element;
using iText.Layout.Properties;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Documents {
public class WineQualityStatistics : Document {
public new static string Name => "Qualitätsstatistik";
public readonly string[][] QualIds = [["WEI"], ["RSW", "LDW"], ["QUW"], ["KAB"]];
public readonly Dictionary<string, string> QualityLevels = new() {
["WEI"] = "Wein",
["RSW"] = "Rebsortenwein",
["LDW"] = "Landwein",
["QUW"] = "Qualitätswein",
["KAB"] = "Kabinett",
};
public string Filter;
public WineQualityStatisticsData Data;
public bool UseOe => Data.UseOe;
public WineQualityStatistics(string filter, WineQualityStatisticsData data) :
base($"{Name} {filter}") {
Filter = filter;
Data = data;
}
protected override void RenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.RenderBody(doc, pdf);
doc.Add(new KernedParagraph(Name, 24)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginBottomMM(2));
doc.Add(new KernedParagraph(Filter, 14)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)
.SetMarginBottomMM(10));
foreach (var sec in Data.Sections) {
doc.Add(NewQualitySectionTable(sec).SetMarginBottomMM(5));
}
}
protected Table NewQualityColumnTable(string[] qualIds, WineQualityStatisticsData.QualitySection sec) {
var tbl = new Table(ColsMM(9.5, 10, 19.5))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetMarginsMM(1, 0, 1, 0)
.AddCell(NewCell(new KernedParagraph(UseOe ? "[°Oe]" : "[°KMW]", 8)
.SetTextAlignment(TextAlignment.CENTER).SetFont(IF)).SetPaddingsMM(1, 1, 1, 2))
.AddCell(NewCell(new KernedParagraph("[#]", 8)
.SetTextAlignment(TextAlignment.CENTER).SetFont(IF)).SetPaddingsMM(1, 1, 1, 1))
.AddCell(NewCell(new KernedParagraph("[kg]", 8)
.SetTextAlignment(TextAlignment.CENTER).SetFont(IF)).SetPaddingsMM(1, 2, 1, 1));
foreach (var qualId in qualIds) {
tbl.AddCell(NewCell(new KernedParagraph(QualityLevels.GetValueOrDefault(qualId, qualId), 10)
.SetFont(BI).SetTextAlignment(TextAlignment.CENTER), colspan: 3)
.SetPaddingsMM(2, 0, 2, 0));
foreach (var (grad, avgKmw, num, weight) in sec.Data.GetValueOrDefault(qualId, Array.Empty<(double, double, int, int)>())) {
tbl.AddCell(NewCell(new KernedParagraph(UseOe ? $"{grad:N0}" : $"{grad:N1}", 10)
.SetTextAlignment(TextAlignment.CENTER)).SetPaddingsMM(0, 0, 0, 2))
.AddCell(NewCell(new KernedParagraph($"{num:N0}", 10)
.SetTextAlignment(TextAlignment.RIGHT)).SetPaddingsMM(0, 0, 0, 0))
.AddCell(NewCell(new KernedParagraph($"{weight:N0}", 10)
.SetTextAlignment(TextAlignment.RIGHT)).SetPaddingsMM(0, 2, 0, 0));
}
}
return tbl;
}
protected Table NewQualitySumTable(double kmw, int num, int weight) {
return new Table(ColsMM(9.5, 10, 19.5))
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetMarginsMM(1, 0, 1, 0)
.AddCell(NewCell(new KernedParagraph(weight == 0 ? "-" : UseOe ? $"{Utils.KmwToOe(kmw):N0}" : $"{kmw:N1}", 10)
.SetTextAlignment(TextAlignment.CENTER).SetFont(BF)).SetPaddingsMM(0, 0, 0, 2))
.AddCell(NewCell(new KernedParagraph($"{num:N0}", 10)
.SetTextAlignment(TextAlignment.RIGHT).SetFont(BF)).SetPaddingsMM(0, 0, 0, 0))
.AddCell(NewCell(new KernedParagraph($"{weight:N0}", 10)
.SetTextAlignment(TextAlignment.RIGHT).SetFont(BF)).SetPaddingsMM(0, 2, 0, 0));
}
protected Table NewQualitySectionTable(WineQualityStatisticsData.QualitySection sec) {
var tbl = new Table(4)
.SetWidth(UnitValue.CreatePercentValue(100)).SetFixedLayout()
.SetBorderCollapse(BorderCollapsePropertyValue.COLLAPSE)
.SetBorder(new SolidBorder(BorderThickness))
.SetKeepTogether(true);
var bgColor = sec.Type == "R" ? new DeviceRgb(0xff, 0xc0, 0xc0) : sec.Type == "W" ? new DeviceRgb(0xc0, 0xff, 0xc0) : new DeviceRgb(0xe0, 0xe0, 0xe0);
tbl.AddCell(NewCell(new KernedParagraph(sec.Name, 14).SetFont(BF), colspan: 4)
.SetBackgroundColor(bgColor).SetPaddingsMM(1, 2, 1, 2));
foreach (var qualIds in QualIds) {
tbl.AddCell(NewCell().SetPadding(0).Add(NewQualityColumnTable(qualIds, sec))
.SetBorder(new SolidBorder(BorderThickness)));
}
foreach (var qualIds in QualIds) {
var quals = qualIds.Select(q => sec.Data.GetValueOrDefault(q, Array.Empty<(double Grad, double AvgKmw, int Num, int Weight)>()));
var weight = quals.Sum(q => q.Sum(kv => kv.Weight));
var num = quals.Sum(q => q.Sum(kv => kv.Num));
var kmw = quals.Sum(q => q.Sum(kv => kv.AvgKmw * kv.Weight)) / weight;
tbl.AddCell(NewCell().SetPaddingsMM(0.5f, 0, 0.5f, 0).Add(NewQualitySumTable(kmw, num, weight))
.SetBorder(new SolidBorder(BorderThickness)));
}
var totalWeight = sec.Data.Values.Sum(q => q.Sum(kv => kv.Weight));
var totalNum = sec.Data.Values.Sum(q => q.Sum(kv => kv.Num));
var totalKmw = sec.Data.Values.Sum(q => q.Sum(kv => kv.AvgKmw * kv.Weight)) / totalWeight;
tbl.AddCell(NewCell(colspan: 3).SetBackgroundColor(bgColor))
.AddCell(NewCell().SetPadding(0).Add(NewQualitySumTable(totalKmw, totalNum, totalWeight))
.SetBackgroundColor(bgColor));
return tbl;
}
}
}
-142
View File
@@ -1,142 +0,0 @@
.address-wrapper, aside, main {
overflow: hidden;
}
.spacing {
height: 20mm;
}
.info-wrapper {
width: 100%;
height: 45mm;
margin: 0 0 2mm 0;
position: relative;
}
.info-wrapper .date {
text-align: right;
position: absolute;
right: 0;
bottom: -1.5em;
}
.address-wrapper {
height: 45mm;
width: 85mm;
margin: 0;
padding: 5mm;
position: absolute;
left: -5mm;
top: 0;
}
.address-wrapper .sender {
height: 4em;
font-size: 8pt;
padding: 1em 0;
}
address {
height: 5em;
white-space: pre-line;
font-size: 12pt;
font-style: normal;
}
aside {
height: 40mm;
width: 75mm;
margin: 0;
position: absolute;
left: 100mm;
top: 5mm;
}
aside table {
border-collapse: collapse;
border: 0.5pt solid #808080;
width: 65mm;
margin-right: 10mm;
}
aside table thead:not(:first-child) tr {
border-top: 0.5pt solid #808080;
}
aside table thead th {
background-color: #E0E0E0;
font-size: 10pt;
}
aside table tbody th,
aside table tbody td {
text-align: left;
font-size: 10pt;
}
aside table tbody th {
padding: 0.25mm 0.5mm 0.25mm 1mm;
}
aside table tbody td {
padding: 0.25mm 0;
}
aside table tbody th {
font-weight: normal;
}
main {
margin: 2em 0 1em 0;
}
main > *:first-child {
margin-top: 0;
}
main h1, .main-wrapper p {
font-size: 12pt;
margin: 1em 0;
text-align: justify;
}
.main-wrapper p {
widows: 3;
orphans: 3;
hyphens: manual;
}
main h1 {
margin-top: 0;
margin-bottom: 2em;
}
.main-wrapper p.comment {
font-size: 10pt;
}
.main-wrapper .hidden {
break-before: avoid;
}
.main-wrapper .bottom {
bottom: 0;
position: absolute;
width: 165mm;
}
.main-wrapper .signatures {
width: 100%;
display: flex;
justify-content: space-around;
margin: 20mm 0 2mm 0;
}
.main-wrapper .signatures > * {
width: 50mm;
border-top: 0.5pt solid black;
padding-top: 1mm;
text-align: center;
font-size: 10pt;
}
-64
View File
@@ -1,64 +0,0 @@
table.credit {
font-size: 10pt;
}
table.credit th,
table.credit td {
padding: 0 0.25mm;
}
table.credit thead {
font-size: 8pt
}
table.credit thead th {
font-weight: normal;
font-style: italic;
vertical-align: bottom;
}
table.credit td {
vertical-align: top;
}
table.credit .oe,
table.credit .kmw {
text-align: center;
}
table.credit .dpnr {
text-align: center;
}
table.credit .amount,
table.credit .weight {
text-align: right;
}
table.credit .rel,
table.credit .abs {
text-align: center;
}
table.credit .amount.sum {
vertical-align: bottom;
padding-bottom: 1mm;
}
table.credit tbody tr:not(.first):not(.last) {
break-before: avoid;
break-after: avoid;
}
table.credit tbody tr.first td {
padding-top: 1mm;
}
table.credit tbody tr.last td {
padding-bottom: 1mm;
}
table.credit tbody tr.new {
border-top: 0.5pt solid black;
}
-47
View File
@@ -1,47 +0,0 @@
h1 {
text-align: center;
font-size: 24pt;
margin-bottom: 2mm;
}
h2 {
text-align: center;
font-size: 14pt;
margin-top: 2mm;
}
table.journal {
font-size: 10pt;
}
table.journal thead {
font-size: 8pt;
}
table.journal th {
font-weight: normal;
font-style: italic;
}
table.journal td {
overflow: hidden;
white-space: nowrap;
}
table.journal .mgnr,
table.journal .weight {
text-align: right;
}
table.journal .grad {
text-align: center;
}
table.journal tr.sum {
font-weight: bold;
}
table.journal tr.sum td {
border-top: 0.5pt solid black;
}
-99
View File
@@ -1,99 +0,0 @@
main h1 {
margin-bottom: 1.5em !important;
}
table.delivery {
margin-bottom: 5mm;
}
table.delivery tr:not(.main) {
break-before: avoid;
}
table.delivery th {
font-weight: normal;
font-style: italic;
font-size: 10pt;
}
table.delivery th.main {
text-align: left;
}
table.delivery tr.main td {
font-weight: bold;
padding-top: 2mm;
}
table.delivery tbody tr:not(.main) td {
font-size: 8pt;
}
table.delivery tr.tight:not(.first) td {
padding-top: 0;
padding-bottom: 0;
}
table.delivery tr.tight.first td {
padding-bottom: 0;
}
/* FIXME update version of WeasyPrint
table.delivery tr.tight:has(+ tr:not(.tight)) td {
padding-bottom: 0.5mm !important;
}
*/
table.delivery tr.sum {
border-top: 0.5pt solid black;
break-before: avoid;
}
table.delivery tr.sum td {
padding-top: 1mm;
}
table.delivery-stats {
font-size: 8pt;
break-inside: avoid;
break-after: avoid;
}
table.delivery-stats.expanded th,
table.delivery-stats.expanded td {
padding: 0.25mm 0;
}
table.delivery-stats:not(.expanded) th,
table.delivery-stats:not(.expanded) td {
padding: 0.125mm 0;
}
table.delivery-stats:not(.expanded) tr.optional {
display: none;
}
table.delivery-stats thead th {
font-weight: normal;
font-style: italic;
text-align: right;
}
table.delivery-stats thead th:first-child {
text-align: left;
}
table.delivery-stats.expanded tbody {
font-size: 10pt;
}
table.delivery-stats td {
text-align: right;
}
table.delivery-stats tbody th {
font-weight: normal;
font-style: italic;
text-align: left;
}
-74
View File
@@ -1,74 +0,0 @@
.m1, .m2, .m3 {
height: 0;
width: 10mm;
position: fixed;
left: -25mm;
border-top: 0.5pt solid black;
}
.m1.r, .m2.r, .m3.r {
left: initial;
right: -20mm;
}
.m1 {top: 80mm;}
.m2 {top: 123.5mm;}
.m3 {top: 185mm;}
.page-break {
break-before: page;
}
hr.page-break {
display: none;
}
.document-break {
break-before: page;
}
@page {
size: A4;
margin: 25mm 20mm 35mm 25mm;
@bottom-center {
content: element(page-footer);
}
}
@media screen {
body, header, .footer-wrapper {
width: 210mm;
}
header, .address-wrapper, aside, main {
border: 1px solid lightgray;
}
.m1, .m2, .m3 {
display: none;
}
header {
top: 0;
}
.spacing {
height: 45mm;
}
.main-wrapper {
margin: 0 20mm 40mm 25mm;
}
.footer-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
}
}
@media print {
.page::after {
content: "Seite " counter(page) " von " counter(pages) !important;
}
}
-101
View File
@@ -1,101 +0,0 @@
:root {
font-family: "Times New Roman", serif;
line-height: 1;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
table td,
table th {
padding: 0.5mm 1mm;
}
table th {
text-align: center;
}
header {
height: 45mm;
padding: 10mm 0 0 0;
position: absolute;
top: -25mm;
left: 0;
right: 0;
text-align: center;
overflow: hidden;
}
header .name {
font-size: 18pt;
margin-top: 10mm;
font-weight: bold;
}
header .suffix {
font-size: 12pt;
font-weight: bold;
}
.footer-wrapper {
position: running(page-footer);
width: 165mm;
}
.pre-footer {
margin: 1em 0;
font-size: 10pt;
}
.pre-footer > * {
display: inline-block;
width: 33%;
}
.pre-footer .date {
text-align: left;
}
.pre-footer .doc-id {
text-align: center;
font-style: italic;
}
.pre-footer .page {
text-align: right;
float: right;
}
.pre-footer .page::after {
content: "Seite 1 von 1";
}
footer {
font-size: 10pt;
border-top: 0.5pt solid black;
height: 25mm;
padding-top: 1mm;
text-align: center;
}
.hidden {
visibility: hidden;
}
hr {
border: none;
border-top: 0.5pt solid black;
margin: 5mm 0;
}
+26 -23
View File
@@ -2,39 +2,42 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net10.0-windows</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<PreserveCompilationContext>true</PreserveCompilationContext>
<ApplicationIcon>elwig.ico</ApplicationIcon>
<Version>0.3.4</Version>
<ApplicationIcon>Resources\Images\Elwig.ico</ApplicationIcon>
<Version>1.1.2.2</Version>
<SatelliteResourceLanguages>de-AT</SatelliteResourceLanguages>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ApplicationManifest>App.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<Content Include="elwig.ico" />
<Resource Include="Resources\Images\Elwig.png" />
<Content Include="Resources\Images\Elwig.ico" />
<EmbeddedResource Include="Resources\Schemas\PaymentVariantData.json" />
<EmbeddedResource Include="Resources\Sql\*" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Balbarak.WeasyPrint" Version="2.0.2" />
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.5.1" />
<PackageReference Include="ini-parser" Version="2.5.2" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="6.0.22" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.11" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.1938.49" />
<PackageReference Include="RazorLight" Version="2.3.1" />
<PackageReference Include="ScottPlot.WPF" Version="4.1.67" />
<PackageReference Include="System.IO.Ports" Version="7.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" />
<PackageReference Include="bblanchon.PDFium.Win32" Version="155.0.8044" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageReference Include="itext" Version="9.7.0" />
<PackageReference Include="itext.bouncy-castle-adapter" Version="9.7.0" />
<PackageReference Include="LinqKit" Version="1.3.11" />
<PackageReference Include="MailKit" Version="4.18.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.12" />
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="10.0.12" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.4191.47" />
<PackageReference Include="NJsonSchema" Version="11.6.1" />
<PackageReference Include="ScottPlot.WPF" Version="5.1.59" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.5" />
<PackageReference Include="System.IO.Hashing" Version="10.0.12" />
<PackageReference Include="System.IO.Ports" Version="10.0.12" />
<PackageReference Include="System.Management" Version="10.0.12" />
</ItemGroup>
<ItemGroup>
<Resource Include="elwig.png" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="call fetch-resources.bat" />
</Target>
</Project>
+22
View File
@@ -0,0 +1,22 @@
using System;
using System.Windows.Input;
namespace Elwig.Helpers {
public class ActionCommand : ICommand {
public event EventHandler? CanExecuteChanged;
private readonly Action Action;
public ActionCommand(Action action) {
Action = action;
}
public void Execute(object? parameter) {
Action();
}
public bool CanExecute(object? parameter) {
return true;
}
}
}
+512 -96
View File
@@ -1,16 +1,26 @@
using Microsoft.EntityFrameworkCore;
using Elwig.Models;
using System.Linq;
using System.Threading.Tasks;
using System.IO;
using System;
using System.Windows;
using Microsoft.Extensions.Logging;
using Elwig.Models.Dtos;
using Elwig.Models.Entities;
using Elwig.Services;
using Microsoft.Data.Sqlite;
using System.Text.RegularExpressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Elwig.Helpers {
public record struct MemberHistoryPoint(int Shares, int SharesRed, int SharesWhite, int SharesDormant);
public record struct AreaComBucket(int Area, int Obligation, int Right);
public record struct UnderDelivery(int Weight, int Diff);
public record struct MemberBucket(string Name, int Area, int Obligation, int Right, int Delivery, int DeliveryStrict, int DeliveryTotal, int Payment);
public record struct MemberStat(string Variety, string Discr, int Weight);
public record struct ModifierStat(string ModId, string Name, int Count, decimal? Min, decimal? Max, decimal Sum);
public class AppDbContext : DbContext {
public DbSet<Country> Countries { get; private set; }
@@ -26,6 +36,8 @@ namespace Elwig.Helpers {
public DbSet<WineVar> WineVarieties { get; private set; }
public DbSet<ClientParam> ClientParameters { get; private set; }
public DbSet<WbGl> WbGls { get; private set; }
public DbSet<WbGem> WbGems { get; private set; }
public DbSet<WbKg> WbKgs { get; private set; }
public DbSet<WbRd> WbRde { get; private set; }
public DbSet<WineAttr> WineAttributes { get; private set; }
@@ -35,23 +47,113 @@ namespace Elwig.Helpers {
public DbSet<Member> Members { get; private set; }
public DbSet<BillingAddr> BillingAddresses { get; private set; }
public DbSet<MemberTelNr> MemberTelephoneNrs { get; private set; }
public DbSet<MemberEmailAddr> MemberEmailAddrs { get; private set; }
public DbSet<MemberHistory> MemberHistory { get; private set; }
public DbSet<AreaComContract> AreaCommitmentContracts { get; private set; }
public DbSet<AreaCom> AreaCommitments { get; private set; }
public DbSet<Season> Seasons { get; private set; }
public DbSet<DeliverySchedule> DeliverySchedules { get; private set; }
public DbSet<DeliveryScheduleWineVar> DeliveryScheduleWineVarieties { get; private set; }
public DbSet<DeliveryAncmt> DeliveryAnnouncements { get; private set; }
public DbSet<Modifier> Modifiers { get; private set; }
public DbSet<Delivery> Deliveries { get; private set; }
public DbSet<DeliveryPart> DeliveryParts { get; private set; }
public DbSet<DeliveryPartAttr> DeliveryPartAttributes { get; private set; }
public DbSet<DeliveryPartModifier> DeliveryPartModifiers { get; private set; }
public DbSet<PaymentVar> PaymentVariants { get; private set; }
public DbSet<PaymentMember> MemberPayments { get; private set; }
public DbSet<PaymentDeliveryPart> PaymentDeliveryParts { get; private set; }
public DbSet<PaymentCustom> CustomPayments { get; private set; }
public DbSet<Credit> Credits { get; private set; }
public DbSet<DeliveryPartBucket> DeliveryPartBuckets { get; private set; }
public DbSet<OverUnderDeliveryRow> OverUnderDeliveryRows { get; private set; }
public DbSet<AreaComUnderDeliveryRowSingle> AreaComUnderDeliveryRows { get; private set; }
public DbSet<MemberDeliveryPerVarietyRowSingle> MemberDeliveryPerVariantRows { get; private set; }
public DbSet<MemberAreaComTotalsRowSingle> MemberAreaComTotalsRows { get; private set; }
public DbSet<MemberAreaComsRow> 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; }
public DbSet<PaymentVariantSummaryRow> PaymentVariantSummaryRows { get; private set; }
private readonly StreamWriter? LogFile = null;
public static DateTime LastWriteTime => File.GetLastWriteTime(App.Config.DatabaseFile);
public DateTime SavedLastWriteTime { get; private set; }
public bool HasBackendChanged => SavedLastWriteTime != LastWriteTime;
public static string ConnectionString => $"Data Source=\"{App.Config.DatabaseFile}\"; Foreign Keys=True; Mode=ReadWrite; Cache=Default";
public static string? ConnectionStringOverride { get; set; } = null;
public static string ConnectionString => ConnectionStringOverride ?? $"Data Source=\"{App.Config.DatabaseFile}\"; Mode=ReadWrite; Foreign Keys=True; Cache=Default; Pooling=False";
private static readonly Func<AppDbContext, string?, bool, IAsyncEnumerable<Branch>> _compiledQueryBranches =
EF.CompileAsyncQuery<AppDbContext, string?, bool, Branch>((ctx, zwstid, includeWithoutMembers) => ctx.Branches
.Where(b => includeWithoutMembers || b.Members.Count > 0)
.Where(b => zwstid == null || b.ZwstId == zwstid)
.Include(b => b.PostalDest)
.OrderBy(b => b.Name));
private static readonly Func<AppDbContext, string?, IAsyncEnumerable<WineVar>> _compiledQueryWineVarieties =
EF.CompileAsyncQuery<AppDbContext, string?, WineVar>((ctx, sortid) => ctx.WineVarieties
.Where(v => sortid == null || v.SortId == sortid)
.OrderBy(v => v.Name));
private static readonly Func<AppDbContext, string?, bool, IAsyncEnumerable<WineAttr>> _compiledQueryWineAttributes =
EF.CompileAsyncQuery<AppDbContext, string?, bool, WineAttr>((ctx, attrid, includeNotActive) => ctx.WineAttributes
.Where(a => includeNotActive || a.IsActive)
.Where(a => attrid == null || a.AttrId == attrid)
.OrderBy(a => a.Name));
private static readonly Func<AppDbContext, string?, IAsyncEnumerable<WineCult>> _compiledQueryWineCultivations =
EF.CompileAsyncQuery<AppDbContext, string?, WineCult>((ctx, cultid) => ctx.WineCultivations
.Where(c => cultid == null || c.CultId == cultid)
.OrderBy(v => v.Name));
private static readonly Func<AppDbContext, bool, IAsyncEnumerable<WineQualLevel>> _compiledQueryWineQualityLevels =
EF.CompileAsyncQuery<AppDbContext, bool, WineQualLevel>((ctx, includePredicate) => ctx.WineQualityLevels
.Where(l => includePredicate || !l.IsPredicate)
.OrderBy(l => l.MinKmw));
private static readonly Func<AppDbContext, int?, bool, IAsyncEnumerable<Modifier>> _compiledQueryModifiers =
EF.CompileAsyncQuery<AppDbContext, int?, bool, Modifier>((ctx, year, incudeNotActive) => ctx.Modifiers
.Where(m => (year == null || m.Year == year) && (incudeNotActive || m.IsActive))
.OrderBy(m => m.Year).ThenBy(m => m.Ordering).ThenBy(m => m.Name));
private static readonly Func<AppDbContext, int?, bool, IAsyncEnumerable<Member>> _compiledQueryMembers =
EF.CompileAsyncQuery<AppDbContext, int?, bool, Member>((ctx, mgnr, includeNotActive) => ctx.Members
.Where(m => includeNotActive || m.IsActive)
.Where(m => mgnr == null || m.MgNr == mgnr)
.OrderBy(m => m.Name).ThenBy(m => m.GivenName).ThenBy(m => m.MgNr));
private static readonly Func<AppDbContext, int?, bool, IAsyncEnumerable<Member>> _compiledQueryMembersContactInfo =
EF.CompileAsyncQuery<AppDbContext, int?, bool, Member>((ctx, mgnr, includeNotActive) => ctx.Members
.Where(m => includeNotActive || m.IsActive)
.Where(m => mgnr == null || m.MgNr == mgnr)
.Include(m => m.EmailAddresses)
.Include(m => m.TelephoneNumbers)
.OrderBy(m => m.Name).ThenBy(m => m.GivenName).ThenBy(m => m.MgNr)
.AsSplitQuery());
private static readonly Func<AppDbContext, int?, IAsyncEnumerable<AreaCom>> _compiledQueryAreaCommitments =
EF.CompileAsyncQuery<AppDbContext, int?, AreaCom>((ctx, fbnr) => ctx.AreaCommitments
.Where(c => fbnr == null || c.FbNr == fbnr)
.OrderBy(c => c.FbNr).ThenBy(c => c.RevNr));
private static readonly Func<AppDbContext, int?, IAsyncEnumerable<Season>> _compiledQuerySeasons =
EF.CompileAsyncQuery<AppDbContext, int?, Season>((ctx, year) => ctx.Seasons
.Where(s => year == null || s.Year == year)
.OrderByDescending(s => s.Year));
private static readonly Func<AppDbContext, int?, IAsyncEnumerable<Season>> _compiledQuerySeasonsModifiers =
EF.CompileAsyncQuery<AppDbContext, int?, Season>((ctx, year) => ctx.Seasons
.Where(s => year == null || s.Year == year)
.Include(s => s.Modifiers)
.OrderByDescending(s => s.Year));
private readonly Dictionary<int, Dictionary<int, MemberHistoryPoint>> _memberHistory = [];
private readonly Dictionary<int, Dictionary<int, Dictionary<string, AreaComBucket>>> _memberAreaCommitmentBuckets = [];
private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberDeliveryBuckets = [];
private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberDeliveryBucketsStrict = [];
private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberPaymentBuckets = [];
private readonly Dictionary<int, Dictionary<int, Dictionary<string, UnderDelivery>>> _memberUnderDelivery = [];
public AppDbContext() {
if (App.Config.DatabaseLog != null) {
@@ -60,35 +162,92 @@ namespace Elwig.Helpers {
LogFile = new(file) {
AutoFlush = true
};
} catch (Exception e) {
MessageBox.Show($"Unable to open database log file:\n\n{e.Message}", "Database Log", MessageBoxButton.OK, MessageBoxImage.Error);
} catch (Exception exc) {
InteractionService.ShowException("Database Log", $"Unable to open database log file", exc);
}
}
SavedLastWriteTime = LastWriteTime;
SavedChanges += OnSavedChanges;
}
public static SqliteConnection Connect() {
var cnx = new SqliteConnection(ConnectionString);
cnx.CreateFunction<string, string?, bool?>("REGEXP", (pattern, value) => value == null ? null : Regex.Match(value, pattern).Success, true);
private static void CreateFunctions(SqliteConnection cnx) {
cnx.CreateFunction<string, string?, bool?>("REGEXP", (pattern, value) => {
return value == null ? null : Regex.IsMatch(value, pattern);
}, true);
cnx.CreateFunction<string?, string?, string?>("NORMALIZE_ADDRESS", (address, locality) => {
return string.IsNullOrWhiteSpace(address) ? null : Normalizer.NormalizeAddress(address, locality);
}, true);
}
public static SqliteConnection Connect(string? connectionString = null) {
var cnx = new SqliteConnection(connectionString ?? ConnectionString);
CreateFunctions(cnx);
cnx.Open();
return cnx;
}
public static async Task<SqliteConnection> ConnectAsync() {
var cnx = new SqliteConnection(ConnectionString);
cnx.CreateFunction<string, string?, bool?>("REGEXP", (pattern, value) => value == null ? null : Regex.Match(value, pattern).Success, true);
public static async Task<SqliteConnection> ConnectAsync(string? connectionString = null) {
var cnx = new SqliteConnection(connectionString ?? ConnectionString);
CreateFunctions(cnx);
await cnx.OpenAsync();
return cnx;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
optionsBuilder.UseSqlite(ConnectionString);
optionsBuilder.UseLazyLoadingProxies();
optionsBuilder.LogTo(Log, LogLevel.Information);
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<WbKg>().Navigation(k => k.AtKg).AutoInclude();
modelBuilder.Entity<WbKg>().Navigation(k => k.Gl).AutoInclude();
modelBuilder.Entity<AT_Kg>().Navigation(k => k.Gem).AutoInclude();
modelBuilder.Entity<PostalDest>().Navigation(p => p.Country).AutoInclude();
modelBuilder.Entity<PostalDest>().Navigation(p => p.AtPlz).AutoInclude();
modelBuilder.Entity<AT_PlzDest>().Navigation(p => p.AtPlz).AutoInclude();
modelBuilder.Entity<AT_PlzDest>().Navigation(p => p.Ort).AutoInclude();
modelBuilder.Entity<Member>().Navigation(m => m.Branch).AutoInclude();
modelBuilder.Entity<Member>().Navigation(m => m.DefaultWbKg).AutoInclude();
modelBuilder.Entity<Member>().Navigation(m => m.Country).AutoInclude();
modelBuilder.Entity<Member>().Navigation(m => m.PostalDest).AutoInclude();
modelBuilder.Entity<Member>().Navigation(m => m.BillingAddress).AutoInclude();
modelBuilder.Entity<BillingAddr>().Navigation(a => a.Country).AutoInclude();
modelBuilder.Entity<BillingAddr>().Navigation(a => a.PostalDest).AutoInclude();
modelBuilder.Entity<Modifier>().Navigation(m => m.Season).AutoInclude();
modelBuilder.Entity<Season>().Navigation(s => s.Currency).AutoInclude();
modelBuilder.Entity<PaymentVar>().Navigation(v => v.Season).AutoInclude();
modelBuilder.Entity<PaymentDeliveryPart>().Navigation(p => p.Variant).AutoInclude();
modelBuilder.Entity<Credit>().Navigation(c => c.Payment).AutoInclude();
modelBuilder.Entity<Delivery>().Navigation(d => d.Member).AutoInclude();
modelBuilder.Entity<Delivery>().Navigation(d => d.Season).AutoInclude();
modelBuilder.Entity<Delivery>().Navigation(d => d.Branch).AutoInclude();
modelBuilder.Entity<DeliveryPart>().Navigation(p => p.Quality).AutoInclude();
modelBuilder.Entity<DeliveryPart>().Navigation(p => p.Variety).AutoInclude();
modelBuilder.Entity<DeliveryPart>().Navigation(p => p.Attribute).AutoInclude();
modelBuilder.Entity<DeliveryPart>().Navigation(p => p.Cultivation).AutoInclude();
modelBuilder.Entity<DeliveryPart>().Navigation(p => p.Kg).AutoInclude();
modelBuilder.Entity<DeliveryPart>().Navigation(p => p.Rd).AutoInclude();
modelBuilder.Entity<DeliveryPartModifier>().Navigation(m => m.Modifier).AutoInclude();
modelBuilder.Entity<AreaComContract>().Navigation(c => c.Kg).AutoInclude();
modelBuilder.Entity<AreaComContract>().Navigation(c => c.Rd).AutoInclude();
modelBuilder.Entity<AreaCom>().Navigation(c => c.Contract).AutoInclude();
modelBuilder.Entity<AreaCom>().Navigation(c => c.WineCult).AutoInclude();
modelBuilder.Entity<AreaCom>().Navigation(c => c.AreaComType).AutoInclude();
modelBuilder.Entity<AreaComType>().Navigation(c => c.WineVar).AutoInclude();
modelBuilder.Entity<AreaComType>().Navigation(c => c.WineAttr).AutoInclude();
modelBuilder.Entity<PaymentMember>().Navigation(c => c.Credit).AutoInclude();
modelBuilder.Entity<PaymentMember>().Navigation(c => c.Member).AutoInclude();
modelBuilder.Entity<PaymentMember>().Navigation(c => c.Variant).AutoInclude();
modelBuilder.Entity<DeliveryAncmt>().Navigation(a => a.Member).AutoInclude();
modelBuilder.Entity<DeliveryAncmt>().Navigation(a => a.Schedule).AutoInclude();
modelBuilder.Entity<DeliveryAncmt>().Navigation(a => a.Variety).AutoInclude();
modelBuilder.Entity<DeliverySchedule>().Navigation(s => s.Branch).AutoInclude();
modelBuilder.Entity<MemberHistory>().Navigation(s => s.FromMember).AutoInclude();
modelBuilder.Entity<MemberHistory>().Navigation(s => s.ToMember).AutoInclude();
modelBuilder.Entity<MemberHistory>().Navigation(s => s.Currency).AutoInclude();
}
public override void Dispose() {
base.Dispose();
LogFile?.Dispose();
@@ -104,127 +263,384 @@ namespace Elwig.Helpers {
}
public async Task<bool> MgNrExists(int mgnr) {
return await Members.FindAsync(mgnr) != null;
return await _compiledQueryMembers.Invoke(this, mgnr, true).AnyAsync();
}
public async Task<bool> FbNrExists(int fbnr) {
return await AreaCommitments.FindAsync(fbnr) != null;
return await _compiledQueryAreaCommitments.Invoke(this, fbnr).AnyAsync();
}
public async Task<bool> SortIdExists(string sortId) {
return await WineVarieties.FindAsync(sortId) != null;
return await _compiledQueryWineVarieties.Invoke(this, sortId).AnyAsync();
}
public async Task<bool> AttrIdExists(string attrId) {
return await WineAttributes.FindAsync(attrId) != null;
return await _compiledQueryWineAttributes.Invoke(this, attrId, true).AnyAsync();
}
public async Task<bool> CultIdExists(string cultId) {
return await _compiledQueryWineCultivations.Invoke(this, cultId).AnyAsync();
}
public async Task<int> NextMgNr() {
int c = await Members.Select(m => m.MgNr).MinAsync();
int c = 0;
(await Members.OrderBy(m => m.MgNr).Select(m => m.MgNr).ToListAsync())
.ForEach(a => { if (a <= c + 1000) c = a; });
.ForEach(a => { if (a <= c + 10000) c = a; });
return c + 1;
}
public async Task<int> NextFbNr() {
int c = await AreaCommitments.Select(ac => ac.FbNr).MinAsync();
(await AreaCommitments.OrderBy(ac => ac.FbNr).Select(ac => ac.FbNr).ToListAsync())
.ForEach(a => { if (a <= c + 1000) c = a; });
int c = 0;
(await AreaCommitmentContracts.OrderBy(ac => ac.FbNr).Select(ac => ac.FbNr).ToListAsync())
.ForEach(a => { if (a <= c + 10000) c = a; });
return c + 1;
}
public async Task<int> NextLNr(DateOnly date) {
public async Task<int> NextRevNr(int fbnr) {
return (await AreaCommitments.Where(c => c.FbNr == fbnr).Select(c => (int?)c.RevNr).MaxAsync() ?? 0) + 1;
}
public async Task<int> NextLNr(DateOnly date, string zwstid) {
var dateStr = date.ToString("yyyy-MM-dd");
int c = 0;
(await Deliveries.Where(d => d.DateString == dateStr).Select(d => d.LNr).ToListAsync())
.ForEach(a => { if (a <= c + 100) c = a; });
return c + 1;
return (await Deliveries.Where(d => d.DateString == dateStr && d.ZwstId == zwstid).Select(d => (int?)d.LNr).MaxAsync() ?? 0) + 1;
}
public async Task<int> NextDId(int year) {
int c = 0;
(await Deliveries.Where(d => d.Year == year).Select(d => d.DId).ToListAsync())
.ForEach(a => { if (a <= c + 100) c = a; });
return c + 1;
return (await Deliveries.Where(d => d.Year == year).Select(d => (int?)d.DId).MaxAsync() ?? 0) + 1;
}
public async Task<int> NextDPNr(int year, int did) {
int c = 0;
(await DeliveryParts.Where(p => p.Year == year && p.DId == did).Select(d => d.DPNr).ToListAsync())
.ForEach(a => { if (a <= c + 100) c = a; });
return c + 1;
return (await DeliveryParts.Where(p => p.Year == year && p.DId == did).Select(p => (int?)p.DPNr).MaxAsync() ?? 0) + 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)
.LastOrDefaultAsync();
public async Task<int> NextRdNr(int kgnr) {
return (await WbRde.Where(r => r.KgNr == kgnr).Select(r => (int?)r.RdNr).MaxAsync() ?? 0) + 1;
}
public async Task UpdateDeliveryPartAttributes(DeliveryPart part, IEnumerable<WineAttr> attributes) {
foreach (var a in WineAttributes) {
var attr = part.PartAttributes.Where(pa => pa.AttrId == a.AttrId).FirstOrDefault();
if (attributes.Contains(a)) {
DeliveryPartAttr dpa = attr ?? this.CreateProxy<DeliveryPartAttr>();
dpa.Year = part.Year;
dpa.DId = part.DId;
dpa.DPNr = part.DPNr;
dpa.AttrId = a.AttrId;
if (attr == null) {
await AddAsync(dpa);
} else {
Update(dpa);
}
} else {
if (attr != null) {
Remove(attr);
}
}
public async Task<int> NextAvNr(int year) {
return (await PaymentVariants.Where(v => v.Year == year).Select(v => (int?)v.AvNr).MaxAsync() ?? 0) + 1;
}
public async Task<int> NextDsNr(int year) {
return (await DeliverySchedules.Where(s => s.Year == year).Select(v => (int?)v.DsNr).MaxAsync() ?? 0) + 1;
}
public async Task<int> NextHistNr() {
return (await MemberHistory.Select(c => (int?)c.HistNr).MaxAsync() ?? 0) + 1;
}
public IAsyncEnumerable<Branch> FetchBranches(string? zwstid = null, bool includeWithoutMembers = true) {
return _compiledQueryBranches.Invoke(this, zwstid, includeWithoutMembers);
}
public IAsyncEnumerable<WineVar> FetchWineVarieties() {
return _compiledQueryWineVarieties.Invoke(this, null);
}
public IAsyncEnumerable<WineAttr> FetchWineAttributes(bool incudeNotActive = true) {
return _compiledQueryWineAttributes.Invoke(this, null, incudeNotActive);
}
public IAsyncEnumerable<WineCult> FetchWineCultivations() {
return _compiledQueryWineCultivations.Invoke(this, null);
}
public IAsyncEnumerable<WineQualLevel> FetchWineQualityLevels(bool includePredicate = true) {
return _compiledQueryWineQualityLevels.Invoke(this, includePredicate);
}
public IAsyncEnumerable<Modifier> FetchModifiers(int? year, bool incudeNotActive = true) {
return _compiledQueryModifiers.Invoke(this, year, incudeNotActive);
}
public IAsyncEnumerable<Member> FetchMembers(int? mgnr = null, bool includeNotActive = false, bool includeContactInfo = false) {
if (includeContactInfo) {
return _compiledQueryMembersContactInfo.Invoke(this, mgnr, mgnr != null || includeNotActive);
} else {
return _compiledQueryMembers.Invoke(this, mgnr, mgnr != null || includeNotActive);
}
}
public async Task UpdateDeliveryPartModifiers(DeliveryPart part, IEnumerable<Modifier> modifiers) {
foreach (var m in Modifiers.Where(m => m.Year == part.Year)) {
var mod = part.PartModifiers.Where(pa => pa.ModId == m.ModId).FirstOrDefault();
if (modifiers.Contains(m)) {
DeliveryPartModifier dpm = mod ?? this.CreateProxy<DeliveryPartModifier>();
dpm.Year = part.Year;
dpm.DId = part.DId;
dpm.DPNr = part.DPNr;
dpm.ModId = m.ModId;
if (mod == null) {
await AddAsync(dpm);
public IAsyncEnumerable<Season> FetchSeasons(int? year = null, bool includeModifiers = false) {
if (includeModifiers) {
return _compiledQuerySeasonsModifiers.Invoke(this, year);
} else {
return _compiledQuerySeasons.Invoke(this, year);
}
}
public async Task UpdateDeliveryPartModifiers(DeliveryPart part, IEnumerable<string> oldModIds, IEnumerable<string> newModIds) {
foreach (var m in await FetchModifiers(part.Year).ToListAsync()) {
var mod = new DeliveryPartModifier {
Year = part.Year,
DId = part.DId,
DPNr = part.DPNr,
ModId = m.ModId,
};
var old = oldModIds.Contains(m.ModId);
if (newModIds.Contains(m.ModId)) {
if (!old) {
Add(mod);
} else {
Update(dpm);
Update(mod);
}
} else {
if (mod != null) {
if (old) {
Remove(mod);
}
}
}
}
public async Task<IEnumerable<(string, string, int, int, int)>> GetMemberBuckets(Member m, int year) {
using var cnx = await ConnectAsync();
var (rights, obligations) = await Billing.Billing.GetMemberRightsObligations(m.MgNr, year, cnx);
var buckets = await Billing.Billing.GetMemberBucketWeights(m.MgNr, year, cnx);
var list = new List<(string, string, int, int, int)>();
foreach (var id in rights.Keys.Union(obligations.Keys).Union(buckets.Keys)) {
var s = await WineVarieties.FindAsync(id[..2]);
var attrIds = id[2..];
var a = await WineAttributes.Where(a => attrIds.Contains(a.AttrId)).ToListAsync();
var name = (s?.Name ?? "") + (a.Count > 0 ? $" ({string.Join(" / ", a.Select(a => a.Name))})" : "");
list.Add((
id, name,
rights.TryGetValue(id, out var v1) ? v1 : 0,
obligations.TryGetValue(id, out var v2) ? v2 : 0,
buckets.TryGetValue(id, out var v3) ? v3 : 0
));
public async Task UpdateDeliveryScheduleWineVarieties(DeliverySchedule schedule, IEnumerable<(string, int)> oldVarieties, IEnumerable<(string, int)> newVarieties) {
foreach (var v in await FetchWineVarieties().ToArrayAsync()) {
var e = new DeliveryScheduleWineVar {
Year = schedule.Year,
DsNr = schedule.DsNr,
SortId = v.SortId,
Priority = 1,
};
var o = oldVarieties.Where(x => x.Item1 == e.SortId).Select(x => x.Item2).FirstOrDefault(-1);
var n = newVarieties.Where(x => x.Item1 == e.SortId).Select(x => x.Item2).FirstOrDefault(-1);
if (n != -1) {
e.Priority = n;
if (o == -1) {
Add(e);
} else {
Update(e);
}
} else {
if (o != -1) {
Remove(e);
}
}
}
}
private async Task FetchMemberHistory(int year, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var history = new Dictionary<int, MemberHistoryPoint>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"SELECT mgnr, shares, shares_red, shares_white, shares_dormant FROM v_member_history WHERE year = {year}";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
var mgnr = reader.GetInt32(0);
history[mgnr] = new(reader.GetInt32(1), reader.GetInt32(2), reader.GetInt32(3), reader.GetInt32(4));
}
}
if (ownCnx) await cnx.DisposeAsync();
_memberHistory[year] = history;
}
private async Task FetchMemberAreaCommitmentBuckets(int year, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var buckets = new Dictionary<int, Dictionary<string, AreaComBucket>>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"SELECT mgnr, bucket, area, min_kg, max_kg FROM v_area_commitment_bucket WHERE year = {year}";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
var mgnr = reader.GetInt32(0);
var vtrgid = reader.GetString(1);
if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = [];
buckets[mgnr][vtrgid] = new(reader.GetInt32(2), reader.GetInt32(3), reader.GetInt32(4));
}
}
if (ownCnx) await cnx.DisposeAsync();
_memberAreaCommitmentBuckets[year] = buckets;
}
private async Task FetchMemberDeliveryBuckets(int year, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var buckets = new Dictionary<int, Dictionary<string, int>>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"SELECT mgnr, bucket, weight FROM v_delivery_bucket WHERE year = {year}";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
var mgnr = reader.GetInt32(0);
var bucket = reader.GetString(1);
if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = [];
buckets[mgnr][bucket] = reader.GetInt32(2);
}
}
if (ownCnx) await cnx.DisposeAsync();
_memberDeliveryBuckets[year] = buckets;
}
private async Task FetchMemberDeliveryBucketsStrict(int year, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var buckets = new Dictionary<int, Dictionary<string, int>>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"SELECT mgnr, bucket, weight FROM v_delivery_bucket_strict WHERE year = {year}";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
var mgnr = reader.GetInt32(0);
var bucket = reader.GetString(1);
if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = [];
buckets[mgnr][bucket] = reader.GetInt32(2);
}
}
if (ownCnx) await cnx.DisposeAsync();
_memberDeliveryBucketsStrict[year] = buckets;
}
private async Task FetchMemberPaymentBuckets(int year, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var buckets = new Dictionary<int, Dictionary<string, int>>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"SELECT mgnr, bucket, weight FROM v_payment_bucket WHERE year = {year}";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
var mgnr = reader.GetInt32(0);
var bucket = reader.GetString(1);
if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = [];
buckets[mgnr][bucket] = reader.GetInt32(2);
}
}
if (ownCnx) await cnx.DisposeAsync();
_memberPaymentBuckets[year] = buckets;
}
private async Task FetchMemberUnderDelivery(int year, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var buckets = new Dictionary<int, Dictionary<string, UnderDelivery>>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"SELECT mgnr, bucket, weight, diff FROM v_under_delivery WHERE year = {year}";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
var mgnr = reader.GetInt32(0);
var bucket = reader.GetString(1);
if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = [];
buckets[mgnr][bucket] = new(reader.GetInt32(2), reader.GetInt32(3));
}
}
if (ownCnx) await cnx.DisposeAsync();
_memberUnderDelivery[year] = buckets;
}
public async Task<MemberHistoryPoint> GetMemberHistory(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberHistory.ContainsKey(year))
await FetchMemberHistory(year, cnx);
return _memberHistory[year].GetValueOrDefault(mgnr, new());
}
public async Task<Dictionary<string, AreaComBucket>> GetMemberAreaCommitmentBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberAreaCommitmentBuckets.ContainsKey(year))
await FetchMemberAreaCommitmentBuckets(year, cnx);
return _memberAreaCommitmentBuckets[year].GetValueOrDefault(mgnr, []);
}
public async Task<Dictionary<string, int>> GetMemberDeliveryBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberDeliveryBuckets.ContainsKey(year))
await FetchMemberDeliveryBuckets(year, cnx);
return _memberDeliveryBuckets[year].GetValueOrDefault(mgnr, []);
}
public async Task<Dictionary<string, int>> GetMemberDeliveryBucketsStrict(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberDeliveryBucketsStrict.ContainsKey(year))
await FetchMemberDeliveryBucketsStrict(year, cnx);
return _memberDeliveryBucketsStrict[year].GetValueOrDefault(mgnr, []);
}
public async Task<Dictionary<string, int>> GetMemberPaymentBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberPaymentBuckets.ContainsKey(year))
await FetchMemberPaymentBuckets(year, cnx);
return _memberPaymentBuckets[year].GetValueOrDefault(mgnr, []);
}
public async Task<Dictionary<string, UnderDelivery>> GetMemberUnderDelivery(int year, int mgnr, SqliteConnection? cnx = null) {
if (!_memberUnderDelivery.ContainsKey(year))
await FetchMemberUnderDelivery(year, cnx);
return _memberUnderDelivery[year].GetValueOrDefault(mgnr, []);
}
public async Task<Dictionary<string, MemberBucket>> GetMemberBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var rightsAndObligations = await GetMemberAreaCommitmentBuckets(year, mgnr, cnx);
var deliveryBuckets = await GetMemberDeliveryBuckets(year, mgnr, cnx);
var deliveryBucketsStrict = await GetMemberDeliveryBucketsStrict(year, mgnr, cnx);
var paymentBuckets = await GetMemberPaymentBuckets(year, mgnr, cnx);
if (ownCnx) await cnx.DisposeAsync();
var varieties = await WineVarieties.ToDictionaryAsync(v => v.SortId);
var attributes = await WineAttributes.ToDictionaryAsync(a => a.AttrId);
var buckets = new Dictionary<string, MemberBucket>();
foreach (var id in rightsAndObligations.Keys.Union(deliveryBuckets.Keys).Union(paymentBuckets.Keys)) {
var variety = varieties.GetValueOrDefault(id[..2]);
var attribute = attributes.GetValueOrDefault(id[2..]);
var name = (variety?.Name ?? "") + (id[2..] == "_" ? " (kein Qual.Wein)" : attribute != null ? $" ({attribute})" : "");
buckets[id] = new(
name,
rightsAndObligations.GetValueOrDefault(id).Area,
rightsAndObligations.GetValueOrDefault(id).Obligation,
rightsAndObligations.GetValueOrDefault(id).Right,
deliveryBuckets.GetValueOrDefault(id),
deliveryBucketsStrict.GetValueOrDefault(id),
deliveryBuckets.GetValueOrDefault(id) + deliveryBuckets.GetValueOrDefault(id + "_"),
paymentBuckets.GetValueOrDefault(id)
);
}
return buckets;
}
public static async Task<List<MemberStat>> GetMemberStats(int year, int mgnr, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var list = new List<MemberStat>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"""
SELECT v.name AS variety,
COALESCE(a.name, '') || IIF(a.name IS NOT NULL AND c.name IS NOT NULL, ' / ', '') || COALESCE(c.name, '') AS disc,
SUM(weight) AS weight
FROM v_delivery d
LEFT JOIN wine_variety v ON v.sortid = d.sortid
LEFT JOIN wine_attribute a ON a.attrid = d.attrid
LEFT JOIN wine_cultivation c ON c.cultid = d.cultid
WHERE d.year = {year} AND d.mgnr = {mgnr}
GROUP BY d.sortid, d.attrid, d.cultid
ORDER BY variety, disc;
""";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
list.Add(new(reader.GetString(0), reader.GetString(1), reader.GetInt32(2)));
}
}
if (ownCnx) await cnx.DisposeAsync();
return list;
}
public static async Task<List<ModifierStat>> GetModifierStats(int year, int avnr, SqliteConnection? cnx = null) {
var ownCnx = cnx == null;
cnx ??= await ConnectAsync();
var list = new List<ModifierStat>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"""
SELECT m.modid, m.name, m.count, m.min, m.max, m.sum, s.precision
FROM v_stat_modifier m
JOIN season s ON s.year = m.year
WHERE m.year = {year} AND m.avnr = {avnr}
""";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
var prec = (byte)reader.GetInt16(6);
long? min = reader.IsDBNull(3) ? null : reader.GetInt64(3);
long? max = reader.IsDBNull(4) ? null : reader.GetInt64(4);
var sum = reader.GetInt64(5);
if (min != null && max != null && Math.Abs((long)min) > Math.Abs((long)max))
(min, max) = (max, min);
list.Add(new(reader.GetString(0), reader.GetString(1), reader.GetInt32(2),
min == null ? null : Utils.DecFromDb((long)min, prec),
max == null ? null : Utils.DecFromDb((long)max, prec),
Utils.DecFromDb(sum, prec)));
}
}
if (ownCnx) await cnx.DisposeAsync();
return list;
}
}
+135 -55
View File
@@ -1,85 +1,165 @@
using Elwig.Services;
using Microsoft.Data.Sqlite;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Input;
namespace Elwig.Helpers {
public static class AppDbUpdater {
public static readonly int RequiredSchemaVersion = 2;
// Don't forget to update value in Tests/fetch-resources.bat!
public static readonly int RequiredSchemaVersion = 42;
private static int _versionOffset = 0;
private static readonly Action<SqliteConnection>[] _updaters = new[] {
UpdateDbSchema_1_To_2, UpdateDbSchema_2_To_3
};
private static int VersionOffset = 0;
private static void ExecuteNonQuery(SqliteConnection cnx, string sql) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = sql;
cmd.ExecuteNonQuery();
}
public static async Task<Version> CheckDb() {
if (!File.Exists(App.Config.DatabaseFile))
throw new FileNotFoundException($"Die Datei \"{App.Config.DatabaseFile}\" exisitiert nicht");
private static object? ExecuteScalar(SqliteConnection cnx, string sql) {
using var cmd = cnx.CreateCommand();
cmd.CommandText = sql;
return cmd.ExecuteScalar();
}
long? applId, schemaVers;
using (var cnx = await AppDbContext.ConnectAsync()) {
public static string CheckDb() {
using var cnx = AppDbContext.Connect();
await cnx.QuickCheck();
var applId = (long?)ExecuteScalar(cnx, "PRAGMA application_id") ?? 0;
if (applId != 0x454C5747) throw new Exception("Invalid application_id of database");
applId = (long?)await cnx.ExecuteScalar("PRAGMA application_id") ?? 0;
if (applId != 0x454C5747)
throw new FileFormatException($"Invalid application_id in database (0x{applId:X08})");
var schemaVers = (long?)ExecuteScalar(cnx, "PRAGMA schema_version") ?? 0;
_versionOffset = (int)(schemaVers % 100);
if (_versionOffset != 0) {
// schema was modified manually/externally
// TODO issue warning
}
UpdateDbSchema(cnx, (int)(schemaVers / 100), RequiredSchemaVersion);
var userVers = (long?)ExecuteScalar(cnx, "PRAGMA user_version") ?? 0;
var major = userVers >> 24;
var minor = (userVers >> 16) & 0xFF;
var patch = userVers & 0xFFFF;
if (App.VersionMajor > major ||
(App.VersionMajor == major && App.VersionMinor > minor) ||
(App.VersionMajor == major && App.VersionMinor == minor && App.VersionPatch > patch))
{
long vers = (App.VersionMajor << 24) | (App.VersionMinor << 16) | App.VersionPatch;
ExecuteNonQuery(cnx, $"PRAGMA user_version = {vers}");
schemaVers = (long?)await cnx.ExecuteScalar("PRAGMA schema_version") ?? 0;
VersionOffset = (int)(schemaVers % 100);
if (VersionOffset != 0) {
// schema was modified manually/externally
// TODO issue warning
}
}
return $"{major}.{minor}.{patch}";
await UpdateDbSchema((int)(schemaVers / 100), RequiredSchemaVersion);
Version v;
using (var cnx = await AppDbContext.ConnectAsync()) {
var userVers = (long?)await cnx.ExecuteScalar("PRAGMA user_version") ?? 0;
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 << 8) | App.Version.Revision;
await cnx.ExecuteBatch($"PRAGMA user_version = {vers}");
}
}
return v;
}
private static void UpdateDbSchema(SqliteConnection cnx, int fromVersion, int toVersion) {
private static async Task UpdateDbSchema(int fromVersion, int toVersion) {
if (fromVersion == toVersion) {
return;
} else if (fromVersion > toVersion) {
throw new Exception("schema_version of database is too new");
} else if (toVersion - 1 > _updaters.Length) {
throw new Exception("Unable to update database schema: Updater not implemented");
} else if (fromVersion <= 0) {
throw new Exception("schema_version of database is invalid");
}
ExecuteNonQuery(cnx, "PRAGMA locking_mode = EXCLUSIVE");
ExecuteNonQuery(cnx, "BEGIN EXCLUSIVE");
for (int i = fromVersion; i < toVersion; i++) {
_updaters[i - 1](cnx);
var asm = Assembly.GetExecutingAssembly();
(int From, int To, string Name)[] scripts = [.. asm.GetManifestResourceNames()
.Where(n => n.StartsWith("Elwig.Resources.Sql."))
.Select(n => {
var p = n.Split(".")[^2].Split("-");
return (int.Parse(p[0]), int.Parse(p[1]), n);
})
.OrderBy(s => s.Item1).ThenBy(s => s.Item2)];
List<(int ToVersion, string File)> toExecute = [];
var vers = fromVersion;
while (vers < toVersion) {
var (_, to, name) = scripts.Last(s => s.From == vers);
toExecute.Add((to, name));
vers = to;
}
if (toExecute.Count == 0)
return;
var backup = Path.ChangeExtension(App.Config.DatabaseFile, $".v{fromVersion}.sqlite3");
File.Copy(App.Config.DatabaseFile, backup, true);
try {
using var cnx = await AppDbContext.ConnectAsync();
await cnx.ExecuteBatch("PRAGMA locking_mode = EXCLUSIVE");
await cnx.IntegrityCheck();
await cnx.ForeignKeyCheck();
foreach (var (to, script) in toExecute) {
await cnx.ExecuteEmbeddedScript(asm, script);
if (to == 42) {
await UpdateDbSchema_41_To_42(cnx);
}
}
await cnx.IntegrityCheck();
await cnx.ForeignKeyCheck();
await cnx.ExecuteBatch("VACUUM");
await cnx.ExecuteBatch($"PRAGMA schema_version = {toVersion * 100 + VersionOffset}");
} catch (Exception) {
File.Move(backup, App.Config.DatabaseFile, true);
throw;
} finally {
File.Delete(backup);
}
ExecuteNonQuery(cnx, "COMMIT");
ExecuteNonQuery(cnx, "VACUUM");
ExecuteNonQuery(cnx, $"PRAGMA schema_version = {toVersion * 100 + _versionOffset}");
}
private static void UpdateDbSchema_1_To_2(SqliteConnection cnx) {
ExecuteNonQuery(cnx, "DROP VIEW v_area_commitment");
ExecuteNonQuery(cnx, "ALTER TABLE delivery_part DROP COLUMN weighing_reason");
ExecuteNonQuery(cnx, "ALTER TABLE delivery_part ADD COLUMN weighing_reason TEXT CHECK(NOT (manual_weighing = FALSE AND weighing_reason IS NOT NULL))");
}
private static async Task UpdateDbSchema_41_To_42(SqliteConnection cnx) {
List<(int MgNr, string LfbisNr, string Name, string Address, int Plz)> members = [];
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = """
SELECT m.mgnr, m.lfbis_nr,
COALESCE(a.name, (COALESCE(m.prefix || ' ', '') || COALESCE(m.given_name || ' ', '') || m.name || COALESCE(' ' || m.middle_names, '') || COALESCE(' ' || m.suffix, ''))),
COALESCE(a.address, m.address),
COALESCE(aplz.plz, mplz.plz)
FROM member m
JOIN postal_dest mp ON (mp.country, mp.id) = (m.country, m.postal_dest)
JOIN AT_plz_dest ma ON (ma.country, ma.id) = (mp.country, mp.id)
JOIN AT_plz mplz ON mplz.plz = ma.plz
LEFT JOIN member_billing_address a ON a.mgnr = m.mgnr
LEFT JOIN postal_dest ap ON (ap.country, ap.id) = (a.country, a.postal_dest)
LEFT JOIN AT_plz_dest aa ON (aa.country, aa.id) = (ap.country, mp.id)
LEFT JOIN AT_plz aplz ON aplz.plz = aa.plz
WHERE organic
""";
using var reader = await cmd.ExecuteReaderAsync();
var header = await reader.GetColumnSchemaAsync();
while (await reader.ReadAsync()) {
members.Add((reader.GetInt32(0), reader.GetString(1), reader.GetString(2), reader.GetString(3), reader.GetInt32(4)));
}
}
private static void UpdateDbSchema_2_To_3(SqliteConnection cnx) { }
if (members.Count > 0)
InteractionService.ShowInformation("Datenbank-Update", "Es wird versucht, die Bio-Kontrollstellen-Informationen der Mitglieder automatisch abzurufen. Dies könnte einen kurzen Moment dauern.");
try {
Mouse.OverrideCursor = Cursors.Wait;
foreach (var m in members) {
string? oocId = null;
string? authCode = null;
var certs = await OrganicService.FetchTracesCertificates(lfbisNr: m.LfbisNr, address: (m.Name, m.Address, m.Plz.ToString()));
if (certs.Length > 0) {
oocId = certs[0].OperatorId;
authCode = certs[0].AuthorityCode;
}
if (oocId != null || authCode != null) {
oocId = oocId == null ? "NULL" : $"'{oocId}'";
authCode = authCode == null ? "NULL" : $"'{authCode}'";
await cnx.ExecuteBatch($"UPDATE member SET ooc_id = {oocId}, org_auth_code = {authCode} WHERE mgnr = {m.MgNr}");
}
}
} finally {
Mouse.OverrideCursor = null;
}
}
}
}
+220 -94
View File
@@ -1,130 +1,256 @@
using Elwig.Models.Entities;
using Microsoft.Data.Sqlite;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
namespace Elwig.Helpers.Billing {
public class Billing {
private readonly int Year;
private readonly int AvNr;
private readonly AppDbContext Context;
private readonly Dictionary<string, string> Attributes;
private readonly Dictionary<string, (decimal?, decimal?)> Modifiers;
private readonly Dictionary<string, (string, string?, string?, string?, int?, int?, decimal?)> AreaComTypes;
protected readonly int Year;
protected readonly Season Season;
protected readonly Dictionary<string, string> Attributes;
protected readonly Dictionary<string, (decimal?, decimal?)> Modifiers;
protected readonly Dictionary<string, (string, string?, string?, int?, decimal?)> AreaComTypes;
public Billing(int year, int avnr) {
protected Billing(int year, Season season,
Dictionary<string, string> attributes,
Dictionary<string, (decimal?, decimal?)> modifiers,
Dictionary<string, (string, string?, string?, int?, decimal?)> areaComTypes
) {
Year = year;
AvNr = avnr;
Context = new AppDbContext();
Attributes = Context.WineAttributes.ToDictionary(a => a.AttrId, a => a.Name);
Modifiers = Context.Modifiers.Where(m => m.Year == Year).ToDictionary(m => m.ModId, m => (m.Abs, m.Rel));
AreaComTypes = Context.AreaCommitmentTypes.ToDictionary(v => v.VtrgId, v => (v.SortId, v.AttrId1, v.AttrId2, v.Discriminator, v.MinKgPerHa, v.MaxKgPerHa, v.PenaltyAmount));
Season = season;
Attributes = attributes;
Modifiers = modifiers;
AreaComTypes = areaComTypes;
}
protected async Task DeleteInDb() {
protected static async Task<(Season, Dictionary<string, string>, Dictionary<string, (decimal?, decimal?)>, Dictionary<string, (string, string?, string?, int?, decimal?)>)> LoadData(AppDbContext ctx, int year) {
var season = await ctx.FetchSeasons(year).SingleOrDefaultAsync() ?? throw new ArgumentException("Invalid season");
var attributes = await ctx.FetchWineAttributes().ToDictionaryAsync(a => a.AttrId, a => a.Name);
var modifiers = await ctx.FetchModifiers(year).ToDictionaryAsync(m => m.ModId, m => (m.Abs, m.Rel));
var areaComTypes = ctx.AreaCommitmentTypes.ToDictionary(v => v.VtrgId, v => (v.SortId, v.AttrId, v.Discriminator, v.MinKgPerHa, v.PenaltyAmount));
return (season, attributes, modifiers, areaComTypes);
}
public static async Task<Billing> Create(int year) {
using var ctx = new AppDbContext();
var (season, attributes, modifiers, areaComTypes) = await LoadData(ctx, year);
return new Billing(year, season, attributes, modifiers, areaComTypes);
}
public async Task FinishSeason() {
using var cnx = await AppDbContext.ConnectAsync();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"DELETE FROM payment_delivery_part WHERE (year, avnr) = ({Year}, {AvNr})";
await cmd.ExecuteNonQueryAsync();
}
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"DELETE FROM payment_member WHERE (year, avnr) = ({Year}, {AvNr})";
await cmd.ExecuteNonQueryAsync();
}
await cnx.ExecuteBatch($"""
UPDATE season
SET (start_date, end_date) = (SELECT MIN(date), MAX(date) FROM delivery WHERE year = {Year})
WHERE year = {Year};
""");
}
public async Task Calculate() {
await DeleteInDb();
var tasks = new List<Task>();
foreach (var mgnr in Context.Members.Select(m => m.MgNr)) {
tasks.Add(Task.Run(() => CalculateMember(mgnr)));
}
await Task.WhenAll(tasks);
}
public static async Task<(Dictionary<string, int>, Dictionary<string, int>)> GetMemberRightsObligations(int mgnr, int year, SqliteConnection cnx) {
var rights = new Dictionary<string, int>();
var obligations = new Dictionary<string, int>();
using var cmd = cnx.CreateCommand();
cmd.CommandText = $"""
SELECT t.vtrgid,
SUM(COALESCE(area * min_kg_per_ha, 0)) / 10000 AS min_kg,
SUM(COALESCE(area * max_kg_per_ha, 0)) / 10000 AS max_kg
FROM area_commitment c
JOIN area_commitment_type t ON t.vtrgid = c.vtrgid
WHERE mgnr = {mgnr} AND (year_from IS NULL OR year_from <= {year}) AND (year_to IS NULL OR year_to >= {year})
GROUP BY t.vtrgid
ORDER BY LENGTH(t.vtrgid) DESC, t.vtrgid
""";
var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
var vtrgid = reader.GetString(0);
obligations[vtrgid] = reader.GetInt32(1);
rights[vtrgid] = reader.GetInt32(2);
}
return (rights, obligations);
}
public static async Task<Dictionary<string, int>> GetMemberBucketWeights(int mgnr, int year, SqliteConnection cnx) {
var buckets = new Dictionary<string, int>();
using var cmd = cnx.CreateCommand();
cmd.CommandText = $"""
SELECT bucket, weight
FROM v_bucket
WHERE (year, mgnr) = ({year}, {mgnr})
""";
var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
var bucket = reader.GetString(0);
buckets[bucket] = reader.GetInt32(1);
}
return buckets;
}
protected async Task CalculateMember(int mgnr) {
public async Task AutoAdjustBusinessShares(DateOnly date, int allowanceKg = 0, double allowanceShares = 0, int allowanceKgPerShare = 0, double allowanceRel = 0, int addMinShares = 1) {
if (addMinShares < 1) addMinShares = 1;
using var cnx = await AppDbContext.ConnectAsync();
var (rights, obligations) = await GetMemberRightsObligations(mgnr, Year, cnx);
await cnx.ExecuteBatch($"""
DELETE FROM member_history WHERE source = 'elwig' AND reason = '{MemberHistory.REASON_PRESCRIPTION}' AND deduct_year = {Year};
INSERT INTO member_history (histnr, from_mgnr, from_type, to_mgnr, to_type, date_notice, date_effective, reason, source, deduct_year, shares, value_per_share, currency, comment)
SELECT COALESCE((SELECT MAX(histnr) AS histnr FROM member_history), 0) + ROW_NUMBER() OVER(ORDER BY m.mgnr),
NULL, NULL, u.mgnr, 1, '{date:yyyy-MM-dd}', '{date:yyyy-MM-dd}', '{MemberHistory.REASON_PRESCRIPTION}', 'elwig', s.year,
CEIL((u.diff - {allowanceKg}.0 - {allowanceKgPerShare}.0 * u.shares) / s.max_kg_per_share
- {allowanceShares.ToString(CultureInfo.InvariantCulture)}
- {allowanceRel.ToString(CultureInfo.InvariantCulture)} * u.shares) AS adjust_shares,
s.share_value / POW(10, s.precision - 2), s.currency, 'durch automatische Nachzeichnung'
FROM v_total_under_delivery u
JOIN season s ON s.year = u.year
JOIN member m ON m.mgnr = u.mgnr
WHERE s.year = {Year} AND adjust_shares >= {addMinShares} AND m.active
""");
}
var deliveries = new List<(int, int, string, int, double, string, string[], string[])>();
public async Task UnAdjustBusinessShares() {
using var cnx = await AppDbContext.ConnectAsync();
await cnx.ExecuteBatch($"""
DELETE FROM member_history WHERE source = 'elwig' AND reason = '{MemberHistory.REASON_PRESCRIPTION}' AND deduct_year = {Year};
""");
}
public async Task CalculateBuckets(
bool? honorGebundenField = null,
bool? allowAttributesIntoLower = null,
bool? avoidUnderDeliveries = null,
SqliteConnection? cnx = null
) {
using var ctx = new AppDbContext();
var honorGebunden = honorGebundenField ?? Season.Billing_HonorGebunden;
var allowAttrsIntoLower = allowAttributesIntoLower ?? Season.Billing_AllowAttrsIntoLower;
var avoidUnderDlvrs = avoidUnderDeliveries ?? Season.Billing_AvoidUnderDeliveries;
var attrVals = ctx.WineAttributes.ToDictionary(a => a.AttrId, a => (a.IsStrict, a.FillLower));
var attrForced = attrVals.Where(a => a.Value.IsStrict && a.Value.FillLower == 0).Select(a => a.Key).ToArray();
var ownCnx = cnx == null;
cnx ??= await AppDbContext.ConnectAsync();
await ctx.GetMemberAreaCommitmentBuckets(Year, 0, cnx);
var inserts = new List<(int, int, int, string, int)>();
var deliveries = new List<(int, int, int, string, int, double, string, string?, string[], bool?)>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"""
SELECT did, dpnr, sortid, weight, kmw, qualid, attributes, modifiers
SELECT mgnr, did, dpnr, sortid, weight, kmw, qualid, attrid, modifiers, gebunden
FROM v_delivery
WHERE (year, mgnr) = ({Year}, {mgnr})
ORDER BY kmw DESC, weight DESC, did, dpnr
WHERE year = {Year}
ORDER BY mgnr, sortid, abgewertet ASC, {(honorGebunden ? "gebunden IS NOT NULL DESC, gebunden DESC," : "")}
attribute_prio DESC, COALESCE(attrid, '~'), kmw DESC, lsnr, dpnr
""";
var reader = await cmd.ExecuteReaderAsync();
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
deliveries.Add((
reader.GetInt32(0), reader.GetInt32(1), reader.GetString(2), reader.GetInt32(3),
reader.GetDouble(4), reader.GetString(5), reader.GetString(6).Split(","), reader.GetString(7).Split(",")
reader.GetInt32(0), reader.GetInt32(1), reader.GetInt32(2), reader.GetString(3), reader.GetInt32(4),
reader.GetDouble(5), reader.GetString(6),
reader.IsDBNull(7) ? null : reader.GetString(7),
reader.IsDBNull(8) ? [] : reader.GetString(8).Split(",").Order().ToArray(),
reader.IsDBNull(9) ? null : reader.GetBoolean(9)
));
}
}
foreach (var (did, dpnr, sortid, weight, kmw, qualid, attributes, modifiers) in deliveries) {
if (qualid == "WEI" || qualid == "RSW" || qualid == "LDW") {
int lastMgNr = 0;
Dictionary<string, AreaComBucket>? rightsAndObligations = null;
Dictionary<string, int> used = [];
foreach (var (mgnr, did, dpnr, sortid, weight, kmw, qualid, attrid, modifiers, gebunden) in deliveries) {
if (lastMgNr != mgnr) {
rightsAndObligations = await ctx.GetMemberAreaCommitmentBuckets(Year, mgnr);
used = [];
}
if ((honorGebunden && gebunden == false) ||
rightsAndObligations == null || rightsAndObligations.Count == 0 ||
qualid == "WEI" || qualid == "RSW" || qualid == "LDW")
{
// Explizit als ungebunden markiert,
// Mitglied hat keine Flächenbindungen, oder
// Nicht mindestens Qualitätswein (QUW)
using var cmd = cnx.CreateCommand();
// TODO
cmd.CommandText = $"""
INSERT INTO payment_delivery_part (year, did, dpnr, avnr, mod_abs, mod_rel, )
VALUES ({Year}, {did}, {dpnr}, {AvNr}, )
""";
await cmd.ExecuteNonQueryAsync();
// -> ungebunden
inserts.Add((did, dpnr, 0, "_", weight));
continue;
}
// TODO
int w = weight;
var attributes = attrid == null ? [] : new string[] { attrid };
var isStrict = attrid != null && attrVals[attrid].IsStrict;
foreach (var p in Utils.Permutate(attributes, attributes.Intersect(attrForced))) {
var c = p.Count();
var key = sortid + string.Join("", p);
if (rightsAndObligations.TryGetValue(key, out AreaComBucket value)) {
int i = (c == 0) ? 1 : 2;
var u = used.GetValueOrDefault(key, 0);
var vr = Math.Max(0, Math.Min(value.Right - u, w));
var vo = Math.Max(0, Math.Min(value.Obligation - u, w));
var v = (attributes.Length == c || attributes.Select(a => !attrVals[a].IsStrict ? 2 : attrVals[a].FillLower).Min() == 2) ? vr : vo;
used[key] = u + v;
if (key.Length > 2 && !isStrict) used[key[..2]] = used.GetValueOrDefault(key[..2], 0) + v;
inserts.Add((did, dpnr, i, key[2..], v));
w -= v;
}
if (w == 0 || (!allowAttrsIntoLower && isStrict)) break;
}
inserts.Add((did, dpnr, 0, "_", w));
lastMgNr = mgnr;
}
await cnx.ExecuteBatch($"UPDATE delivery_part_bucket SET value = 0 WHERE year = {Year}");
if (inserts.Count > 0) {
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
SET discr = excluded.discr, value = value + excluded.value;
""");
}
if (!avoidUnderDlvrs) {
if (ownCnx) await cnx.DisposeAsync();
return;
}
// FIXME avoidUnderDelivery-calculations not always right!
var underDeliveries = new Dictionary<(int, string), int>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"""
SELECT c.mgnr, c.bucket, COALESCE(p.weight, 0) - c.min_kg AS diff
FROM v_area_commitment_bucket c
LEFT JOIN v_payment_bucket p ON (p.year, p.mgnr, p.bucket) = (c.year, c.mgnr, c.bucket)
WHERE c.year = {Year} AND LENGTH(c.bucket) = 2 AND diff < 0
""";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
underDeliveries[(reader.GetInt32(0), reader.GetString(1))] = reader.GetInt32(2);
}
}
var fittingBuckets = new Dictionary<(int, string), int>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"""
SELECT c.mgnr, c.bucket, COALESCE(p.weight, 0) - c.min_kg AS diff
FROM v_area_commitment_bucket c
LEFT JOIN v_payment_bucket p ON (p.year, p.mgnr, p.bucket) = (c.year, c.mgnr, c.bucket)
WHERE c.year = {Year} AND LENGTH(c.bucket) = 3 AND diff > 0
""";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
fittingBuckets[(reader.GetInt32(0), reader.GetString(1))] = reader.GetInt32(2);
}
}
foreach (var item in fittingBuckets) {
var mgnr = item.Key.Item1;
var id = item.Key.Item2[..2];
var attr = item.Key.Item2[2..];
var available = item.Value;
var needed = -underDeliveries.GetValueOrDefault((mgnr, id), 0);
if (needed <= 0) continue;
var fittingDeliveries = new List<(int, int, int, int)>();
using (var cmd = cnx.CreateCommand()) {
cmd.CommandText = $"""
SELECT d.did, d.dpnr, d.weight, b.value
FROM v_delivery d
JOIN delivery_part_bucket b ON (b.year, b.did, b.dpnr) = (d.year, d.did, d.dpnr) AND b.discr = '{attr}'
WHERE d.year = {Year} AND mgnr = {mgnr} AND sortid = '{id}' AND attrid = '{attr}'
ORDER BY kmw ASC, lsnr DESC, d.dpnr DESC
""";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
fittingDeliveries.Add((
reader.GetInt32(0), reader.GetInt32(1), reader.GetInt32(2), reader.GetInt32(3)
));
}
}
var negChanges = new List<(int, int, int, int)>();
var posChanges = new List<(int, int, int, int)>();
foreach (var (did, dpnr, _, w) in fittingDeliveries) {
int v = Math.Min(needed, w);
needed -= v;
posChanges.Add((did, dpnr, 1, v));
negChanges.Add((did, dpnr, 2, w - v));
if (needed == 0) break;
}
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
SET value = value + excluded.value;
INSERT INTO delivery_part_bucket (year, did, dpnr, bktnr, discr, value)
VALUES {string.Join(",\n ", negChanges.Select(i => $"({Year}, {i.Item1}, {i.Item2}, {i.Item3}, '', {i.Item4})"))}
ON CONFLICT DO UPDATE
SET value = excluded.value;
""");
if (ownCnx) await cnx.DisposeAsync();
}
}
}
}
+489
View File
@@ -0,0 +1,489 @@
using Newtonsoft.Json;
using NJsonSchema;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
namespace Elwig.Helpers.Billing {
public class BillingData {
public enum CalculationMode { Elwig, WgMaster }
public enum CurveMode { Oe, Kmw }
public record struct Curve(CurveMode Mode, Dictionary<double, decimal> Normal, Dictionary<double, decimal>? Gebunden);
public static JsonSchema? Schema { get; private set; }
public static async Task Init() {
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Elwig.Resources.Schemas.PaymentVariantData.json");
Schema = await JsonSchema.FromJsonAsync(stream ?? throw new ArgumentException("JSON schema not found"));
}
public readonly JsonObject Data;
public readonly CalculationMode Mode;
public bool ConsiderDelieryModifiers {
get => GetConsider("consider_delivery_modifiers");
set => SetConsider(value, "consider_delivery_modifiers");
}
public bool ConsiderContractPenalties {
get => GetConsider("consider_contract_penalties");
set => SetConsider(value, "consider_contract_penalties");
}
public bool ConsiderTotalPenalty {
get => GetConsider("consider_total_penalty");
set => SetConsider(value, "consider_total_penalty");
}
public bool ConsiderAutoBusinessShares {
get => GetConsider("consider_auto_business_shares");
set => SetConsider(value, "consider_auto_business_shares");
}
public bool ConsiderCustomModifiers {
get => GetConsider("consider_custom_modifiers");
set => SetConsider(value, "consider_custom_modifiers");
}
public double NetWeightModifier {
get => GetWeightModifier("net_weight_modifier", "Rebelzuschlag");
set => SetWeightModifier(value, "net_weight_modifier", "Rebelzuschlag");
}
public double GrossWeightModifier {
get => GetWeightModifier("gross_weight_modifier");
set => SetWeightModifier(value, "gross_weight_modifier");
}
private bool GetConsider(string name, string? wgMasterName = null) {
return ((Mode == CalculationMode.Elwig) ? Data[name] : Data[wgMasterName ?? ""])?.AsValue().GetValue<bool>() ?? false;
}
private void SetConsider(bool value, string name, string? wgMasterName = null) {
if (Mode == CalculationMode.WgMaster && wgMasterName == null) {
return;
} else if (value) {
Data[(Mode == CalculationMode.Elwig) ? name : wgMasterName ?? ""] = value;
} else {
Data.Remove((Mode == CalculationMode.Elwig) ? name : wgMasterName ?? "");
}
}
private double GetWeightModifier(string name, string? wgMasterName = null) {
var isElwig = (Mode == CalculationMode.Elwig);
var val = (isElwig ? Data[name] : Data[wgMasterName ?? ""])?.AsValue().GetValue<double>() ?? 0;
return isElwig ? val : val / 100.0;
}
private void SetWeightModifier(double value, string name, string? wgMasterName = null) {
var isElwig = (Mode == CalculationMode.Elwig);
if (Mode == CalculationMode.WgMaster && wgMasterName == null) {
return;
} else if (value != 0) {
Data[isElwig ? name : wgMasterName ?? ""] = isElwig ? value : value * 100.0;
} else {
Data.Remove(isElwig ? name : wgMasterName ?? "");
}
}
public BillingData(JsonObject data) {
Data = data;
var mode = Data["mode"]?.GetValue<string>();
Mode = (mode == "elwig") ? CalculationMode.Elwig : CalculationMode.WgMaster;
}
public BillingData() {
Data = new JsonObject {
["mode"] = "elwig",
["version"] = 1,
};
Mode = CalculationMode.Elwig;
}
protected static JsonObject ParseJson(string json) {
if (Schema == null) throw new InvalidOperationException("Schema has to be initialized first");
try {
var errors = Schema.Validate(json);
if (errors.Count != 0) throw new ArgumentException("Invalid JSON data");
return JsonNode.Parse(json)?.AsObject() ?? throw new ArgumentException("Invalid JSON data");
} catch (JsonReaderException) {
throw new ArgumentException("Invalid JSON data");
}
}
public static BillingData FromJson(string json) {
return new(ParseJson(json));
}
public string ToJsonString(JsonSerializerOptions? options = null) {
return Data.ToJsonString(options);
}
protected JsonArray GetCurvesEntry() {
return Data[Mode == CalculationMode.Elwig ? "curves" : "Kurven"]?.AsArray() ?? throw new InvalidOperationException();
}
protected JsonNode GetPaymentEntry() {
return Data[Mode == CalculationMode.Elwig ? "payment" : "AuszahlungSorten"] ?? throw new InvalidOperationException();
}
protected JsonObject? GetQualityEntry() {
return Data[Mode == CalculationMode.Elwig ? "quality" : "AuszahlungSortenQualitätsstufe"]?.AsObject();
}
private static Dictionary<double, decimal> GetCurveData(JsonObject data, CurveMode mode) {
var dict = new Dictionary<double, decimal>();
foreach (var (index, price) in data) {
double idx;
bool? gtlt = index.StartsWith('>') ? true : index.StartsWith('<') ? false : null;
if (index.EndsWith("kmw")) {
idx = double.Parse(index[(gtlt != null ? 1 : 0)..^3], CultureInfo.InvariantCulture);
if (mode == CurveMode.Oe) {
idx = Utils.KmwToOe(idx);
}
} else if (index.EndsWith("oe")) {
idx = double.Parse(index[(gtlt != null ? 1 : 0)..^2], CultureInfo.InvariantCulture);
if (mode == CurveMode.Kmw) {
idx = Utils.OeToKmw(idx);
}
} else {
throw new InvalidOperationException();
}
if (gtlt == true) {
idx = Math.BitIncrement(idx);
} else if (gtlt == false) {
idx = Math.BitDecrement(idx);
}
dict[idx] = price?.AsValue().GetValue<decimal>() ?? throw new InvalidOperationException();
}
return dict;
}
protected Dictionary<int, Curve> GetCurves() {
var dict = new Dictionary<int, Curve>();
var curves = GetCurvesEntry();
foreach (var c in curves) {
var obj = c?.AsObject() ?? throw new InvalidOperationException();
var id = obj["id"]?.GetValue<int>() ?? throw new InvalidOperationException();
var cMode = (obj["mode"]?.GetValue<string>() == "kmw") ? CurveMode.Kmw : CurveMode.Oe;
double quw = cMode == CurveMode.Oe ? 73 : 15;
Dictionary<double, decimal> c1;
Dictionary<double, decimal>? c2 = null;
var norm = obj["data"];
if (norm is JsonObject) {
c1 = GetCurveData(norm.AsObject(), cMode);
} else if (norm?.AsValue().TryGetValue(out decimal v) == true) {
c1 = new() { { quw, v } };
} else {
throw new InvalidOperationException();
}
var geb = obj["geb"];
if (geb is JsonObject) {
c2 = GetCurveData(geb.AsObject(), cMode);
} else if (geb?.AsValue().TryGetValue(out decimal v) == true) {
var splitVal = GetCurveValueAt(c1, quw);
c2 = c1.ToDictionary(e => e.Key, e => e.Value + (e.Key >= quw ? v : 0));
c2[quw] = splitVal + v;
c2[Math.BitDecrement(quw)] = splitVal;
}
dict.Add(id, new(cMode, c1, c2));
}
return dict;
}
protected static Dictionary<RawVaribute, JsonValue> GetSelection(JsonNode value, IEnumerable<RawVaribute> vaributes) {
if (value is JsonValue flatRate) {
return vaributes.ToDictionary(e => e, _ => flatRate);
} if (value is not JsonObject data) {
throw new InvalidOperationException();
}
Dictionary<RawVaribute, JsonValue> dict;
if (data["default"] is JsonValue def) {
dict = vaributes.ToDictionary(e => e, _ => def);
} else {
dict = [];
}
var conv = data
.Where(p => p.Key != "default")
.Select(p => (new RawVaribute(p.Key), p.Value))
.OrderBy(p => (p.Item1.SortId != null ? 10 : 0) + (p.Item1.AttrId != null ? 12 : 0) + (p.Item1.CultId != null ? 11 : 0))
.ToList();
foreach (var (idx, v) in conv) {
var curve = v?.AsValue() ?? throw new InvalidOperationException();
foreach (var i in vaributes.Where(e =>
(idx.SortId == null || idx.SortId == e.SortId) &&
(idx.AttrId == null || idx.AttrId == e.AttrId) &&
(idx.CultId == null || idx.CultId == e.CultId))) {
dict[i] = curve;
}
}
return dict;
}
public static decimal GetCurveValueAt(Dictionary<double, decimal> curve, double key) {
if (curve.Count == 1) return curve.First().Value;
var lt = curve.Keys.Where(v => v <= key);
var gt = curve.Keys.Where(v => v >= key);
if (!lt.Any()) {
return curve[gt.Min()];
} else if (!gt.Any()) {
return curve[lt.Max()];
}
var max = lt.Max();
var min = gt.Min();
if (max == min) return curve[key];
var p1 = ((decimal)key - (decimal)min) / ((decimal)max - (decimal)min);
var p2 = 1 - p1;
return curve[min] * p2 + curve[max] * p1;
}
protected static JsonObject GraphToJson(Graph graph, string mode) {
var x = graph.DataX;
var y = graph.DataY;
var prec = graph.Precision;
try {
return new JsonObject() {
["15kmw"] = Math.Round(y.Distinct().Single(), prec)
};
} catch { }
var data = new JsonObject();
if (y[0] != y[1]) {
data[$"{x[0]}{mode}"] = Math.Round(y[0], prec);
}
for (int i = 1; i < x.Length - 1; i++) {
var d1 = Math.Round(y[i] - y[i - 1], prec);
var d2 = Math.Round(y[i + 1] - y[i], prec);
if (d1 != d2) {
data[$"{x[i]}{mode}"] = Math.Round(y[i], prec);
}
}
if (y[^1] != y[^2]) {
data[$"{x[^1]}{mode}"] = Math.Round(y[^1], prec);
}
return data;
}
protected static JsonNode GraphEntryToJson(GraphEntry entry) {
try {
if (entry.GebundenFlatBonus == null) {
return JsonValue.Create((decimal)entry.DataGraph.DataY.Distinct().Single());
}
} catch { }
var curve = new JsonObject {
["id"] = entry.Id,
["mode"] = entry.Mode.ToString().ToLower(),
};
curve["data"] = GraphToJson(entry.DataGraph, entry.Mode.ToString().ToLower());
if (entry.GebundenFlatBonus != null) {
curve["geb"] = (decimal)entry.GebundenFlatBonus;
} else if (entry.GebundenGraph != null) {
curve["geb"] = GraphToJson(entry.GebundenGraph, entry.Mode.ToString().ToLower());
}
return curve;
}
protected static (Dictionary<string, List<string>>, Dictionary<decimal, List<string>>) GetReverseKeys(JsonObject data, bool strict = true) {
Dictionary<string, List<string>> rev1 = [];
Dictionary<decimal, List<string>> rev2 = [];
foreach (var (k, v) in data) {
if (k == "default" || (strict && (k.StartsWith('/') || !k.Contains('/'))) || v is not JsonValue val) {
continue;
} else if (val.TryGetValue<decimal>(out var dec)) {
rev2[dec] = rev2.GetValueOrDefault(dec) ?? [];
rev2[dec].Add(k);
} else if (val.TryGetValue<string>(out var cur)) {
rev1[cur] = rev1.GetValueOrDefault(cur) ?? [];
rev1[cur].Add(k);
}
}
return (rev1, rev2);
}
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) {
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, 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) {
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, originalData, vaributes, useDefault);
return;
}
}
}
var attributes = data
.Select(e => e.Key)
.Where(k => k.Length > 3 && k.Contains('/'))
.Select(k => k.Split('/')[1])
.Distinct()
.ToList();
foreach (var idx in attributes) {
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)) {
foreach (var k in myKs) data.Remove(k);
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)) {
foreach (var k in myKs) data.Remove(k);
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;
}
}
}
}
if (!useDefault)
return;
var keys = data.Select(p => p.Key).ToList();
foreach (var k in keys) {
if (k.Length == 3 && k.EndsWith('/') && !keys.Contains(k[..2])) {
data.Remove(k, out var val);
data.Add(k[..2], val);
} 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)
.Where(e => e.Length > 0 && !e.Contains('-') && e != "default")
.Distinct()
.ToList();
foreach (var idx in keyVaributes) {
var len = data.Count(e => e.Key == idx || (e.Key.Length > idx.Length && e.Key.StartsWith(idx) && e.Key[idx.Length] == '-'));
foreach (var (v, ks) in rev1) {
var myKs = ks.Where(k => k == idx || (k.Length > idx.Length && k.StartsWith(idx) && k[idx.Length] == '-' && !data.ContainsKey(k[idx.Length..]))).ToList();
if (myKs.Count == len) {
foreach (var k in myKs) {
if (k != idx) data.Remove(k);
}
}
}
foreach (var (v, ks) in rev2) {
var myKs = ks.Where(k => k == idx || (k.Length > idx.Length && k.StartsWith(idx) && k[idx.Length] == '-' && !data.ContainsKey(k[idx.Length..]))).ToList();
if (myKs.Count == len) {
foreach (var k in myKs) {
if (k != idx) data.Remove(k);
}
}
}
}
}
public static BillingData FromGraphEntries(
IEnumerable<GraphEntry> graphEntries,
BillingData? origData = null,
IEnumerable<RawVaribute>? vaributes = null,
bool useDefaultPayment = true,
bool useDefaultQuality = true
) {
var payment = new JsonObject();
var qualityWei = new JsonObject();
var curves = new JsonArray();
int curveId = 0;
foreach (var entry in graphEntries) {
var curve = GraphEntryToJson(entry);
JsonValue node;
if (curve is JsonObject obj) {
obj["id"] = ++curveId;
node = JsonValue.Create($"curve:{curveId}");
curves.Add(obj);
} else if (curve is JsonValue val && val.TryGetValue<decimal>(out var flat)) {
node = JsonValue.Create(flat);
} else {
continue;
}
foreach (var c in entry.Vaributes) {
var v = new RawVaribute(c.Variety!.SortId, c.Attribute?.AttrId ?? "", c.Cultivation?.CultId);
if (v.CultId == "") v.CultId = null;
if (entry.Abgewertet) {;
qualityWei[v.ToString()] = node.DeepClone();
} else {
payment[v.ToString()] = node.DeepClone();
}
}
}
CollapsePaymentData(payment, payment.DeepClone().AsObject(), vaributes ?? [.. payment.Select(e => new RawVaribute(e.Key))], useDefaultPayment);
CollapsePaymentData(qualityWei, qualityWei.DeepClone().AsObject(), vaributes ?? [.. qualityWei.Select(e => new RawVaribute(e.Key))], useDefaultQuality);
BillingData data = origData != null && origData.Mode == CalculationMode.Elwig ? new BillingData((JsonObject)origData.Data.DeepClone()) : new BillingData();
if (payment.Count == 0) {
data.Data["payment"] = 0;
} else if (payment.Count == 1 && payment.First().Key == "default") {
data.Data["payment"] = payment.Single().Value?.DeepClone();
} else {
data.Data["payment"] = payment;
}
if (qualityWei.Count == 1 && qualityWei.First().Key == "default") {
data.Data["quality"] = new JsonObject() {
["WEI"] = qualityWei.Single().Value?.DeepClone()
};
} else if (qualityWei.Count >= 1) {
data.Data["quality"] = new JsonObject() {
["WEI"] = qualityWei
};
} else {
data.Data.Remove("quality");
}
data.Data["curves"] = curves;
return data;
}
}
}
+246
View File
@@ -0,0 +1,246 @@
using Elwig.Models.Entities;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Elwig.Helpers.Billing {
public class BillingVariant : Billing {
protected readonly int AvNr;
protected PaymentVar PaymentVariant;
protected PaymentBillingData Data;
protected BillingVariant(int year, int avnr, Season season,
Dictionary<string, string> attributes,
Dictionary<string, (decimal?, decimal?)> modifiers,
Dictionary<string, (string, string?, string?, int?, decimal?)> areaComTypes,
PaymentVar paymentVar, PaymentBillingData data) :
base(year, season, attributes, modifiers, areaComTypes) {
AvNr = avnr;
PaymentVariant = paymentVar;
Data = data;
}
protected static async Task<(PaymentVar, PaymentBillingData)> LoadData(AppDbContext ctx, int year, int avnr) {
var paymentVar = await ctx.PaymentVariants.Where(v => v.Year == year && v.AvNr == avnr).SingleAsync();
var data = PaymentBillingData.FromJson(paymentVar.Data, await Utils.GetVaributes(ctx, year, onlyDelivered: false));
return (paymentVar, data);
}
public static async Task<BillingVariant> Create(int year, int avnr) {
using var ctx = new AppDbContext();
var (season, attributes, modifiers, areaComTypes) = await LoadData(ctx, year);
var (paymentVar, data) = await LoadData(ctx, year, avnr);
return new BillingVariant(year, avnr, season, attributes, modifiers, areaComTypes, paymentVar, data);
}
public async Task Calculate(bool strictPrices = true, bool? honorGebunden = null, bool? allowAttrsIntoLower = null, bool? avoidUnderDeliveries = null) {
if (PaymentVariant == null || Data == null) throw new Exception("Call Load before Calculate");
using var cnx = await AppDbContext.ConnectAsync();
using var tx = await cnx.BeginTransactionAsync();
await CalculateBuckets(honorGebunden, allowAttrsIntoLower, avoidUnderDeliveries, cnx);
await DeleteInDb(cnx);
await SetCalcTime(cnx);
KeyNotFoundException? exception = null;
try {
await CalculatePrices(cnx, strictPrices);
} catch (KeyNotFoundException e) {
if (strictPrices) throw;
exception = e;
}
if (Data.ConsiderDelieryModifiers) {
await CalculateDeliveryModifiers(cnx);
}
if (Data.ConsiderCustomModifiers) {
await CalculateMemberModifiers(cnx);
}
await tx.CommitAsync();
if (exception != null)
throw exception;
}
public async Task Commit() {
await Revert();
using var cnx = await AppDbContext.ConnectAsync();
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,
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)) +
IIF({Data.ConsiderCustomModifiers}, COALESCE(x.amount, 0), 0)
AS 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
AND l.avnr = (SELECT avnr
FROM payment_variant
WHERE year = s.year AND NOT test_variant
ORDER BY COALESCE(transfer_date, date) DESC, avnr DESC
LIMIT 1)
LEFT JOIN (SELECT year, MAX(tgnr) AS tgnr FROM credit GROUP BY year) t ON t.year = s.year
JOIN (SELECT DISTINCT year, mgnr FROM delivery) d ON d.year = s.year
JOIN member m ON m.mgnr = d.mgnr
LEFT JOIN payment_member lp ON (lp.year, lp.avnr, lp.mgnr) = (l.year, l.avnr, m.mgnr)
LEFT JOIN payment_member p ON (p.year, p.avnr, p.mgnr) = (v.year, v.avnr, m.mgnr)
LEFT JOIN credit lc ON (lc.year, lc.avnr, lc.mgnr) = (l.year, l.avnr, m.mgnr)
LEFT JOIN v_penalty_business_shares b ON (b.year, b.mgnr) = (s.year, m.mgnr)
LEFT JOIN v_penalty_area_commitments u ON (u.year, u.mgnr) = (s.year, m.mgnr)
LEFT JOIN v_auto_business_shares a ON (a.year, a.mgnr) = (s.year, m.mgnr)
LEFT JOIN payment_custom x ON (x.year, x.mgnr) = (s.year, m.mgnr)
WHERE s.year = {Year} AND v.avnr = {AvNr} AND p.amount != COALESCE(lp.amount, 0);
""");
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 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 cnx.ExecuteBatch($"""
UPDATE payment_variant SET calc_time = UNIXEPOCH() WHERE (year, avnr) = ({Year}, {AvNr})
""");
}
protected async Task DeleteInDb(SqliteConnection 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});
UPDATE payment_variant SET calc_time = NULL WHERE (year, avnr) = ({Year}, {AvNr});
""");
}
protected async Task CalculateMemberModifiers(SqliteConnection cnx) {
if (App.Client.IsMatzen) {
var lastYears = 3;
var multiplier = 0.50;
var includePredecessor = true;
var modName = "Treue%";
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.weight_total * COALESCE(m.abs, 0)),
COALESCE(m.rel, 0)
FROM (SELECT {Year} AS year, m.mgnr,
ROUND(AVG(COALESCE(a.weight_total, b.weight_total)) * {multiplier}) AS baseline,
COUNT(*) = {lastYears} AND MIN(COALESCE(a.weight_total, b.weight_total)) > 0 AS allowed
FROM member m
LEFT JOIN v_stat_member a ON a.mgnr = m.mgnr
FULL OUTER JOIN v_stat_member b ON b.mgnr = m.predecessor_mgnr AND b.year = a.year AND {(includePredecessor ? "TRUE" : "FALSE")}
WHERE a.year > {Year} - {lastYears}
GROUP BY m.mgnr
HAVING allowed) c
JOIN v_stat_member s ON (s.year, s.mgnr) = (c.year, c.mgnr)
LEFT JOIN modifier m ON m.year = c.year AND m.name LIKE '{modName}'
WHERE weight_total >= baseline
ON CONFLICT DO UPDATE
SET mod_abs = mod_abs + excluded.mod_abs,
mod_rel = mod_rel + excluded.mod_rel
""");
}
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
JOIN season s ON s.year = x.year
WHERE x.year = {Year}
ON CONFLICT DO UPDATE
SET mod_abs = mod_abs + excluded.mod_abs,
mod_rel = mod_rel + excluded.mod_rel
""");
}
protected async Task CalculatePrices(SqliteConnection cnx, 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 = $"""
SELECT d.year, d.did, d.dpnr, b.bktnr, d.sortid, d.attrid, d.cultid, b.discr, b.value, d.oe, d.kmw, d.qualid, COALESCE(a.area_com, TRUE)
FROM delivery_part_bucket b
JOIN v_delivery d ON (d.year, d.did, d.dpnr) = (b.year, b.did, b.dpnr)
LEFT JOIN v_wine_attribute a ON a.attrid = d.attrid
WHERE b.year = {Year}
""";
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync()) {
parts.Add((
reader.GetInt32(0), reader.GetInt32(1), reader.GetInt32(2), reader.GetInt32(3),
reader.GetString(4), reader.IsDBNull(5) ? null : reader.GetString(5),
reader.IsDBNull(6) ? null : reader.GetString(6), reader.GetString(7),
reader.GetInt32(8), reader.GetDouble(9), reader.GetDouble(10), reader.GetString(11),
reader.GetBoolean(12)
));
}
}
var inserts = new List<(int Year, int DId, int DPNr, int BktNr, long Price, long Amount)>();
foreach (var part in parts) {
if (part.Value == 0)
continue;
var ungeb = part.Discr == "_";
var payAttrId = (part.Discr is "" or "_") ? null : part.Discr;
var attrId = part.AttrAreaCom ? payAttrId : part.AttrId;
var geb = !ungeb && (payAttrId == attrId || !part.AttrAreaCom);
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));
}
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 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
WHERE d.year = {Year}
ON CONFLICT DO UPDATE
SET mod_rel = mod_rel + excluded.mod_rel;
INSERT INTO payment_delivery_part (year, did, dpnr, avnr, net_amount, mod_abs, mod_rel)
SELECT d.year, d.did, d.dpnr, {AvNr}, 0, COALESCE(m.abs, 0) * d.weight, COALESCE(m.rel, 0)
FROM delivery_part d
LEFT JOIN delivery_part_modifier p ON (p.year, p.did, p.dpnr) = (d.year, d.did, d.dpnr)
LEFT JOIN modifier m ON (m.year, m.modid) = (d.year, p.modid)
WHERE d.year = {Year}
ON CONFLICT DO UPDATE
SET mod_abs = mod_abs + excluded.mod_abs,
mod_rel = mod_rel + excluded.mod_rel;
""");
}
}
}
+108
View File
@@ -0,0 +1,108 @@
using Elwig.Models.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
namespace Elwig.Helpers.Billing {
public class EditBillingData : BillingData {
protected readonly IEnumerable<RawVaribute> Vaributes;
public EditBillingData(JsonObject data, IEnumerable<RawVaribute> vaributes) :
base(data) {
Vaributes = vaributes;
}
public static EditBillingData FromJson(string json, IEnumerable<RawVaribute> vaributes) {
return new(ParseJson(json), vaributes);
}
private (Dictionary<int, Curve>, Dictionary<int, List<RawVaribute>>) GetGraphEntries(JsonNode root) {
Dictionary<int, List<string>> dict1 = [];
Dictionary<decimal, List<string>> dict2 = [];
if (root is JsonObject paymentObj) {
foreach (var (selector, node) in paymentObj) {
var val = node?.AsValue();
if (val == null) {
continue;
} else if (val.TryGetValue<decimal>(out var price)) {
if (!dict2.ContainsKey(price)) dict2[price] = [];
dict2[price].Add(selector);
} else if (val.TryGetValue<string>(out var curve)) {
var idx = int.Parse(curve.Split(":")[1] ?? "0");
if (!dict1.ContainsKey(idx)) dict1[idx] = [];
dict1[idx].Add(selector);
}
}
} else if (root is JsonValue paymentVal) {
if (paymentVal.TryGetValue<decimal>(out var price)) {
if (!dict2.ContainsKey(price)) dict2[price] = [];
dict2[price].Add("default");
} else if (paymentVal.TryGetValue<string>(out var curve)) {
var idx = int.Parse(curve.Split(":")[1] ?? "0");
if (!dict1.ContainsKey(idx)) dict1[idx] = [];
dict1[idx].Add("default");
}
}
var virtOffset = dict1.Count > 0 ? dict1.Max(e => e.Key) + 1 : 1;
Dictionary<int, Curve> curves = GetCurves();
decimal[] virtCurves = [.. dict2.Keys.Order()];
for (int i = 0; i < virtCurves.Length; i++) {
var idx = virtCurves[i];
dict1[i + virtOffset] = dict2[idx];
curves[i + virtOffset] = new Curve(CurveMode.Oe, new() { { 73, idx } }, null);
}
Dictionary<int, List<RawVaribute>> dict3 = curves.ToDictionary(c => c.Key, _ => new List<RawVaribute>());
foreach (var (selector, value) in GetSelection(root, Vaributes)) {
int? idx = null;
if (value.TryGetValue<decimal>(out var val)) {
idx = Array.IndexOf(virtCurves, val) + virtOffset;
} else if (value.TryGetValue<string>(out var str)) {
idx = int.Parse(str.Split(":")[1]);
}
if (idx != null)
dict3[(int)idx].Add(selector);
}
return (curves, dict3);
}
private static async Task<List<GraphEntry>> CreateGraphEntries(
AppDbContext ctx, int precision,
Dictionary<int, Curve> curves,
Dictionary<int, List<RawVaribute>> entries
) {
var vars = await ctx.FetchWineVarieties().ToDictionaryAsync(v => v.SortId, v => v);
var attrs = await ctx.FetchWineAttributes().ToDictionaryAsync(a => a.AttrId, a => a);
var cults = await ctx.FetchWineCultivations().ToDictionaryAsync(c => c.CultId, c => c);
return entries
.Select(e => new GraphEntry(e.Key, precision, curves[e.Key], e.Value
.Select(s => new Varibute(s, vars, attrs, cults))
.ToList()))
.ToList();
}
public async Task<IEnumerable<GraphEntry>> GetPaymentGraphEntries(AppDbContext ctx, Season season) {
var root = GetPaymentEntry();
var (curves, entries) = GetGraphEntries(root);
return (await CreateGraphEntries(ctx, season.Precision, curves, entries)).Where(e => e.Vaributes.Count > 0);
}
public async Task<IEnumerable<GraphEntry>> GetQualityGraphEntries(AppDbContext ctx, Season season, int idOffset = 0) {
var root = GetQualityEntry();
if (root == null || root["WEI"] is not JsonNode qualityWei)
return [];
var (curves, entries) = GetGraphEntries(qualityWei);
var list = (await CreateGraphEntries(ctx, season.Precision, curves, entries)).Where(e => e.Vaributes.Count > 0);
foreach (var e in list) {
e.Id += idOffset;
e.Abgewertet = true;
}
return list;
}
}
}
+65 -77
View File
@@ -1,119 +1,107 @@
using ScottPlot;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;
namespace Elwig.Helpers.Billing {
public class Graph : ICloneable {
public string Type { get; set; }
public int Num { get; set; }
private int MinX { get; set; }
private int MaxX { get; set; }
public string Contracts { get; set; }
public readonly int Precision;
public double[] DataX { get; set; }
public double[] DataY { get; set; }
public int MinX { get; set; }
public int MaxX { get; set; }
public Graph(int num, int minX, int maxX) {
Type = "oe";
Num = num;
Contracts = "";
public Graph(int precision, int minX, int maxX) {
Precision = precision;
MinX = minX;
MaxX = maxX;
DataX = DataGen.Range(MinX, MaxX + 1);
DataY = DataGen.Zeros(MaxX - MinX + 1);
DataX = Enumerable.Range(minX, maxX - minX + 1).Select(n => (double)n).ToArray();
DataY = new double[DataX.Length];
}
public Graph(string type, int num, JsonObject graphData, string contracts, int minX, int maxX) {
Type = type;
Num = num;
Contracts = contracts;
public Graph(Dictionary<double, decimal> data, int precision, int minX, int maxX) {
Precision = precision;
MinX = minX;
MaxX = maxX;
DataX = DataGen.Range(MinX, MaxX + 1);
DataY = DataGen.Zeros(MaxX - MinX + 1);
ParseGraphData(graphData);
DataX = Enumerable.Range(minX, maxX - minX + 1).Select(n => (double)n).ToArray();
DataY = DataX.Select(i => (double)BillingData.GetCurveValueAt(data, i)).ToArray();
}
public Graph(string type, int num, int minX, int maxX, string contracts, double[] dataX, double[] dataY) {
Type = type;
Num = num;
public Graph(double[] values, int precision, int minX, int maxX) {
Precision = precision;
MinX = minX;
MaxX = maxX;
DataX = Enumerable.Range(MinX, MaxX - MinX + 1).Select(i => (double)i).ToArray();
DataY = values;
}
private Graph(double[] dataX, double[] dataY, int precision, int minX, int maxX) {
Precision = precision;
MinX = minX;
MaxX = maxX;
Contracts = contracts;
DataX = dataX;
DataY = dataY;
}
private void ParseGraphData(JsonObject graphData) {
var GraphPoints = graphData.ToDictionary(p => int.Parse(p.Key[..^2]), p => (double)p.Value?.AsValue());
public double GetOechsleAt(int index) {
return DataX[index];
}
if (GraphPoints.Keys.Count < 1) {
return;
}
public void SetOechsleAt(int index, double oechsle) {
DataX[index] = oechsle;
}
var minKey = GraphPoints.Keys.Order().First();
var maxKey = GraphPoints.Keys.OrderDescending().First();
public void SetPriceAt(int index, double price) {
DataY[index] = price;
}
if (!GraphPoints.ContainsKey(MinX)) {
GraphPoints.Add(MinX, GraphPoints.GetValueOrDefault(minKey));
}
if (!GraphPoints.ContainsKey(MaxX)) {
GraphPoints.Add(MaxX, GraphPoints.GetValueOrDefault(maxKey));
}
public double GetPriceAt(int index) {
return DataY[index];
}
var keys = GraphPoints.Keys.Order().ToArray();
public double GetPriceAtOe(double oe) {
return DataY[Array.IndexOf(DataX, oe)];
}
for (int i = 0; i < keys.Length; i++) {
double point1Value = GraphPoints[keys[i]];
if (i + 1 < keys.Length) {
double point2Value = GraphPoints[keys[i + 1]];
if (point1Value == point2Value) {
for (int j = keys[i] - MinX; j < keys[i + 1] - MinX; j++) {
DataY[j] = point1Value;
}
} else {
int steps = Math.Abs(keys[i + 1] - keys[i]);
double step = (point2Value - point1Value) / steps;
DataY[keys[i] - MinX] = point1Value;
DataY[keys[i + 1] - MinX] = point2Value;
for (int j = keys[i] - MinX; j < keys[i + 1] - MinX - 1; j++) {
DataY[j + 1] = Math.Round(DataY[j] + step, 4); // TODO richtig runden
}
}
}
else {
for (int j = keys[i] - MinX; j < DataX.Length; j++) {
DataY[j] = point1Value;
}
}
private void FlattenGraph(int begin, int end, double value) {
for (int i = begin; i <= end; i++) {
DataY[i] = value;
}
}
public JsonObject ToJson() {
JsonObject graph = new();
public void FlattenGraphLeft(int pointIndex) {
FlattenGraph(0, pointIndex, DataY[pointIndex]);
}
if (DataY[0] != DataY[1]) {
graph.Add(new KeyValuePair<string, JsonNode?>(DataX[0] + Type.ToLower(), Math.Round(DataY[0], 4)));
public void FlattenGraphRight(int pointIndex) {
FlattenGraph(pointIndex, DataY.Length - 1, DataY[pointIndex]);
}
private void LinearIncreaseGraph(int begin, int end, double inc) {
for (int i = begin; i < end; i++) {
DataY[i + 1] = Math.Round(DataY[i] + inc, Precision);
}
for (int i = 1; i < DataX.Length - 1; i++) {
if (Math.Round(DataY[i] - DataY[i - 1], 4) != Math.Round(DataY[i + 1] - DataY[i], 4)) {
graph.Add(new KeyValuePair<string, JsonNode?>(DataX[i] + Type.ToLower(), Math.Round(DataY[i], 4)));
}
}
public void LinearIncreaseGraphToEnd(int begin, double inc) {
LinearIncreaseGraph(begin, DataY.Length - 1, inc);
}
public void InterpolateGraph(int firstPoint, int secondPoint) {
int steps = Math.Abs(firstPoint - secondPoint);
if (firstPoint == -1 || secondPoint == -1 || steps < 2) {
return;
}
if (DataY[^1] != DataY[^2]) {
graph.Add(new KeyValuePair<string, JsonNode?>(DataX[^1] + Type.ToLower(), Math.Round(DataY[^1], 4)));
var (lowIndex, highIndex) = firstPoint < secondPoint ? (firstPoint, secondPoint) : (secondPoint, firstPoint);
double step = (DataY[highIndex] - DataY[lowIndex]) / steps;
for (int i = lowIndex; i < highIndex - 1; i++) {
DataY[i + 1] = Math.Round(DataY[i] + step, Precision);
}
return graph;
}
public object Clone() {
return new Graph(Type, Num, MinX, MaxX, Contracts, (double[])DataX.Clone(), (double[])DataY.Clone());
return new Graph((double[])DataX.Clone(), (double[])DataY.Clone(), Precision, MinX, MaxX);
}
}
}
+90
View File
@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Media;
namespace Elwig.Helpers.Billing {
public class GraphEntry {
public const int MinX = 50;
public const int MinXGeb = 73;
public const int MaxX = 120;
public int Id { get; set; }
public BillingData.CurveMode Mode { get; set; }
public bool Abgewertet { get; set; }
public Graph DataGraph { get; set; }
public Graph? GebundenGraph { get; set; }
public double? GebundenFlatBonus {
get {
try {
var val = GebundenGraph?.DataX.Zip(GebundenGraph.DataY)
.Select(e => Math.Round(e.Second - DataGraph.GetPriceAtOe(e.First), Precision))
.Distinct()
.Single();
return (val == 0) ? null : val;
} catch {
return null;
}
}
set {
if (value is not double v) return;
var values = Enumerable.Range(MinXGeb, MaxX - MinXGeb + 1)
.Select(i => Math.Round(DataGraph.GetPriceAtOe(i) + v, Precision))
.ToArray();
GebundenGraph = new Graph(values, Precision, MinXGeb, MaxX);
}
}
public List<Varibute> Vaributes { get; set; }
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) {
Id = id;
Precision = precision;
Mode = mode;
DataGraph = new Graph(precision, MinX, MaxX); ;
Vaributes = [];
}
public GraphEntry(int id, int precision, BillingData.CurveMode mode, Dictionary<double, decimal> data, Dictionary<double, decimal>? gebunden) :
this(id, precision, mode) {
DataGraph = new Graph(data, precision, MinX, MaxX);
if (gebunden != null) GebundenGraph = new Graph(gebunden, precision, MinXGeb, MaxX);
}
public GraphEntry(int id, int precision, BillingData.Curve curve, List<Varibute> vaributes) :
this(id, precision, curve.Mode) {
DataGraph = new Graph(curve.Normal, precision, MinX, MaxX);
if (curve.Gebunden != null)
GebundenGraph = new Graph(curve.Gebunden, precision, MinXGeb, MaxX);
Vaributes = vaributes;
}
private GraphEntry(int id, int precision, BillingData.CurveMode mode, Graph dataGraph, Graph? gebundenGraph, List<Varibute> vaributes) {
Id = id;
Precision = precision;
Mode = mode;
DataGraph = dataGraph;
GebundenGraph = gebundenGraph;
Vaributes = vaributes;
}
public void AddGebundenGraph() {
GebundenGraph ??= new Graph(Precision, MinXGeb, MaxX);
}
public void RemoveGebundenGraph() {
GebundenGraph = null;
}
public GraphEntry Copy(int id) {
return new GraphEntry(id, Precision, Mode, (Graph)DataGraph.Clone(), (Graph?)GebundenGraph?.Clone(), []);
}
}
}
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;
namespace Elwig.Helpers.Billing {
public class PaymentBillingData : BillingData {
protected readonly Dictionary<int, Curve> Curves;
protected readonly Dictionary<RawVaribute, Curve> PaymentData;
protected readonly Dictionary<RawQualVaribute, Curve> QualityData;
protected readonly IEnumerable<RawVaribute> Vaributes;
public PaymentBillingData(JsonObject data, IEnumerable<RawVaribute> vaributes) :
base(data) {
Vaributes = vaributes;
Curves = GetCurves();
PaymentData = GetPaymentData();
QualityData = GetQualityData();
}
public static PaymentBillingData FromJson(string json, IEnumerable<RawVaribute> vaributes) {
return new(ParseJson(json), vaributes);
}
private Dictionary<RawVaribute, Curve> GetData(JsonNode data) {
return GetSelection(data, Vaributes).ToDictionary(e => e.Key, e => LookupCurve(e.Value));
}
protected Dictionary<RawVaribute, Curve> GetPaymentData() {
return GetData(GetPaymentEntry());
}
protected Dictionary<RawQualVaribute, Curve> GetQualityData() {
Dictionary<RawQualVaribute, Curve> dict = [];
var q = GetQualityEntry();
if (q == null) return dict;
foreach (var (qualid, data) in q) {
foreach (var (idx, d) in GetData(data ?? throw new InvalidOperationException())) {
dict[new(qualid, idx.SortId, idx.AttrId, idx.CultId)] = d;
}
}
return dict;
}
public decimal CalculatePrice(string sortid, string? attrid, string? cultid, string qualid, bool gebunden, double oe, double kmw) {
var curve = GetQualityCurve(qualid, sortid, attrid, cultid) ?? GetCurve(sortid, attrid, cultid);
return GetCurveValueAt((gebunden ? curve.Gebunden : null) ?? curve.Normal, curve.Mode == CurveMode.Oe ? oe : kmw);
}
private Curve LookupCurve(JsonValue val) {
if (val.TryGetValue(out string? curve)) {
var curveId = int.Parse(curve.Split(":")[1]);
return Curves[curveId];
} else if (val.TryGetValue(out decimal value)) {
return new(CurveMode.Oe, new() { { 73, value } }, null);
}
throw new InvalidOperationException();
}
protected Curve GetCurve(string sortid, string? attrid, string? cultid) {
return PaymentData[new(sortid, attrid ?? "", cultid ?? "")];
}
protected Curve? GetQualityCurve(string qualid, string sortid, string? attrid, string? cultid) {
return QualityData.TryGetValue(new(qualid, sortid, attrid ?? "", cultid ?? ""), out var curve) ? curve : null;
}
}
}
-32
View File
@@ -1,32 +0,0 @@
using Elwig.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Elwig.Helpers.Billing {
public class Transaction {
public readonly Member Member;
public readonly long AmountCent;
public readonly string Currency;
public readonly int Nr;
public Transaction(Member m, decimal amount, string currency, int nr) {
Member = m;
AmountCent = (long)Math.Round(amount * 100);
Currency = currency;
Nr = nr;
}
public static IEnumerable<Transaction> FromPaymentVariant(PaymentVar variant) {
var last = variant.Season.PaymentVariants.Where(v => v.TransferDate != null).OrderBy(v => v.TransferDate).LastOrDefault();
var dict = last?.MemberPayments.ToDictionary(m => m.MgNr, m => m.Amount) ?? new();
return variant.Credits
.OrderBy(c => c.MgNr)
.Select(c => new Transaction(c.Member, c.Amount, variant.Season.CurrencyCode, c.TgNr))
.ToList();
}
public static string FormatAmountCent(long cents) => $"{cents / 100}.{cents % 100:00}";
}
}
+91
View File
@@ -0,0 +1,91 @@
using Elwig.Models.Entities;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
namespace Elwig.Helpers.Billing {
public record struct RawQualVaribute {
public string QualId;
public string? SortId;
public string? AttrId;
public string? CultId;
public RawQualVaribute(string qualid, string? sortid, string? attrid, string? cultid) {
QualId = qualid;
SortId = sortid;
AttrId = attrid;
CultId = cultid;
}
}
public record struct RawVaribute : IComparable<RawVaribute> {
public string? SortId;
public string? AttrId;
public string? CultId;
public RawVaribute(string? sortid, string? attrid, string? cultid) {
SortId = sortid;
AttrId = attrid;
CultId = cultid;
}
public RawVaribute(string id) {
var p1 = id.Split('/')[0].Split('-')[0];
SortId = p1 == "" ? null : p1;
AttrId = id.Contains('/') ? id.Split('/')[1].Split('-')[0] : null;
CultId = id.Contains('-') ? id.Split('-')[1] : null;
}
public readonly override string ToString() {
return $"{SortId}" + (AttrId != null ? $"/{AttrId}" : "") + (CultId != null ? $"-{CultId}" : "");
}
public readonly int CompareTo(RawVaribute other) {
return $"{SortId}/{AttrId}-{CultId}".CompareTo($"{other.SortId}/{other.AttrId}-{other.CultId}");
}
}
[PrimaryKey("Listing")]
public class Varibute : IComparable<Varibute> {
public WineVar? Variety { get; }
public WineAttr? Attribute { get; }
public WineCult? Cultivation { get; }
public int? AssignedGraphId { get; set; }
public int? AssignedAbgewGraphId { get; set; }
public string Listing => $"{Variety?.SortId}" +
(Attribute != null ? $"/{Attribute.AttrId}" : "") +
(Cultivation != null ? $"-{Cultivation.CultId}" : "");
public string FullName => $"{Variety?.Name}" +
(Variety != null && Attribute != null ? " " : "") + $"{Attribute?.Name}" +
((Variety != null || Attribute != null) && Cultivation != null ? " " : "") + $"{Cultivation?.Name}";
public Varibute(RawVaribute raw) :
this(raw.SortId != null ? new WineVar(raw.SortId, raw.SortId) : null,
raw.AttrId != null ? new WineAttr() { AttrId = raw.AttrId, Name = raw.AttrId } : null,
raw.CultId != null ? new WineCult() { CultId = raw.CultId, Name = raw.CultId } : null) {
}
public Varibute(RawVaribute raw, Dictionary<string, WineVar> vars, Dictionary<string, WineAttr> attrs, Dictionary<string, WineCult> cults) :
this(raw.SortId != null && raw.SortId != "" ? vars[raw.SortId] : null,
raw.AttrId != null && raw.AttrId != "" ? attrs[raw.AttrId] : null,
raw.CultId != null && raw.CultId != "" ? cults[raw.CultId] : null) {
}
public Varibute(WineVar? var, WineAttr? attr, WineCult? cult) {
Variety = var;
Attribute = attr;
Cultivation = cult;
}
public override string ToString() {
return Listing;
}
public int CompareTo(Varibute? other) {
return Listing.CompareTo(other?.Listing);
}
}
}
+205 -26
View File
@@ -1,32 +1,42 @@
using Elwig.Models;
using Elwig.Models.Entities;
using Microsoft.Data.Sqlite;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
namespace Elwig.Helpers {
public class ClientParameters {
public enum Type { Matzen, Winzerkeller };
public enum Type { Matzen, Winzerkeller, Weinland, Baden, Seewinkel };
public bool IsMatzen => Client == Type.Matzen;
public bool IsWinzerkeller => Client == Type.Winzerkeller;
public bool IsWolkersdorf => Client == Type.Winzerkeller && App.ZwstId == "W";
public bool IsHaugsdorf => Client == Type.Winzerkeller && App.ZwstId == "H";
public bool IsSitzendorf => Client == Type.Winzerkeller && App.ZwstId == "S";
public bool IsWeinland => Client == Type.Weinland;
public bool IsBaden => Client == Type.Baden;
public bool IsSeewinkel => Client == Type.Seewinkel;
public bool IsWolkersdorf => IsWinzerkeller && App.ZwstId == "W";
public bool IsHaugsdorf => IsWinzerkeller && App.ZwstId == "H";
public bool IsSitzendorf => IsWinzerkeller && App.ZwstId == "S";
public bool IsGrInzersdorf => IsWeinland;
public bool IsPamhagen => IsSeewinkel;
public string NameToken;
public string NameShort;
public string Name;
public string? NameSuffix;
public string NameType;
public string NameTypeFull => NameType.Replace(".", "").Replace(" ", "").ToLower() switch {
"reggenmbh" => "registrierte Genossenschaft mit beschränkter Haftung",
_ => NameType,
};
public string NameFull => NameSuffix == null ? $"{Name} {NameType}" : $"{Name}, {NameSuffix}, {NameType}";
public Type? Client;
public PostalDest PostalDest {
set {
Plz = value.AtPlz.Plz;
Ort = value.AtPlz.Ort.Name;
Plz = value.AtPlz!.Plz;
Ort = value.AtPlz!.Ort.Name;
}
}
public int Plz;
@@ -39,19 +49,38 @@ namespace Elwig.Helpers {
public string? Bic;
public string? UstIdNr;
public string? LfbisNr;
public string? OrganicOperatorId;
public string? OrganicAuthorityCode;
public string? PhoneNr;
public string? FaxNr;
public string? EmailAddress;
public string? Website;
public int DeliveryObligation;
public int DeliveryRight;
public decimal VatNormal;
public decimal VatReduced;
public decimal VatFlatRate;
public bool EnableMemberHistory;
public int ModeBusinessShares;
public bool HasRedWhite => ModeBusinessShares == 1;
public int ModeDeliveryNoteStats;
public int ModeWineQualityStatistics;
public int OrderingMemberList;
public string? TextDeliveryNote;
public string? TextDeliveryConfirmation;
public string? TextCreditNote;
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;
public (int? AllowanceKg, double? AllowanceShares, int? AllowanceKgPerShare, double? AllowancePercent, int? MinShares) AutoAdjustShares;
public ClientParameters(AppDbContext ctx) : this(ctx.ClientParameters.ToDictionary(e => e.Param, e => e.Value)) { }
@@ -63,8 +92,16 @@ namespace Elwig.Helpers {
NameSuffix = parameters.GetValueOrDefault("CLIENT_NAME_SUFFIX");
NameType = parameters["CLIENT_NAME_TYPE"] ?? throw new KeyNotFoundException();
switch (Name) {
case "Winzergenossenschaft für Matzen und Umgebung": Client = Type.Matzen; break;
case "Winzerkeller im Weinviertel": Client = Type.Winzerkeller; break;
case "Winzergenossenschaft für Matzen und Umgebung":
Client = Type.Matzen; break;
case "Winzerkeller im Weinviertel":
Client = Type.Winzerkeller; break;
case "Winzergenossenschaft Weinland":
Client = Type.Weinland; break;
case "Winzergenossenschaft Baden - Bad Vöslau":
Client = Type.Baden; break;
case "Winzerkeller Seewinkel":
Client = Type.Seewinkel; break;
};
Plz = int.Parse(parameters["CLIENT_PLZ"] ?? "");
@@ -78,22 +115,150 @@ namespace Elwig.Helpers {
UstIdNr = parameters.GetValueOrDefault("CLIENT_USTIDNR");
Bic = parameters.GetValueOrDefault("CLIENT_BIC");
Iban = parameters.GetValueOrDefault("CLIENT_IBAN");
OrganicOperatorId = parameters.GetValueOrDefault("CLIENT_ORGANIC_OPERATORID");
OrganicAuthorityCode = parameters.GetValueOrDefault("CLIENT_ORGANIC_AUTHORITYCODE");
DeliveryObligation = int.Parse(parameters["DELIVERY_OBLIGATION"] ?? "");
DeliveryRight = int.Parse(parameters["DELIVERY_RIGHT"] ?? "");
VatNormal = decimal.Parse((parameters["VAT_NORMAL"] ?? "").Replace(".", ","));
VatReduced = decimal.Parse((parameters["VAT_REDUCED"] ?? "").Replace(".", ","));
VatFlatRate = decimal.Parse((parameters["VAT_FLATRATE"] ?? "").Replace(".", ","));
EnableMemberHistory = (parameters.GetValueOrDefault("ENABLE_MEMBERHISTORY")?.ToUpper()) switch {
"1" or "TRUE" or "YES" or "JA" => true,
_ => false,
};
switch (parameters.GetValueOrDefault("MODE_BUSINESSSHARES", "SIMPLE")?.ToUpper()) {
case "SIMPLE": ModeBusinessShares = 0; break;
case "RED_WHITE": ModeBusinessShares = 1; break;
}
switch (parameters.GetValueOrDefault("MODE_DELIVERYNOTE_STATS", "SHORT")?.ToUpper()) {
case "NONE": ModeDeliveryNoteStats = 0; break;
case "GA_ONLY": ModeDeliveryNoteStats = 1; break;
case "SHORT": ModeDeliveryNoteStats = 2; break;
case "FULL": ModeDeliveryNoteStats = 3; break;
}
switch (parameters.GetValueOrDefault("MODE_WINEQUALITYSTATISTICS", "OE")?.ToUpper()) {
case "OE": ModeWineQualityStatistics = 0; break;
case "KMW/1": ModeWineQualityStatistics = 1; break;
case "KMW/2": ModeWineQualityStatistics = 2; break;
case "KMW/5": ModeWineQualityStatistics = 3; break;
case "KMW/10": ModeWineQualityStatistics = 4; break;
}
switch (parameters.GetValueOrDefault("ORDERING_MEMBERLIST", "MGNR")?.ToUpper()) {
case "MGNR": OrderingMemberList = 0; break;
case "NAME": OrderingMemberList = 1; break;
case "KG": OrderingMemberList = 2; break;
}
Sender2 = parameters.GetValueOrDefault("DOCUMENT_SENDER") ?? "";
TextDeliveryNote = parameters.GetValueOrDefault("TEXT_DELIVERYNOTE");
if (TextDeliveryNote == "") TextDeliveryNote = null;
TextDeliveryConfirmation = parameters.GetValueOrDefault("TEXT_DELIVERYCONFIRMATION");
if (TextDeliveryConfirmation == "") TextDeliveryConfirmation = null;
TextCreditNote = parameters.GetValueOrDefault("TEXT_CREDITNOTE");
if (TextCreditNote == "") TextCreditNote = null;
TextEmailSubject = parameters.GetValueOrDefault("TEXT_EMAIL_SUBJECT");
if (TextEmailSubject == "") TextEmailSubject = null;
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;
case "LINES": ExportEbicsAddress = 1; break;
case "FULL": ExportEbicsAddress = 2; break;
}
var autoAdjust = (parameters.GetValueOrDefault("AUTOADJUST_BUSINESSSHARES") ?? "").Split(';');
AutoAdjustShares = autoAdjust.Length == 5 ? (
int.TryParse(autoAdjust[0], out var v1) ? v1 : null,
double.TryParse(autoAdjust[1], out var v2) ? v2 : null,
int.TryParse(autoAdjust[2], out var v3) ? v3 : null,
double.TryParse(autoAdjust[3], out var v4) ? v4 : null,
int.TryParse(autoAdjust[4], out var v5) ? v5 : null
) : (null, null, null, null, null);
} catch {
throw new KeyNotFoundException();
}
}
private IEnumerable<(string, string?)> GetParamValues() {
return new (string, string?)[] {
string businessShares = "SIMPLE";
switch (ModeBusinessShares) {
case 0: businessShares = "SIMPLE"; break;
case 1: businessShares = "RED_WHITE"; break;
}
string deliveryNoteStats = "SHORT";
switch (ModeDeliveryNoteStats) {
case 0: deliveryNoteStats = "NONE"; break;
case 1: deliveryNoteStats = "GA_ONLY"; break;
case 2: deliveryNoteStats = "SHORT"; break;
case 3: deliveryNoteStats = "FULL"; break;
}
string modeWineQualityStatistics = "OE";
switch (ModeWineQualityStatistics) {
case 0: modeWineQualityStatistics = "OE"; break;
case 1: modeWineQualityStatistics = "KMW/1"; break;
case 2: modeWineQualityStatistics = "KMW/2"; break;
case 3: modeWineQualityStatistics = "KMW/5"; break;
case 4: modeWineQualityStatistics = "KMW/10"; break;
}
string orderingMemberList = "MGNR";
switch (OrderingMemberList) {
case 0: orderingMemberList = "MGNR"; break;
case 1: orderingMemberList = "NAME"; break;
case 2: orderingMemberList = "KG"; break;
}
string mailSendPostal = "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;
case 1: exportEbicsAddress = "LINES"; break;
case 2: exportEbicsAddress = "FULL"; break;
}
string autoAdjust = $"{AutoAdjustShares.AllowanceKg};{AutoAdjustShares.AllowanceShares?.ToString(CultureInfo.InvariantCulture)};" +
$"{AutoAdjustShares.AllowanceKgPerShare};{AutoAdjustShares.AllowancePercent?.ToString(CultureInfo.InvariantCulture)};" +
$"{AutoAdjustShares.MinShares}";
return [
("CLIENT_NAME_TOKEN", NameToken),
("CLIENT_NAME_SHORT", NameShort),
("CLIENT_NAME", Name),
@@ -110,14 +275,28 @@ namespace Elwig.Helpers {
("CLIENT_USTIDNR", UstIdNr),
("CLIENT_BIC", Bic),
("CLIENT_IBAN", Iban),
("DELIVERY_OBLIGATION", DeliveryObligation.ToString()),
("DELIVERY_RIGHT", DeliveryRight.ToString()),
("VAT_NORMAL", VatNormal.ToString().Replace(",", ".")),
("VAT_REDUCED", VatReduced.ToString().Replace(",", ".")),
("VAT_FLATRATE", VatFlatRate.ToString().Replace(",", ".")),
("CLIENT_ORGANIC_OPERATORID", OrganicOperatorId),
("CLIENT_ORGANIC_AUTHORITYCODE", OrganicAuthorityCode),
("ENABLE_MEMBERHISTORY", EnableMemberHistory ? "YES" : "NO"),
("MODE_BUSINESSSHARES", businessShares),
("MODE_DELIVERYNOTE_STATS", deliveryNoteStats),
("MODE_WINEQUALITYSTATISTICS", modeWineQualityStatistics),
("ORDERING_MEMBERLIST", orderingMemberList),
("DOCUMENT_SENDER", Sender2),
("TEXT_DELIVERYNOTE", TextDeliveryNote),
};
("TEXT_DELIVERYCONFIRMATION", TextDeliveryConfirmation),
("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),
];
}
public async Task UpdateValues() {
+105 -65
View File
@@ -1,19 +1,82 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using IniParser;
using IniParser.Model;
using Microsoft.Extensions.Configuration;
namespace Elwig.Helpers {
public enum WeighingMode { Gross, Net, Box }
public record struct ScaleConfig {
public string Id;
public string? Type;
public string? Model;
public string? Connection;
public string? Empty;
public string? Filling;
public string? Limit;
public bool Required;
public bool SyncTime;
public string? Log;
public string? _Log;
public ScaleConfig(string id, string? type, string? model, string? cnx, string? empty, string? filling, string? limit, bool? required, bool? syncTime, string? log) {
Id = id;
Type = type;
Model = model;
Connection = cnx;
Empty = empty;
Filling = filling;
Limit = limit;
Required = required ?? true;
SyncTime = syncTime ?? true;
_Log = log;
Log = log != null ? Path.Combine(App.DataPath, log) : null;
}
}
public class Config {
private static readonly string[] TrueValues = ["1", "true", "yes", "on"];
private static readonly string[] FalseValues = ["0", "false", "no", "off"];
private readonly string FileName;
public bool Debug;
public string DatabaseFile = App.DataPath + "database.sqlite3";
public string DatabaseFile = Path.Combine(App.DataPath, "database.sqlite3");
public string? DatabaseLog = null;
public string? Branch = null;
public IList<string?[]> Scales;
private readonly List<string?[]> ScaleList = new();
public WeighingMode? WeighingMode;
public bool BackupAuto = true;
public string BackupPath = Path.Combine(App.DataPath, "backups");
public int BackupRetainHours = 36;
public int BackupRetainDays = 8;
public int BackupRetainWeeks = 6;
public int BackupRetainMonths = 18;
public string? UpdateUrl = null;
public bool UpdateAuto = false;
public string? SyncUrl = null;
public string SyncUsername = "";
public string SyncPassword = "";
public bool SyncMain = false;
public string? SmtpHost = null;
public int? SmtpPort = null;
public string? SmtpMode = null;
public string? SmtpUsername = null;
public string? SmtpPassword = null;
public string? SmtpFrom = null;
public double SmtpWait = 0;
public (string Host, int Port, string Mode, string Username, string Password, string From, double Wait)? Smtp =>
SmtpHost == null || SmtpPort == null || SmtpMode == null || SmtpUsername == null || SmtpPassword == null || SmtpFrom == null ?
null : (SmtpHost, (int)SmtpPort, SmtpMode, SmtpUsername, SmtpPassword, SmtpFrom, SmtpWait);
public IList<ScaleConfig> Scales;
private readonly List<ScaleConfig> ScaleList = [];
public Config(string filename) {
FileName = filename;
@@ -22,73 +85,50 @@ namespace Elwig.Helpers {
}
public void Read() {
var parser = new FileIniDataParser();
IniData? ini = null;
try {
ini = parser.ReadFile(FileName, Utils.UTF8);
} catch {}
var config = new ConfigurationBuilder().AddIniFile(FileName).Build();
if (ini == null || !ini.TryGetKey("database.file", out string db)) {
DatabaseFile = App.DataPath + "database.sqlite3";
} else if (db.Length > 1 && (db[1] == ':' || db[0] == '/' || db[0] == '\\')) {
DatabaseFile = db;
} else {
DatabaseFile = App.DataPath + db;
}
DatabaseFile = Path.Combine(Path.GetDirectoryName(FileName) ?? App.DataPath, config["database:file"] ?? "database.sqlite3");
var log = config["database:log"];
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;
if (ini == null || !ini.TryGetKey("database.log", out string log)) {
DatabaseLog = null;
} else if (log.Length > 1 && (log[1] == ':' || log[0] == '/' || log[0] == '\\')) {
DatabaseLog = log;
} else {
DatabaseLog = App.DataPath + log;
}
BackupAuto = !FalseValues.Contains(config["backup:auto"]?.ToLower());
BackupPath = Path.Combine(Path.GetDirectoryName(DatabaseFile) ?? App.DataPath, config["backup:path"] ?? "backups");
BackupRetainHours = int.TryParse(config["backup:retain_hours"], out var hours) ? hours : 36;
BackupRetainDays = int.TryParse(config["backup:retain_days"], out var days) ? days : 8;
BackupRetainWeeks = int.TryParse(config["backup:retain_weeks"], out var weeks) ? weeks : 6;
BackupRetainMonths = int.TryParse(config["backup:retain_months"], out var months) ? months : 18;
if (ini == null || !ini.TryGetKey("general.branch", out string branch)) {
Branch = null;
} else {
Branch = branch;
}
UpdateUrl = config["update:url"];
UpdateAuto = TrueValues.Contains(config["update:auto"]?.ToLower());
if (ini == null || !ini.TryGetKey("general.debug", out string debug)) {
Debug = false;
} else {
debug = debug.ToLower();
Debug = debug == "1" || debug == "true" || debug == "yes" || debug == "on";
}
SyncUrl = config["sync:url"];
SyncUsername = config["sync:username"] ?? "";
SyncPassword = config["sync:password"] ?? "";
SyncMain = TrueValues.Contains(config["sync:main"]?.ToLower());
SmtpHost = config["smtp:host"];
SmtpPort = config["smtp:port"]?.All(char.IsAsciiDigit) == true && config["smtp:port"]?.Length > 0 ? int.Parse(config["smtp:port"]!) : null;
SmtpMode = config["smtp:mode"];
SmtpUsername = config["smtp:username"];
SmtpPassword = config["smtp:password"];
SmtpFrom = config["smtp:from"];
SmtpWait = double.TryParse(config["smtp:wait"], out var res) ? res : 0;
var scales = config.AsEnumerable().Where(i => i.Key.StartsWith("scale.")).GroupBy(i => i.Key.Split(':')[0][6..]).Select(i => i.Key).Order();
ScaleList.Clear();
Scales = ScaleList;
if (ini != null) {
foreach (var s in ini.Sections.Where(s => s.SectionName.StartsWith("scale."))) {
string? scaleLog = null;
if (s.Keys["log"] != null) {
scaleLog = s.Keys["log"];
if (scaleLog.Length <= 1 || (scaleLog[1] != ':' && scaleLog[0] != '/' && scaleLog[0] != '\\')) {
scaleLog = App.DataPath + scaleLog;
}
}
ScaleList.Add(new string?[] {
s.SectionName[6..], s.Keys["type"], s.Keys["model"], s.Keys["connection"],
s.Keys["empty"], s.Keys["filling"], s.Keys["limit"], scaleLog
});
}
}
}
public void Write() {
using var file = new StreamWriter(FileName, false, Utils.UTF8);
file.Write($"\r\n[general]\r\n");
if (Branch != null) file.Write($"branch = {Branch}\r\n");
if (Debug) file.Write("debug = true\r\n");
file.Write($"\r\n[database]\r\nfile = {DatabaseFile}\r\n");
if (DatabaseLog != null) file.Write($"log = {DatabaseLog}\r\n");
foreach (var s in ScaleList) {
file.Write($"\r\n[scale.{s[0]}]\r\ntype = {s[1]}\r\nmodel = {s[2]}\r\nconnection = {s[3]}\r\n");
if (s[4] != null) file.Write($"empty = {s[4]}\r\n");
if (s[5] != null) file.Write($"filling = {s[5]}\r\n");
if (s[6] != null) file.Write($"limit = {s[6]}\r\n");
if (s[7] != null) file.Write($"log = {s[7]}\r\n");
foreach (var s in scales) {
ScaleList.Add(new(
s, config[$"scale.{s}:type"], config[$"scale.{s}:model"], config[$"scale.{s}:connection"],
config[$"scale.{s}:empty"], config[$"scale.{s}:filling"], config[$"scale.{s}:limit"],
config[$"scale.{s}:required"] != null ? TrueValues.Contains(config[$"scale.{s}:required"]?.ToLower()) : null,
config[$"scale.{s}:synctime"] != null ? !FalseValues.Contains(config[$"scale.{s}:synctime"]?.ToLower()) : null,
config[$"scale.{s}:log"]
));
}
}
}
+99 -68
View File
@@ -5,18 +5,16 @@ using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Threading;
using Brush = System.Windows.Media.Brush;
using Brushes = System.Windows.Media.Brushes;
namespace Elwig.Helpers {
public class ControlUtils {
public enum RenewSourceDefault {
None,
IfOnly,
First
}
public enum RenewSourceDefault { None, IfOnly, First }
private static void SetControlBrush(Control input, Brush brush) {
private static void SetControlBorderBrush(Control input, Brush brush) {
if (input is ComboBox cb) {
var border = GetComboBoxBorder(cb);
if (border != null) border.BorderBrush = brush;
@@ -26,15 +24,15 @@ namespace Elwig.Helpers {
}
public static void SetInputNotDefault(Control input) {
SetControlBrush(input, Brushes.Gold);
SetControlBorderBrush(input, Brushes.Gold);
}
public static void SetInputChanged(Control input) {
SetControlBrush(input, Brushes.Orange);
SetControlBorderBrush(input, Brushes.Orange);
}
public static void SetInputInvalid(Control input) {
SetControlBrush(input, Brushes.Red);
SetControlBorderBrush(input, Brushes.Red);
}
public static void ClearInputState(Control input) {
@@ -83,13 +81,13 @@ namespace Elwig.Helpers {
return null;
}
public static void RenewItemsSource(Selector selector, IEnumerable? source, Func<object?, object?> getId, SelectionChangedEventHandler? handler = null, RenewSourceDefault def = RenewSourceDefault.None) {
public static void RenewItemsSource(Selector selector, IEnumerable? source, SelectionChangedEventHandler? handler = null, RenewSourceDefault def = RenewSourceDefault.None) {
if (selector.ItemsSource == source)
return;
var selectedId = getId(selector.SelectedItem);
var selectedId = Utils.GetEntityIdentifier(selector.SelectedItem);
object? selItem = null;
if (selectedId != null && source != null)
selItem = source.Cast<object>().FirstOrDefault(i => selectedId.Equals(getId(i)));
if (selectedId != 0 && source != null)
selItem = source.Cast<object>().FirstOrDefault(i => selectedId.Equals(Utils.GetEntityIdentifier(i)));
if (source != null && selItem == null) {
if ((def == RenewSourceDefault.IfOnly && source.Cast<object>().Count() == 1) || def == RenewSourceDefault.First) {
selItem = source.Cast<object>().First();
@@ -101,28 +99,16 @@ namespace Elwig.Helpers {
selector.SelectedItem = selItem;
}
public static void RenewItemsSource(Xceed.Wpf.Toolkit.Primitives.Selector selector, IEnumerable? source, Func<object?, object?> getId) {
if (selector.ItemsSource == source)
return;
var selectedIds = selector.SelectedItems.Cast<object>().Select(i => getId(i)).ToList();
selector.ItemsSource = source;
if (source != null) {
selector.SelectedItems.Clear();
foreach (var i in source.Cast<object>().Where(i => selectedIds.Contains(getId(i))))
selector.SelectedItems.Add(i);
}
}
public static void RenewItemsSource(DataGrid dataGrid, IEnumerable? source, Func<object?, object?> getId, SelectionChangedEventHandler? handler = null, RenewSourceDefault def = RenewSourceDefault.None, bool keepSort = true) {
public static void RenewItemsSource(DataGrid dataGrid, IEnumerable? source, SelectionChangedEventHandler? handler = null, RenewSourceDefault def = RenewSourceDefault.None, bool keepSort = true) {
if (dataGrid.ItemsSource == source)
return;
var column = dataGrid.CurrentCell.Column;
var sortColumns = dataGrid.Columns.Select(c => c.SortDirection).ToList();
var sort = dataGrid.Items.SortDescriptions.ToList();
var selectedId = getId(dataGrid.SelectedItem);
var selectedId = Utils.GetEntityIdentifier(dataGrid.SelectedItem);
object? selItem = null;
if (selectedId != null && source != null)
selItem = source.Cast<object>().FirstOrDefault(i => selectedId.Equals(getId(i)));
if (selectedId != 0 && source != null)
selItem = source.Cast<object>().FirstOrDefault(i => selectedId.Equals(Utils.GetEntityIdentifier(i)));
if (source != null && selItem == null) {
if ((def == RenewSourceDefault.IfOnly && source.Cast<object>().Count() == 1) || def == RenewSourceDefault.First) {
selItem = source.Cast<object>().First();
@@ -142,83 +128,128 @@ namespace Elwig.Helpers {
dataGrid.CurrentCell = new(dataGrid.SelectedItem, column);
}
public static void RenewItemsSource(ListBox listBox, IEnumerable? source, Func<object?, object?> getId, SelectionChangedEventHandler? handler = null, RenewSourceDefault def = RenewSourceDefault.None) {
public static void RenewItemsSource(ListBox listBox, IEnumerable? source, SelectionChangedEventHandler? handler = null, RenewSourceDefault def = RenewSourceDefault.None) {
if (listBox.ItemsSource == source)
return;
var selectedId = getId(listBox.SelectedItem);
object? selItem = null;
if (selectedId != null && source != null)
selItem = source.Cast<object>().FirstOrDefault(i => selectedId.Equals(getId(i)));
if (source != null && selItem == null) {
if ((def == RenewSourceDefault.IfOnly && source.Cast<object>().Count() == 1) || def == RenewSourceDefault.First) {
selItem = source.Cast<object>().FirstOrDefault();
if (listBox.SelectionMode == SelectionMode.Single) {
var selectedId = Utils.GetEntityIdentifier(listBox.SelectedItem);
object? selItem = null;
if (selectedId != 0 && source != null)
selItem = source.Cast<object>().FirstOrDefault(i => selectedId.Equals(Utils.GetEntityIdentifier(i)));
if (source != null && selItem == null) {
if ((def == RenewSourceDefault.IfOnly && source.Cast<object>().Count() == 1) || def == RenewSourceDefault.First) {
selItem = source.Cast<object>().FirstOrDefault();
}
}
if (handler != null && selItem != null) listBox.SelectionChanged -= handler;
listBox.ItemsSource = source;
if (handler != null && selItem != null) listBox.SelectionChanged += handler;
listBox.SelectedItem = selItem;
} else {
var selectedIds = listBox.SelectedItems.Cast<object>().Select(Utils.GetEntityIdentifier).ToList();
if (handler != null && selectedIds != null) listBox.SelectionChanged -= handler;
listBox.ItemsSource = source;
if (source != null && selectedIds != null) {
listBox.SelectedItems.Clear();
foreach (var i in source.Cast<object>().Where(i => selectedIds.Contains(Utils.GetEntityIdentifier(i))))
listBox.SelectedItems.Add(i);
}
if (handler != null && selectedIds != null) listBox.SelectionChanged += handler;
}
if (handler != null && selItem != null) listBox.SelectionChanged -= handler;
listBox.ItemsSource = source;
if (handler != null && selItem != null) listBox.SelectionChanged += handler;
listBox.SelectedItem = selItem;
}
public static object? GetItemFromSource(IEnumerable source, Func<object?, object?> getId, object? id) {
public static object? GetItemFromSource(IEnumerable source, int? hash) {
if (source == null)
return null;
var items = source.Cast<object>();
var item = items.Where(i => getId(i)?.Equals(id) ?? false).FirstOrDefault();
var item = items.Where(i => Utils.GetEntityIdentifier(i) == hash).FirstOrDefault();
if (item == null && items.Any(i => i is NullItem))
return items.Where(i => i is NullItem).First();
return item;
}
public static object? GetItemFromSource(IEnumerable source, object? item, Func<object?, object?> getId) {
return GetItemFromSource(source, getId, getId(item));
public static T? GetItemFromSource<T>(IEnumerable source, T? item) {
return (T?)GetItemFromSource(source, Utils.GetEntityIdentifier(item));
}
public static void SelectComboBoxItem(ComboBox cb, Func<object?, object?> getId, object? id) {
cb.SelectedItem = GetItemFromSource(cb.ItemsSource, getId, id);
public static object? GetItemFromSourceWithPk(IEnumerable source, params object?[] primaryKey) {
return GetItemFromSource(source, (int?)Utils.GetEntityIdetifierForPk(primaryKey));
}
public static void SelectComboBoxItem(ComboBox cb, object? item, Func<object?, object?> getId) {
SelectComboBoxItem(cb, getId, getId(item));
public static void SelectItemWithHash(Selector input, int? hash) {
if (hash == null) {
input.SelectedItem = null;
} else {
input.SelectedItem = GetItemFromSource(input.ItemsSource, hash);
}
if (input is ListBox lb && lb.SelectedItem is object lbItem) {
lb.ScrollIntoView(lbItem);
} else if (input is DataGrid dg && dg.SelectedItem is object dgItem) {
dg.ScrollIntoView(dgItem);
}
}
public static IEnumerable<object?> GetItemsFromSource(IEnumerable source, Func<object?, object?> getId, IEnumerable<object?> ids) {
public static void SelectItemWithPk(Selector input, params object?[] pk) {
SelectItemWithHash(input, Utils.GetEntityIdentifier(pk));
}
public static void SelectItem(Selector input, object? item) {
SelectItemWithHash(input, Utils.GetEntityIdentifier(item));
}
public static IEnumerable<object?> GetItemsFromSource(IEnumerable source, IEnumerable<int?> ids) {
if (source == null)
return Array.Empty<object>();
return source.Cast<object>().Where(i => ids.Any(c => c?.Equals(getId(i)) ?? false));
return [];
return source.Cast<object>().Where(i => ids.Any(c => c == Utils.GetEntityIdentifier(i)));
}
public static IEnumerable<object?> GetItemsFromSource(IEnumerable source, IEnumerable<object?>? items, Func<object?, object?> getId) {
public static IEnumerable<object?> GetItemsFromSource(IEnumerable source, IEnumerable<object?>? items) {
if (items == null)
return Array.Empty<object>();
return GetItemsFromSource(source, getId, items.Select(i => getId(i)));
return [];
return GetItemsFromSource(source, items.Select(Utils.GetEntityIdentifier));
}
public static void SelectCheckComboBoxItems(Xceed.Wpf.Toolkit.CheckComboBox ccb, Func<object?, object?> getId, IEnumerable<object?>? ids) {
ccb.SelectedItems.Clear();
public static void SelectItems(ListBox lb, IEnumerable<int?>? ids) {
lb.SelectedItems.Clear();
if (ids == null) return;
foreach (var id in ids)
ccb.SelectedItems.Add(GetItemFromSource(ccb.ItemsSource, getId, id));
lb.SelectedItems.Add(GetItemFromSource(lb.ItemsSource, id));
}
public static void SelectCheckComboBoxItems(Xceed.Wpf.Toolkit.CheckComboBox ccb, IEnumerable<object>? items, Func<object?, object?> getId) {
SelectCheckComboBoxItems(ccb, getId, items?.Select(i => getId(i)));
public static void SelectItems(ListBox lb, IEnumerable<object>? items) {
SelectItems(lb, items?.Select(Utils.GetEntityIdentifier));
}
public static object? GetInputValue(Control input) {
public static int? GetInputHashCode(Control input) {
if (input is TextBox tb) {
return tb.Text;
return Utils.GetEntityIdentifier(tb.Text);
} else if (input is ComboBox sb) {
return sb.SelectedItem;
} else if (input is Xceed.Wpf.Toolkit.CheckComboBox ccb) {
return ccb.SelectedItems.Cast<object>().ToArray();
return Utils.GetEntityIdentifier(sb.SelectedItem);
} else if (input is ListBox lb) {
return Utils.GetEntityIdentifier(lb.SelectedItems);
} else if (input is CheckBox cb) {
return cb.IsChecked?.ToString();
return Utils.GetEntityIdentifier(cb.IsChecked);
} else if (input is RadioButton rb) {
return rb.IsChecked?.ToString();
return Utils.GetEntityIdentifier(rb.IsChecked);
} else {
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();
};
}
}
}
+9 -9
View File
@@ -6,7 +6,7 @@ using System;
namespace Elwig.Helpers.Export {
using Row = Tuple<(string?, string?, string?, string?, string, int, string, int), (string, int, string, string, string, int, string, double, double)>;
using Row = Tuple<(string?, string?, string?, string?, string, int, string, bool, int), (string, int, string, string, string, int, string, double, double)>;
public class Bki : Csv<Row> {
@@ -22,24 +22,24 @@ namespace Elwig.Helpers.Export {
""";
var c = App.Client;
var (a1, a2) = Utils.SplitAddress(c.Address);
_clientData = $"{c.LfbisNr};{c.NameFull};;{a1};{a2};{c.Plz};{c.Ort}";
_clientData = $"{c.LfbisNr};{c.NameFull};;{a1};\t{a2};{c.Plz};{c.Ort}";
}
public async Task ExportAsync(int year) {
using var cnx = await AppDbContext.ConnectAsync();
using var cmd = cnx.CreateCommand();
cmd.CommandText = $"""
SELECT lfbis_nr, family_name, name, billing_name, address, plz, ort, area,
SELECT lfbis_nr, name, other_names, billing_name, address, plz, ort, vollablieferer, area,
date, weight, type, sortid, qualid, year, hkid, kmw, oe
FROM v_bki_delivery
WHERE year = {year}
""";
var r = await cmd.ExecuteReaderAsync();
List<Row> rows = new();
List<Row> rows = [];
while (await r.ReadAsync()) {
rows.Add(new(
(r.IsDBNull(0) ? null : r.GetString(0), r.IsDBNull(1) ? null : r.GetString(1), r.IsDBNull(2) ? null : r.GetString(2), r.IsDBNull(3) ? null : r.GetString(3), r.GetString(4), r.GetInt32(5), r.GetString(6), r.GetInt32(7)),
(r.GetString(8), r.GetInt32(9), r.GetString(10), r.GetString(11), r.GetString(12), r.GetInt32(13), r.GetString(14), r.GetDouble(15), r.GetDouble(16))
(r.IsDBNull(0) ? null : r.GetString(0), r.IsDBNull(1) ? null : r.GetString(1), r.IsDBNull(2) ? null : r.GetString(2), r.IsDBNull(3) ? null : r.GetString(3), r.GetString(4), r.GetInt32(5), r.GetString(6), r.GetBoolean(7), r.GetInt32(8)),
(r.GetString(9), r.GetInt32(10), r.GetString(11), r.GetString(12), r.GetString(13), r.GetInt32(14), r.GetString(15), r.GetDouble(16), r.GetDouble(17))
));
}
@@ -52,14 +52,14 @@ namespace Elwig.Helpers.Export {
public override string FormatRow(Row row) {
var (member, delivery) = row;
var (lfBisNr, familyName, name, billingName, address, plz, ort, area) = member;
var (lfBisNr, familyName, name, billingName, address, plz, ort, vollablieferer, area) = member;
var (date, weight, type, sortid, qualid, year, hkid, kmw, oe) = delivery;
var (n1, n2) = billingName == null ? (familyName, name) : Utils.SplitName(billingName, familyName);
var (a1, a2) = Utils.SplitAddress(address);
var memberData = $"{lfBisNr};{n1};{n2};{a1};{a2};{plz};{ort}";
var memberData = $"{lfBisNr};{n1};{n2};{a1};\t{a2};{plz};{ort}";
var deliveryData = $"{string.Join(".", date.Split("-").Reverse())};{weight};TB;{(type == "W" ? "J" : "")};{(type == "R" ? "J" : "")};{sortid};;;{qualid};{year};{hkid};{kmw:0.0};{oe:0}";
var vollData = $"N;;;{area / 10_000.0}";
var vollData = $"{(vollablieferer ? "J" : "N")};{(vollablieferer ? area / 10_000.0 : null):N4};{(vollablieferer ? area / 10_000.0 : null):N4};{area / 10_000.0:N4}";
return $"{_clientData};{memberData};{deliveryData};{vollData}";
}
+21 -1
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;
}));
}
}
}
+259
View File
@@ -0,0 +1,259 @@
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) {
var tmp = filename + ".tmp";
try {
File.Delete(tmp);
if (zipFile) {
using var zip = ZipFile.Open(tmp, 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, tmp);
}
File.Move(tmp, filename, true);
} finally {
File.Delete(tmp);
}
}
public static async Task ExportSql(string filename, bool zipFile) {
var tmp = filename + ".tmp";
try {
File.Delete(tmp);
if (zipFile) {
using var zip = ZipFile.Open(tmp, 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(tmp);
using var writer = new StreamWriter(stream, Utils.UTF8);
await ExportSql(writer);
}
File.Move(tmp, filename, true);
} finally {
File.Delete(tmp);
}
}
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, bool keepSource = true) {
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, keepSource: false);
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);
if (keepSource) {
File.Copy(filename, App.Config.DatabaseFile, false);
} else {
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, keepSource: false);
} finally {
if (File.Exists(newName)) File.Delete(newName);
}
}
}
}
+69 -62
View File
@@ -1,38 +1,36 @@
using Elwig.Helpers.Billing;
using Elwig.Models;
using Elwig.Models.Dtos;
using Elwig.Models.Entities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security;
using System.Threading.Tasks;
namespace Elwig.Helpers.Export {
public class Ebics : IBankingExporter {
public class Ebics(PaymentVar variant, string filename, int version, Ebics.AddressMode mode = Ebics.AddressMode.Full) : IBankingExporter {
public enum AddressMode { Omit = 0, Lines = 1, Full = 2 }
public static string FileExtension => "xml";
private readonly StreamWriter _writer;
private readonly DateOnly _date;
private readonly int _year;
private readonly string _name;
private readonly int _nr;
public Ebics(PaymentVar variant, string filename) {
_writer = new(filename, false, Utils.UTF8);
_date = variant.TransferDate ?? DateOnly.Parse("2021-01-10"); //throw new ArgumentException("TransferDate has to be set in PaymentVar");
_year = variant.Year;
_name = variant.Name;
_nr = variant.AvNr;
}
private readonly StreamWriter Writer = new(filename, false, Utils.UTF8);
private readonly DateOnly Date = variant.TransferDate ?? throw new ArgumentException("TransferDate has to be set in PaymentVar");
private readonly int Year = variant.Year;
private readonly string Name = variant.Name;
private readonly int AvNr = variant.AvNr;
private readonly int Version = version;
private readonly AddressMode ShowAddresses = mode;
public void Dispose() {
GC.SuppressFinalize(this);
_writer.Dispose();
Writer.Dispose();
}
public ValueTask DisposeAsync() {
GC.SuppressFinalize(this);
return _writer.DisposeAsync();
return Writer.DisposeAsync();
}
public void Export(IEnumerable<Transaction> transactions, IProgress<double>? progress = null) {
@@ -40,69 +38,78 @@ namespace Elwig.Helpers.Export {
}
public async Task ExportAsync(IEnumerable<Transaction> transactions, IProgress<double>? progress = null) {
if (transactions.Any(tx => tx.Amount < 0)) {
throw new ArgumentException("Tranaction amount may not be negative");
} else if (App.Client.Iban == null) {
throw new ArgumentException("Client IBAN has to be set");
}
progress?.Report(0.0);
var nbOfTxs = transactions.Count();
int count = nbOfTxs + 2, i = 0;
var ctrlSum = Transaction.FormatAmountCent(transactions.Sum(tx => tx.AmountCent));
var msgId = $"ELWIG-{App.Client.NameToken}-{_year}-AV{_nr:00}";
var ctrlSum = transactions.Sum(tx => tx.Amount);
var msgId = $"ELWIG-{App.Client.NameToken}-{Year}-AV{AvNr:00}";
var pmtInfId = $"{msgId}-1";
await _writer.WriteLineAsync($"""
await Writer.WriteLineAsync($"""
<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 pain.001.001.09.xsd">
<CstmrCdtTrfInitn>
<GrpHdr>
<MsgId>{msgId}</MsgId>
<CreDtTm>{DateTime.UtcNow:o}</CreDtTm>
<NbOfTxs>{nbOfTxs}</NbOfTxs>
<CtrlSum>{ctrlSum}</CtrlSum>
<InitgPty><Nm>{App.Client.NameFull}</Nm></InitgPty>
</GrpHdr>
<PmtInf>
<PmtInfId>{pmtInfId}</PmtInfId>
<PmtMtd>TRF</PmtMtd>
<NbOfTxs>{nbOfTxs}</NbOfTxs>
<CtrlSum>{ctrlSum}</CtrlSum>
<ReqdExctnDt><Dt>{_date:yyyy-MM-dd}</Dt></ReqdExctnDt>
<Dbtr><Nm>{App.Client.NameFull}</Nm></Dbtr>
<DbtrAcct><Id><IBAN>{App.Client.Iban?.Replace(" ", "")}</IBAN></Id></DbtrAcct>
<DbtrAgt><FinInstnId><BICFI>{App.Client.Bic ?? "NOTPROVIDED"}</BICFI></FinInstnId></DbtrAgt>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.{Version:00}">
<CstmrCdtTrfInitn>
<GrpHdr>
<MsgId>{msgId}</MsgId>
<CreDtTm>{DateTime.UtcNow:o}</CreDtTm>
<NbOfTxs>{nbOfTxs}</NbOfTxs>
<CtrlSum>{Transaction.FormatAmount(ctrlSum)}</CtrlSum>
<InitgPty><Nm>{SecurityElement.Escape(App.Client.NameFull)}</Nm></InitgPty>
</GrpHdr>
<PmtInf>
<PmtInfId>{pmtInfId}</PmtInfId>
<PmtMtd>TRF</PmtMtd>
<NbOfTxs>{nbOfTxs}</NbOfTxs>
<CtrlSum>{Transaction.FormatAmount(ctrlSum)}</CtrlSum>
<ReqdExctnDt>{(Version >= 8 ? "<Dt>" : "")}{Date:yyyy-MM-dd}{(Version >= 8 ? "</Dt>" : "")}</ReqdExctnDt>
<Dbtr><Nm>{SecurityElement.Escape(App.Client.NameFull)}</Nm></Dbtr>
<DbtrAcct><Id><IBAN>{App.Client.Iban!.Replace(" ", "")}</IBAN></Id></DbtrAcct>
<DbtrAgt><FinInstnId>{(Version >= 4 ? "<BICFI>" : "<BIC>")}{App.Client.Bic ?? "NOTPROVIDED"}{(Version >= 4 ? "</BICFI>" : "</BIC>")}</FinInstnId></DbtrAgt>
""");
progress?.Report(100.0 * ++i / count);
foreach (var tx in transactions) {
var a = (IAddress?)tx.Member.BillingAddress ?? tx.Member;
var (a1, a2) = Utils.SplitAddress(a.Address);
var id = $"ELWIG-{App.Client.NameToken}-{_year}-TG{tx.Nr:0000}";
var info = $"{_name} - Traubengutschrift {_year}/{tx.Nr:000}";
await _writer.WriteLineAsync($"""
<CdtTrfTxInf>
<PmtId><EndToEndId>{id}</EndToEndId></PmtId>
<Amt><InstdAmt Ccy="{tx.Currency}">{Transaction.FormatAmountCent(tx.AmountCent)}</InstdAmt></Amt>
<Cdtr>
<Nm>{a.Name}</Nm>
<PstlAdr>
<StrtNm>{a1}</StrtNm><BldgNb>{a2}</BldgNb>
<PstCd>{a.PostalDest.AtPlz?.Plz}</PstCd><TwnNm>{a.PostalDest.AtPlz?.Ort.Name}</TwnNm>
<Ctry>{a.PostalDest.Country.Alpha2}</Ctry>
</PstlAdr>
</Cdtr>
<CdtrAcct><Id><IBAN>{tx.Member.Iban}</IBAN></Id><CdtrAcct>
<CdtrAgt><FinInstnId><BICFI>{tx.Member.Bic ?? "NOTPROVIDED"}</BICFI></FinInstnId></CdtrAgt>
<RmtInf><Ustrd>{info}</Ustrd></RmtInf>
</CdtTrfTxInf>
var id = $"ELWIG-{App.Client.NameToken}-{Year}-TG{tx.Nr:0000}";
var info = $"{Name} - Traubengutschrift Nr. {Year}/{tx.Nr:000}";
await Writer.WriteLineAsync($"""
<CdtTrfTxInf>
<PmtId><EndToEndId>{id}</EndToEndId></PmtId>
<Amt><InstdAmt Ccy="{tx.Currency}">{Transaction.FormatAmount(tx.Amount)}</InstdAmt></Amt>
<Cdtr>
<Nm>{SecurityElement.Escape(a.FullName[..Math.Min(140, a.FullName.Length)])}</Nm>
""");
if (ShowAddresses != AddressMode.Omit) {
var full = ShowAddresses == AddressMode.Full;
await Writer.WriteLineAsync($"""
<PstlAdr>{(full ? "" : $"\r\n <Ctry>{a.PostalDest.Country.Alpha2}</Ctry>")}
{(full ? $"<StrtNm>{SecurityElement.Escape(a1?[..Math.Min(70, a1.Length)])}</StrtNm> <BldgNb>{SecurityElement.Escape(a2?[..Math.Min(16, a2.Length)])}</BldgNb>" :
$"<AdrLine>{SecurityElement.Escape(a.Address[..Math.Min(70, a.Address.Length)])}</AdrLine>")}
<{(full ? "PstCd" : "AdrLine")}>{a.PostalDest.AtPlz?.Plz}{(full ? "</PstCd> <TwnNm>" : " ")}{SecurityElement.Escape(a.PostalDest.AtPlz?.Ort.Name)}</{(full ? "TwnNm" : "AdrLine")}>
{(full ? $" <Ctry>{a.PostalDest.Country.Alpha2}</Ctry>\r\n " : "")}</PstlAdr>
""");
}
await Writer.WriteLineAsync($"""
</Cdtr>
<CdtrAcct><Id><IBAN>{tx.Member.Iban!}</IBAN></Id></CdtrAcct>
<RmtInf><Ustrd>{SecurityElement.Escape(info)}</Ustrd></RmtInf>
</CdtTrfTxInf>
""");
progress?.Report(100.0 * ++i / count);
}
await _writer.WriteLineAsync("""
</PmtInf>
</CstmrCdtTrfInitn>
await Writer.WriteLineAsync("""
</PmtInf>
</CstmrCdtTrfInitn>
</Document>
""");
await _writer.FlushAsync();
await Writer.FlushAsync();
progress?.Report(100.0);
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
using Elwig.Helpers.Billing;
using Elwig.Models.Dtos;
using System;
namespace Elwig.Helpers.Export {
File diff suppressed because it is too large. Load diff
Loaded 100 of 397 files, more files were not shown because too many files have changed in this diff. Show more