Compare commits

...

15 Commits

Author SHA1 Message Date
9172222307 Bump version to 0.6.7
Some checks failed
Deploy / Build and Deploy (push) Has been cancelled
2024-02-21 15:16:24 +01:00
05a75a52cc Windows: Use App.HintContextChange() where applicable 2024-02-21 15:12:45 +01:00
8732141e6b MemberDataSheet: Show area com buckets of current year (regardless of season) 2024-02-21 15:10:27 +01:00
99ca12b276 Weighing: Restructure class structure 2024-02-21 12:57:55 +01:00
7ff069d068 ScaleTestMatzen: Use hard coded date instead of current time 2024-02-21 12:06:50 +01:00
583d5b4e3e ClientParameters: Add WG Weinland and Baden 2024-02-21 11:16:52 +01:00
3f2b5b684c Weighing: Remove unused scales 2024-02-21 11:00:30 +01:00
5db14c09ad UtilsTest: Add Scale from Gr.Inzersdorf 2024-02-21 10:50:59 +01:00
791eaddf58 Bump version to 0.6.6
Some checks failed
Deploy / Build and Deploy (push) Has been cancelled
2024-02-18 23:21:29 +01:00
5cb29aa75f AppDbContext: Do not use Min() to avoid errors when no members/FBs are present 2024-02-18 23:19:56 +01:00
3c0fea30f5 DeliveryAdminWindow: Allow users to create deliveries in current year before march/july 2024-02-18 23:19:06 +01:00
f2df121435 SystecScale: Remove .ToString() 2024-02-18 22:24:10 +01:00
7f4cfdc1b5 ScaleTestMatzen: Add more tests 2024-02-18 20:47:12 +01:00
f4eb6456be Tests: Add WeighingTests 2024-02-18 17:31:10 +01:00
f13fb3aaf0 UtilsTest: Add Test_CalcCrc16Modbus 2024-02-18 17:29:50 +01:00
25 changed files with 518 additions and 241 deletions

View File

@ -63,7 +63,7 @@ namespace Elwig {
Directory.CreateDirectory(TempPath);
Directory.CreateDirectory(DataPath);
MainDispatcher = Dispatcher;
Scales = Array.Empty<IScale>();
Scales = [];
CurrentApp = this;
OverrideCulture();
}
@ -96,7 +96,7 @@ namespace Elwig {
return;
}
Dictionary<string, (string, string, int?, string?, string?, string?, string?, string?)> branches = new();
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));
try {
@ -115,23 +115,11 @@ namespace Elwig {
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}\"");
}
list.Add(Scale.FromConfig(s));
} 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(new InvalidScale(s.Id));
MessageBox.Show($"Unable to create scale {s.Id}:\n\n{e.Message}", "Scale Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
Scales = list;

View File

@ -15,7 +15,7 @@ namespace Elwig.Documents {
public MemberDataSheet(Member m, AppDbContext ctx) : base($"{Name} {m.AdministrativeName}", m) {
DocumentId = $"{Name} {m.MgNr}";
Season = ctx.Seasons.ToList().MaxBy(s => s.Year) ?? throw new ArgumentException("invalid season");
MemberBuckets = ctx.GetMemberBuckets(Season.Year, m.MgNr).GetAwaiter().GetResult();
MemberBuckets = ctx.GetMemberBuckets(Utils.CurrentYear, m.MgNr).GetAwaiter().GetResult();
}
}
}

View File

@ -7,7 +7,7 @@
<UseWPF>true</UseWPF>
<PreserveCompilationContext>true</PreserveCompilationContext>
<ApplicationIcon>Resources\Images\Elwig.ico</ApplicationIcon>
<Version>0.6.5</Version>
<Version>0.6.7</Version>
<SatelliteResourceLanguages>de-AT</SatelliteResourceLanguages>
</PropertyGroup>

View File

@ -160,16 +160,16 @@ namespace Elwig.Helpers {
}
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; });
return c + 1;
}
public async Task<int> NextFbNr() {
int c = await AreaCommitments.Select(ac => ac.FbNr).MinAsync();
int c = 0;
(await AreaCommitments.OrderBy(ac => ac.FbNr).Select(ac => ac.FbNr).ToListAsync())
.ForEach(a => { if (a <= c + 1000) c = a; });
.ForEach(a => { if (a <= c + 10000) c = a; });
return c + 1;
}

View File

@ -9,7 +9,7 @@ namespace Elwig.Helpers {
public static class AppDbUpdater {
// Don't forget to update value in Tests/fetch-resources.bat!
public static readonly int RequiredSchemaVersion = 16;
public static readonly int RequiredSchemaVersion = 17;
private static int VersionOffset = 0;

View File

@ -7,20 +7,20 @@ using System.Threading.Tasks;
namespace Elwig.Helpers {
public class ClientParameters {
public enum Type { Matzen, Winzerkeller };
public enum Type { Matzen, Winzerkeller, Weinland, Baden };
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 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 HasRebler(string? zwstId) => IsMatzen || (IsWinzerkeller && zwstId == "W");
public bool HasRebler(Branch? b) => HasRebler(b?.ZwstId);
public bool HasRebler() => HasRebler(App.ZwstId);
public bool HasKisten(string? zwstId) => IsWinzerkeller && (zwstId == "H" || zwstId == "S");
public bool HasKisten(Branch? b) => HasKisten(b?.ZwstId);
public bool HasKisten() => HasKisten(App.ZwstId);
public bool HasNetWeighing(string? zwstId) => IsMatzen || (IsWinzerkeller && zwstId == "W");
public bool HasNetWeighing(Branch? b) => HasNetWeighing(b?.ZwstId);
public bool HasNetWeighing() => HasNetWeighing(App.ZwstId);
public string NameToken;
public string NameShort;
@ -36,8 +36,8 @@ namespace Elwig.Helpers {
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;
@ -72,8 +72,14 @@ 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;
};
Plz = int.Parse(parameters["CLIENT_PLZ"] ?? "");
@ -115,7 +121,7 @@ namespace Elwig.Helpers {
case 2: deliveryNoteStats = "SHORT"; break;
case 3: deliveryNoteStats = "FULL"; break;
}
return new (string, string?)[] {
return [
("CLIENT_NAME_TOKEN", NameToken),
("CLIENT_NAME_SHORT", NameShort),
("CLIENT_NAME", Name),
@ -137,7 +143,7 @@ namespace Elwig.Helpers {
("TEXT_DELIVERYNOTE", TextDeliveryNote),
("TEXT_DELIVERYCONFIRMATION", TextDeliveryConfirmation),
("TEXT_CREDITNOTE", TextCreditNote),
};
];
}
public async Task UpdateValues() {

View File

@ -4,6 +4,31 @@ using System.Linq;
using Microsoft.Extensions.Configuration;
namespace Elwig.Helpers {
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 string? Log;
public string? _Log;
public ScaleConfig(string id, string? type, string? model, string? cnx, string? empty, string? filling, string? limit, string? log) {
Id = id;
Type = type;
Model = model;
Connection = cnx;
Empty = empty;
Filling = filling;
Limit = limit;
_Log = log;
Log = log != null ? Path.Combine(App.DataPath, log) : null;
}
}
public class Config {
private readonly string FileName;
@ -11,8 +36,8 @@ namespace Elwig.Helpers {
public string DatabaseFile = App.DataPath + "database.sqlite3";
public string? DatabaseLog = null;
public string? Branch = null;
public IList<string?[]> Scales;
private readonly List<string?[]> ScaleList = [];
public IList<ScaleConfig> Scales;
private readonly List<ScaleConfig> ScaleList = [];
private static readonly string[] trueValues = ["1", "true", "yes", "on"];
public Config(string filename) {
@ -34,12 +59,10 @@ namespace Elwig.Helpers {
ScaleList.Clear();
Scales = ScaleList;
foreach (var s in scales) {
string? scaleLog = config[$"scale.{s}:log"];
if (scaleLog != null) scaleLog = Path.Combine(App.DataPath, scaleLog);
ScaleList.Add([
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"], scaleLog
]);
config[$"scale.{s}:empty"], config[$"scale.{s}:filling"], config[$"scale.{s}:limit"], config[$"scale.{s}:log"]
));
}
}
@ -51,11 +74,11 @@ namespace Elwig.Helpers {
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");
file.Write($"\r\n[scale.{s.Id}]\r\ntype = {s.Type}\r\nmodel = {s.Model}\r\nconnection = {s.Connection}\r\n");
if (s.Empty != null) file.Write($"empty = {s.Empty}\r\n");
if (s.Filling != null) file.Write($"filling = {s.Filling}\r\n");
if (s.Limit != null) file.Write($"limit = {s.Limit}\r\n");
if (s._Log != null) file.Write($"log = {s._Log}\r\n");
}
}
}

View File

@ -1,64 +0,0 @@
using System;
using System.IO;
using System.IO.Ports;
using System.Text;
using System.Threading.Tasks;
namespace Elwig.Helpers.Weighing {
public class GassnerScale : IScale {
protected SerialPort Serial = null;
protected StreamReader Reader;
protected StreamWriter Writer;
public string Manufacturer => "Gassner";
public int InternalScaleNr => 1;
public string Model { get; private set; }
public string ScaleId { get; private set; }
public bool IsReady { get; private set; }
public bool HasFillingClearance { get; private set; }
public int? WeightLimit { get; private set; }
public string? LogPath { get; private set; }
public GassnerScale(string id, string model, string connection) {
ScaleId = id;
Model = model;
IsReady = true;
HasFillingClearance = false;
if (!connection.StartsWith("serial:"))
throw new ArgumentException("Unsupported scheme");
Serial = Utils.OpenSerialConnection(connection);
Writer = new(Serial.BaseStream, Encoding.ASCII, -1, true);
Reader = new(Serial.BaseStream, Encoding.ASCII, false, -1, true);
}
public void Dispose() {
Writer.Close();
Reader.Close();
Serial.Close();
GC.SuppressFinalize(this);
}
public async Task<WeighingResult> Weigh(bool incIdentNr) {
await Writer.WriteAsync(incIdentNr ? "\x05" : "?");
// TODO receive response
return new();
}
public async Task<WeighingResult> GetCurrentWeight() {
return await Weigh(false);
}
public async Task<WeighingResult> Weigh() {
return await Weigh(true);
}
public async Task Empty() { }
public async Task GrantFillingClearance() { }
public async Task RevokeFillingClearance() { }
}
}

View File

@ -0,0 +1,95 @@
using System.IO.Ports;
using System.IO;
using System.Net.Sockets;
using System;
namespace Elwig.Helpers.Weighing {
public abstract class Scale : IDisposable {
protected enum Output { RTS, DTR, OUT1, OUT2 };
protected SerialPort? Serial = null;
protected SerialPort? ControlSerialEmpty = null, ControlSerialFilling = null;
protected TcpClient? Tcp = null;
protected Stream Stream;
protected readonly Output? EmptyMode = null;
protected readonly Output? FillingClearanceMode = null;
protected readonly int EmptyDelay;
public int? WeightLimit { get; private set; }
public string? LogPath { get; private set; }
public static IScale FromConfig(ScaleConfig config) {
int? limit = config.Limit != null ? int.Parse(config.Limit) : null;
if (config.Type == "SysTec-IT") {
return new SysTecITScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log);
} else if (config.Type == "Schember-evt") {
return new SchemberEventScale(config.Id, config.Model!, config.Connection!, config.Empty, config.Filling, limit, config.Log);
} else {
throw new ArgumentException($"Invalid scale type: \"{config.Type}\"");
}
}
protected Scale(string cnx, string? empty, string? filling, int? limit, string? log) {
if (cnx.StartsWith("serial:")) {
Serial = Utils.OpenSerialConnection(cnx);
Stream = Serial.BaseStream;
} else if (cnx.StartsWith("tcp:")) {
Tcp = Utils.OpenTcpConnection(cnx);
Stream = Tcp.GetStream();
} else {
throw new ArgumentException("Unsupported scheme");
}
LogPath = log;
if (empty != null) {
var parts = empty.Split(':');
if (parts.Length == 3) {
if (parts[0] != Serial?.PortName)
ControlSerialEmpty = Utils.OpenSerialConnection($"serial://{parts[0]}:9600");
} else if (parts.Length != 2) {
throw new ArgumentException("Invalid value for 'empty'");
}
EmptyMode = ConvertOutput(parts[^2]);
EmptyDelay = int.Parse(parts[^1]);
}
WeightLimit = limit;
if (filling != null) {
var parts = filling.Split(':');
if (parts.Length == 2) {
if (parts[0] != Serial?.PortName)
ControlSerialFilling = parts[0] != ControlSerialEmpty?.PortName ? Utils.OpenSerialConnection($"serial://{parts[0]}:9600") : ControlSerialEmpty;
} else if (parts.Length != 1) {
throw new ArgumentException("Invalid value for 'filling'");
}
FillingClearanceMode = ConvertOutput(parts[^1]);
}
if (FillingClearanceMode != null && WeightLimit == null)
throw new ArgumentException("Weight limit has to be set, if filling clearance supervision is enabled");
}
public void Dispose() {
Stream.Close();
Serial?.Close();
ControlSerialEmpty?.Close();
ControlSerialFilling?.Close();
Tcp?.Close();
GC.SuppressFinalize(this);
}
protected static Output? ConvertOutput(string? value) {
return value switch {
null => null,
"RTS" => Output.RTS,
"DTR" => Output.DTR,
"OUT1" => Output.OUT1,
"OUT2" => Output.OUT2,
_ => throw new ArgumentException($"Invalid value for argument: '{value}'"),
};
}
}
}

View File

@ -0,0 +1,22 @@
namespace Elwig.Helpers.Weighing {
public class SchemberEventScale : Scale, IEventScale {
public string Manufacturer => "Schember";
public int InternalScaleNr => 1;
public string Model { get; private set; }
public string ScaleId { get; private set; }
public bool IsReady { get; private set; }
public bool HasFillingClearance { get; private set; }
public SchemberEventScale(string id, string model, string cnx, string? empty = null, string? filling = null, int? limit = null, string? log = null) :
base(cnx, empty, filling, limit, log) {
ScaleId = id;
Model = model;
IsReady = true;
HasFillingClearance = false;
Stream.WriteTimeout = 250;
Stream.ReadTimeout = 6000;
}
}
}

View File

@ -1,22 +0,0 @@
using System;
namespace Elwig.Helpers.Weighing {
public class SchemberScale : IEventScale {
public string Manufacturer => "Schember";
public string Model => throw new NotImplementedException();
public string ScaleId => throw new NotImplementedException();
public int InternalScaleNr => throw new NotImplementedException();
public bool IsReady => throw new NotImplementedException();
public bool HasFillingClearance => throw new NotImplementedException();
public int? WeightLimit => throw new NotImplementedException();
public string? LogPath => throw new NotImplementedException();
public void Dispose() {
throw new NotImplementedException();
}
}
}

View File

@ -1,23 +1,11 @@
using System;
using System.IO;
using System.IO.Ports;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace Elwig.Helpers.Weighing {
public class SystecScale : ICommandScale {
protected enum Output { RTS, DTR, OUT1, OUT2 };
protected SerialPort? Serial = null;
protected SerialPort? ControlSerialEmpty = null, ControlSerialFilling = null;
protected TcpClient? Tcp = null;
protected Stream Stream;
protected readonly Output? EmptyMode = null;
protected readonly Output? FillingClearanceMode = null;
protected readonly int EmptyDelay;
public class SysTecITScale : Scale, ICommandScale {
public string Manufacturer => "SysTec";
public int InternalScaleNr => 1;
@ -25,72 +13,15 @@ namespace Elwig.Helpers.Weighing {
public string ScaleId { get; private set; }
public bool IsReady { get; private set; }
public bool HasFillingClearance { get; private set; }
public int? WeightLimit { get; private set; }
public string? LogPath { get; private set; }
public SystecScale(string id, string model, string connection, string? empty = null, string? filling = null, int? limit = null, string? log = null) {
public SysTecITScale(string id, string model, string cnx, string? empty = null, string? filling = null, int? limit = null, string? log = null) :
base(cnx, empty, filling, limit, log) {
ScaleId = id;
Model = model;
IsReady = true;
HasFillingClearance = false;
LogPath = log;
if (connection.StartsWith("serial:")) {
Serial = Utils.OpenSerialConnection(connection);
Stream = Serial.BaseStream;
} else if (connection.StartsWith("tcp:")) {
Tcp = Utils.OpenTcpConnection(connection);
Stream = Tcp.GetStream();
} else {
throw new ArgumentException("Unsupported scheme");
}
if (empty != null) {
var parts = empty.Split(':');
if (parts.Length == 3) {
if (parts[0] != Serial?.PortName)
ControlSerialEmpty = Utils.OpenSerialConnection($"serial://{parts[0]}:9600");
} else if (parts.Length != 2) {
throw new ArgumentException("Invalid value for 'empty'");
}
EmptyMode = ConvertOutput(parts[^2]);
EmptyDelay = int.Parse(parts[^1]);
}
WeightLimit = limit;
if (filling != null) {
var parts = filling.Split(':');
if (parts.Length == 2) {
if (parts[0] != Serial?.PortName)
ControlSerialFilling = parts[0] != ControlSerialEmpty?.PortName ? Utils.OpenSerialConnection($"serial://{parts[0]}:9600") : ControlSerialEmpty;
} else if (parts.Length != 1) {
throw new ArgumentException("Invalid value for 'filling'");
}
FillingClearanceMode = ConvertOutput(parts[^1]);
}
if (FillingClearanceMode != null && WeightLimit == null)
throw new ArgumentException("Weight limit has to be set, if filling clearance supervision is enabled");
}
public void Dispose() {
Stream.Close();
Serial?.Close();
ControlSerialEmpty?.Close();
ControlSerialFilling?.Close();
Tcp?.Close();
GC.SuppressFinalize(this);
}
protected static Output? ConvertOutput(string? value) {
return value switch {
null => null,
"RTS" => Output.RTS,
"DTR" => Output.DTR,
"OUT1" => Output.OUT1,
"OUT2" => Output.OUT2,
_ => throw new ArgumentException($"Invalid value for argument: '{value}'"),
};
Stream.WriteTimeout = 250;
Stream.ReadTimeout = 11000;
}
protected async Task SendCommand(string command) {
@ -105,7 +36,7 @@ namespace Elwig.Helpers.Weighing {
line = await reader.ReadLineAsync();
if (LogPath != null) await File.AppendAllTextAsync(LogPath, $"{line}\r\n");
}
if (line == null || line.Length < 4 || !line.StartsWith("<") || !line.EndsWith(">")) {
if (line == null || line.Length < 4 || !line.StartsWith('<') || !line.EndsWith('>')) {
throw new IOException("Invalid response from scale");
}
@ -162,7 +93,7 @@ namespace Elwig.Helpers.Weighing {
var crc16 = line[52..60].Trim();
if (Utils.CalcCrc16Modbus(record[..54]) != ushort.Parse(crc16)) {
throw new IOException($"Invalid response from scale: Invalid CRC16 checksum ({crc16} != {Utils.CalcCrc16Modbus(record[..54]).ToString()})");
throw new IOException($"Invalid response from scale: Invalid CRC16 checksum ({crc16} != {Utils.CalcCrc16Modbus(record[..54])})");
} else if (unit != "kg") {
throw new IOException($"Unsupported unit in weighing response: '{unit}'");
}

View File

@ -4,36 +4,38 @@ namespace Elwig.Helpers.Weighing {
/// <summary>
/// Result of a weighing process on an industrial scale
/// </summary>
public class WeighingResult {
public struct WeighingResult {
/// <summary>
/// Measured net weight in kg
/// </summary>
public int? Weight = null;
public int? Weight;
/// <summary>
/// Weighing id (or IdentNr) provided by the scale
/// </summary>
public string? WeighingId = null;
public string? WeighingId;
/// <summary>
/// Wheighing id (or IdentNr) provided by the scale optionally combined with the current date
/// </summary>
public string? FullWeighingId = null;
public string? FullWeighingId;
/// <summary>
/// Date string provided by the scale
/// </summary>
public DateOnly? Date = null;
public DateOnly? Date;
/// <summary>
/// Time string provided by the scale
/// </summary>
public TimeOnly? Time = null;
public TimeOnly? Time;
/// <returns>&lt;Weight/WeighingId/Date/Time&gt;</returns>
override public string ToString() {
public override readonly string ToString() {
var w = Weight != null ? $"{Weight}kg" : "";
return $"<{w}/{WeighingId}/{Date:yyyy-MM-dd}/{Time:HH:mm}>";
}
}
}

View File

@ -0,0 +1,12 @@
-- schema version 16 to 17
CREATE VIEW v_virtual_season AS
SELECT year, max_kg_per_ha
FROM season
UNION
SELECT strftime('%Y', date()) + 0, (SELECT max_kg_per_ha FROM season ORDER BY year DESC LIMIT 1);
PRAGMA writable_schema = ON;
UPDATE sqlite_schema SET sql = REPLACE(sql, 'season s', 'v_virtual_season s')
WHERE type = 'view' AND name = 'v_area_commitment_bucket_strict';
PRAGMA writable_schema = OFF;

View File

@ -16,21 +16,21 @@ namespace Elwig.Windows {
public int MgNr => Member.MgNr;
private readonly Member Member;
private List<string> TextFilter = new();
private List<string> TextFilter = [];
public AreaComAdminWindow(int mgnr) {
InitializeComponent();
Member = Context.Members.Find(mgnr) ?? throw new ArgumentException("MgNr argument has invalid value");
Title = $"Flächenbindungen - {Member.AdministrativeName} - Elwig";
ExemptInputs = new Control[] {
ExemptInputs = [
MgNrInput, AreaCommitmentList, NewAreaCommitmentButton,
EditAreaCommitmentButton, DeleteAreaCommitmentButton, AreaCommitmentSaveButton,
AreaCommitmentResetButton, AreaCommitmentCancelButton, SearchInput, ActiveAreaCommitmentInput
};
RequiredInputs = new Control[] {
];
RequiredInputs = [
FbNrInput, YearFromInput, KgInput, RdInput,
GstNrInput, AreaInput.TextBox, AreaComTypeInput, WineCultivationInput
};
];
}
private void Window_Loaded(object sender, RoutedEventArgs e) {
@ -76,10 +76,10 @@ namespace Elwig.Windows {
}
private async Task<(List<string>, IQueryable<AreaCom>, List<string>)> GetFilters() {
List<string> filterNames = new();
List<string> filterNames = [];
IQueryable<AreaCom> areaComQuery = Context.AreaCommitments.Where(a => a.MgNr == Member.MgNr).OrderBy(a => a.FbNr);
if (ActiveAreaCommitmentInput.IsChecked == true) {
areaComQuery = areaComQuery.Where(a => (a.YearFrom <= Utils.CurrentNextSeason) && (a.YearTo == null || a.YearTo >= Utils.CurrentNextSeason));
areaComQuery = areaComQuery.Where(a => (a.YearFrom <= Utils.CurrentYear) && (a.YearTo == null || a.YearTo >= Utils.CurrentYear));
filterNames.Add("aktiv");
}
@ -151,7 +151,7 @@ namespace Elwig.Windows {
YearToInput.Text = a.YearTo.ToString();
KgInput.SelectedItem = a.Kg.AtKg;
RdInput.SelectedItem = a.Rd;
RdInput.SelectedItem = a.Rd ?? RdInput.Items[0];
GstNrInput.Text = a.GstNr;
AreaInput.Text = a.Area.ToString();
@ -230,8 +230,8 @@ namespace Elwig.Windows {
a.RdNr = RdInput.SelectedItem.GetType() == typeof(NullItem) ? null : ((WbRd)RdInput.SelectedItem).RdNr;
a.GstNr = GstNrInput.Text;
a.Area = int.Parse(AreaInput.Text);
a.VtrgId = (AreaComTypeInput.SelectedItem as AreaComType)?.VtrgId;
a.CultId = (WineCultivationInput.SelectedItem as WineCult)?.CultId;
a.VtrgId = (AreaComTypeInput.SelectedItem as AreaComType)!.VtrgId;
a.CultId = (WineCultivationInput.SelectedItem as WineCult)!.CultId;
a.Comment = (CommentInput.Text == "") ? null : CommentInput.Text;
EntityEntry<AreaCom>? tr = null;
@ -275,7 +275,7 @@ namespace Elwig.Windows {
MessageBox.Show(str, "Flächenbindung aktualisieren", MessageBoxButton.OK, MessageBoxImage.Error);
}
return a;
return a!;
}
private async void AreaCommitmentSaveButton_Click(object sender, RoutedEventArgs evt) {
@ -287,7 +287,7 @@ namespace Elwig.Windows {
ShowAreaCommitmentNewEditDeleteButtons();
LockInputs();
UnlockSearchInputs();
await RefreshAreaCommitmentList();
await App.HintContextChange();
AreaCommitmentList.SelectedItem = a;
}

View File

@ -218,7 +218,7 @@ namespace Elwig.Windows {
}
private void InitialDefaultInputs() {
if (App.Client.HasRebler(BranchInput.SelectedValue as Branch)) {
if (App.Client.HasNetWeighing(BranchInput.SelectedValue as Branch)) {
GerebeltGewogenInput.IsEnabled = false;
SetDefaultValue(GerebeltGewogenInput, true);
} else {
@ -226,7 +226,7 @@ namespace Elwig.Windows {
UnsetDefaultValue(GerebeltGewogenInput);
}
if (App.Client.HasKisten(BranchInput.SelectedValue as Branch)) {
if (!App.Client.HasNetWeighing(BranchInput.SelectedValue as Branch)) {
LesewagenInput.IsEnabled = false;
SetDefaultValue(LesewagenInput, false);
HandPickedInput.IsThreeState = false;
@ -256,9 +256,9 @@ namespace Elwig.Windows {
ClearOriginalValues();
ClearDefaultValues();
GerebeltGewogenInput.IsChecked = App.Client.HasRebler(BranchInput.SelectedValue as Branch);
GerebeltGewogenInput.IsChecked = App.Client.HasNetWeighing(BranchInput.SelectedValue as Branch);
LesewagenInput.IsChecked = false;
HandPickedInput.IsChecked = App.Client.HasKisten(BranchInput.SelectedValue as Branch) ? true : null;
HandPickedInput.IsChecked = !App.Client.HasNetWeighing(BranchInput.SelectedValue as Branch) ? true : null;
GebundenInput.IsChecked = null;
InitialDefaultInputs();
@ -863,7 +863,7 @@ namespace Elwig.Windows {
var originalMemberKgNr = d?.Member?.DefaultKgNr;
if (d == null) {
d = Context.CreateProxy<Delivery>();
year = Utils.CurrentNextSeason;
year = Utils.CurrentYear;
did = await Context.NextDId(year);
} else {
year = d.Year;
@ -1731,14 +1731,14 @@ namespace Elwig.Windows {
}
private void GerebeltGewogenInput_Changed(object sender, RoutedEventArgs evt) {
if (App.Client.HasKisten(BranchInput.SelectedValue as Branch)) {
if (!App.Client.HasNetWeighing(BranchInput.SelectedValue as Branch)) {
HandPickedInput.IsChecked = !GerebeltGewogenInput.IsChecked;
}
CheckBox_Changed(sender, evt);
}
private void HandPickedInput_Changed(object sender, RoutedEventArgs evt) {
if (App.Client.HasKisten(BranchInput.SelectedValue as Branch)) {
if (!App.Client.HasNetWeighing(BranchInput.SelectedValue as Branch)) {
GerebeltGewogenInput.IsChecked = !HandPickedInput.IsChecked;
}
CheckBox_Changed(sender, evt);

View File

@ -9,6 +9,7 @@ namespace Elwig.Windows {
InitializeComponent();
var v = Assembly.GetExecutingAssembly().GetName().Version;
VersionField.Text = "Version: " + (v == null ? "?" : $"{v.Major}.{v.Minor}.{v.Build}") + $" {App.BranchName}";
if (App.Client.Client == null) VersionField.Text += " (Unbekannt)";
if (!App.Config.Debug) {
TestWindowButton.Visibility = Visibility.Hidden;
//QueryWindowButton.Visibility = Visibility.Hidden;

View File

@ -219,7 +219,7 @@ namespace Elwig.Windows {
try {
Context.Remove(v);
await Context.SaveChangesAsync();
await HintContextChange();
await App.HintContextChange();
} catch (Exception exc) {
var str = "Der Eintrag konnte nicht in der Datenbank aktualisiert werden!\n\n" + exc.Message;
if (exc.InnerException != null) str += "\n\n" + exc.InnerException.Message;

View File

@ -100,5 +100,52 @@ namespace Tests.HelperTests {
Assert.That(Utils.SplitName("ABC GesbR", "Bauer"), Is.EqualTo(((string, string?))("ABC GesbR", null)));
});
}
[Test]
public void Test_CalcCrc16Modbus() {
Assert.Multiple(() => {
Assert.That(Utils.CalcCrc16Modbus(""), Is.EqualTo(0xFFFF));
Assert.That(Utils.CalcCrc16Modbus("abcd"), Is.EqualTo(0x1D97));
Assert.That(Utils.CalcCrc16Modbus("ABCD"), Is.EqualTo(0x0F85));
// Matzen (SysTec IT3000A)
Assert.That(Utils.CalcCrc16Modbus("000011.08.2319:03 01 0 0 0kg 1"), Is.EqualTo(43166));
Assert.That(Utils.CalcCrc16Modbus("000011.08.2319:04 01 0 0 0kg 1"), Is.EqualTo(21615));
Assert.That(Utils.CalcCrc16Modbus("000011.08.2319:05 01 0 0 0kg 1"), Is.EqualTo(40446));
Assert.That(Utils.CalcCrc16Modbus("000011.08.2319:06 01 0 0 0kg 1"), Is.EqualTo(34638));
Assert.That(Utils.CalcCrc16Modbus("000011.08.2319:07 01 0 0 0kg 1"), Is.EqualTo(20191));
Assert.That(Utils.CalcCrc16Modbus("000011.08.2319:08 01 0 0 0kg 1"), Is.EqualTo(16047));
Assert.That(Utils.CalcCrc16Modbus("000011.08.2319:09 01 0 0 0kg 1"), Is.EqualTo(63294));
Assert.That(Utils.CalcCrc16Modbus("000011.08.2319:10 01 0 0 0kg 1"), Is.EqualTo(7718));
Assert.That(Utils.CalcCrc16Modbus("000011.08.2319:12 01 0 0 0kg 1"), Is.EqualTo(52487));
// Wolkersdorf, Waage 1 (SysTec IT6000E)
Assert.That(Utils.CalcCrc16Modbus("000004.09.2315:50 51 0 0 0kg A"), Is.EqualTo(10187));
Assert.That(Utils.CalcCrc16Modbus("000004.09.2315:50 61 0 0 0kg A"), Is.EqualTo(20683));
Assert.That(Utils.CalcCrc16Modbus("000004.09.2315:50 71 0 0 0kg A"), Is.EqualTo(48586));
Assert.That(Utils.CalcCrc16Modbus("000004.09.2315:50 81 0 0 0kg A"), Is.EqualTo(22217));
Assert.That(Utils.CalcCrc16Modbus("000004.09.2315:50 91 0 0 0kg A"), Is.EqualTo(48072));
Assert.That(Utils.CalcCrc16Modbus("000004.09.2315:51 101 0 0 0kg A"), Is.EqualTo(30119));
Assert.That(Utils.CalcCrc16Modbus("000004.09.2315:51 111 0 0 0kg A"), Is.EqualTo(39078));
// Wolkersdorf, Waage 2 (SysTec IT6000E)
Assert.That(Utils.CalcCrc16Modbus("000004.09.2315:51 41 0 0 0kg B"), Is.EqualTo(539));
Assert.That(Utils.CalcCrc16Modbus("000004.09.2315:51 51 0 0 0kg B"), Is.EqualTo(61210));
Assert.That(Utils.CalcCrc16Modbus("000004.09.2315:51 61 0 0 0kg B"), Is.EqualTo(38938));
Assert.That(Utils.CalcCrc16Modbus("000004.09.2315:51 71 0 0 0kg B"), Is.EqualTo(29979));
Assert.That(Utils.CalcCrc16Modbus("000004.09.2315:51 81 0 0 0kg B"), Is.EqualTo(40472));
Assert.That(Utils.CalcCrc16Modbus("000004.09.2315:51 91 0 0 0kg B"), Is.EqualTo(29465));
Assert.That(Utils.CalcCrc16Modbus("000004.09.2315:51 101 0 0 0kg B"), Is.EqualTo(29927));
// Gr.Inzersdorf (L246/IT3)
Assert.That(Utils.CalcCrc16Modbus("000019.02.2410:49 11 0 0 0kg 001"), Is.EqualTo(27556));
Assert.That(Utils.CalcCrc16Modbus("000019.02.2410:49 21 0 0 0kg 001"), Is.EqualTo(7332));
Assert.That(Utils.CalcCrc16Modbus("000019.02.2410:49 31 0 0 0kg 001"), Is.EqualTo(61861));
Assert.That(Utils.CalcCrc16Modbus("000019.02.2410:49 41 0 0 0kg 001"), Is.EqualTo(62116));
Assert.That(Utils.CalcCrc16Modbus("000019.02.2410:49 51 0 0 0kg 001"), Is.EqualTo(8101));
Assert.That(Utils.CalcCrc16Modbus("000019.02.2410:49 61 0 0 0kg 001"), Is.EqualTo(26789));
Assert.That(Utils.CalcCrc16Modbus("000019.02.2410:49 71 50 0 50kg 001"), Is.EqualTo(16188));
Assert.That(Utils.CalcCrc16Modbus("000019.02.2410:50 81 35 0 35kg 001"), Is.EqualTo(12015));
Assert.That(Utils.CalcCrc16Modbus("000019.02.2410:50 91 40 0 40kg 001"), Is.EqualTo(60047));
Assert.That(Utils.CalcCrc16Modbus("000019.02.2410:50 101 40 0 40kg 001"), Is.EqualTo(60785));
Assert.That(Utils.CalcCrc16Modbus("000019.02.2410:50 111 45 0 45kg 001"), Is.EqualTo(35918));
});
}
}
}

View File

@ -0,0 +1,71 @@
using System.Net.Sockets;
using System.Net;
using System.Text;
namespace Tests.WeighingTests {
public abstract class MockScale : IDisposable {
protected readonly TcpListener Server;
public int IdentNr { get; set; } = 0;
public int Weight { get; set; } = 0;
public string? Error { get; set; } = null;
protected MockScale(int port) {
Server = new TcpListener(IPAddress.Loopback, port);
Server.Start(4);
}
public void Dispose() {
Server.Dispose();
GC.SuppressFinalize(this);
}
}
public class CommandMockScale : MockScale {
private readonly Func<string, int, string?, int, (string, bool)> Handler;
private readonly Thread ServerThread;
private bool IsRunning = true;
public CommandMockScale(int port, Func<string, int, string?, int, (string, bool)> handler) :
base(port) {
Handler = handler;
ServerThread = new Thread(new ParameterizedThreadStart(Serve));
ServerThread.Start();
}
private async void Serve(object? parameters) {
byte[] buffer = new byte[16];
while (IsRunning) {
try {
using var client = await Server.AcceptTcpClientAsync();
if (client == null) continue;
using var stream = client.GetStream();
while (true) {
int read = await stream.ReadAsync(buffer);
var (res, inc) = Handler(Encoding.ASCII.GetString(buffer, 0, read), Weight, Error, IdentNr + 1);
if (inc) IdentNr++;
await stream.WriteAsync(Encoding.ASCII.GetBytes(res));
}
} catch (Exception) {
// ignore
}
}
}
public new void Dispose() {
IsRunning = false;
ServerThread.Interrupt();
ServerThread.Join();
base.Dispose();
}
}
public class EventMockScale : MockScale {
public EventMockScale(int port, Func<int, string?, int, (string, bool)> handler) :
base(port) {
// TODO
}
}
}

View File

@ -0,0 +1,6 @@
namespace Tests.WeighingTests {
[TestFixture]
public class ScaleTestGrInzersdorf {
// TODO
}
}

View File

@ -0,0 +1,147 @@
using Elwig.Helpers.Weighing;
namespace Tests.WeighingTests {
[TestFixture]
class ScaleTestMatzen {
private MockScale? Mock;
private SysTecITScale? Scale;
private static (string, bool) ScaleHandler(string req, int weight, string? error, int identNr) {
var modes = error?.Split(';') ?? [];
var overloaded = modes.Contains("overloaded");
var moving = modes.Contains("moving");
var invalid = modes.Contains("invalid");
var crc = modes.Contains("crc");
var unit = modes.Contains("unit");
if (invalid) {
return ("abcd\r\n", false);
} else if (!req.StartsWith('<') || !req.EndsWith('>')) {
return ("<31>\r\n", false);
}
req = req[1..^1];
bool incr;
if (req.Length > 3) {
return ("<32>\r\n", false);
} else if (req.StartsWith("RN")) {
incr = true;
} else if (req.StartsWith("RM")) {
incr = false;
} else {
return ("<32>\r\n", false);
}
if (overloaded) {
return ("<12>\r\n", false);
} else if (weight == 0) {
incr = false;
}
if (moving && incr)
return ("<13>\r\n", false);
string data = $"00{(moving ? 1 : 0)}0{new DateTime(2020, 10, 15, 12, 34, 0):dd.MM.yyHH:mm}{(incr ? identNr : 0),4}1" +
$"{weight,8}{0,8}{weight,8}{(unit ? "lb" : "kg")} {1,3}";
ushort checksum = Elwig.Helpers.Utils.CalcCrc16Modbus(data);
if (crc) checksum += 10;
return ($"<{data}{checksum,8}>\r\n", incr);
}
[OneTimeSetUp]
public void SetupScale() {
Mock = new CommandMockScale(12345, ScaleHandler);
Scale = new("1", "IT3000A", "tcp://127.0.0.1:12345");
}
[OneTimeTearDown]
public void TeardownScale() {
Mock?.Dispose();
Scale?.Dispose();
}
[SetUp]
public void ResetScale() {
Mock!.IdentNr = 0;
Mock!.Weight = 0;
Mock!.Error = null;
}
[Test]
public async Task Test_01_CurrentWeight() {
Mock!.Weight = 1234;
Assert.That(await Scale!.GetCurrentWeight(), Is.EqualTo(new WeighingResult {
Weight = 1234, Date = new DateOnly(2020, 10, 15), Time = new TimeOnly(12, 34),
}));
Mock!.Weight = 1235;
Assert.That(await Scale!.GetCurrentWeight(), Is.EqualTo(new WeighingResult {
Weight = 1235, Date = new DateOnly(2020, 10, 15), Time = new TimeOnly(12, 34),
}));
Mock!.Weight = 1236;
Assert.That(await Scale!.GetCurrentWeight(), Is.EqualTo(new WeighingResult {
Weight = 1236, Date = new DateOnly(2020, 10, 15), Time = new TimeOnly(12, 34),
}));
}
[Test]
public async Task Test_02_Normal() {
Mock!.Weight = 1234;
Assert.That(await Scale!.Weigh(), Is.EqualTo(new WeighingResult {
Weight = 1234, WeighingId = "1",
FullWeighingId = $"2020-10-15/1",
Date = new DateOnly(2020, 10, 15), Time = new TimeOnly(12, 34),
}));
Mock!.Weight = 3333;
Assert.That(await Scale!.Weigh(), Is.EqualTo(new WeighingResult {
Weight = 3333, WeighingId = "2",
FullWeighingId = $"2020-10-15/2",
Date = new DateOnly(2020, 10, 15), Time = new TimeOnly(12, 34),
}));
Mock!.Weight = 4321;
Assert.That(await Scale!.Weigh(), Is.EqualTo(new WeighingResult {
Weight = 4321, WeighingId = "3",
FullWeighingId = $"2020-10-15/3",
Date = new DateOnly(2020, 10, 15), Time = new TimeOnly(12, 34),
}));
}
[Test]
public void Test_03_Moving() {
Mock!.Weight = 1_000;
Mock!.Error = "moving";
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
Assert.That(ex.Message, Contains.Substring("Waage in Bewegung"));
}
[Test]
public void Test_04_Overloaded() {
Mock!.Weight = 10_000;
Mock!.Error = "overloaded";
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
Assert.That(ex.Message, Contains.Substring("Waage in Überlast"));
}
[Test]
public void Test_05_InvalidResponse() {
Mock!.Weight = 1_000;
Mock!.Error = "invalid";
Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
}
[Test]
public void Test_06_InvalidCrc() {
Mock!.Weight = 1_000;
Mock!.Error = "crc";
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
Assert.That(ex.Message, Contains.Substring("Invalid CRC16 checksum"));
}
[Test]
public void Test_07_InvalidUnit() {
Mock!.Weight = 1_000;
Mock!.Error = "unit";
IOException ex = Assert.ThrowsAsync<IOException>(async () => await Scale!.Weigh());
}
}
}

View File

@ -0,0 +1,6 @@
namespace Tests.WeighingTests {
[TestFixture]
public class ScaleTestWolkersdorf {
// TODO
}
}

View File

@ -0,0 +1,6 @@
namespace Tests.WeighingTests {
public static class Utils {
public static DateOnly Today => DateOnly.FromDateTime(DateTime.Now);
public static TimeOnly Time => new(DateTime.Now.Hour, DateTime.Now.Minute);
}
}

View File

@ -1 +1 @@
curl -s -L "https://www.necronda.net/elwig/files/create.sql?v=16" -u "elwig:ganzGeheim123!" -o "Resources\Sql\Create.sql"
curl -s -L "https://www.necronda.net/elwig/files/create.sql?v=17" -u "elwig:ganzGeheim123!" -o "Resources\Sql\Create.sql"