507 lines
20 KiB
C#
507 lines
20 KiB
C#
// Copyright (c) 2017- schick Informatik
|
|
// Description: Display dialog for customs XML data upload app
|
|
//
|
|
|
|
using bsmd.database.EasyPeasy;
|
|
using ENI2.Util;
|
|
using Microsoft.Win32;
|
|
using System;
|
|
using System.Collections.ObjectModel;
|
|
using System.Collections.Specialized;
|
|
using System.ComponentModel;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Data;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media.Imaging;
|
|
using System.Xml;
|
|
using System.Xml.Serialization;
|
|
|
|
namespace ENI2.Controls
|
|
{
|
|
/// <summary>
|
|
/// Interaction logic for EasyPeasyControl.xaml
|
|
/// </summary>
|
|
public partial class EasyPeasyControl : UserControl
|
|
{
|
|
|
|
private ProofRequest _vm;
|
|
|
|
#region Construction
|
|
|
|
public EasyPeasyControl()
|
|
{
|
|
InitializeComponent();
|
|
|
|
this.dataGridGoodsItems.ContextMenu = new ContextMenu();
|
|
|
|
MenuItem addItem = new MenuItem();
|
|
addItem.Header = Properties.Resources.textAdd;
|
|
addItem.Icon = new Image { Source = new BitmapImage(new Uri("pack://application:,,,/Resources/add.png")) };
|
|
addItem.Click += AddItem_Click;
|
|
this.dataGridGoodsItems.ContextMenu.Items.Add(addItem);
|
|
|
|
MenuItem deleteItem = new MenuItem();
|
|
deleteItem.Header = Properties.Resources.textDelete;
|
|
deleteItem.Icon = new Image { Source = new BitmapImage(new Uri("pack://application:,,,/Resources/delete.png")) };
|
|
deleteItem.Click += DeleteItem_Click;
|
|
this.dataGridGoodsItems.ContextMenu.Items.Add(deleteItem);
|
|
|
|
// Add separator and paste option
|
|
this.dataGridGoodsItems.ContextMenu.Items.Add(new Separator());
|
|
|
|
MenuItem pasteItem = new MenuItem();
|
|
pasteItem.Header = "Paste";
|
|
pasteItem.Click += (s, e) => HandlePasteOperation();
|
|
this.dataGridGoodsItems.ContextMenu.Items.Add(pasteItem);
|
|
|
|
// Add command bindings for proper keyboard handling
|
|
this.dataGridGoodsItems.CommandBindings.Add(new CommandBinding(
|
|
ApplicationCommands.Paste,
|
|
(s, e) => HandlePasteOperation(),
|
|
(s, e) => e.CanExecute = Clipboard.ContainsText()));
|
|
|
|
}
|
|
|
|
#endregion
|
|
|
|
public void SaveState()
|
|
{
|
|
try
|
|
{
|
|
EasyPeasyState.Save(_vm);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
#region context menu event handler
|
|
|
|
private void AddItem_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (_vm?.ProofInformationT2LT2LF?.GoodsShipmentForT2LT2LF?.GoodsItemsForT2LT2LF == null) return;
|
|
var list = _vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF.GoodsItemsForT2LT2LF;
|
|
int nextItemNo = list.Any() ? list.Max(x => x.GoodsItemNumber) + 1 : 1;
|
|
var item = new GoodsItemForT2LT2LF
|
|
{
|
|
GoodsItemNumber = nextItemNo
|
|
};
|
|
list.Add(item);
|
|
}
|
|
|
|
private void DeleteItem_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
foreach(GoodsItemForT2LT2LF item in this.dataGridGoodsItems.SelectedItems.Cast<GoodsItemForT2LT2LF>().ToArray())
|
|
{
|
|
if (_vm?.ProofInformationT2LT2LF?.GoodsShipmentForT2LT2LF?.GoodsItemsForT2LT2LF == null) return;
|
|
var list = _vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF.GoodsItemsForT2LT2LF;
|
|
list.Remove(item);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region button event handler
|
|
|
|
private void buttonClear_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
CleanupAutoCalculation();
|
|
|
|
this._vm = EasyPeasyState.CreateDefault();
|
|
if (_vm.ProofInformationT2LT2LF?.GoodsShipmentForT2LT2LF?.GoodsItemsForT2LT2LF == null)
|
|
_vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF.GoodsItemsForT2LT2LF = new ObservableCollection<GoodsItemForT2LT2LF>();
|
|
|
|
_vm.ProofInformationT2LT2LF.DeclarationDate = DateTime.Now; // reset to today
|
|
_vm.ProofInformationT2LT2LF.RequestedValidityOfTheProof.NumberOfDays = 90; // default 90 days
|
|
|
|
this.DataContext = this._vm;
|
|
SetupAutoCalculation();
|
|
}
|
|
|
|
private void buttonExport_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var dlg = new SaveFileDialog
|
|
{
|
|
FileName = "proofRequest.xml",
|
|
Filter = "XML file|*.xml",
|
|
OverwritePrompt = true
|
|
};
|
|
if (dlg.ShowDialog() == true)
|
|
{
|
|
try
|
|
{
|
|
var ser = new XmlSerializer(typeof(ProofRequest));
|
|
|
|
// Namespaces (if needed)
|
|
// var ns = new XmlSerializerNamespaces();
|
|
// ns.Add("xsd", "http://www.w3.org/2001/XMLSchema");
|
|
// ns.Add("xsi", "http://www.w3.org/2001/XMLSchema-instance");
|
|
|
|
var settings = new XmlWriterSettings
|
|
{
|
|
Indent = true,
|
|
OmitXmlDeclaration = true
|
|
};
|
|
|
|
using (var fs = File.Create(dlg.FileName))
|
|
using (var xw = XmlWriter.Create(fs, settings))
|
|
{
|
|
ser.Serialize(xw, _vm); //, ns);
|
|
}
|
|
|
|
MessageBox.Show("Exported successfully.", "easy-peasy", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show("Export failed:\n" + ex.Message, "easy-peasy", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void buttonImport_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
OpenFileDialog ofd = new OpenFileDialog();
|
|
ofd.Filter = "XML file|*.xml";
|
|
ofd.RestoreDirectory = true;
|
|
ofd.Multiselect = false;
|
|
|
|
if (ofd.ShowDialog() == true)
|
|
{
|
|
using (var fs = File.OpenRead(ofd.FileName))
|
|
{
|
|
CleanupAutoCalculation();
|
|
var ser = new XmlSerializer(typeof(ProofRequest));
|
|
_vm = (ProofRequest)ser.Deserialize(fs);
|
|
// after loading/creating _vm
|
|
if (_vm.ProofInformationT2LT2LF?.GoodsShipmentForT2LT2LF?.GoodsItemsForT2LT2LF == null)
|
|
_vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF.GoodsItemsForT2LT2LF = new ObservableCollection<GoodsItemForT2LT2LF>();
|
|
|
|
_vm.ProofInformationT2LT2LF.DeclarationDate = DateTime.Now; // reset to today
|
|
if(_vm.ProofInformationT2LT2LF.RequestedValidityOfTheProof.NumberOfDays == 9)
|
|
_vm.ProofInformationT2LT2LF.RequestedValidityOfTheProof.NumberOfDays = 90; // default 90 days
|
|
|
|
this.DataContext = _vm;
|
|
SetupAutoCalculation();
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region loaded/unloaded event handler
|
|
|
|
private void UserControl_Loaded(object sender, RoutedEventArgs e)
|
|
{
|
|
_vm = EasyPeasyState.LoadOrCreate();
|
|
if (_vm.ProofInformationT2LT2LF == null)
|
|
_vm.ProofInformationT2LT2LF = new ProofInformationT2LT2LF();
|
|
if (_vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF == null)
|
|
_vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF = new GoodsShipmentForT2LT2LF
|
|
{
|
|
LocationOfGoods = new LocationOfGoods(),
|
|
TransportDocuments = new TransportDocuments()
|
|
};
|
|
_vm.ProofInformationT2LT2LF.DeclarationDate = DateTime.Now; // reset to today
|
|
this.DataContext = _vm;
|
|
|
|
SetupAutoCalculation();
|
|
}
|
|
|
|
private void UserControl_Unloaded(object sender, RoutedEventArgs e)
|
|
{
|
|
CleanupAutoCalculation();
|
|
|
|
try
|
|
{
|
|
EasyPeasyState.Save(_vm);
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region auto calculation total gross mass
|
|
|
|
private void SetupAutoCalculation()
|
|
{
|
|
if (_vm?.ProofInformationT2LT2LF?.GoodsShipmentForT2LT2LF?.GoodsItemsForT2LT2LF != null)
|
|
{
|
|
// Subscribe to collection changes (add/remove items)
|
|
_vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF.GoodsItemsForT2LT2LF.CollectionChanged += GoodsItems_CollectionChanged;
|
|
|
|
// Subscribe to DataGrid cell changes
|
|
dataGridGoodsItems.CellEditEnding += DataGridGoodsItems_CellEditEnding;
|
|
|
|
// Calculate initial total
|
|
CalculateTotalGrossMass();
|
|
}
|
|
}
|
|
|
|
private void CleanupAutoCalculation()
|
|
{
|
|
if (_vm?.ProofInformationT2LT2LF?.GoodsShipmentForT2LT2LF?.GoodsItemsForT2LT2LF != null)
|
|
{
|
|
// Unsubscribe from collection changes
|
|
_vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF.GoodsItemsForT2LT2LF.CollectionChanged -= GoodsItems_CollectionChanged;
|
|
dataGridGoodsItems.CellEditEnding -= DataGridGoodsItems_CellEditEnding;
|
|
}
|
|
}
|
|
|
|
private void DataGridGoodsItems_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
|
|
{
|
|
// Check if the edited column is GrossMass
|
|
if (e.Column.Header.ToString() == "Gross")
|
|
{
|
|
// Delay calculation to allow the binding to update
|
|
Dispatcher.BeginInvoke(new Action(() => {
|
|
CalculateTotalGrossMass();
|
|
}), System.Windows.Threading.DispatcherPriority.Background);
|
|
}
|
|
}
|
|
|
|
private void GoodsItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
// Recalculate total after any collection change
|
|
CalculateTotalGrossMass();
|
|
}
|
|
|
|
private void CalculateTotalGrossMass()
|
|
{
|
|
if (_vm?.ProofInformationT2LT2LF?.GoodsShipmentForT2LT2LF?.GoodsItemsForT2LT2LF != null)
|
|
{
|
|
var total = _vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF.GoodsItemsForT2LT2LF
|
|
.Sum(item => item.GoodsMeasure?.GrossMass ?? 0m);
|
|
|
|
_vm.ProofInformationT2LT2LF.TotalGrossMassKg = total;
|
|
|
|
// Force UI update by refreshing the binding
|
|
var binding = BindingOperations.GetBindingExpression(
|
|
FindTotalGrossMassTextBox(), TextBox.TextProperty);
|
|
binding?.UpdateTarget();
|
|
}
|
|
}
|
|
|
|
// Simple property changed notification helper
|
|
public event PropertyChangedEventHandler PropertyChanged;
|
|
protected virtual void OnPropertyChanged(string propertyName)
|
|
{
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
}
|
|
|
|
private TextBox FindTotalGrossMassTextBox()
|
|
{
|
|
// Find the TextBox that displays TotalGrossMassKg
|
|
return this.FindName("textBoxTotalGrossMass") as TextBox ??
|
|
this.GetTemplateChild("textBoxTotalGrossMass") as TextBox;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region cut & paste logic
|
|
|
|
private void HandlePasteOperation()
|
|
{
|
|
if (Clipboard.ContainsText())
|
|
{
|
|
var text = Clipboard.GetText();
|
|
if (!TryPaste_EspHsPkgsGross(text))
|
|
PasteGoodsItems(text);
|
|
}
|
|
}
|
|
|
|
private void DataGrid_PreviewKeyDown(object sender, KeyEventArgs e)
|
|
{
|
|
if (e.Key == Key.V && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
|
|
{
|
|
if (Clipboard.ContainsText())
|
|
{
|
|
var text = Clipboard.GetText();
|
|
|
|
if(!TryPaste_EspHsPkgsGross(text))
|
|
PasteGoodsItems(text);
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void PasteGoodsItems(string text)
|
|
{
|
|
if (_vm?.ProofInformationT2LT2LF?.GoodsShipmentForT2LT2LF == null) return;
|
|
|
|
var lines = text.Replace("\r\n", "\n").Replace('\r', '\n')
|
|
.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
foreach (var line in lines)
|
|
{
|
|
// Split by tab first; if only one column, try CSV
|
|
var cells = line.Split('\t');
|
|
if (cells.Length == 1) cells = SplitCsv(line);
|
|
|
|
// Expected order: HS, Item#, Description, Gross, Net, Pkgs, Type, Marks
|
|
var item = new GoodsItemForT2LT2LF();
|
|
|
|
if (cells.Length > 0) item.Commodity.HarmonizedSystemSubHeadingCode = cells[0].Trim();
|
|
if (cells.Length > 1 && int.TryParse(cells[1], out var n)) item.GoodsItemNumber = n;
|
|
if (cells.Length > 2) item.DescriptionOfGoods = cells[2].Trim();
|
|
if (cells.Length > 3 && decimal.TryParse(cells[3], out var gross)) item.GoodsMeasure.GrossMass = gross;
|
|
if (cells.Length > 4 && decimal.TryParse(cells[4], out var net)) item.GoodsMeasure.NetMass = net;
|
|
if (cells.Length > 5 && int.TryParse(cells[5], out var pkgs)) item.Packaging.NumberOfPackages = pkgs;
|
|
if (cells.Length > 6) item.Packaging.TypeOfPackages = cells[6].Trim();
|
|
if (cells.Length > 7) item.Packaging.ShippingMarks = cells[7].Trim();
|
|
|
|
_vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF.GoodsItemsForT2LT2LF.Add(item);
|
|
}
|
|
}
|
|
|
|
// Very small CSV splitter (handles quotes)
|
|
private static string[] SplitCsv(string line)
|
|
{
|
|
var res = new System.Collections.Generic.List<string>();
|
|
var sb = new StringBuilder();
|
|
bool inQuotes = false;
|
|
for (int i = 0; i < line.Length; i++)
|
|
{
|
|
char c = line[i];
|
|
if (c == '\"')
|
|
{
|
|
if (inQuotes && i + 1 < line.Length && line[i + 1] == '\"')
|
|
{ sb.Append('\"'); i++; }
|
|
else { inQuotes = !inQuotes; }
|
|
}
|
|
else if (c == ',' && !inQuotes)
|
|
{ res.Add(sb.ToString()); sb.Clear(); }
|
|
else { sb.Append(c); }
|
|
}
|
|
res.Add(sb.ToString());
|
|
return res.ToArray();
|
|
}
|
|
|
|
private bool TryPaste_EspHsPkgsGross(string text)
|
|
{
|
|
if (_vm?.ProofInformationT2LT2LF?.GoodsShipmentForT2LT2LF == null) return false;
|
|
|
|
// Normalize and split lines
|
|
var lines = text.Replace("\r\n", "\n").Replace('\r', '\n')
|
|
.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
|
if (lines.Length == 0) return false;
|
|
|
|
// Determine next item number
|
|
var list = _vm.ProofInformationT2LT2LF.GoodsShipmentForT2LT2LF.GoodsItemsForT2LT2LF;
|
|
int nextItemNo = list.Any() ? list.Max(x => x.GoodsItemNumber) + 1 : 1;
|
|
|
|
bool anyAdded = false;
|
|
bool countrySeen = false;
|
|
|
|
foreach (var raw in lines)
|
|
{
|
|
var line = raw; // do not Trim() entirely; keep leading tab as empty first cell
|
|
var cells = line.Split('\t'); // keeps empty entries
|
|
|
|
// Expected:
|
|
// - 4 cells: [ESP or ""], [HS], [Pkgs], [Gross]
|
|
// - 3 cells: [HS], [Pkgs], [Gross]
|
|
string hs = null, pkgs = null, gross = null;
|
|
|
|
if (cells.Length >= 4)
|
|
{
|
|
string c0 = cells[0]?.Trim();
|
|
// Optionally capture the first token like "ESP" (country tag),
|
|
// only once and only if alphabetic (won't throw if numeric)
|
|
if (!countrySeen && !string.IsNullOrWhiteSpace(c0) && c0.All(ch => char.IsLetter(ch)))
|
|
{
|
|
// If you decide later this should set a field, uncomment:
|
|
// if (string.IsNullOrWhiteSpace(_vm.Country)) _vm.Country = c0;
|
|
countrySeen = true;
|
|
}
|
|
|
|
hs = (cells.Length > 1 ? cells[1] : null);
|
|
pkgs = (cells.Length > 2 ? cells[2] : null);
|
|
gross = (cells.Length > 3 ? cells[3] : null);
|
|
}
|
|
else if (cells.Length == 3)
|
|
{
|
|
hs = cells[0];
|
|
pkgs = cells[1];
|
|
gross = cells[2];
|
|
}
|
|
else
|
|
{
|
|
// Not enough data for this format; skip the row
|
|
continue;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(hs)) continue;
|
|
|
|
var item = new GoodsItemForT2LT2LF
|
|
{
|
|
GoodsItemNumber = nextItemNo++,
|
|
DescriptionOfGoods = "" // per spec
|
|
};
|
|
item.Commodity.HarmonizedSystemSubHeadingCode = hs.Trim();
|
|
|
|
if (TryParseIntFlexible(pkgs, out var pk))
|
|
item.Packaging.NumberOfPackages = pk;
|
|
|
|
if (TryParseDecimalFlexible(gross, out var g))
|
|
{
|
|
item.GoodsMeasure.GrossMass = g;
|
|
var net = g - 1m;
|
|
if (net < 0m) net = 0m;
|
|
item.GoodsMeasure.NetMass = net;
|
|
}
|
|
|
|
item.DescriptionOfGoods = "Brand New Vehicles"; // per spec
|
|
item.Packaging.TypeOfPackages = "UN"; // per spec
|
|
item.Packaging.ShippingMarks = "-"; // per spec
|
|
|
|
list.Add(item);
|
|
anyAdded = true;
|
|
}
|
|
|
|
return anyAdded;
|
|
}
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
#region static utils
|
|
|
|
// this will go somewhere else later
|
|
|
|
// Try parse decimal with current culture, invariant, and comma/dot flip
|
|
private static bool TryParseDecimalFlexible(string s, out decimal value)
|
|
{
|
|
s = (s ?? "").Trim();
|
|
// 1) current culture
|
|
if (decimal.TryParse(s, NumberStyles.Number, CultureInfo.CurrentCulture, out value)) return true;
|
|
// 2) invariant
|
|
if (decimal.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out value)) return true;
|
|
// 3) flip comma/dot and retry (helps when clipboard mixes locales)
|
|
string flipped = s.Contains(",") ? s.Replace(",", ".") : s.Replace(".", ",");
|
|
if (decimal.TryParse(flipped, NumberStyles.Number, CultureInfo.CurrentCulture, out value)) return true;
|
|
if (decimal.TryParse(flipped, NumberStyles.Number, CultureInfo.InvariantCulture, out value)) return true;
|
|
value = 0m;
|
|
return false;
|
|
}
|
|
|
|
private static bool TryParseIntFlexible(string s, out int value)
|
|
{
|
|
s = (s ?? "").Trim();
|
|
// Extract leading integer if something like "12 pcs"
|
|
var digits = new string(s.TakeWhile(ch => char.IsDigit(ch) || ch == '-' || ch == '+').ToArray());
|
|
if (string.IsNullOrEmpty(digits)) digits = s;
|
|
return int.TryParse(digits, NumberStyles.Integer, CultureInfo.CurrentCulture, out value)
|
|
|| int.TryParse(digits, NumberStyles.Integer, CultureInfo.InvariantCulture, out value);
|
|
}
|
|
|
|
#endregion
|
|
|
|
}
|
|
|
|
}
|
|
|