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