@@ -96,6 +96,9 @@ namespace Elwig.Models.Entities {
|
|||||||
[Column("ustid_nr")]
|
[Column("ustid_nr")]
|
||||||
public string? UstIdNr { get; set; }
|
public string? UstIdNr { get; set; }
|
||||||
|
|
||||||
|
[Column("org_op_id")]
|
||||||
|
public string? OrganicOperatorId { get; set; }
|
||||||
|
|
||||||
[Column("org_auth_code")]
|
[Column("org_auth_code")]
|
||||||
public string? OrganicAuthorityCode { get; set; }
|
public string? OrganicAuthorityCode { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
-- schema version 41 to 42
|
-- schema version 41 to 42
|
||||||
|
|
||||||
|
ALTER TABLE member ADD COLUMN org_op_id TEXT CHECK (org_op_id REGEXP '^[0-9]{3}-[0-9]{7}$') DEFAULT NULL;
|
||||||
ALTER TABLE member ADD COLUMN org_auth_code TEXT CHECK (org_auth_code REGEXP '^[A-Z]{2}-[A-ZÖØ]{3}-[0-9]{2,3}(-[A-Z]{2})?$') DEFAULT NULL;
|
ALTER TABLE member ADD COLUMN org_auth_code TEXT CHECK (org_auth_code REGEXP '^[A-Z]{2}-[A-ZÖØ]{3}-[0-9]{2,3}(-[A-Z]{2})?$') DEFAULT NULL;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
namespace Elwig.Services {
|
namespace Elwig.Services {
|
||||||
public static class MemberService {
|
public static class MemberService {
|
||||||
@@ -188,10 +189,9 @@ namespace Elwig.Services {
|
|||||||
.GroupBy(d => d.Year)
|
.GroupBy(d => d.Year)
|
||||||
.ToDictionaryAsync(g => g.Key, g => g.Any());
|
.ToDictionaryAsync(g => g.Key, g => g.Any());
|
||||||
|
|
||||||
if (m.MgNr != vm.MgNr)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await App.MainDispatcher.BeginInvoke(() => {
|
await App.MainDispatcher.BeginInvoke(() => {
|
||||||
|
if (m.MgNr != vm.MgNr)
|
||||||
|
return;
|
||||||
var (d1Grid, _) = DeliveryService.GenerateToolTip(d1GridData, []);
|
var (d1Grid, _) = DeliveryService.GenerateToolTip(d1GridData, []);
|
||||||
var (d2Grid, _) = DeliveryService.GenerateToolTip(d2GridData, []);
|
var (d2Grid, _) = DeliveryService.GenerateToolTip(d2GridData, []);
|
||||||
var grid = AreaComService.GenerateToolTip(gridData);
|
var grid = AreaComService.GenerateToolTip(gridData);
|
||||||
@@ -208,6 +208,58 @@ namespace Elwig.Services {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vm.OrganicTest1 = null;
|
||||||
|
vm.OrganicTest2 = "...";
|
||||||
|
vm.OrganicTest3 = Brushes.Black;
|
||||||
|
vm.OrganicTest4 = null;
|
||||||
|
Utils.RunBackground("Bio-Zertifikatsdaten laden", async () => {
|
||||||
|
if (App.MainDispatcher == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var certs = await OrganicService.FetchTracesCertificatesOfMember(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.MemberHasEmail = m.EmailAddresses.Count > 0;
|
||||||
vm.MemberCanSendEmail = App.Config.Smtp != null && 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).ToList();
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 async static Task<TracesCertificate[]> FetchTracesCertificatesOfMember(Member m, bool? tryNameAndAddress = null) {
|
||||||
|
bool searchedByName = false;
|
||||||
|
|
||||||
|
JsonArray? jsonCerts = null;
|
||||||
|
if (m.OrganicOperatorId != null) {
|
||||||
|
try {
|
||||||
|
jsonCerts = await TryFetchTracesCertificates(new Dictionary<string, string> {
|
||||||
|
{ "operatorIdentifierType", "ooc_identifier" },
|
||||||
|
{ "operatorIdentifierSearchOperator", "STRICT" },
|
||||||
|
{ "operatorIdentifier", m.OrganicOperatorId },
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
jsonCerts = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonCerts == null && m.LfbisNr != null) {
|
||||||
|
try {
|
||||||
|
jsonCerts = await TryFetchTracesCertificates(new Dictionary<string, string> {
|
||||||
|
{ "operatorIdentifierType", "comp_reg" },
|
||||||
|
{ "operatorIdentifierSearchOperator", "STRICT" },
|
||||||
|
{ "operatorIdentifier", m.LfbisNr.TrimStart('0') },
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
jsonCerts = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonCerts == null && (tryNameAndAddress ?? m.IsOrganic)) {
|
||||||
|
try {
|
||||||
|
jsonCerts = await TryFetchTracesCertificates(new Dictionary<string, string> {
|
||||||
|
{ "operatorPostalCode", $"{(m.BillingAddress?.PostalDest ?? m.PostalDest).AtPlz?.Plz}" },
|
||||||
|
{ "query", m.BillingAddress is BillingAddr a ? $"{a.FullName} {a.Address}" : $"{m.FullName} {m.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 : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ using System.Collections.ObjectModel;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
namespace Elwig.ViewModels {
|
namespace Elwig.ViewModels {
|
||||||
public partial class MemberAdminViewModel : ObservableObject {
|
public partial class MemberAdminViewModel : ObservableObject {
|
||||||
@@ -124,6 +125,15 @@ namespace Elwig.ViewModels {
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _isOrganic;
|
private bool _isOrganic;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string? _organicTest1;
|
||||||
|
[ObservableProperty]
|
||||||
|
private string? _organicTest2;
|
||||||
|
[ObservableProperty]
|
||||||
|
private Brush? _organicTest3;
|
||||||
|
[ObservableProperty]
|
||||||
|
private string? _organicTest4;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string? _entryDate;
|
private string? _entryDate;
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
|||||||
@@ -568,6 +568,10 @@
|
|||||||
<CheckBox x:Name="OrganicInput" Content="Bio" IsChecked="{Binding IsOrganic, Mode=TwoWay}" IsEnabled="False"
|
<CheckBox x:Name="OrganicInput" Content="Bio" IsChecked="{Binding IsOrganic, Mode=TwoWay}" IsEnabled="False"
|
||||||
Checked="CheckBox_Changed" Unchecked="CheckBox_Changed"
|
Checked="CheckBox_Changed" Unchecked="CheckBox_Changed"
|
||||||
Grid.Column="2" HorizontalAlignment="Left" Margin="10,45,0,0" VerticalAlignment="Top"/>
|
Grid.Column="2" HorizontalAlignment="Left" Margin="10,45,0,0" VerticalAlignment="Top"/>
|
||||||
|
<Label Content="Bio-Zert.:" Margin="10,10,150,0" Grid.Column="2" HorizontalAlignment="Right"/>
|
||||||
|
<TextBlock FontSize="14" Margin="10,12,10,0" Grid.Column="2" HorizontalAlignment="Right" ToolTip="{Binding OrganicTest4}">
|
||||||
|
<Run Text="{Binding OrganicTest1}" FontWeight="Bold" Foreground="{Binding OrganicTest3}"/><Run FontSize="12" Text="{Binding OrganicTest2}"/>
|
||||||
|
</TextBlock>
|
||||||
<Label Content="Bio-KSt.:" Margin="10,40,95,0" Grid.Column="2" HorizontalAlignment="Right" ToolTip="Bio-Kontrollstelle"/>
|
<Label Content="Bio-KSt.:" Margin="10,40,95,0" Grid.Column="2" HorizontalAlignment="Right" ToolTip="Bio-Kontrollstelle"/>
|
||||||
<TextBox x:Name="OrganicAuthorityCodeInput" Text="{Binding OrganicAuthorityCode, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
<TextBox x:Name="OrganicAuthorityCodeInput" Text="{Binding OrganicAuthorityCode, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||||
Margin="0,40,10,0" Grid.Column="2" Width="82" HorizontalAlignment="Right"
|
Margin="0,40,10,0" Grid.Column="2" Width="82" HorizontalAlignment="Right"
|
||||||
|
|||||||
Reference in New Issue
Block a user