diff --git a/AyaNovaQBI/AyaNovaQBI.csproj b/AyaNovaQBI/AyaNovaQBI.csproj index 50c1266..a4920ee 100644 --- a/AyaNovaQBI/AyaNovaQBI.csproj +++ b/AyaNovaQBI/AyaNovaQBI.csproj @@ -223,6 +223,14 @@ Waiting.cs + + + + + + + + Form diff --git a/AyaNovaQBI/MainForm.cs b/AyaNovaQBI/MainForm.cs index dde83fa..39c83bb 100644 --- a/AyaNovaQBI/MainForm.cs +++ b/AyaNovaQBI/MainForm.cs @@ -16,7 +16,7 @@ namespace AyaNovaQBI public MainForm() { InitializeComponent(); - this.Icon = AyaNovaQBI.Properties.Resources.logo; + Icon = AyaNovaQBI.Properties.Resources.logo; } async private void MainForm_Load(object sender, EventArgs e) @@ -32,28 +32,37 @@ namespace AyaNovaQBI await Task.Run(() => MessageBox.Show($"AyaNova QBI was unable to start:\r\n{initErrors.ToString()}")); } Close(); + return; } - else - { - //Confirm main settings and set any that are missing: - if (await util.ValidateSettings(false) == util.pfstat.Cancel) - { - await util.IntegrationLog("PFC: User settings not completed, user selected cancel"); - Close(); - } - //check if setup is required - //if (util.QBIntegration.Items.Count == 0) - //{ - // MessageBox.Show("STUB: mainform,no maps, no integration data set"); - //} + //Confirm main settings and set any that are missing: + if (await util.ValidateSettings(false) == util.pfstat.Cancel) + { + await util.IntegrationLog("PFC: User settings not completed, user selected cancel"); + Close(); + return; } + Text = "AyaNova QBI - " + util.QCompanyName; + + //See if there are *any* data mappings, if not then we will prompt the user to start that process + if (util.QBIntegration.Items.Count == 0) + { + //show message about mapping + MessageBox.Show( + "AyaNova QBI now needs you to map data between QuickBooks and AyaNova.", + "Setup mapping", MessageBoxButtons.OK, MessageBoxIcon.Information); + + Map m = new Map(); + if (m.ShowDialog() == DialogResult.Abort) + Close(); + + } + //Display billable workorders + InitInvoices(); + grid.Visible = true; menuStrip1.Enabled = true; - //MessageBox.Show("DONE / OK"); - - // grid.DataSource = util.GetInvoiceableItems(); } private void grid_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) @@ -116,18 +125,17 @@ namespace AyaNovaQBI private async void preferencesToolStripMenuItem_Click(object sender, EventArgs e) { await util.ValidateSettings(true); - //TODO: CODE THIS InitInvoices(); } private void mapAndImportToolStripMenuItem_Click(object sender, EventArgs e) { Map m = new Map(); if (m.ShowDialog() == DialogResult.Abort) - this.Close(); + Close(); else { m.Dispose(); - //todo: this.InitInvoices(); + InitInvoices(); } } @@ -142,7 +150,7 @@ namespace AyaNovaQBI MessageBox.Show(sVersion, "About"); } - + private void onlineManualToolStripMenuItem_Click(object sender, EventArgs e) { util.OpenWebURL("https://ayanova.com/qbi/docs"); @@ -154,14 +162,224 @@ namespace AyaNovaQBI await util.PopulateAyaListCache(); } - private async void invoiceDescriptiveTextTemplateToolStripMenuItem_Click(object sender, EventArgs e) + private async void invoiceDescriptiveTextTemplateToolStripMenuItem_Click(object sender, EventArgs e) { InvoiceTemplateBuilder b = new InvoiceTemplateBuilder(); b.ShowDialog(); - if (util.QDat.IsDirty) + if (util.QDat.IsDirty) await util.SaveIntegrationObject(); b.Dispose(); } - } -} + + + + + + #region Main workorder grid + + /// + /// Adjusts main form display to either show a list of billable workorders + /// or a status indicating there are none and why + /// + private void SetState() + { + fixProblemsToolStripMenuItem.Enabled = _MisMatches.Count > 0; + + if (grid.Rows.Count > 0) + { + + grid.Visible = true; + this.lblStatus.Visible = false; + } + else + { + StringBuilder sb = new StringBuilder(); + sb.Append("No invoiceable work orders found in AyaNova\r\n\r\n"); + sb.Append("A work order is invoiceable and will be listed here if it has:\r\n"); + sb.Append(" - \"Invoice number\" field empty\r\n"); + if (util.QDat.PreWOStatus != Guid.Empty) + { + sb.Append(" - \"Status\" field set to: "); + sb.Append(NameFetcher.GetItem("aWorkorderStatus", "aName", util.QDat.PreWOStatus)); + sb.Append("\r\n"); + sb.Append(" (You can change this status under Tools->Preferences in the menu)"); + + } + this.lblStatus.Text = sb.ToString(); + + grid.Visible = false; + this.lblStatus.Visible = true; + } + } + + private WorkorderServiceBillableList _wolist = null; + private ArrayList _MisMatches = new ArrayList(); + private ArrayList _PartPriceOverrides = new ArrayList(); + /// + /// Initialize invoices dataset + /// from scratch. If a previous + /// initialize was done, wipe it and + /// repopulate from scratch + /// + private void InitInvoices() + { + Waiting w = new Waiting(); + w.Show(); + w.Ops = "Validating invoices..."; + + + try + { + + _MisMatches.Clear(); + grid.BeginUpdate(); + dsInvoices.Clear(); + _wolist = WorkorderServiceBillableList.GetList(Util.QDat.PreWOStatus, true); + DataTable dtInvoice = dsInvoices.Tables["Invoices"]; + DataTable dtWorkorder = dsInvoices.Tables["Workorders"]; + foreach (WorkorderServiceBillableList.WorkorderServiceBillableListInfo i in _wolist) + { + bool bLinked = Util.ScanLinksOK(i.ID, _MisMatches, _PartPriceOverrides); + + DataRow dri = InvoiceRowForClientID(i.ClientID); + w.Step = "WO: " + i.ServiceNumber; + if (dri == null) + { + dri = dtInvoice.NewRow(); + dri["Client"] = i.Client; + dri["ClientID"] = i.ClientID; + dtInvoice.Rows.Add(dri); + } + + //If any one single workorder is linked + //then the invoice is flagged as linked because + //you can invoice out anything under it that is linked and the + //not linked items simply won't invoice + if (bLinked) + dri["Linked"] = true; + + DataRow drw = dtWorkorder.NewRow(); + drw["InvoiceWorkingID"] = (int)dri["WorkingID"]; + drw["WorkorderID"] = i.ID; + drw["Status"] = i.Status; + drw["ServiceNumber"] = i.ServiceNumber; + drw["ServiceDate"] = i.ServiceDate; + drw["Project"] = i.Project; + drw["StatusARGB"] = i.StatusARGB; + + drw["Linked"] = bLinked; + dtWorkorder.Rows.Add(drw); + + + + } + + grid.DisplayLayout.Rows.CollapseAll(false); + foreach (UltraGridRow r in grid.Rows) + { + foreach (UltraGridRow rr in r.ChildBands[0].Rows) + { + if ((bool)rr.Cells["Linked"].Value == false) + r.Expanded = true; + } + + } + grid.EndUpdate(); + } + finally + { + w.Close(); + } + + SetState(); + } + + + /// + /// Helper for grouping workorders by client + /// + /// + /// null if not found else datarow containing invoice for client + private DataRow InvoiceRowForClientID(Guid ClientID) + { + foreach (DataRow r in dsInvoices.Tables["Invoices"].Rows) + { + if ((Guid)r["ClientID"] == ClientID) + { + return r; + + } + } + return null; + } + + + + + private void InitializeGrid() + { + grid.DataSource = dsInvoices; + string currentAssemblyDirectoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + + //Load the grid layout from file + if (System.IO.File.Exists(currentAssemblyDirectoryName + "\\MainGrid.lyt")) + grid.DisplayLayout.Load(currentAssemblyDirectoryName + "\\MainGrid.lyt"); + grid.DisplayLayout.Bands[0].Columns["WorkingID"].Hidden = true; + grid.DisplayLayout.Bands[0].Columns["ClientID"].Hidden = true; + grid.DisplayLayout.Bands[0].Columns["Linked"].Hidden = true; + grid.DisplayLayout.Bands[0].Columns["Client"].Header.Caption = "Invoice"; + + grid.DisplayLayout.Bands[1].Columns["InvoiceWorkingID"].Hidden = true; + grid.DisplayLayout.Bands[1].Columns["WorkorderID"].Hidden = true; + grid.DisplayLayout.Bands[1].Columns["StatusARGB"].Hidden = true; + grid.DisplayLayout.Bands[1].Columns["Linked"].Hidden = true; + } + + private void grid_InitializeRow(object sender, Infragistics.Win.UltraWinGrid.InitializeRowEventArgs e) + { + if (e.Row.Band.Index == 0) + { + //Prepare invoice row + if ((bool)e.Row.Cells["Linked"].Value == true) + { + e.Row.Cells["Client"].Appearance.Image = Util.AyaImage("OK16");//Util.Image("OK16.png"); + } + else + { + e.Row.Cells["Client"].Appearance.Image = Util.AyaImage("Cancel16");//Util.Image("Cancel16.png"); + } + } + else + { + //prepare workorder row + + //if backcolor==0 that means no color was set + int nColor = (int)e.Row.Cells["StatusARGB"].Value; + if (nColor != 0) + { + e.Row.Cells["Status"].Appearance.BackColor = Color.FromArgb(nColor); + e.Row.Cells["Status"].Appearance.ForeColor = Util.InvertColor(Color.FromArgb(nColor)); + + } + + //flag whether billable (linked) or not + if ((bool)e.Row.Cells["Linked"].Value == true) + { + e.Row.Cells["ServiceNumber"].Appearance.Image = Util.AyaImage("OK16"); + } + else + { + e.Row.Cells["ServiceNumber"].Appearance.Image = Util.AyaImage("Cancel16"); + } + } + + } + #endregion + + + + + + }//eoc +}//eons diff --git a/AyaNovaQBI/WorkOrder.cs b/AyaNovaQBI/WorkOrder.cs new file mode 100644 index 0000000..97fcd4c --- /dev/null +++ b/AyaNovaQBI/WorkOrder.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AyaNovaQBI +{ + internal class WorkOrder + { + public long Id { get; set; } + public uint Concurrency { get; set; } + + + public long Serial { get; set; } + + public string Notes { get; set; }//WAS "SUMMARY" + public string Wiki { get; set; } + public string CustomFields { get; set; } + public List Tags { get; set; } = new List(); + + + public long CustomerId { get; set; } + + public string CustomerViz { get; set; } + + + public string CustomerTechNotesViz { get; set; } + + public string CustomerPhone1Viz { get; set; } + + public string CustomerPhone2Viz { get; set; } + + public string CustomerPhone3Viz { get; set; } + + public string CustomerPhone4Viz { get; set; } + + public string CustomerPhone5Viz { get; set; } + + public string CustomerEmailAddressViz { get; set; } + + public long? ProjectId { get; set; } + + public string ProjectViz { get; set; } + public string InternalReferenceNumber { get; set; } + public string CustomerReferenceNumber { get; set; } + public string CustomerContactName { get; set; } + public long? FromQuoteId { get; set; } + public long? FromPMId { get; set; } + + public DateTime CreatedDate { get; set; } = DateTime.UtcNow; + public DateTime? ServiceDate { get; set; } + public DateTime? CompleteByDate { get; set; } + public TimeSpan DurationToCompleted { get; set; } = TimeSpan.Zero; + public string InvoiceNumber { get; set; } + public string CustomerSignature { get; set; } + public string CustomerSignatureName { get; set; } + public DateTime? CustomerSignatureCaptured { get; set; } + public string TechSignature { get; set; } + public string TechSignatureName { get; set; } + public DateTime? TechSignatureCaptured { get; set; } + public bool Onsite { get; set; } + public long? ContractId { get; set; } + + public string ContractViz { get; set; } + + //redundant field to speed up list queries + //(added after status system already coded) + public long? LastStatusId { get; set; } + + + //POSTAL ADDRESS / "BILLING ADDRESS" + public string PostAddress { get; set; } + public string PostCity { get; set; } + public string PostRegion { get; set; } + public string PostCountry { get; set; } + public string PostCode { get; set; } + + //PHYSICAL ADDRESS / "SERVICE ADDRESS" + public string Address { get; set; } + public string City { get; set; } + public string Region { get; set; } + public string Country { get; set; } + public decimal? Latitude { get; set; } + public decimal? Longitude { get; set; } + + public List Items { get; set; } = new List(); + public List States { get; set; } = new List(); + + + //UTILITY FIELDS + + public bool IsLockedAtServer { get; set; } = false;//signal to client that it came from the server in a locked state + + public string AlertViz { get; set; } = null; + + public string FromQuoteViz { get; set; } + + public string FromPMViz { get; set; } + + + public string LastStateUserViz { get; set; } + + public string LastStateNameViz { get; set; } + + public string LastStateColorViz { get; set; } + + public bool LastStateCompletedViz { get; set; } + + public bool LastStateLockedViz { get; set; } + + + + public bool IsCompleteRecord { get; set; } = true;//indicates if some items were removed due to user role / type restrictions (i.e. woitems they are not scheduled on) + + + public bool UserIsRestrictedType { get; set; } + + public bool UserIsTechRestricted { get; set; } + + public bool UserIsSubContractorFull { get; set; } + + public bool UserIsSubContractorRestricted { get; set; } + + public bool UserCanViewPartCosts { get; set; } + + public bool UserCanViewLaborOrTravelRateCosts { get; set; } + + public bool UserCanViewLoanerCosts { get; set; } + } +} diff --git a/AyaNovaQBI/WorkOrderItem.cs b/AyaNovaQBI/WorkOrderItem.cs new file mode 100644 index 0000000..23fb782 --- /dev/null +++ b/AyaNovaQBI/WorkOrderItem.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AyaNovaQBI +{ + internal class WorkOrderItem + { + public long Id { get; set; } + public uint Concurrency { get; set; } + public string Notes { get; set; }//"Summary" field + public string Wiki { get; set; } + public string CustomFields { get; set; } + public List Tags { get; set; } = new List(); + + + public long WorkOrderId { get; set; } + public string TechNotes { get; set; } + public long? WorkOrderItemStatusId { get; set; } + + public string WorkOrderItemStatusNameViz { get; set; } + + public string WorkOrderItemStatusColorViz { get; set; } + + public long? WorkOrderItemPriorityId { get; set; } + + public string WorkOrderItemPriorityNameViz { get; set; } + + public string WorkOrderItemPriorityColorViz { get; set; } + + public DateTime? RequestDate { get; set; } + public bool WarrantyService { get; set; } = false; + public int Sequence { get; set; } + + public long? FromCSRId { get; set; } + + public string FromCSRViz { get; set; } + + //workaround for notification + + public string Name { get; set; } + + //Principle + + public WorkOrder WorkOrder { get; set; } + //dependents + public List Expenses { get; set; } = new List(); + public List Labors { get; set; } = new List(); + public List Loans { get; set; } = new List(); + public List Parts { get; set; } = new List(); + public List PartRequests { get; set; } = new List(); + public List ScheduledUsers { get; set; } = new List(); + public List Tasks { get; set; } = new List(); + public List Travels { get; set; } = new List(); + public List Units { get; set; } = new List(); + public List OutsideServices { get; set; } = new List(); + } +} diff --git a/AyaNovaQBI/WorkOrderItemExpense.cs b/AyaNovaQBI/WorkOrderItemExpense.cs new file mode 100644 index 0000000..fc9767e --- /dev/null +++ b/AyaNovaQBI/WorkOrderItemExpense.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AyaNovaQBI +{ + internal class WorkOrderItemExpense + { + public long Id { get; set; } + public uint Concurrency { get; set; } + + public string Description { get; set; } + public string Name { get; set; } + + public decimal TotalCost { get; set; } + + public decimal ChargeAmount { get; set; } + + public decimal TaxPaid { get; set; } + public long? ChargeTaxCodeId { get; set; } + + public string TaxCodeViz { get; set; } + + public bool ReimburseUser { get; set; } = false; + public long? UserId { get; set; } + + public string UserViz { get; set; } + public bool ChargeToCustomer { get; set; } = false; + + + public decimal TaxAViz { get; set; } + + public decimal TaxBViz { get; set; } + + public decimal LineTotalViz { get; set; } + + public long WorkOrderItemId { get; set; } + } +} diff --git a/AyaNovaQBI/WorkOrderItemLabor.cs b/AyaNovaQBI/WorkOrderItemLabor.cs new file mode 100644 index 0000000..ae0f3fe --- /dev/null +++ b/AyaNovaQBI/WorkOrderItemLabor.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AyaNovaQBI +{ + internal class WorkOrderItemLabor + { + public long Id { get; set; } + public uint Concurrency { get; set; } + + public long? UserId { get; set; } + + public string UserViz { get; set; } + public DateTime? ServiceStartDate { get; set; } + public DateTime? ServiceStopDate { get; set; } + public long? ServiceRateId { get; set; } + + public string ServiceRateViz { get; set; } + public string ServiceDetails { get; set; } + + public decimal ServiceRateQuantity { get; set; } + + public decimal NoChargeQuantity { get; set; } + //public long? ServiceBankId { get; set; } + public long? TaxCodeSaleId { get; set; } + + public string TaxCodeViz { get; set; } + + + //Standard pricing fields (mostly to support printed reports though some show in UI) + //some not to be sent with record depending on role (i.e. cost and charge in some cases) + public decimal? PriceOverride { get; set; }//user entered manually overridden price, if null then ignored in calcs otherwise this *is* the price even if zero + + public decimal CostViz { get; set; }//cost from source record (e.g. serviceRate) or zero if no cost entered + + public decimal ListPriceViz { get; set; }//List price from source record (e.g. serviceRate) or zero if no cost entered + + public string UnitOfMeasureViz { get; set; }//"each", "hour" etc + + public decimal PriceViz { get; set; }//per unit price used in calcs after discounts or manual price if non-null or just ListPrice if no discount or manual override + + public decimal NetViz { get; set; }//quantity * price (before taxes line total essentially) + + public decimal TaxAViz { get; set; }//total amount of taxA + + public decimal TaxBViz { get; set; }//total amount of taxB + + public decimal LineTotalViz { get; set; }//line total netViz + taxes + + + public long WorkOrderItemId { get; set; } + + + } +} diff --git a/AyaNovaQBI/WorkOrderItemLoan.cs b/AyaNovaQBI/WorkOrderItemLoan.cs new file mode 100644 index 0000000..ade1410 --- /dev/null +++ b/AyaNovaQBI/WorkOrderItemLoan.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AyaNovaQBI +{ + internal enum LoanUnitRateUnit : int + { + None = 0, + Hours = 1, + HalfDays = 2, + Days = 3, + Weeks = 4, + Months = 5, + Years = 6 + } + internal class WorkOrderItemLoan + { + public long Id { get; set; } + public uint Concurrency { get; set; } + public string Notes { get; set; } + public DateTime? OutDate { get; set; } + public DateTime? DueDate { get; set; } + public DateTime? ReturnDate { get; set; } + // + // public decimal Charges { get; set; }//removed in favor of ListPRice snapshot which normalizes fields to other objects + public long? TaxCodeId { get; set; } + + public string TaxCodeViz { get; set; } + + public long LoanUnitId { get; set; } + + public string LoanUnitViz { get; set; } + + public decimal Quantity { get; set; } + + public LoanUnitRateUnit Rate { get; set; } + + public decimal Cost { get; set; }//cost from source record (e.g. serviceRate) or zero if no cost entered + public decimal ListPrice { get; set; }//List price from source record (e.g. serviceRate) or zero if no cost entered + + //Standard pricing fields (mostly to support printed reports though some show in UI) + //some not to be sent with record depending on role (i.e. cost and charge in some cases) + public decimal? PriceOverride { get; set; }//user entered manually overridden price, if null then ignored in calcs otherwise this *is* the price even if zero + + + public string UnitOfMeasureViz { get; set; }//"each", "hour" etc + + public decimal PriceViz { get; set; }//per unit price used in calcs after discounts or manual price if non-null or just ListPrice if no discount or manual override + + public decimal NetViz { get; set; }//quantity * price (before taxes line total essentially) + + public decimal TaxAViz { get; set; }//total amount of taxA + + public decimal TaxBViz { get; set; }//total amount of taxB + + public decimal LineTotalViz { get; set; }//line total netViz + taxes + + //workaround for notification + + public List Tags { get; set; } = new List(); + + public string Name { get; set; } + + public long WorkOrderItemId { get; set; } + } +} diff --git a/AyaNovaQBI/WorkOrderItemOutsideService.cs b/AyaNovaQBI/WorkOrderItemOutsideService.cs new file mode 100644 index 0000000..d02050f --- /dev/null +++ b/AyaNovaQBI/WorkOrderItemOutsideService.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AyaNovaQBI +{ + internal class WorkOrderItemOutsideService + { + public long Id { get; set; } + public uint Concurrency { get; set; } + public string Notes { get; set; } + + + public long UnitId { get; set; } + + public string UnitViz { get; set; } + public long? VendorSentToId { get; set; } + + public string VendorSentToViz { get; set; } + public long? VendorSentViaId { get; set; } + + public string VendorSentViaViz { get; set; } + public string RMANumber { get; set; } + public string TrackingNumber { get; set; } + + public decimal RepairCost { get; set; } + + public decimal RepairPrice { get; set; } + + public decimal ShippingCost { get; set; } + + public decimal ShippingPrice { get; set; } + public DateTime? SentDate { get; set; } + public DateTime? ETADate { get; set; } + public DateTime? ReturnDate { get; set; } + public long? TaxCodeId { get; set; } + + public string TaxCodeViz { get; set; } + + public decimal CostViz { get; set; }//Total cost shipping + repairs + + public decimal PriceViz { get; set; }//Total price shipping + repairs + + public decimal NetViz { get; set; }//=priceViz for standardization not because it's necessary (before taxes line total essentially) + + public decimal TaxAViz { get; set; }//total amount of taxA + + public decimal TaxBViz { get; set; }//total amount of taxB + + public decimal LineTotalViz { get; set; }//line total netViz + taxes + + //workaround for notification + + public List Tags { get; set; } = new List(); + + public string Name { get; set; } + + + public long WorkOrderItemId { get; set; } + } +} diff --git a/AyaNovaQBI/WorkOrderItemPart.cs b/AyaNovaQBI/WorkOrderItemPart.cs new file mode 100644 index 0000000..64a2b4d --- /dev/null +++ b/AyaNovaQBI/WorkOrderItemPart.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AyaNovaQBI +{ + internal class WorkOrderItemPart + { + public long Id { get; set; } + public uint Concurrency { get; set; } + public string Description { get; set; } + public string Serials { get; set; } + + public long PartId { get; set; } + + public string PartDescriptionViz { get; set; } + + public string PartNameViz { get; set; } + + public string UpcViz { get; set; } + + public long PartWarehouseId { get; set; } + + public string PartWarehouseViz { get; set; } + + public decimal Quantity { get; set; } + public decimal SuggestedQuantity { get; set; } + public long? TaxPartSaleId { get; set; } + + public string TaxCodeViz { get; set; } + + //NOTE: part prices are volatile and expected to be frequently edited so snapshotted when newly added unlike other things like rates etc that are protected from change + public decimal Cost { get; set; }//cost from source record (e.g. serviceRate) or zero if no cost entered + public decimal ListPrice { get; set; }//List price from source record (e.g. serviceRate) or zero if no cost entered + + //Standard pricing fields (mostly to support printed reports though some show in UI) + //some not to be sent with record depending on role (i.e. cost and charge in some cases) + public decimal? PriceOverride { get; set; }//user entered manually overridden price, if null then ignored in calcs otherwise this *is* the price even if zero + + + + public string UnitOfMeasureViz { get; set; }//"each", "hour" etc + + public decimal PriceViz { get; set; }//per unit price used in calcs after discounts or manual price if non-null or just ListPrice if no discount or manual override + + public decimal NetViz { get; set; }//quantity * price (before taxes line total essentially) + + public decimal TaxAViz { get; set; }//total amount of taxA + + public decimal TaxBViz { get; set; }//total amount of taxB + + public decimal LineTotalViz { get; set; }//line total netViz + taxes + + //workaround for notification + + public List Tags { get; set; } = new List(); + + public string Name { get; set; } + + + public long WorkOrderItemId { get; set; } + } +} diff --git a/AyaNovaQBI/WorkOrderItemTravel.cs b/AyaNovaQBI/WorkOrderItemTravel.cs new file mode 100644 index 0000000..f313968 --- /dev/null +++ b/AyaNovaQBI/WorkOrderItemTravel.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AyaNovaQBI +{ + internal class WorkOrderItemTravel + { + public long Id { get; set; } + public uint Concurrency { get; set; } + + public long? UserId { get; set; } + + public string UserViz { get; set; } + public DateTime? TravelStartDate { get; set; } + public DateTime? TravelStopDate { get; set; } + public long? TravelRateId { get; set; } + + public string TravelRateViz { get; set; } + public string TravelDetails { get; set; } + public decimal TravelRateQuantity { get; set; } + public decimal NoChargeQuantity { get; set; } + //public long? ServiceBankId { get; set; } + public long? TaxCodeSaleId { get; set; } + + public string TaxCodeViz { get; set; } + public decimal Distance { get; set; } + + + //Standard pricing fields (mostly to support printed reports though some show in UI) + //some not to be sent with record depending on role (i.e. cost and charge in some cases) + public decimal? PriceOverride { get; set; }//user entered manually overridden price, if null then ignored in calcs otherwise this *is* the price even if zero + + public decimal CostViz { get; set; }//cost from source record (e.g. serviceRate) or zero if no cost entered + + public decimal ListPriceViz { get; set; }//List price from source record (e.g. serviceRate) or zero if no cost entered + + public string UnitOfMeasureViz { get; set; }//"each", "hour" etc + + public decimal PriceViz { get; set; }//per unit price used in calcs after discounts or manual price if non-null or just ListPrice if no discount or manual override + + public decimal NetViz { get; set; }//quantity * price (before taxes line total essentially) + + public decimal TaxAViz { get; set; }//total amount of taxA + + public decimal TaxBViz { get; set; }//total amount of taxB + + public decimal LineTotalViz { get; set; }//line total netViz + taxes + + //workaround for notification + + public List Tags { get; set; } = new List(); + + public string Name { get; set; } + + + public long WorkOrderItemId { get; set; } + } +} diff --git a/AyaNovaQBI/util.cs b/AyaNovaQBI/util.cs index 03b5a53..3d905cd 100644 --- a/AyaNovaQBI/util.cs +++ b/AyaNovaQBI/util.cs @@ -5654,6 +5654,314 @@ namespace AyaNovaQBI #endregion export to quickbooks + + + #region Workorder mismatch scanning + + public enum MisMatchReason + { + NotLinkedToQB = 0, + PriceDifferent = 1, + NothingToInvoice = 2 + + } + /// + /// Mismatch properties + /// A structure for storing mismatches identified + ///so user can resolve them. + /// + public class MisMatch + { + //public Guid WorkorderID; + public long ObjectId { get; set; } + public AyaType ObjectType { get; set; } + public string Name { get; set; } + public MisMatchReason mReason { get; set; } + public decimal AyaPrice { get; set; } + public decimal QBPrice { get; set; } + public long WorkorderItemPartId { get; set; } + public string QBListID { get; set; } + + } + + + /// + /// Given a workorder ID + /// scans the objects in the workorder + /// that need to be linked to QB for invoicing + /// and on any error found adds them to the + /// mismatched object array list + /// + /// Id of workorder being scanned + /// An list of mismatch objects + /// An list of id values of workorderitemparts that have been set by + /// user to forcibly use the price set on the workorderitem part even though + /// it differs from the quickbooks price + /// True if all links ok, false if there are any mismatches at all + public static bool ScanLinksOK(long WorkorderID, List MisMatches, List PriceOverrides) + { + bool bReturn = true; + bool bSomethingToInvoice = false; + + + Workorder w = Workorder.GetItem(WorkorderID); + + //Client ok? + if (!QBIntegration.Items.Any(z=>z.AType==AyaType.Customer && z.ObjectId==w.CustomerId)) + { + bReturn = false; + AddMisMatch(AyaClientList[w.ClientID].Name, w.ClientID, AyaType.Customer, MisMatchReason.NotLinkedToQB, MisMatches); + } + + //Service rates: + foreach (WorkorderItem wi in w.WorkorderItems) + { + #region Labor + foreach (WorkorderItemLabor wl in wi.Labors) + { + + //If there's *any* labor then there is something to invoice + bSomethingToInvoice = true; + + //Check that rate isn't actually guid.empty + //it's possible that some users have not selected a rate on the workorder + if (wl.ServiceRateID == Guid.Empty) + throw new System.ApplicationException("ERROR: Workorder " + w.WorkorderService.ServiceNumber.ToString() + " has a labor item with no rate selected\r\n" + + "This is a serious problem for QBI and needs to be rectified before QBI can be used.\r\n"); + + if (!QBI.Maps.Contains(wl.ServiceRateID)) + { + bReturn = false; + AddMisMatch(AyaRateList[wl.ServiceRateID].Name, wl.ServiceRateID, RootObjectTypes.Rate, MisMatchReason.NotLinkedToQB, MisMatches); + + } + + } + #endregion + + #region Travel + foreach (WorkorderItemTravel wt in wi.Travels) + { + //If there's *any* travel then there is something to invoice + bSomethingToInvoice = true; + + //Check that rate isn't actually guid.empty + //it's possible that some users have not selected a rate on the workorder + if (wt.TravelRateID == Guid.Empty) + throw new System.ApplicationException("ERROR: Workorder " + w.WorkorderService.ServiceNumber.ToString() + " has a travel item with no rate selected\r\n" + + "This is a serious problem for QBI and needs to be rectified before QBI can be used.\r\n"); + + if (!QBI.Maps.Contains(wt.TravelRateID)) + { + bReturn = false; + AddMisMatch(AyaRateList[wt.TravelRateID].Name, wt.TravelRateID, RootObjectTypes.Rate, MisMatchReason.NotLinkedToQB, MisMatches); + + } + + } + #endregion + + #region Parts + foreach (WorkorderItemPart wp in wi.Parts) + { + //If there's *any* parts then there is something to invoice + bSomethingToInvoice = true; + + //Changed: 14-Nov-2006 to check that linked item id exists in qb + if (!QBI.Maps.Contains(wp.PartID) || QBItems.Rows.Find(QBI.Maps[wp.PartID].ForeignID) == null) + { + bReturn = false; + //Changed: 21-June-2006 to use display formatted name + AddMisMatch(AyaPartList[wp.PartID].DisplayName(Util.GlobalSettings.DefaultPartDisplayFormat), wp.PartID, RootObjectTypes.Part, MisMatchReason.NotLinkedToQB, MisMatches); + + } + else + { + //check the price + if (!PriceOverrides.Contains(wp.ID)) + { + + decimal qbPrice = (decimal)QBItems.Rows.Find(QBI.Maps[wp.PartID].ForeignID)["Price"]; + + //------------DISCOUNT----------------- + string disco = ""; + //Added:20-July-2006 to incorporate discounts on parts into qb invoice + decimal charge; + + //Changed: 18-Nov-2006 CASE 158 + //this is all wrong, it was multiplying price by quantity to calculate charge when it shouldn't + //removed quanty * price in next line to just price + charge = decimal.Round(wp.Price, 2, MidpointRounding.AwayFromZero); + charge = charge - (decimal.Round(charge * wp.Discount, 2, MidpointRounding.AwayFromZero)); + if (wp.Discount != 0) + { + disco = " (Price " + wp.Price.ToString("c") + " discounted on workorder " + wp.Discount.ToString("p") + ") \r\n"; + } + + //----------------------------- + + //It's a match, let's see if the price matches as well + if (charge != qbPrice) + { + bReturn = false; + AddMisMatch("WO: " + w.WorkorderService.ServiceNumber.ToString() + disco + " Part: " + AyaPartList[wp.PartID].DisplayName(Util.GlobalSettings.DefaultPartDisplayFormat),//Changed: 21-June-2006 to use display formatted name + wp.PartID, RootObjectTypes.Part, MisMatchReason.PriceDifferent, MisMatches, qbPrice, charge, wp.ID, + QBI.Maps[wp.PartID].ForeignID); + + } + } + } + + } + #endregion + + #region Outside service charges + + if (wi.HasOutsideService) + { + if (wi.OutsideService.RepairPrice != 0 || wi.OutsideService.ShippingPrice != 0) + { + bSomethingToInvoice = true; + //there is something billable, just need to make sure + //that there is a QB charge defined for outside service + if (QDat.OutsideServiceChargeAs == null || + QDat.OutsideServiceChargeAs == "" || + !QBItems.Rows.Contains(QDat.OutsideServiceChargeAs)) + { + bReturn = false; + AddMisMatch("Outside service", Guid.Empty, RootObjectTypes.WorkorderItemOutsideService, MisMatchReason.NotLinkedToQB, MisMatches); + + } + } + } + #endregion + + #region Workorder item loan charges + + if (wi.HasLoans) + { + foreach (WorkorderItemLoan wil in wi.Loans) + { + + if (wil.Charges != 0) + { + //case 772 + bSomethingToInvoice = true; + + //there is something billable, just need to make sure + //that there is a QB charge defined for loaned item charges + if (QDat.WorkorderItemLoanChargeAs == null || + QDat.WorkorderItemLoanChargeAs == "" || + !QBItems.Rows.Contains(QDat.WorkorderItemLoanChargeAs)) + { + bReturn = false; + AddMisMatch("Workorder item loan", Guid.Empty, RootObjectTypes.WorkorderItemLoan, MisMatchReason.NotLinkedToQB, MisMatches); + break; + + } + } + } + } + #endregion + + #region Workorder item misc expenses + + if (wi.HasExpenses) + { + foreach (WorkorderItemMiscExpense wie in wi.Expenses) + { + + + if (wie.ChargeToClient) + { + bSomethingToInvoice = true; + //there is something billable, just need to make sure + //that there is a QB charge defined for misc expense item charges + if (QDat.MiscExpenseChargeAs == null || + QDat.MiscExpenseChargeAs == "" || + !QBItems.Rows.Contains(QDat.MiscExpenseChargeAs)) + { + bReturn = false; + AddMisMatch("Workorder item expense", Guid.Empty, RootObjectTypes.WorkorderItemMiscExpense, MisMatchReason.NotLinkedToQB, MisMatches); + break; + + } + } + } + } + #endregion + + + }//workorder items loop + + //If there are no mismatches so far, + //maybe it's because it's got nothing to invoice? + if (bReturn && !bSomethingToInvoice) + { + bReturn = false; + AddMisMatch("WO: " + w.WorkorderService.ServiceNumber.ToString() + " - Nothing chargeable on it, will not be invoiced", + Guid.Empty, RootObjectTypes.Nothing, MisMatchReason.NothingToInvoice, MisMatches); + + } + + return bReturn; + + + } + + + + private static void AddMisMatch(string Name, Guid RootObjectID, RootObjectTypes RootObjectType, MisMatchReason Reason, ArrayList Mismatches) + { + AddMisMatch(Name, RootObjectID, RootObjectType, Reason, Mismatches, 0m, 0m, Guid.Empty, ""); + } + + private static void AddMisMatch(string Name, Guid RootObjectID, RootObjectTypes RootObjectType, + MisMatchReason Reason, ArrayList Mismatches, decimal QBPrice, decimal AyaPrice, Guid WorkorderItemPartID, string QBListID) + { + bool bDuplicate = false; + //scan through list of existing mismatches, + //only add a not linked item if it's + //not there already + //other types of mismatches need to be added because + //they need to be resolved on a case by case basis or are unresolvable + if (Reason == MisMatchReason.NotLinkedToQB) + { + + foreach (object o in Mismatches) + { + MisMatch m = (MisMatch)o; + //Have to check ID and type here because for outside service + //and loans and misc expenses the id is always empty so type is + //the only way to differentiate + if (m.RootObjectID == RootObjectID && m.ObjectType == RootObjectType) + { + bDuplicate = true; + break; + } + } + } + + if (!bDuplicate) + { + MisMatch m = new MisMatch(); + m.mName = Name; + m.mRootObjectID = RootObjectID; + m.mObjectType = RootObjectType; + m.mReason = Reason; + m.mAyaPrice = AyaPrice; + m.mQBPrice = QBPrice; + m.mWorkorderItemPartID = WorkorderItemPartID; + m.mQBListID = QBListID; + Mismatches.Add(m); + + } + } + + + #endregion wo_mismatch_scan + + #endregion qbi stuff (anything not api) #region general utils diff --git a/docs/docs/img/main-menu-invoice-fix-problems-item.png b/docs/docs/img/main-menu-invoice-fix-problems-item.png new file mode 100644 index 0000000..9a931b2 Binary files /dev/null and b/docs/docs/img/main-menu-invoice-fix-problems-item.png differ diff --git a/docs/docs/img/main-menu-invoice-refresh-invoices-item.png b/docs/docs/img/main-menu-invoice-refresh-invoices-item.png new file mode 100644 index 0000000..e1a14a8 Binary files /dev/null and b/docs/docs/img/main-menu-invoice-refresh-invoices-item.png differ diff --git a/docs/docs/img/main-menu-invoice-selected-multiple-item.png b/docs/docs/img/main-menu-invoice-selected-multiple-item.png new file mode 100644 index 0000000..13c2b67 Binary files /dev/null and b/docs/docs/img/main-menu-invoice-selected-multiple-item.png differ diff --git a/docs/docs/img/main-menu-invoice-selected-one-item.png b/docs/docs/img/main-menu-invoice-selected-one-item.png new file mode 100644 index 0000000..8023804 Binary files /dev/null and b/docs/docs/img/main-menu-invoice-selected-one-item.png differ diff --git a/docs/docs/img/main-menu-tools-invoice-template-item.png b/docs/docs/img/main-menu-tools-invoice-template-item.png new file mode 100644 index 0000000..571225c Binary files /dev/null and b/docs/docs/img/main-menu-tools-invoice-template-item.png differ diff --git a/docs/docs/img/main-menu-tools-item.png b/docs/docs/img/main-menu-tools-item.png new file mode 100644 index 0000000..ab73f9b Binary files /dev/null and b/docs/docs/img/main-menu-tools-item.png differ diff --git a/docs/docs/img/main-menu-tools-link-and-sync-item.png b/docs/docs/img/main-menu-tools-link-and-sync-item.png new file mode 100644 index 0000000..b9e34c3 Binary files /dev/null and b/docs/docs/img/main-menu-tools-link-and-sync-item.png differ diff --git a/docs/docs/img/main-menu-tools-preferences-item.png b/docs/docs/img/main-menu-tools-preferences-item.png new file mode 100644 index 0000000..3b11efd Binary files /dev/null and b/docs/docs/img/main-menu-tools-preferences-item.png differ diff --git a/docs/docs/img/main-menu-tools-refresh-cached-data-item.png b/docs/docs/img/main-menu-tools-refresh-cached-data-item.png new file mode 100644 index 0000000..58d3190 Binary files /dev/null and b/docs/docs/img/main-menu-tools-refresh-cached-data-item.png differ