pull/1/head
Basimodo 2021-06-08 16:35:24 +07:00
parent 42a3b38534
commit 136f7868ce
9 changed files with 501 additions and 325 deletions

@ -28,6 +28,7 @@
<PackageReference Include="gong-wpf-dragdrop" Version="2.3.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.5" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.818.41" />
<PackageReference Include="morelinq" Version="3.3.2" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="5.0.0" />
</ItemGroup>

@ -448,18 +448,6 @@ namespace Gremlin.GremlinData.DBClasses
//}
}
//public static bool ImportFromCSV<T>(T type, string filepath = "", string separator = ";", bool dataHasHeading = true)
//{
// return type switch
// {
// Account => ImportAccountsFromCSV(filepath, separator, dataHasHeading),
// Contact => ImportContactsFromCSV(filepath, separator, dataHasHeading),
// _ => default,
// };
//}
public static bool ImportContactsFromCSV(string filepath = "", string separator = ";", bool dataHasHeading = true)
{
//Pfad abfragen über Dtei-Öffnen-Dialog:
@ -767,267 +755,6 @@ namespace Gremlin.GremlinData.DBClasses
return true;
}
public static bool GenericImporter(string filepath = "")//, string separator = "|")
//Ein (möglichst) generischer Importer
//1. Dateipfad erfassen
//2. Column <-> Property Mapping
//3. Typ der zu importierenden Daten herausfinden
//4. Daten einlesen, konvertieren, validieren, Metadaten setzen und alles in Liste(n) speichern
//5. Datenliste(n) in DB speichern
{
if (filepath == "") filepath = FileHelper.GetFilepathFromUser();
if (filepath == "") return false;
using (TextFieldParser csvParser = new(filepath, FileHelper.GetEncoding(filepath)))
{
//Parser configuration:
//csvParser.Delimiters = new string[] { separator };
csvParser.CommentTokens = new string[] { "#" };
csvParser.HasFieldsEnclosedInQuotes = true;
//dynamische Spaltenzuordnung in Dictonary speichern
Dictionary<string, string> ColumnToPropertyMapping = FileIO.ReadMappingTableFromFile();
Dictionary<string, int> columnNumberOf = new();
string[] fields = csvParser.ReadFields();
columnNumberOf = GenericColumnMapper(fields, ColumnToPropertyMapping);
//determine data type to be imported
string dataInFile = RecognizeData(columnNumberOf, FileIO.ReadDataIdentifierFromFile());
//prepare lists
bool dataIsValid;
while (!csvParser.EndOfData)
{
//read
//new data instance
//convert
//validate
//set metadata: SetMetadataForImport(IMetadata)
//add to list
fields = csvParser.ReadFields(); // Read current line fields, pointer moves to the next line.
if (fields[0] == "") break; // Am Ende hängt eine leere Zeile, die im Parser einen Fehler auslösen würde.
}
using (GremlinContext db = new())
{
//add to context
//savechanges
}
}
return true;
}
private static string RecognizeData(Dictionary<string,int> mappingTable, List<DataIdentifier> dataIdentifier, bool MatchAllIdentifier = true)
{
List<string> AvailableTypes = new();
Dictionary<string, int> Score = new();
ValueTuple<string, int, bool> Scoring = new();
//build list of unique types
for (int i = 0; i < dataIdentifier.Count; i++)
{
DataIdentifier qualifier = dataIdentifier[i];
if (AvailableTypes.Contains(qualifier.DataType) == false)
{
AvailableTypes.Add(qualifier.DataType);
Score.Add(qualifier.DataType, 0);
}
}
bool _continue;
foreach (string datatype in AvailableTypes) //for each group of identifiers...
{
_continue = true;
do
{
foreach (DataIdentifier qualifier in dataIdentifier) //...iterate through its corresponding identfiers...
{
if (qualifier.DataType == datatype
&& mappingTable.ContainsKey(qualifier.Identifier)) //...and check if identifier is in data set columns headings
{
int score = Score.GetValueOrDefault(datatype);
score++;
_ = Score.Remove(datatype);
Score.Add(datatype, score);
}
else //identifier nicht gefunden
{
if (MatchAllIdentifier)
{
//go to next datatype (outer foreach):
_continue = false;
}
}
if (qualifier.DataType == datatype
&& qualifier.Identifier == "NumberOfColumns=2")
{
int score = Score.GetValueOrDefault(datatype);
score += 3;
Score.Remove(datatype);
Score.Add(datatype, score);
}
}
}
while (_continue);
}
//return type with most hits:
return Score.Aggregate((x,y) => x.Value > y.Value ? x : y).Key;
}
private static string RecognizeDataStatic(Dictionary<string, int> mappingDictionary)
{
//Logik zur Kategorisierung des Datensatzes:
//Alle verpflichtenden Angaben zu einer Klasse vorhanen?
//Alle vom Importer erwarteten Angaben vorhanden?
//Bei den Enums zusätzlich noch proüfem, dass nur zwei Spalten vorhanden sind, sonst könnten LSAG-Daten falsch identifiziert werden.
//
//
//Products
if (//required by DB:
mappingDictionary.ContainsKey("ProductNumber")
&& mappingDictionary.ContainsKey("ListPrice")
//required by importer:
&& mappingDictionary.ContainsKey("Weight")
//unique identifier:
&& mappingDictionary.ContainsKey("BreakRangeFrom")
)
{
return "Product";
}
//LSAG Contact List Tool List
if (//required by DB:
mappingDictionary.ContainsKey("SAPAccountNumber")
&& mappingDictionary.ContainsKey("SAPContactNumber")
&& mappingDictionary.ContainsKey("AccountTypeCode")
&& mappingDictionary.ContainsKey("SubMarketCode")
&& mappingDictionary.ContainsKey("LastName")
&& mappingDictionary.ContainsKey("AccountName")
//required by importer:
//unique identifier:
&& mappingDictionary.ContainsKey("MA_ProductInterests")
)
{
return "LSAG Contact List Tool List";
}
//Accounts
if (//required by DB:
mappingDictionary.ContainsKey("AccountName")
&& mappingDictionary.ContainsKey("Street")
&& mappingDictionary.ContainsKey("ZIP")
&& mappingDictionary.ContainsKey("City")
&& mappingDictionary.ContainsKey("PhoneNumber")
&& mappingDictionary.ContainsKey("SAPAccountNumber")
&& mappingDictionary.ContainsKey("AccountCreatedInSAPOn")
&& mappingDictionary.ContainsKey("AccountTypeCode")
&& mappingDictionary.ContainsKey("SubMarketCode")
//required by importer:
)
{
return "Account";
}
//Contacts
if (//required by DB:
mappingDictionary.ContainsKey("LastName")
&& mappingDictionary.ContainsKey("SAPContactNumber")
//required by importer:
&& mappingDictionary.ContainsKey("FirstName")
&& mappingDictionary.ContainsKey("Gender")
)
{
return "Contact";
}
//Custom Description
if (//required by DB:
mappingDictionary.ContainsKey("Heading")
//required by importer:
&& mappingDictionary.ContainsKey("ProductNumber")
&& mappingDictionary.ContainsKey("OptionNumber")
&& mappingDictionary.ContainsKey("DescriptionText")
//unique identifier:
&& mappingDictionary.ContainsKey("CoverletterText")
)
{
return "CustomDescription";
}
//Pseudo-Enums AccountTypes, SubMarkets, ProductLines for DB-initializing
if (mappingDictionary.Count == 2)
{
if (mappingDictionary.ContainsKey("AccountTypeCode")
&& mappingDictionary.ContainsKey("AccountTypeDescription")
&& mappingDictionary.Count == 2)
{
return "AccountType";
}
if (mappingDictionary.ContainsKey("SubMarketCode")
&& mappingDictionary.ContainsKey("SubMarketDescription")
&& mappingDictionary.Count == 2)
{
return "SubMarket";
}
if (mappingDictionary.ContainsKey("ProductLineCode")
&& mappingDictionary.ContainsKey("ProductLineDescription")
&& mappingDictionary.Count == 2)
{
return "ProductLine";
}
}
return "No entity type unambigiously identified!";
}
public static IMetadata SetMetadataForImport(IMetadata entity,string datamodifiedby = "", string dataversioncomment = "", [CallerMemberName] string callername ="")
{
entity.DataCreationDate = DateTime.Now;
entity.DataModificationDate = DateTime.Now;
entity.DataModificationByUser = datamodifiedby == "" ? callername : datamodifiedby;
entity.DataStatus = Status.Active.ToString();
entity.DataValidFrom = DateTime.Now;
entity.DataValidUntil = FarInTheFuture;
entity.DataVersionNumber++;
entity.DataVersionComment = dataversioncomment;
return entity;
}
public static Dictionary<string, int> GenericColumnMapper(string[] headings, Dictionary<string, string> mappingTable)
{
Dictionary<string, int> result = new();
for (int i = 0; i < headings.Length; i++)
{
string heading = headings[i].ToLower(CultureInfo.CurrentCulture)
.Trim()
.Replace(" ", "")
.Replace("-", "")
.Replace("_", "")
.Replace(".", "")
.Replace(":", "");
if (mappingTable.TryGetValue(heading, out string value))
{
result.Add(value, i);
}
}
return result;
}
public static bool ImportLSAGContactListToolData(string filepath = "", string separator = ";")
///Importiert Accounts und Contacts aus CSV in die DB

@ -0,0 +1,240 @@
using Gremlin.GremlinUtilities;
using Gremlin.GremlinUtilities.GUClasses;
using Microsoft.VisualBasic.FileIO;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Linq;
using static Gremlin.Context;
using System.Collections;
namespace Gremlin.GremlinData.DBClasses
{
public class GenericImporter
{
private TextFieldParser _csvParser;
public GenericImporter()
{
}
public bool ImportFile(string filepath = "", string separator = "|")
//Ein (möglichst) generischer Importer
//1. Dateipfad erfassen
//2. Column <-> Property Mapping
//3. Typ der zu importierenden Daten herausfinden
//4. Daten einlesen, konvertieren, validieren, Metadaten setzen und alles in Liste(n) speichern
//5. Datenliste(n) in DB speichern
{
if (filepath == "") filepath = FileHelper.GetFilepathFromUser();
if (filepath == "") return false;
using (TextFieldParser csvParser = new(filepath, FileHelper.GetEncoding(filepath)))
{
//Parser configuration:
//csvParser.Delimiters = new string[] { separator };
csvParser.CommentTokens = new string[] { "#" };
csvParser.HasFieldsEnclosedInQuotes = true;
//dynamische Spaltenzuordnung in Dictonary speichern
string[] fields = csvParser.ReadFields();
Dictionary<string, string> ColumnToPropertyMapping = ReadMappingDictionaryFromFile();
Dictionary<string, int> mappingTable = MapDataHeading(fields, ColumnToPropertyMapping);
//determine data type to be imported
DataIdentificator dataIdentificator = new(mappingTable);
List<string> detectedDataTypes = dataIdentificator.Identify();
foreach (string detectedDataType in detectedDataTypes)
{
//create object of winner type
var DataType = Activator.CreateInstance(Assembly.GetExecutingAssembly().ToString(), detectedDataType);
//Get Type of it:
Type t = DataType.GetType();
//Create an one-dimensional array with 100.000 elements:
object obj = Array.CreateInstance(t, 100000);
var listType = typeof(List<>).MakeGenericType(t);
var list = (IList)Activator.CreateInstance(listType);
}
bool dataIsValid;
while (!csvParser.EndOfData)
{
//read
//new data instance
//convert
//validate
//set metadata: SetMetadataForImport(IMetadata)
//add to list
fields = csvParser.ReadFields(); // Read current line fields, pointer moves to the next line.
if (fields[0] == "") break; // Am Ende hängt eine leere Zeile, die im Parser einen Fehler auslösen würde.
}
using (GremlinContext db = new())
{
//add to context
//savechanges
}
}
return true;
}
public static Dictionary<string, string> ReadMappingDictionaryFromFile()
{
Dictionary<string, string> result = new();
string fileInput = FileIO.ReadResource("MappingDictionary.txt");
string[] lines = fileInput.Split(Environment.NewLine);
foreach (string line in lines)
{
string[] fields;
fields = line.Split("|");
result.Add(fields[0], fields[1]);
}
return result;
}
public static Dictionary<string, int> MapDataHeading(string[] headings, Dictionary<string, string> columnPropertyMapping)
{
Dictionary<string, int> result = new();
for (int i = 0; i < headings.Length; i++)
{
string heading = headings[i].ToLower(CultureInfo.CurrentCulture)
.Trim()
.Replace(" ", "")
.Replace("-", "")
.Replace("_", "")
.Replace(".", "")
.Replace(":", "");
if (columnPropertyMapping.TryGetValue(heading, out string value))
{
result.Add(value, i);
}
}
return result;
}
//Kann gelöscht werden, sobald die generische Funktion zuverlässig funktioniert.
private static string RecognizeDataStatic(Dictionary<string, int> mappingDictionary)
{
//Logik zur Kategorisierung des Datensatzes:
//Alle verpflichtenden Angaben zu einer Klasse vorhanen?
//Alle vom Importer erwarteten Angaben vorhanden?
//Bei den Enums zusätzlich noch proüfem, dass nur zwei Spalten vorhanden sind, sonst könnten LSAG-Daten falsch identifiziert werden.
//
//
//Products
if (//required by DB:
mappingDictionary.ContainsKey("ProductNumber")
&& mappingDictionary.ContainsKey("ListPrice")
//required by importer:
&& mappingDictionary.ContainsKey("Weight")
//unique identifier:
&& mappingDictionary.ContainsKey("BreakRangeFrom")
)
{
return "Product";
}
//LSAG Contact List Tool List
if (//required by DB:
mappingDictionary.ContainsKey("SAPAccountNumber")
&& mappingDictionary.ContainsKey("SAPContactNumber")
&& mappingDictionary.ContainsKey("AccountTypeCode")
&& mappingDictionary.ContainsKey("SubMarketCode")
&& mappingDictionary.ContainsKey("LastName")
&& mappingDictionary.ContainsKey("AccountName")
//required by importer:
//unique identifier:
&& mappingDictionary.ContainsKey("MA_ProductInterests")
)
{
return "LSAG Contact List Tool List";
}
//Accounts
if (//required by DB:
mappingDictionary.ContainsKey("AccountName")
&& mappingDictionary.ContainsKey("Street")
&& mappingDictionary.ContainsKey("ZIP")
&& mappingDictionary.ContainsKey("City")
&& mappingDictionary.ContainsKey("PhoneNumber")
&& mappingDictionary.ContainsKey("SAPAccountNumber")
&& mappingDictionary.ContainsKey("AccountCreatedInSAPOn")
&& mappingDictionary.ContainsKey("AccountTypeCode")
&& mappingDictionary.ContainsKey("SubMarketCode")
//required by importer:
)
{
return "Account";
}
//Contacts
if (//required by DB:
mappingDictionary.ContainsKey("LastName")
&& mappingDictionary.ContainsKey("SAPContactNumber")
//required by importer:
&& mappingDictionary.ContainsKey("FirstName")
&& mappingDictionary.ContainsKey("Gender")
)
{
return "Contact";
}
//Custom Description
if (//required by DB:
mappingDictionary.ContainsKey("Heading")
//required by importer:
&& mappingDictionary.ContainsKey("ProductNumber")
&& mappingDictionary.ContainsKey("OptionNumber")
&& mappingDictionary.ContainsKey("DescriptionText")
//unique identifier:
&& mappingDictionary.ContainsKey("CoverletterText")
)
{
return "CustomDescription";
}
//Pseudo-Enums AccountTypes, SubMarkets, ProductLines for DB-initializing
if (mappingDictionary.Count == 2)
{
if (mappingDictionary.ContainsKey("AccountTypeCode")
&& mappingDictionary.ContainsKey("AccountTypeDescription")
&& mappingDictionary.Count == 2)
{
return "AccountType";
}
if (mappingDictionary.ContainsKey("SubMarketCode")
&& mappingDictionary.ContainsKey("SubMarketDescription")
&& mappingDictionary.Count == 2)
{
return "SubMarket";
}
if (mappingDictionary.ContainsKey("ProductLineCode")
&& mappingDictionary.ContainsKey("ProductLineDescription")
&& mappingDictionary.Count == 2)
{
return "ProductLine";
}
}
return "No entity type unambigiously identified!";
}
}
}

@ -0,0 +1,26 @@
using System;
using System.Globalization;
using System.Runtime.CompilerServices;
using static Gremlin.Enums;
namespace Gremlin.GremlinData.DBClasses
{
class MetaDataSetter
{
private static readonly DateTime FarInTheFuture = DateTime.Parse("2050-12-31t00:00:00.000000z", CultureInfo.CurrentCulture);
public IMetadata ForImport(IMetadata entity, string datamodifiedby = "", string dataversioncomment = "", [CallerMemberName] string callername = "")
{
entity.DataCreationDate = DateTime.Now;
entity.DataModificationDate = DateTime.Now;
entity.DataModificationByUser = datamodifiedby == "" ? callername : datamodifiedby;
entity.DataStatus = Status.Active.ToString();
entity.DataValidFrom = DateTime.Now;
entity.DataValidUntil = FarInTheFuture;
entity.DataVersionNumber++;
entity.DataVersionComment = dataversioncomment;
return entity;
}
}
}

@ -1,45 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq;
using System.IO;
using System.Reflection;
using Gremlin.GremlinUtilities.GUClasses;
namespace Gremlin.GremlinUtilities
{
public static class FileIO
{
public static List<DataIdentifier> ReadDataIdentifierFromFile()
{
List <DataIdentifier> result = new(50);
string fileInput = ReadResource("DataIdentifier.txt");
string[] lines = fileInput.Split(Environment.NewLine);
foreach (string line in lines)
{
string[] fields;
DataIdentifier identifier = new();
fields = line.Split("|");
identifier.DataType = fields[0];
identifier.Identifier = fields[1];
result.Add(identifier);
}
return result;
}
public static Dictionary<string, string> ReadMappingTableFromFile()
{
Dictionary<string, string> result = new();
string fileInput = ReadResource("MappingDictionary.txt");
string[] lines = fileInput.Split(Environment.NewLine);
foreach (string line in lines)
{
string[] fields;
fields = line.Split("|");
result.Add(fields[0], fields[1]);
}
return result;
}
public static string ReadResource(string name)
//Source and credit to: https://stackoverflow.com/questions/3314140/how-to-read-embedded-resource-text-file
{

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Gremlin.GremlinUtilities.GUClasses
{
class DataIdType
{
private readonly string _dataType;
private readonly List<string> _qualifiers;
private List<string> _matchedQualifiers;
private readonly int _numberOfQualifiers;
private int _score = 0;
private bool _allQualifierMatched = false;
public string DataType
{
get => _dataType;
}
public List<string> Qualifiers
{
get => _qualifiers;
}
public int Score
{
get => _score;
}
public bool AllQualifierMatched
{
get => _allQualifierMatched;
}
public DataIdType(string DataType, IEnumerable<string> Qualifiers)
{
_dataType = DataType;
_qualifiers = Qualifiers.ToList();
_numberOfQualifiers = _qualifiers.Count();
}
public void AddFoundQualfier(string QualifierFound)
{
_matchedQualifiers.Add(QualifierFound);
_score++;
_allQualifierMatched = (_matchedQualifiers == _qualifiers) ? true : false;
}
public bool CheckForQualifier(string qualifier)
{
return _qualifiers.Contains(qualifier);
}
public void IncreaseScore(int value = 1)
{
_score += value;
}
public void ResetScore()
{
_score = 0;
}
public override bool Equals(object obj)
{
return obj is DataIdType type
&& _dataType == type._dataType;
}
public override int GetHashCode()
{
return HashCode.Combine(_dataType);
}
}
}

@ -0,0 +1,146 @@
using MoreLinq;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Gremlin.GremlinUtilities.GUClasses
{
class DataIdentificator
{
private readonly string _source; //Qualifier-Liste
private readonly Dictionary<string, int> _mappingTable; //Mapping Spaltenzahl <-> Property/(übersetzte) Spaltenüberschrift
public List<DataIdType> DataTypes { get; set; }
public DataIdentificator(Dictionary<string, int> MappingTable)
{
_mappingTable = MappingTable;
//DataTypes populieren. Dazu: einlesen, nach DT qualifier in temp. Liste speichern, damit neuen DIDT erzeugen und zur Liste hinzufügen
this.Initialize();
}
private void Initialize()
{
string _lastDataType;
List<DataIdentifier> dataIdentifier = ReadDataIdentifierFromFile();
_lastDataType = dataIdentifier[0].DataType;
List<string> tempQualifier = new();
foreach (DataIdentifier qualifier in dataIdentifier)
{
if (qualifier.DataType == _lastDataType)
{
tempQualifier.Add(qualifier.Identifier);
if (qualifier == dataIdentifier[^0]) //letztes Element der Liste
{
DataIdType tempType = new(_lastDataType, tempQualifier);
DataTypes.Add(tempType);
_lastDataType = qualifier.DataType;
}
continue;
}
else
{
DataIdType tempType = new(_lastDataType, tempQualifier);
DataTypes.Add(tempType);
_lastDataType = qualifier.DataType;
}
}
}
public List<string> Identify(bool MustMatchAllQualifer = true)
{
List<string> winners = new();
foreach (DataIdType datatype in DataTypes)
{
foreach (string qualifier in datatype.Qualifiers)
{
if (_mappingTable.ContainsKey(qualifier))
{
datatype.AddFoundQualfier(qualifier);
}
}
}
if (MustMatchAllQualifer)
{
foreach (DataIdType dataType in DataTypes) //may return multiple winners
{
if (dataType.AllQualifierMatched)
{
winners.Add(dataType.DataType);
}
}
}
else //return the winner by highest score:
{
winners.Add(DataTypes.Aggregate((x, y) => x.Score > y.Score ? x : y).DataType.ToString());
}
//Plausibilty checkpoint:
//1. Remove "PL", "AccountType", "Submarket" from Winner-List, if detected datatype is "LSAG" or "Account".
//2. LSAG = Account + Contact --> when LSAG is a winner, remove "LSAG" and add "Account" and "Contact" to use their dedicated import methods.
//3. Remove any other winner, when winner is one of "PL", "AccountType", "Submarket" AND fields[].count = 2.
if (MustMatchAllQualifer == true && winners.Contains("LSAG"))
{
if (winners.Contains("ProductLine")) winners.Remove("ProductLine");
if (winners.Contains("AccountType")) winners.Remove("AccountType");
if (winners.Contains("SubMarket")) winners.Remove("SubMarket");
if (winners.Contains("Account")) winners.Remove("Account");
if (winners.Contains("Contact")) winners.Remove("Contact");
winners.Remove("LSAG");
winners.Add("Account");
winners.Add("Contact");
}
if (MustMatchAllQualifer == true && winners.Contains("Account"))
{
if (winners.Contains("ProductLine")) winners.Remove("ProductLine");
if (winners.Contains("AccountType")) winners.Remove("AccountType");
if (winners.Contains("SubMarket")) winners.Remove("SubMarket");
}
if (MustMatchAllQualifer == false && _mappingTable.Count == 2)
{
if (winners.Contains("ProductLine"))
{
winners.Clear();
winners.Add("ProductLine");
}
if (winners.Contains("AccountType"))
{
winners.Clear();
winners.Add("AccountType");
}
if (winners.Contains("SubMarket"))
{
winners.Clear();
winners.Add("SubMarket");
}
}
return winners;
}
public List<DataIdentifier> ReadDataIdentifierFromFile()
{
List<DataIdentifier> result = new(50);
string fileInput = FileIO.ReadResource("DataIdentifier.txt");
string[] lines = fileInput.Split(Environment.NewLine);
foreach (string line in lines)
{
string[] fields;
DataIdentifier identifier = new();
fields = line.Split("|");
identifier.DataType = fields[0];
identifier.Identifier = fields[1];
result.Add(identifier);
}
return result;
}
}
}

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Gremlin.GremlinUtilities.GUClasses
namespace Gremlin.GremlinUtilities.GUClasses
{
public class DataIdentifier
{

@ -1,3 +1,9 @@
AccountType|AccountTypeCode
AccountType|AccountTypeDescription
SubMarket|SubMarketCode
SubMarket|SubMarketDescription
ProductLine|ProductLineCode
ProductLine|ProductLineDescription
Product|ProductNumber
Product|ListPrice
Product|Weight
@ -9,6 +15,10 @@ LSAG|SubMarketCode
LSAG|LastName
LSAG|AccountName
LSAG|MA_ProductInterests
LSAG|Street
LSAG|ZIP
LSAG|City
LSAG|PhoneNumber
Account|AccountName
Account|Street
Account|ZIP
@ -26,13 +36,4 @@ CustomDescription|Heading
CustomDescription|ProductNumber
CustomDescription|OptionNumber
CustomDescription|DescriptionText
CustomDescription|CoverletterText
AccountType|AccountTypeCode
AccountType|AccountTypeDescription
AccountType|NumberOfColumns=2
SubMarket|SubMarketCode
SubMarket|SubMarketDescription
SubMarket|NumberOfColumns=2
ProductLine|ProductLineCode
ProductLine|ProductLineDescription
ProductLine|NumberOfColumns=2
CustomDescription|CoverletterText