diff --git a/BealeEngineering/BealeEngineering.Accounts/Form1.cs b/BealeEngineering/BealeEngineering.Accounts/Form1.cs index 9f09f2a..f8239c7 100644 --- a/BealeEngineering/BealeEngineering.Accounts/Form1.cs +++ b/BealeEngineering/BealeEngineering.Accounts/Form1.cs @@ -135,7 +135,7 @@ namespace BealeEngineering.Accounts { string fileInputPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) - + @"\Dropbox\Beale Engineering Services Ltd\BE Accounts\Xero-Export-Invoices - Orig.csv"; + + @"\Dropbox\Beale Engineering Services Ltd\BE Accounts\Xero-Export-Invoices.csv"; string dialogText = "Importing file from location: " + Environment.NewLine + Environment.NewLine + fileInputPath; diff --git a/BealeEngineering/BealeEngineering.Core/Data/Database/Sale/ReadInvoice.cs b/BealeEngineering/BealeEngineering.Core/Data/Database/Sale/ReadInvoice.cs index 110b517..cd81668 100644 --- a/BealeEngineering/BealeEngineering.Core/Data/Database/Sale/ReadInvoice.cs +++ b/BealeEngineering/BealeEngineering.Core/Data/Database/Sale/ReadInvoice.cs @@ -54,6 +54,7 @@ namespace BealeEngineering.Core.Data.Database.Sale ,SaleInvoice.InvoiceTotal ,SaleInvoice.TaxTotal ,SaleInvoice.IsCreditNote + ,SaleInvoice.Status ,SaleInvoiceLine.SaleInvoiceLineID ,SaleInvoiceLine.SaleInvoiceID ,SaleInvoiceLine.LineNumber diff --git a/BealeEngineering/BealeEngineering.Core/Data/Database/Sale/ReadInvoiceHeader.cs b/BealeEngineering/BealeEngineering.Core/Data/Database/Sale/ReadInvoiceHeader.cs index acc59c6..c6cb273 100644 --- a/BealeEngineering/BealeEngineering.Core/Data/Database/Sale/ReadInvoiceHeader.cs +++ b/BealeEngineering/BealeEngineering.Core/Data/Database/Sale/ReadInvoiceHeader.cs @@ -139,6 +139,7 @@ namespace BealeEngineering.Core.Data.Database.Sale ,SaleInvoice.InvoiceTotal ,SaleInvoice.TaxTotal ,SaleInvoice.IsCreditNote + ,SaleInvoice.Status ,Contact.ContactName FROM SaleInvoice INNER JOIN Contact ON SaleInvoice.ContactID = Contact.ContactID"; diff --git a/BealeEngineering/BealeEngineering.Core/Data/Database/Sale/UpdateInvoice.cs b/BealeEngineering/BealeEngineering.Core/Data/Database/Sale/UpdateInvoice.cs index 7d9271d..fe3543a 100644 --- a/BealeEngineering/BealeEngineering.Core/Data/Database/Sale/UpdateInvoice.cs +++ b/BealeEngineering/BealeEngineering.Core/Data/Database/Sale/UpdateInvoice.cs @@ -85,6 +85,7 @@ namespace BealeEngineering.Core.Data.Database.Sale ,[InvoiceTotal] = @invoiceTotal ,[TaxTotal] = @taxTotal ,[IsCreditNote] = @isCreditNote + ,[Status] = @status WHERE SaleInvoiceID = @saleInvoiceId; DELETE FROM SaleInvoiceLine WHERE SaleInvoiceID = @saleInvoiceId;"; @@ -102,6 +103,7 @@ namespace BealeEngineering.Core.Data.Database.Sale ,[InvoiceTotal] ,[TaxTotal] ,[IsCreditNote] + ,[Status] ) OUTPUT INSERTED.SaleInvoiceID VALUES( @@ -114,6 +116,7 @@ namespace BealeEngineering.Core.Data.Database.Sale ,@invoiceTotal ,@taxTotal ,@isCreditNote + ,@status )"; } else @@ -134,6 +137,7 @@ namespace BealeEngineering.Core.Data.Database.Sale cmd.Parameters.AddWithValue("@invoiceTotal", invoice.InvoiceTotal); cmd.Parameters.AddWithValue("@taxTotal", invoice.TaxTotal); cmd.Parameters.AddWithValue("@isCreditNote", invoice.IsCreditNote); + cmd.Parameters.AddWithValue("@status", invoice.Status); if (saleInvoiceId == 0) { diff --git a/BealeEngineering/BealeEngineering.Core/Logic/Adapter/SaleInvoice.cs b/BealeEngineering/BealeEngineering.Core/Logic/Adapter/SaleInvoice.cs index d24ccae..2664182 100644 --- a/BealeEngineering/BealeEngineering.Core/Logic/Adapter/SaleInvoice.cs +++ b/BealeEngineering/BealeEngineering.Core/Logic/Adapter/SaleInvoice.cs @@ -8,6 +8,21 @@ namespace BealeEngineering.Core.Logic.Adapter { public class SaleInvoice { + public SaleInvoice() + { + // ensure sale invoice hasn't changed + int propertyCount = new Model.Sale.Invoice().GetType().GetProperties().Count(); + if (propertyCount != 14) + { + throw new Exception("Model.Import.XeroInvoiceFlatFile has changed, it's adapter class may need updating."); + } + propertyCount = new Model.Sale.Invoice.InvoiceLine().GetType().GetProperties().Count(); + if (propertyCount != 10) + { + throw new Exception("Model.Import.XeroInvoiceFlatFile.LineItem has changed, it's adapter class may need updating."); + } + } + public Model.Sale.Invoice XeroInvoiceFlatFIle(Model.Import.XeroInvoiceFlatFile xeroInvoice) { var result = XeroInvoiceFlatFile(new List { xeroInvoice }); @@ -33,6 +48,7 @@ namespace BealeEngineering.Core.Logic.Adapter else { throw new FormatException("Unknow value '" + xeroInvoiceList[i].Type + "' found in 'Type' field"); } invoice.Reference = xeroInvoiceList[i].Reference; invoice.SaleInvoiceNumber = xeroInvoiceList[i].InvoiceNumber; + invoice.Status = xeroInvoiceList[i].Status; invoice.TaxTotal = xeroInvoiceList[i].TaxTotal; invoice.InvoiceLineList = new List(); diff --git a/BealeEngineering/BealeEngineering.Core/Logic/Adapter/XeroInvoiceFlatFile.cs b/BealeEngineering/BealeEngineering.Core/Logic/Adapter/XeroInvoiceFlatFile.cs index f1d685e..00cedf0 100644 --- a/BealeEngineering/BealeEngineering.Core/Logic/Adapter/XeroInvoiceFlatFile.cs +++ b/BealeEngineering/BealeEngineering.Core/Logic/Adapter/XeroInvoiceFlatFile.cs @@ -8,6 +8,21 @@ namespace BealeEngineering.Core.Logic.Adapter { public class XeroInvoiceFlatFile { + public XeroInvoiceFlatFile() + { + // ensure XeroInvoiceFlatFile hasn't changed + int propertyCount = new Model.Import.XeroInvoiceFlatFile().GetType().GetProperties().Count(); + if (propertyCount != 32) + { + throw new Exception("Model.Import.XeroInvoiceFlatFile has changed, it's adapter class may need updating."); + } + propertyCount = new Model.Import.XeroInvoiceFlatFile.LineItem().GetType().GetProperties().Count(); + if (propertyCount != 13) + { + throw new Exception("Model.Import.XeroInvoiceFlatFile.LineItem has changed, it's adapter class may need updating."); + } + } + public List SaleInvoice(List invoiceList) { if (invoiceList == null || !invoiceList.Any()) { return null; } @@ -23,6 +38,7 @@ namespace BealeEngineering.Core.Logic.Adapter xeroInvoice.InvoiceDate = invoiceList[i].InvoiceDate; xeroInvoice.InvoiceNumber = invoiceList[i].SaleInvoiceNumber; xeroInvoice.Reference = invoiceList[i].Reference; + xeroInvoice.Status = invoiceList[i].Status; xeroInvoice.TaxTotal = invoiceList[i].TaxTotal; xeroInvoice.Total = invoiceList[i].InvoiceTotal; if (invoiceList[i].IsCreditNote) { xeroInvoice.Type = "Sales credit note"; } @@ -59,19 +75,21 @@ namespace BealeEngineering.Core.Logic.Adapter // ensure flat data is in invoice number order var invDictionary = new Dictionary(); - string lastNumber = null; + string lastUniqueString = ""; foreach (var line in flatData) { - if (line.InvoiceNumber != lastNumber) + // invoice number isn't unique, can be duplicated if one invoice is void/deleted + string uniqueString = line.InvoiceNumber + line.Status; + if (uniqueString != lastUniqueString) { - lastNumber = line.InvoiceNumber; - if (invDictionary.ContainsKey(lastNumber)) + lastUniqueString = uniqueString; + if (invDictionary.ContainsKey(lastUniqueString)) { throw new Exception("Invoices are not grouped in CSV flatfile."); } else { - invDictionary.Add(lastNumber, 0); + invDictionary.Add(lastUniqueString, 0); } } } diff --git a/BealeEngineering/BealeEngineering.Core/Logic/Adapter/XeroInvoiceFlatFileDTO.cs b/BealeEngineering/BealeEngineering.Core/Logic/Adapter/XeroInvoiceFlatFileDTO.cs index e597556..825e5a6 100644 --- a/BealeEngineering/BealeEngineering.Core/Logic/Adapter/XeroInvoiceFlatFileDTO.cs +++ b/BealeEngineering/BealeEngineering.Core/Logic/Adapter/XeroInvoiceFlatFileDTO.cs @@ -11,12 +11,13 @@ namespace BealeEngineering.Core.Logic.Adapter public XeroInvoiceFlatFileDTO() { // ensure XeroInvoiceFlatFileDTO hasn't changed - int dtoPropertyCouny = new Model.Import.XeroInvoiceFlatFileDTO().GetType().GetProperties().Count(); - if (dtoPropertyCouny != 44) + int propertyCount = new Model.Import.XeroInvoiceFlatFileDTO().GetType().GetProperties().Count(); + if (propertyCount != 44) { - throw new Exception("Model.Import.XeroInvoiceFlatFileDTO has changed, it's adapter class needs updating."); + throw new Exception("Model.Import.XeroInvoiceFlatFileDTO has changed, it's adapter class may need updating."); } } + public List XeroInvoiceFlatFile(List invoices) { //throw new NotImplementedException(); diff --git a/BealeEngineering/BealeEngineering.Core/Logic/Import/XeroInvoiceFlatFile.cs b/BealeEngineering/BealeEngineering.Core/Logic/Import/XeroInvoiceFlatFile.cs index 4543c72..90408f9 100644 --- a/BealeEngineering/BealeEngineering.Core/Logic/Import/XeroInvoiceFlatFile.cs +++ b/BealeEngineering/BealeEngineering.Core/Logic/Import/XeroInvoiceFlatFile.cs @@ -13,7 +13,7 @@ namespace BealeEngineering.Core.Logic.Import this.sqlConnectionString = sqlConnectionString; } - private List XeroInvoiceData { get; set; } + private List XeroFlatData { get; set; } public int InvoicesCreated { get; private set; } = 0; @@ -26,26 +26,41 @@ namespace BealeEngineering.Core.Logic.Import public void ByFilePath(string filePath, bool updateContacts = true) { // get xero data - var data = new Data.Xero.FlatFile.ReadXeroInvoiceFlatFile(); - XeroInvoiceData = data.ByFilePath(filePath); + var flatfileData = new Data.Xero.FlatFile.ReadXeroInvoiceFlatFile(); + XeroFlatData = flatfileData.ByFilePath(filePath); + + // check data + if (XeroFlatData == null || !XeroFlatData.Any()) { return; } + else + { + for (int i = 0; i < XeroFlatData.Count(); i++) + { + if (XeroFlatData[i].Status == "Deleted" || XeroFlatData[i].Status == "Voided") + { throw new Exception("Deleted/Voided invoices found in dataset."); } + // xero flatfiles can have duplicate invoice numbers if one of the invoices is voided/deleted. + // Long story short; to avoid missing invoices from db dataset, any voided/deleted invoices need + // to be manually deleted on the database. While this has it's problems, it's more probable that + // the db side will be correct this way. + } + } // update db contacts UpdateContacts(); // populate/map xero data to invoice model list - var dbInvoiceData = new Logic.Adapter.SaleInvoice().XeroInvoiceFlatFile(XeroInvoiceData); + var xeroInvoiceList = new Logic.Adapter.SaleInvoice().XeroInvoiceFlatFile(XeroFlatData); //check - if (dbInvoiceData == null ||( XeroInvoiceData.Count != dbInvoiceData.Count)) + if (xeroInvoiceList == null ||( XeroFlatData.Count != xeroInvoiceList.Count)) { throw new Exception("Something went wrong while mapping the data."); } // send list to database (it will get validated in data layer) - if (dbInvoiceData.Any()) + if (xeroInvoiceList.Any()) { var updateInvoice = new Data.Database.Sale.UpdateInvoice(sqlConnectionString); - updateInvoice.ByInvoiceList(dbInvoiceData, true); + updateInvoice.ByInvoiceList(xeroInvoiceList, true); InvoicesCreated = updateInvoice.RecordsCreated; InvoicesUpdated = updateInvoice.RecordsUpdated; @@ -65,11 +80,11 @@ namespace BealeEngineering.Core.Logic.Import { var contactAdapter = new Logic.Adapter.Contact(); var dicContacts = new Dictionary(); - for (var i = 0; i < XeroInvoiceData.Count; i++) + for (var i = 0; i < XeroFlatData.Count; i++) { - if (!dicContacts.ContainsKey(XeroInvoiceData[0].ContactName)) + if (!dicContacts.ContainsKey(XeroFlatData[0].ContactName)) { - dicContacts.Add(XeroInvoiceData[0].ContactName, contactAdapter.XeroInvoiceFlatFile(XeroInvoiceData[0])); + dicContacts.Add(XeroFlatData[0].ContactName, contactAdapter.XeroInvoiceFlatFile(XeroFlatData[0])); } } if (dicContacts.Any()) diff --git a/BealeEngineering/BealeEngineering.Core/Model/Sale/Invoice.cs b/BealeEngineering/BealeEngineering.Core/Model/Sale/Invoice.cs index 2f5e10d..84dad91 100644 --- a/BealeEngineering/BealeEngineering.Core/Model/Sale/Invoice.cs +++ b/BealeEngineering/BealeEngineering.Core/Model/Sale/Invoice.cs @@ -10,6 +10,7 @@ namespace BealeEngineering.Core.Model.Sale public class Invoice : InvoiceHeader { public List InvoiceLineList { get; set; } = new List(); + public bool InvoiceLineListIsSet { get @@ -18,26 +19,36 @@ namespace BealeEngineering.Core.Model.Sale else { return true; } } } + public class InvoiceLine { [Required(), Range(0, 255)] public int LineNumber { get; set; } + [Required(), StringLength(50)] public string InventoryItemCode { get; set; } + [StringLength(500)] public string Description { get; set; } + [Required(), Range(0, 255)] public decimal Quantity { get; set; } + [Required()] public decimal UnitAmount { get; set; } + [Range(1, 100)] public int? Discount { get; set; } + [Required(), StringLength(10)] public string AccountCode { get; set; } + [Required(), StringLength(50)] public string TaxType { get; set; } + [Required()] public decimal TaxAmount { get; set; } + /// /// Line amount is Tax Exclusive /// @@ -52,6 +63,7 @@ namespace BealeEngineering.Core.Model.Sale } } } + public override IEnumerable Validate(ValidationContext validationContext) { base.Validate(validationContext); diff --git a/BealeEngineering/BealeEngineering.Core/Model/Sale/InvoiceHeader.cs b/BealeEngineering/BealeEngineering.Core/Model/Sale/InvoiceHeader.cs index 04c1901..83273c1 100644 --- a/BealeEngineering/BealeEngineering.Core/Model/Sale/InvoiceHeader.cs +++ b/BealeEngineering/BealeEngineering.Core/Model/Sale/InvoiceHeader.cs @@ -10,24 +10,37 @@ namespace BealeEngineering.Core.Model.Sale public class InvoiceHeader : ValidateModel { public int SaleInvoiceID { get; set; } + [Required(AllowEmptyStrings = false)] public string ContactName { get; set; } + [Required(AllowEmptyStrings = false), StringLength(50)] public string SaleInvoiceNumber { get; set; } + [Required()] public DateTime InvoiceDate { get; set; } + public DateTime? DueDate { get; set; } + [StringLength(50)] public string Reference { get; set; } + [Required(AllowEmptyStrings = false)] [StringLength(3, MinimumLength = 3)] public string CurrencyCode { get; set; } + [Required()] public decimal InvoiceTotal { get; set;} + [Required()] public decimal TaxTotal { get; set; } + [Required()] public bool IsCreditNote { get; set; } + + [Required()] + public string Status { get; set; } + public override IEnumerable Validate(ValidationContext validationContext) { base.Validate(validationContext);