Compare commits

...
4 Commits
Author SHA1 Message Date
lorenz.stechauner b29afe8817 [WIP] certs
Test / Run tests (push) Successful in 2m4s
2026-09-23 19:30:51 +02:00
lorenz.stechauner 824cfc5636 BusinessDocument: Add OrganicAuthorityCode and OrganicOperatorId to member Aside
Test / Run tests (push) Successful in 2m3s
2026-09-23 19:28:55 +02:00
lorenz.stechauner 7b8117f9ac Entities/MemberHistory: Add CompanyHasSigned 2026-09-23 19:28:55 +02:00
lorenz.stechauner 3c81b432c0 ClientParameters: Add ClientOrganicOperatorId 2026-09-23 19:28:51 +02:00
16 changed files with 343 additions and 36 deletions

No files matched your search

+25 -11
View File
@@ -34,19 +34,23 @@ namespace Elwig.Documents {
}
}
protected Cell NewAsideCell(Paragraph text, int colspan = 1, bool isName = false) {
var cell = NewCell(text, colspan: colspan).SetPaddingsMM(0.25f, 0.5f, 0.25f, isName ? 1 : 0);
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) {
return NewAsideCell(new KernedParagraph(text, 10), colspan, isName);
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) :
@@ -60,17 +64,27 @@ namespace Elwig.Documents {
protected override void BeforeRenderBody(iText.Layout.Document doc, PdfDocument pdf) {
base.BeforeRenderBody(doc, pdf);
var uid = new KernedParagraph(Member.UstIdNr ?? "-", 10);
if (!Member.IsBuchführend) uid.Add(Normal(" ")).Add(Italic("(pauschaliert)"));
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))
.AddCell(NewAsideCell("Mitglieds-Nr.:", 9, isName: true)).AddCell(NewAsideCell($"{Member.MgNr}", 17))
.AddCell(NewAsideCell("Betriebs-Nr.:", 9, isName: true)).AddCell(NewAsideCell((Member.LfbisNr ?? "-") + (Member.OrganicAuthorityCode != null ? $" ({Member.OrganicAuthorityCode})" : ""), 17))
.AddCell(NewAsideCell("UID:", 9, isName: true)).AddCell(NewAsideCell(uid, 17));
.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) {
@@ -126,7 +140,7 @@ namespace Elwig.Documents {
.Item(c.Address).Item($"{c.Plz} {c.Ort}").Item("Österreich").Item("Tel.", c.PhoneNr).Item("Fax", c.FaxNr).NextLine()
.Item(c.EmailAddress != null ? link1 : null)
.Item(c.Website != null ? link2 : null)
.Item("Betriebs-Nr.", c.LfbisNr).Item("Bio-KSt.", c.OrganicAuthority).NextLine()
.Item("Betriebs-Nr.", c.LfbisNr).Item("Bio-KSt.", c.OrganicAuthorityCode).NextLine()
.Item("UID", c.UstIdNr).Item("BIC", c.Bic).Item("IBAN", c.Iban)
.ToLeafElements());
}
+3 -3
View File
@@ -56,9 +56,9 @@ namespace Elwig.Documents {
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:", 9, isName: true)).AddCell(NewAsideCell($"{Data.Rows.DistinctBy(r => r.LsNr).Count():N0} (Teil-Lfrg.: {Data.RowNum:N0})", 17))
.AddCell(NewAsideCell("Zeitraum:", 9, 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)", 17))
.AddCell(NewAsideCell("Menge:", 9, isName: true)).AddCell(NewAsideCell($"{MemberStats.Sum(s => s.Weight):N0} kg", 17));
.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) {
+55 -3
View File
@@ -1,3 +1,5 @@
using Elwig.Services;
using Microsoft.Data.Sqlite;
using System;
using System.Collections.Generic;
using System.IO;
@@ -68,11 +70,11 @@ namespace Elwig.Helpers {
})
.OrderBy(s => s.Item1).ThenBy(s => s.Item2)];
List<string> toExecute = [];
List<(int ToVersion, string File)> toExecute = [];
var vers = fromVersion;
while (vers < toVersion) {
var (_, to, name) = scripts.Last(s => s.From == vers);
toExecute.Add(name);
toExecute.Add((to, name));
vers = to;
}
if (toExecute.Count == 0)
@@ -87,8 +89,11 @@ namespace Elwig.Helpers {
await cnx.IntegrityCheck();
await cnx.ForeignKeyCheck();
foreach (var script in toExecute) {
foreach (var (to, script) in toExecute) {
await cnx.ExecuteEmbeddedScript(asm, script);
if (to == 42) {
await UpdateDbSchema_41_To_42(cnx);
}
}
await cnx.IntegrityCheck();
@@ -103,5 +108,52 @@ namespace Elwig.Helpers {
File.Delete(backup);
}
}
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)));
}
}
if (members.Count > 0)
InteractionService.ShowInformation("Datenbank-Update", "Es wird versucht, die Bio-Kontrollstellen-Informationen der Mitglieder automatisch zu laden. Dies könnte einen kurzen Moment in Anspruch nehmen.");
foreach (var m in members) {
string? oocId = null;
string? authCode = null;
var certs = await OrganicService.FetchTracesCertificates(null, m.LfbisNr, (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}");
}
}
}
}
}
+6 -3
View File
@@ -49,7 +49,8 @@ namespace Elwig.Helpers {
public string? Bic;
public string? UstIdNr;
public string? LfbisNr;
public string? OrganicAuthority;
public string? OrganicOperatorId;
public string? OrganicAuthorityCode;
public string? PhoneNr;
public string? FaxNr;
@@ -114,7 +115,8 @@ namespace Elwig.Helpers {
UstIdNr = parameters.GetValueOrDefault("CLIENT_USTIDNR");
Bic = parameters.GetValueOrDefault("CLIENT_BIC");
Iban = parameters.GetValueOrDefault("CLIENT_IBAN");
OrganicAuthority = parameters.GetValueOrDefault("CLIENT_ORGANIC_AUTHORITY");
OrganicOperatorId = parameters.GetValueOrDefault("CLIENT_ORGANIC_OPERATORID");
OrganicAuthorityCode = parameters.GetValueOrDefault("CLIENT_ORGANIC_AUTHORITYCODE");
EnableMemberHistory = (parameters.GetValueOrDefault("ENABLE_MEMBERHISTORY")?.ToUpper()) switch {
"1" or "TRUE" or "YES" or "JA" => true,
@@ -273,7 +275,8 @@ namespace Elwig.Helpers {
("CLIENT_USTIDNR", UstIdNr),
("CLIENT_BIC", Bic),
("CLIENT_IBAN", Iban),
("CLIENT_ORGANIC_AUTHORITY", OrganicAuthority),
("CLIENT_ORGANIC_OPERATORID", OrganicOperatorId),
("CLIENT_ORGANIC_AUTHORITYCODE", OrganicAuthorityCode),
("ENABLE_MEMBERHISTORY", EnableMemberHistory ? "YES" : "NO"),
("MODE_BUSINESSSHARES", businessShares),
("MODE_DELIVERYNOTE_STATS", deliveryNoteStats),
+4 -2
View File
@@ -766,7 +766,8 @@ namespace Elwig.Helpers.Export {
["reason"] = h.Reason,
["source"] = h.Source,
["shares"] = h.Shares,
["signed"] = h.MemberHasSigned,
["signed_company"] = h.CompanyHasSigned,
["signed_member"] = h.MemberHasSigned,
["value_per_share"] = h.ValuePerShare,
["currency"] = h.CurrencyCode,
["deduct_year"] = h.DeductYear,
@@ -786,7 +787,8 @@ namespace Elwig.Helpers.Export {
Reason = json["reason"]!.AsValue().GetValue<string>(),
Source = json["source"]!.AsValue().GetValue<string>(),
Shares = json["shares"]!.AsValue().GetValue<int>(),
MemberHasSigned = json["signed"]?.AsValue().GetValue<bool>() ?? false,
CompanyHasSigned = json["signed_company"]?.AsValue().GetValue<bool>() ?? false,
MemberHasSigned = json["signed_member"]?.AsValue().GetValue<bool>() ?? false,
ValuePerShare = json["value_per_share"]?.AsValue().GetValue<decimal>(),
CurrencyCode = json["currency"]?.AsValue().GetValue<string>(),
DeductYear = json["deduct_year"]?.AsValue().GetValue<int>(),
+4 -1
View File
@@ -76,7 +76,10 @@ namespace Elwig.Models.Entities {
[Column("shares")]
public int Shares { get; set; }
[Column("signed")]
[Column("signed_company")]
public bool CompanyHasSigned { get; set; }
[Column("signed_member")]
public bool MemberHasSigned { get; set; }
[Column("value_per_share")]
+8
View File
@@ -5,6 +5,9 @@ ALTER TABLE member ADD COLUMN org_auth_code TEXT CHECK (org_auth_
ALTER TABLE wine_cultivation ADD COLUMN organic INTEGER NOT NULL CHECK (organic IN (TRUE, FALSE)) DEFAULT FALSE;
ALTER TABLE member_history RENAME COLUMN signed TO signed_member;
ALTER TABLE member_history ADD COLUMN signed_company INTEGER NOT NULL CHECK (signed_company IN (TRUE, FALSE)) DEFAULT FALSE;
UPDATE member SET org_auth_code = UPPER(SUBSTR(a.name, -11, 10)) FROM member m JOIN member_billing_address a ON a.mgnr = m.mgnr WHERE a.name LIKE '%AT-BIO-___)';
UPDATE member SET org_auth_code = UPPER(SUBSTR(a.name, -10, 10)) FROM member m JOIN member_billing_address a ON a.mgnr = m.mgnr WHERE a.name LIKE '%AT-BIO-___';
UPDATE member SET org_auth_code = UPPER(suffix) WHERE suffix LIKE 'AT-BIO-___';
@@ -12,3 +15,8 @@ UPDATE member SET org_auth_code = UPPER(suffix) WHERE suffix LIKE 'AT-BIO-___';
UPDATE wine_cultivation SET organic = TRUE WHERE name LIKE '%bio%' OR name LIKE '%org%' OR description LIKE '%AT-BIO-%';
UPDATE wine_cultivation SET description = NULL WHERE description LIKE '%AT-BIO-%';
UPDATE wine_cultivation SET description = 'Biologische Produktion' WHERE organic;
UPDATE client_parameter SET param = 'CLIENT_ORGANIC_AUTHORITYCODE' WHERE param = 'CLIENT_ORGANIC_AUTHORITY';
UPDATE member_telephone_number SET comment = NULL WHERE comment = '';
UPDATE member_email_address SET comment = NULL WHERE comment = '';
@@ -26,6 +26,7 @@ namespace Elwig.Services {
vm.DateEffective = h.DateEffective;
vm.Reason = h.Reason + (h.Reason == MemberHistory.REASON_TRANSFER ? $"_{(h.ToMgNr == mgnr ? "from" : "to")}" : "");
vm.Shares = h.Shares;
vm.CompanyHasSigned = h.CompanyHasSigned;
vm.MemberHasSigned = h.MemberHasSigned;
vm.ValuePerShare = h.ValuePerShare;
vm.OtherMgNr = h.FromMgNr != null && h.FromMgNr != mgnr ? h.FromMgNr : h.ToMgNr != null && h.ToMgNr != mgnr ? h.ToMgNr : null;
@@ -63,6 +64,7 @@ namespace Elwig.Services {
Reason = vm.Reason?.Split("_")[0] ?? MemberHistory.REASON_CONVERT,
Source = "manual",
Shares = vm.Shares ?? 0,
CompanyHasSigned = vm.CompanyHasSigned,
MemberHasSigned = vm.MemberHasSigned,
ValuePerShare = vm.ValuePerShare,
CurrencyCode = vm.ValuePerShare != null ? "EUR" : null,
+57 -5
View File
@@ -10,6 +10,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Media;
namespace Elwig.Services {
public static class MemberService {
@@ -189,10 +190,9 @@ namespace Elwig.Services {
.GroupBy(d => d.Year)
.ToDictionaryAsync(g => g.Key, g => g.Any());
if (m.MgNr != vm.MgNr)
return;
await App.MainDispatcher.BeginInvoke(() => {
if (m.MgNr != vm.MgNr)
return;
var (d1Grid, _) = DeliveryService.GenerateToolTip(d1GridData, []);
var (d2Grid, _) = DeliveryService.GenerateToolTip(d2GridData, []);
var grid = AreaComService.GenerateToolTip(gridData);
@@ -205,13 +205,65 @@ namespace Elwig.Services {
vm.StatusAreaCommitmentInfo = $"{Utils.CurrentLastSeason}";
vm.StatusAreaCommitment = text;
vm.StatusAreaCommitmentToolTip = grid;
vm.MemberHasDeliveries = Enumerable.Range(0, 9999).Select(i => deliveries.GetValueOrDefault(i, false)).ToList();
vm.MemberHasDeliveries = [.. Enumerable.Range(0, 9999).Select(i => deliveries.GetValueOrDefault(i, false))];
});
});
vm.OrganicTest1 = null;
vm.OrganicTest2 = "...";
vm.OrganicTest3 = Brushes.Black;
vm.OrganicTest4 = null;
Utils.RunBackground("Bio-Zertifikatsdaten laden", async () => {
if (App.MainDispatcher == null || !m.IsOrganic)
return;
var certs = await OrganicService.GetTracesCertificates(m);
// TODO button for TRACES search url
// TODO button for authority search url
await App.MainDispatcher.BeginInvoke(() => {
if (m.MgNr != vm.MgNr)
return;
vm.OrganicTest2 = null;
if (certs.Length == 0) {
vm.OrganicTest1 = "Nicht gefunden";
vm.OrganicTest3 = Brushes.DarkRed;
} else {
var cert = certs[0];
vm.OrganicTest4 = $"Zertifikat-Nr.: {cert.Id}\nBio-KSt.: {cert.AuthorityCode}\nEU Operator-ID: {cert.OperatorId}\nOperator: {cert.OperatorName}";
if (cert.IsValidForWineProduction) {
vm.OrganicTest1 = "Gültig";
vm.OrganicTest2 = $" (bis {cert.ExpiresOn:dd.MM.yyyy})";
vm.OrganicTest3 = Brushes.DarkGreen;
} else if (cert.IsValid) {
vm.OrganicTest1 = "Teilw. gültig";
vm.OrganicTest2 = $" (bis {cert.ExpiresOn:dd.MM.yyyy})";
vm.OrganicTest3 = Brushes.OrangeRed;
} else {
vm.OrganicTest3 = Brushes.DarkRed;
switch (cert.Status) {
case OrganicService.TracesStatus.ISSUED:
vm.OrganicTest1 = "Ausgestellt";
vm.OrganicTest2 = $" (am {cert.LastStatusUpdateOn:dd.MM.yyyy})"; break;
case OrganicService.TracesStatus.SUSPENDED:
vm.OrganicTest1 = "Gesperrt";
vm.OrganicTest2 = $" (am {cert.LastStatusUpdateOn:dd.MM.yyyy})"; break;
case OrganicService.TracesStatus.EXPIRED:
vm.OrganicTest1 = "Ausgelaufen";
vm.OrganicTest2 = $" (am {cert.LastStatusUpdateOn:dd.MM.yyyy})"; break;
case OrganicService.TracesStatus.WITHDRAWN:
vm.OrganicTest1 = "Widerrufen";
vm.OrganicTest2 = $" (am {cert.LastStatusUpdateOn:dd.MM.yyyy})"; break;
}
}
}
});
});
vm.MemberHasEmail = m.EmailAddresses.Count > 0;
vm.MemberCanSendEmail = App.Config.Smtp != null && m.EmailAddresses.Count > 0;
vm.MemberHasDeliveries = Enumerable.Range(0, 9999).Select(i => false).ToList();
vm.MemberHasDeliveries = [.. Enumerable.Range(0, 9999).Select(i => false)];
}
public static async Task<(List<string>, IQueryable<Member>, List<string>)> GetFilters(this MemberAdminViewModel vm, AppDbContext ctx) {
+143
View File
@@ -0,0 +1,143 @@
using Elwig.Helpers;
using Elwig.Models.Entities;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using System.Web;
namespace Elwig.Services {
public static class OrganicService {
public const string TRACES_API_URL = "https://webgate.ec.europa.eu/tracesnt/directory/publication/organic-operator/for/query";
public const string TRACES_PDF_URL_PREFIX = "https://webgate.ec.europa.eu/tracesnt/directory/publication/organic-operator/digitally-signed/";
public enum TracesStatus {
ISSUED, SUSPENDED, WITHDRAWN, EXPIRED
}
public enum TracesActivity {
PREPARATION, EXPORT, IMPORT, STORING, PRODUCTION, DISTRIBUTION, DISTRIBUTION_PLACING_ON_THE_MARKET
}
public enum TracesCategoryOfProduct {
UNPROCESSED_PLANT_PRODUCTS_INCLUDING_SEEDS, LIVESTOCK_AND_UNPROCESSED_LIVESTOCK_PRODUCTS, ALGAE_AND_UNPROCESSED_AQUACULTURE_PRODUCTS,
PROCESSED_AGRICULTURAL_PRODUCTS_INCLUDING_AQUACULTURE_FOR_USE_AS_FOOD, FEED, WINE, OTHER_PRODUCTS_NOT_COVERED_BY_PREVIOUS_CATEGORIES
}
public record TracesCertificate {
public required string Id { get; init; }
public required string AuthorityCode { get; init; }
public required string OperatorId { get; init; }
public required string OperatorName { get; init; }
public required TracesActivity[] Activities { get; init; }
public required TracesCategoryOfProduct[] CategoriesOfProduct { get; init; }
public required DateOnly IssuedOn { get; init; }
public required DateOnly ExpiresOn { get; init; }
public required DateOnly LastStatusUpdateOn { get; init; }
public required TracesStatus Status { get; init; }
public string PdfUrl => TRACES_PDF_URL_PREFIX + Id + ".pdf";
public bool IsValid => Status == TracesStatus.ISSUED && IssuedOn.ToDateTime(new()) <= DateTime.Today && ExpiresOn.ToDateTime(new()) >= DateTime.Today;
public bool IsValidForWineProduction => IsValid && Activities.Contains(TracesActivity.PRODUCTION) &&
(CategoriesOfProduct.Contains(TracesCategoryOfProduct.UNPROCESSED_PLANT_PRODUCTS_INCLUDING_SEEDS) || CategoriesOfProduct.Contains(TracesCategoryOfProduct.WINE));
}
public async static Task<TracesCertificate[]> GetTracesCertificates(Member m, bool? tryNameAndAddress = null) {
// TODO cache
return await FetchTracesCertificates(m, tryNameAndAddress);
}
private async static Task<JsonArray> TryFetchTracesCertificates(Dictionary<string, string> query) {
var q = HttpUtility.ParseQueryString("");
q.Add("sort", "-issuedOn");
q.Add("countryCode", "AT");
foreach (var (k, v) in query) {
q.Add(k, v);
}
using var client = Utils.GetHttpClient();
using var res = await client.GetAsync($"{TRACES_API_URL}?{q}");
res.EnsureSuccessStatusCode();
var resJson = JsonNode.Parse(await res.Content.ReadAsStringAsync());
var certs = resJson?.AsArray() ?? throw new Exception();
if (certs.Count == 0)
throw new Exception();
return certs;
}
public static Task<TracesCertificate[]> FetchTracesCertificates(Member m, bool? tryNameAndAddress = null) {
return FetchTracesCertificates(m.OrganicOperatorId, m.LfbisNr,
(tryNameAndAddress ?? m.IsOrganic) ? m.BillingAddress is BillingAddr a ?
(a.FullName, a.Address, $"{a.PostalDest.AtPlz?.Plz}") :
(m.FullName, m.Address, $"{m.PostalDest.AtPlz?.Plz}") :
null);
}
public async static Task<TracesCertificate[]> FetchTracesCertificates(string? oocId, string? lfbisNr, (string Name, string Address, string PostalCode)? address) {
bool searchedByName = false;
JsonArray? jsonCerts = null;
if (oocId != null) {
try {
jsonCerts = await TryFetchTracesCertificates(new Dictionary<string, string> {
{ "operatorIdentifierType", "ooc_identifier" },
{ "operatorIdentifierSearchOperator", "STRICT" },
{ "operatorIdentifier", oocId },
});
} catch {
jsonCerts = null;
}
}
if (jsonCerts == null && lfbisNr != null) {
try {
jsonCerts = await TryFetchTracesCertificates(new Dictionary<string, string> {
{ "operatorIdentifierType", "comp_reg" },
{ "operatorIdentifierSearchOperator", "STRICT" },
{ "operatorIdentifier", lfbisNr.TrimStart('0') },
});
} catch {
jsonCerts = null;
}
}
if (jsonCerts == null && address != null) {
try {
jsonCerts = await TryFetchTracesCertificates(new Dictionary<string, string> {
{ "operatorPostalCode", address.Value.PostalCode },
{ "query", $"{address.Value.Name} {address.Value.Address}" },
});
searchedByName = true;
} catch {
jsonCerts = null;
}
}
if (jsonCerts == null)
return [];
TracesCertificate[] certs = [];
try {
certs = [.. jsonCerts.Select(j => new TracesCertificate {
Id = j!["reference"]?.GetValue<string>() ?? throw new Exception(),
AuthorityCode = j["issuingBody"]?["code"]?.GetValue<string>() ?? throw new Exception(),
OperatorId = j["operatorIdentifier"]?.GetValue<string>() ?? throw new Exception(),
OperatorName = j["operator"]?["name"]?.GetValue<string>() ?? throw new Exception(),
Activities = [.. j["activities"]?.AsArray().Select(a => Enum.Parse<TracesActivity>(a?["id"]?.GetValue<string>().ToUpper().Replace("_IMPORT", "IMPORT") ?? throw new Exception())) ?? []],
CategoriesOfProduct = [.. j["categoriesOfProduct"]?.AsArray().Select(c => Enum.Parse<TracesCategoryOfProduct>(c?["id"]?.GetValue<string>() ?? throw new Exception())) ?? []],
IssuedOn = DateOnly.FromDateTime(DateTime.ParseExact(j["issuedOn"]?.GetValue<string>() ?? throw new Exception(), "yyyy-MM-ddTHH:mm:ss.fffK", CultureInfo.InvariantCulture, DateTimeStyles.None)),
ExpiresOn = DateOnly.ParseExact(j["expiresOn"]?.GetValue<string>() ?? throw new Exception(), "yyyy-MM-dd"),
LastStatusUpdateOn = DateOnly.FromDateTime(DateTime.ParseExact(j["lastStatusUpdateDateTime"]?.GetValue<string>() ?? throw new Exception(), "yyyy-MM-ddTHH:mm:ss.fffK", CultureInfo.InvariantCulture, DateTimeStyles.None)),
Status = Enum.Parse<TracesStatus>(j["status"]?["id"]?.GetValue<string>() ?? throw new Exception()),
})];
} catch {
return [];
}
return (!searchedByName || certs.Where(c => c.Status == TracesStatus.ISSUED).Select(c => c.OperatorId).ToHashSet().Count == 1) ? certs : [];
}
}
}
+10
View File
@@ -6,6 +6,7 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Elwig.ViewModels {
public partial class MemberAdminViewModel : ObservableObject {
@@ -126,6 +127,15 @@ namespace Elwig.ViewModels {
[ObservableProperty]
private bool _isOrganic;
[ObservableProperty]
private string? _organicTest1;
[ObservableProperty]
private string? _organicTest2;
[ObservableProperty]
private Brush? _organicTest3;
[ObservableProperty]
private string? _organicTest4;
[ObservableProperty]
private string? _entryDate;
[ObservableProperty]
@@ -52,6 +52,8 @@ namespace Elwig.ViewModels {
[ObservableProperty]
private bool _deduct;
[ObservableProperty]
private bool _companyHasSigned;
[ObservableProperty]
private bool _memberHasSigned;
[ObservableProperty]
private string? _valuePerShareString;
+10 -4
View File
@@ -121,12 +121,18 @@
VerticalAlignment="Top" HorizontalAlignment="Left" Grid.Column="3" Margin="0,130,10,10" Width="64"
TextChanged="LfbisNrInput_TextChanged" LostFocus="LfbisNrInput_LostFocus"/>
<Label Content="Bio-Kontrollstelle:"
VerticalAlignment="Top" HorizontalAlignment="Right" Grid.Column="3" Margin="10,130,110,10"/>
<TextBox x:Name="ClientOrganicAuthorityInput"
VerticalAlignment="Top" HorizontalAlignment="Right" Grid.Column="3" Margin="10,130,10,10" Width="90"
<Label Content="Bio-KSt.:" ToolTip="Bio-Konstrollstelle"
VerticalAlignment="Top" HorizontalAlignment="Right" Grid.Column="3" Margin="10,100,110,10"/>
<TextBox x:Name="ClientOrganicAuthorityCodeInput"
VerticalAlignment="Top" HorizontalAlignment="Right" Grid.Column="3" Margin="10,100,10,10" Width="91"
TextChanged="OrganicAuthorityCodeInput_TextChanged" LostFocus="OrganicAuthorityCodeInput_LostFocus"/>
<Label Content="OOC-ID:" ToolTip="Bio-EU-Operator-ID / Organic Operator Certificate ID"
VerticalAlignment="Top" HorizontalAlignment="Right" Grid.Column="3" Margin="10,130,110,10"/>
<TextBox x:Name="ClientOrganicOperatorIdInput"
VerticalAlignment="Top" HorizontalAlignment="Right" Grid.Column="3" Margin="10,130,10,10" Width="91"
TextChanged="OrganicOperatorIdInput_TextChanged" LostFocus="OrganicOperatorIdInput_LostFocus"/>
<Label Content="Telefon-Nr.:"
VerticalAlignment="Top" HorizontalAlignment="Left" Grid.Column="2" Margin="10,160,0,10"/>
<TextBox x:Name="ClientPhoneNrInput" Margin="0,160,10,10" Grid.Column="3"
+4 -2
View File
@@ -331,7 +331,8 @@ namespace Elwig.Windows {
ClientBicInput.Text = p.Bic;
ClientUstIdNrInput.Text = p.UstIdNr;
ClientLfbisNrInput.Text = p.LfbisNr;
ClientOrganicAuthorityInput.Text = p.OrganicAuthority;
ClientOrganicOperatorIdInput.Text = p.OrganicOperatorId;
ClientOrganicAuthorityCodeInput.Text = p.OrganicAuthorityCode;
ClientPhoneNrInput.Text = p.PhoneNr;
ClientFaxNrInput.Text = p.FaxNr;
ClientEmailAddressInput.Text = p.EmailAddress;
@@ -371,7 +372,8 @@ namespace Elwig.Windows {
p.Bic = string.IsNullOrWhiteSpace(ClientBicInput.Text) ? null : ClientBicInput.Text;
p.UstIdNr = string.IsNullOrWhiteSpace(ClientUstIdNrInput.Text) ? null : ClientUstIdNrInput.Text;
p.LfbisNr = string.IsNullOrWhiteSpace(ClientLfbisNrInput.Text) ? null : ClientLfbisNrInput.Text;
p.OrganicAuthority = string.IsNullOrWhiteSpace(ClientOrganicAuthorityInput.Text) ? null : ClientOrganicAuthorityInput.Text;
p.OrganicOperatorId = string.IsNullOrWhiteSpace(ClientOrganicOperatorIdInput.Text) ? null : ClientOrganicOperatorIdInput.Text;
p.OrganicAuthorityCode = string.IsNullOrWhiteSpace(ClientOrganicAuthorityCodeInput.Text) ? null : ClientOrganicAuthorityCodeInput.Text;
p.PhoneNr = string.IsNullOrWhiteSpace(ClientPhoneNrInput.Text) ? null : ClientPhoneNrInput.Text;
p.FaxNr = string.IsNullOrWhiteSpace(ClientFaxNrInput.Text) ? null : ClientFaxNrInput.Text;
p.EmailAddress = string.IsNullOrWhiteSpace(ClientEmailAddressInput.Text) ? null : ClientEmailAddressInput.Text;
+6 -1
View File
@@ -618,7 +618,7 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="135"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
@@ -665,6 +665,11 @@
Checked="CheckBox_Changed" Unchecked="CheckBox_Changed"
Grid.Column="2" HorizontalAlignment="Left" Margin="10,75,0,0" VerticalAlignment="Top"/>
<Label Content="Bio-Zert.:" Margin="10,100,0,0" Grid.Column="2" HorizontalAlignment="Left"/>
<TextBlock FontSize="14" Margin="70,102,10,0" Grid.Column="2" HorizontalAlignment="Stretch" VerticalAlignment="Top" ToolTip="{Binding OrganicTest4}">
<Run Text="{Binding OrganicTest1}" FontWeight="Bold" Foreground="{Binding OrganicTest3}"/><Run FontSize="12" Text="{Binding OrganicTest2}"/>
</TextBlock>
<Label Content="Stamm-Zwst.:" Margin="10,130,0,0" Grid.Column="0"/>
<ComboBox x:Name="BranchInput" SelectedItem="{Binding Branch, Mode=TwoWay}" ItemsSource="{Binding BranchSource, Mode=TwoWay}"
DisplayMemberPath="Name" TextSearch.TextPath="Name"
@@ -173,8 +173,11 @@
Margin="0,100,10,0" Width="78" Grid.Column="1" HorizontalAlignment="Left" TextAlignment="Right"
TextChanged="DateNoticeInput_TextChanged" LostFocus="DateInput_LostFocus"/>
<CheckBox x:Name="HasCompanySignedInput" IsChecked="{Binding CompanyHasSigned, Mode=TwoWay}" Content="Von WG unterfertigt"
Margin="88,105,10,0" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Top"
Checked="CheckBox_Changed" Unchecked="CheckBox_Changed"/>
<CheckBox x:Name="HasMemberSignedInput" IsChecked="{Binding MemberHasSigned, Mode=TwoWay}" Content="Von Mg. unterfertigt"
Margin="88,105,10,0" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="TOp"
Margin="88,135,10,0" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Top"
Checked="CheckBox_Changed" Unchecked="CheckBox_Changed"/>
<Label Content="Wirksam:" Margin="10,130,0,0" Grid.Column="0"/>