From 95c41a297eb78d6ff9c48f06929fd82a4b03309c Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Wed, 25 Aug 2021 19:20:25 +0000 Subject: [PATCH] --- .vscode/launch.json | 2 +- .../Controllers/PurchaseOrderController.cs | 2 +- .../AyaNova/DataList/PartInventoryDataList.cs | 122 ++++++++++++------ server/AyaNova/biz/AyaType.cs | 5 +- server/AyaNova/biz/BizObjectFactory.cs | 2 + server/AyaNova/biz/BizRoles.cs | 16 +++ .../AyaNova/biz/PartInventoryDataListBiz.cs | 106 +++++++++++++++ server/AyaNova/models/AyContext.cs | 1 + server/AyaNova/models/VPartInventoryList.cs | 40 ++++++ server/AyaNova/util/AySchema.cs | 4 +- 10 files changed, 252 insertions(+), 48 deletions(-) create mode 100644 server/AyaNova/biz/PartInventoryDataListBiz.cs create mode 100644 server/AyaNova/models/VPartInventoryList.cs diff --git a/.vscode/launch.json b/.vscode/launch.json index fcc80a75..b77f3b95 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -53,7 +53,7 @@ "AYANOVA_FOLDER_USER_FILES": "c:\\temp\\RavenTestData\\userfiles", "AYANOVA_FOLDER_BACKUP_FILES": "c:\\temp\\RavenTestData\\backupfiles", "AYANOVA_FOLDER_TEMPORARY_SERVER_FILES": "c:\\temp\\RavenTestData\\tempfiles", - "AYANOVA_SERVER_TEST_MODE": "true", + "AYANOVA_SERVER_TEST_MODE": "false", "AYANOVA_SERVER_TEST_MODE_SEEDLEVEL": "small", "AYANOVA_SERVER_TEST_MODE_TZ_OFFSET": "-7", "AYANOVA_BACKUP_PG_DUMP_PATH": "C:\\data\\code\\postgres_13\\bin\\" diff --git a/server/AyaNova/Controllers/PurchaseOrderController.cs b/server/AyaNova/Controllers/PurchaseOrderController.cs index e15b0a97..8df8f943 100644 --- a/server/AyaNova/Controllers/PurchaseOrderController.cs +++ b/server/AyaNova/Controllers/PurchaseOrderController.cs @@ -156,7 +156,7 @@ namespace AyaNova.Api.Controllers /// optional vendor id will return matches to Part objects manufacturer, wholesaler or alternative wholesaler /// PurchaseOrder [HttpGet("restock-by-vendor/{vendorId}")] - public async Task GetPurchaseOrder([FromRoute] long? vendorId) + public async Task GetRestockByVendor([FromRoute] long? vendorId) { if (!serverState.IsOpen) return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason)); diff --git a/server/AyaNova/DataList/PartInventoryDataList.cs b/server/AyaNova/DataList/PartInventoryDataList.cs index c446046f..fa9f6673 100644 --- a/server/AyaNova/DataList/PartInventoryDataList.cs +++ b/server/AyaNova/DataList/PartInventoryDataList.cs @@ -9,38 +9,14 @@ namespace AyaNova.DataList { public PartInventoryDataList() { - DefaultListAType = AyaType.PartInventory; + DefaultListAType = AyaType.PartInventoryDataList; SQLFrom = "from vpartinventorylist "; -/* -"CREATE VIEW vpartinventorylist AS select apart.partnumber,apartwarehouse.name as whsname,vpartinventorynow.*,vpartsonorder.quantityonorder," -+"vpartsonordercommitted.quantityonordercommitted,apart.name as prtname, apart.active, apart.cost, apartstocklevel.minimumquantity,apart.retail, aws.name AS whslrname, aaws.name AS altwhslrname, " -+"GREATEST( COALESCE(apartstocklevel.minimumquantity, 0) - (COALESCE(vpartinventorynow.balance, 0) + COALESCE(vpartsonorder.quantityonorder, 0) - " -+"COALESCE(vpartsonordercommitted.quantityonordercommitted, 0)) ,0) AS reorderquantity FROM vpartinventorynow LEFT JOIN vpartsonordercommitted ON " -+"vpartinventorynow.partid = vpartsonordercommitted.partid AND vpartinventorynow.partwarehouseid = vpartsonordercommitted.partwarehouseid " -+"LEFT JOIN vpartsonorder ON vpartinventorynow.partid = vpartsonorder.partid AND vpartinventorynow.partwarehouseid = vpartsonorder.partwarehouseid " -+"LEFT JOIN apart ON (vpartinventorynow.partid = apart.id) LEFT JOIN apartwarehouse ON (vpartinventorynow.partwarehouseid = apartwarehouse.id) " -+"left join avendor AS aws on (apart.wholesalerid = aws.id) left join avendor AS aaws on (apart.alternativewholesalerid = aaws.id) " -+"left join apartstocklevel on (apartstocklevel.partid = apart.id AND apartstocklevel.partwarehouseid = vpartinventorynow.partwarehouseid);" - - - - -await ExecQueryAsync("CREATE VIEW vpartinventorylist AS select vpartinventorynow.*, vpartsonordercommitted.quantityonordercommitted, vpartsonorder.quantityonorder from vpartinventorynow " -+ "left join vpartsonordercommitted on (vpartinventorynow.partid = vpartsonordercommitted.partid and vpartinventorynow.partwarehouseid = vpartsonordercommitted.partwarehouseid)" -+ "left join vpartsonorder on (vpartinventorynow.partid = vpartsonorder.partid and vpartinventorynow.partwarehouseid = vpartsonorder.partwarehouseid)"); - -*/ - -//NEEDED COLUMNS -/* -partid, partnumber, partname, partactive, partcost, partretail, partwarehouseid, partwarehousename,wholesalername,wholesalerid,altwholesalername,altwholesalerid, -onhandqty, onorderqty, onordercommittedqty, restocklevel/minimumqty, reorderqty, vpartinventorynow.description, vpartinventorynow.id -*/ + var RoleSet = BizRoles.GetRoleSet(DefaultListAType); AllowedRoles = RoleSet.ReadFullRecord | RoleSet.Change; - DefaultColumns = new List() { "PartPartNumber", "PartWarehouseName", "PartInventoryBalance", "PartByWarehouseInventoryQuantityOnOrder", "PartByWarehouseInventoryQtyOnOrderCommitted" }; + DefaultColumns = new List() { "PartPartNumber", "PartWarehouseName", "PartInventoryBalance", "PartByWarehouseInventoryMinStockLevel","PartByWarehouseInventoryReorderQuantity","PartByWarehouseInventoryQuantityOnOrder", "PartByWarehouseInventoryQtyOnOrderCommitted", "Active" }; DefaultSortBy = new Dictionary() { { "PartPartNumber", "+" }, { "PartWarehouseName", "+" } }; FieldDefinitions = new List(); @@ -50,8 +26,8 @@ onhandqty, onorderqty, onordercommittedqty, restocklevel/minimumqty, reorderqty, FieldKey = "PartPartNumber", AType = (int)AyaType.Part, UiFieldDataType = (int)UiFieldDataType.Text, - SqlIdColumnName = "apart.id", - SqlValueColumnName = "apart.partnumber" + SqlIdColumnName = "partid", + SqlValueColumnName = "partnumber" }); FieldDefinitions.Add(new DataListFieldDefinition @@ -60,8 +36,52 @@ onhandqty, onorderqty, onordercommittedqty, restocklevel/minimumqty, reorderqty, FieldKey = "PartName", AType = (int)AyaType.Part, UiFieldDataType = (int)UiFieldDataType.Text, - SqlIdColumnName = "apart.id", - SqlValueColumnName = "vpartinventorylist.vpartinventorylist" + SqlIdColumnName = "partid", + SqlValueColumnName = "partname" + }); + + FieldDefinitions.Add(new DataListFieldDefinition + { + TKey = "Active", + FieldKey = "Active", + UiFieldDataType = (int)UiFieldDataType.Bool, + SqlValueColumnName = "partactive" + }); + + FieldDefinitions.Add(new DataListFieldDefinition + { + TKey = "PartCost", + FieldKey = "PartCost", + UiFieldDataType = (int)UiFieldDataType.Currency, + SqlValueColumnName = "partcost" + }); + + FieldDefinitions.Add(new DataListFieldDefinition + { + TKey = "PartRetail", + FieldKey = "PartRetail", + UiFieldDataType = (int)UiFieldDataType.Currency, + SqlValueColumnName = "partretail" + }); + + FieldDefinitions.Add(new DataListFieldDefinition + { + FieldKey = "PartWholesalerID", + TKey = "PartWholesalerID", + UiFieldDataType = (int)UiFieldDataType.Text, + AType = (int)AyaType.Vendor, + SqlIdColumnName = "wholesalerid", + SqlValueColumnName = "wholesalername" + }); + + FieldDefinitions.Add(new DataListFieldDefinition + { + FieldKey = "PartAlternativeWholesalerID", + TKey = "PartAlternativeWholesalerID", + UiFieldDataType = (int)UiFieldDataType.Text, + AType = (int)AyaType.Vendor, + SqlIdColumnName = "altwholesalerid", + SqlValueColumnName = "altwholesalername" }); FieldDefinitions.Add(new DataListFieldDefinition @@ -70,8 +90,8 @@ onhandqty, onorderqty, onordercommittedqty, restocklevel/minimumqty, reorderqty, FieldKey = "PartWarehouseName", AType = (int)AyaType.PartWarehouse, UiFieldDataType = (int)UiFieldDataType.Text, - SqlIdColumnName = "apartwarehouse.id", - SqlValueColumnName = "apartwarehouse.name" + SqlIdColumnName = "partwarehouseid", + SqlValueColumnName = "partwarehousename" }); FieldDefinitions.Add(new DataListFieldDefinition @@ -80,8 +100,8 @@ onhandqty, onorderqty, onordercommittedqty, restocklevel/minimumqty, reorderqty, FieldKey = "PartInventoryTransactionDescription", AType = (int)AyaType.PartInventory, UiFieldDataType = (int)UiFieldDataType.Text, - SqlIdColumnName = "vpartinventorylist.id",//NEW: vpartinventorynow.id is actually apartinventory.id required for reporting purposes - SqlValueColumnName = "vpartinventorylist.description",//NEW: vpartinventorynow.description which is actually apartinventory.description + SqlIdColumnName = "partinventoryid", + SqlValueColumnName = "partinventorydescription", IsMeta = true,//only so it doesn't show in the UI but is required for report IsRowId = true }); @@ -91,7 +111,7 @@ onhandqty, onorderqty, onordercommittedqty, restocklevel/minimumqty, reorderqty, TKey = "PartInventoryBalance", FieldKey = "PartInventoryBalance", UiFieldDataType = (int)UiFieldDataType.Decimal, - SqlValueColumnName = "vpartinventorylist.balance" + SqlValueColumnName = "onhandqty" }); @@ -100,7 +120,7 @@ onhandqty, onorderqty, onordercommittedqty, restocklevel/minimumqty, reorderqty, TKey = "PartByWarehouseInventoryQuantityOnOrder", FieldKey = "PartByWarehouseInventoryQuantityOnOrder", UiFieldDataType = (int)UiFieldDataType.Decimal, - SqlValueColumnName = "vpartinventorylist.quantityonorder" + SqlValueColumnName = "onorderqty" }); FieldDefinitions.Add(new DataListFieldDefinition @@ -108,7 +128,25 @@ onhandqty, onorderqty, onordercommittedqty, restocklevel/minimumqty, reorderqty, TKey = "PartByWarehouseInventoryQtyOnOrderCommitted", FieldKey = "PartByWarehouseInventoryQtyOnOrderCommitted", UiFieldDataType = (int)UiFieldDataType.Decimal, - SqlValueColumnName = "vpartinventorylist.quantityonordercommitted" + SqlValueColumnName = "onordercommittedqty" + }); + + + FieldDefinitions.Add(new DataListFieldDefinition + { + TKey = "PartByWarehouseInventoryMinStockLevel", + FieldKey = "PartByWarehouseInventoryMinStockLevel", + UiFieldDataType = (int)UiFieldDataType.Decimal, + SqlValueColumnName = "restockminqty" + }); + + + FieldDefinitions.Add(new DataListFieldDefinition + { + TKey = "PartByWarehouseInventoryReorderQuantity", + FieldKey = "PartByWarehouseInventoryReorderQuantity", + UiFieldDataType = (int)UiFieldDataType.Decimal, + SqlValueColumnName = "reorderqty" }); @@ -118,8 +156,8 @@ onhandqty, onorderqty, onordercommittedqty, restocklevel/minimumqty, reorderqty, FieldKey = "metapartnumber", UiFieldDataType = (int)UiFieldDataType.Text, - SqlIdColumnName = "apart.id", - SqlValueColumnName = "apart.partnumber", + SqlIdColumnName = "partid", + SqlValueColumnName = "partnumber", IsMeta = true }); @@ -128,8 +166,8 @@ onhandqty, onorderqty, onordercommittedqty, restocklevel/minimumqty, reorderqty, { FieldKey = "metawarehouse", UiFieldDataType = (int)UiFieldDataType.Text, - SqlIdColumnName = "apartwarehouse.id", - SqlValueColumnName = "apartwarehouse.name", + SqlIdColumnName = "partwarehouseid", + SqlValueColumnName = "partwarehousename", IsMeta = true }); diff --git a/server/AyaNova/biz/AyaType.cs b/server/AyaNova/biz/AyaType.cs index 8982eb27..6abe9860 100644 --- a/server/AyaNova/biz/AyaType.cs +++ b/server/AyaNova/biz/AyaType.cs @@ -136,7 +136,7 @@ namespace AyaNova.Biz PartInventory = 67, DataListColumnView = 68, PartInventoryRestock = 69,//for list only, synthetic object - PartInventoryRequest = 70,//for list only not, synthetic object + PartInventoryRequest = 70,//for list only, synthetic object WorkOrderStatus = 71, TaskGroup = 72, WorkOrderItemOutsideService = 73, @@ -166,7 +166,8 @@ namespace AyaNova.Biz PMItemTravel = 87, [CoreBizObject] PMItemUnit = 88, - PMItemOutsideService = 89 + PMItemOutsideService = 89, + PartInventoryDataList = 90//for list/reporting only, synthetic object diff --git a/server/AyaNova/biz/BizObjectFactory.cs b/server/AyaNova/biz/BizObjectFactory.cs index 43f0ef6c..bd293099 100644 --- a/server/AyaNova/biz/BizObjectFactory.cs +++ b/server/AyaNova/biz/BizObjectFactory.cs @@ -61,6 +61,8 @@ namespace AyaNova.Biz return new PartAssemblyBiz(ct, userId, translationId, roles); case AyaType.PartInventory: return new PartInventoryBiz(ct, userId, translationId, roles); + case AyaType.PartInventoryDataList: + return new PartInventoryDataListBiz(ct, userId, translationId, roles); case AyaType.Project: diff --git a/server/AyaNova/biz/BizRoles.cs b/server/AyaNova/biz/BizRoles.cs index 9a04dc5c..fb3f5d12 100644 --- a/server/AyaNova/biz/BizRoles.cs +++ b/server/AyaNova/biz/BizRoles.cs @@ -241,6 +241,22 @@ namespace AyaNova.Biz Select = AuthorizationRoles.All }); + ///////////////////////////////////////////////////////////////// + //PartInventoryDataList + // same as PO + // + roles.Add(AyaType.PartInventoryDataList, new BizRoleSet() + { + Change = AuthorizationRoles.Inventory + | AuthorizationRoles.BizAdmin + | AuthorizationRoles.Accounting, + ReadFullRecord = AuthorizationRoles.Service + | AuthorizationRoles.InventoryRestricted + | AuthorizationRoles.BizAdminRestricted + | AuthorizationRoles.ServiceRestricted, + Select = AuthorizationRoles.All + }); + //////////////////////////////////////////////////////////// diff --git a/server/AyaNova/biz/PartInventoryDataListBiz.cs b/server/AyaNova/biz/PartInventoryDataListBiz.cs new file mode 100644 index 00000000..9a1c20be --- /dev/null +++ b/server/AyaNova/biz/PartInventoryDataListBiz.cs @@ -0,0 +1,106 @@ +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using System.Linq; +using AyaNova.Util; +using AyaNova.Api.ControllerHelpers; +using AyaNova.Models; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; + +namespace AyaNova.Biz +{ + internal class PartInventoryDataListBiz : BizObject, IReportAbleObject, IExportAbleObject + { + internal PartInventoryDataListBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles) + { + ct = dbcontext; + UserId = currentUserId; + UserTranslationId = userTranslationId; + CurrentUserRoles = UserRoles; + BizType = AyaType.PartInventoryDataList; + } + + internal static PartInventoryDataListBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null) + { + if (httpContext != null) + return new PartInventoryDataListBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items)); + else + return new PartInventoryDataListBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin); + } + + + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //REPORTING + // + public async Task GetReportData(DataListSelectedRequest dataListSelectedRequest) + { + var idList = dataListSelectedRequest.SelectedRowIds; + JArray ReportData = new JArray(); + while (idList.Any()) + { + var batch = idList.Take(IReportAbleObject.REPORT_DATA_BATCH_SIZE); + idList = idList.Skip(IReportAbleObject.REPORT_DATA_BATCH_SIZE).ToArray(); + //query for this batch, comes back in db natural order unfortunately + var batchResults = await ct.VPartInventoryList.AsNoTracking().Where(z => batch.Contains(z.Id)).ToArrayAsync(); + //order the results back into original + var orderedList = from id in batch join z in batchResults on id equals z.Id select z; + + //cache frequent viz data + var AyaTypesEnumList = await AyaNova.Api.Controllers.EnumListController.GetEnumList( + StringUtil.TrimTypeName(typeof(AyaType).ToString()), + UserTranslationId, + CurrentUserRoles); + using (var command = ct.Database.GetDbConnection().CreateCommand()) + { + ct.Database.OpenConnection(); + + foreach (PartInventory w in orderedList) + { + await PopulateVizFields(w, AyaTypesEnumList, command); + var jo = JObject.FromObject(w); + if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"])) + jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]); + ReportData.Add(jo); + } + } + } + return ReportData; + } + + //populate viz fields from provided object + private async Task PopulateVizFields(PartInventory o, List ayaTypesEnumList, System.Data.Common.DbCommand cmd) + { + o.PartViz = await ct.Part.AsNoTracking().Where(x => x.Id == o.PartId).Select(x => x.PartNumber).FirstOrDefaultAsync(); + o.PartWarehouseViz = await ct.PartWarehouse.AsNoTracking().Where(x => x.Id == o.PartWarehouseId).Select(x => x.Name).FirstOrDefaultAsync(); + if (o.SourceType != null) + o.SourceTypeViz = ayaTypesEnumList.Where(x => x.Id == (long)o.SourceType).Select(x => x.Name).First(); + if (o.SourceType != null && o.SourceId != null) + o.SourceViz = BizObjectNameFetcherDirect.Name((AyaType)o.SourceType, (long)o.SourceId, cmd); + + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + // IMPORT EXPORT + // + public async Task GetExportData(DataListSelectedRequest dataListSelectedRequest) + { + //for now just re-use the report data code + //this may turn out to be the pattern for most biz object types but keeping it seperate allows for custom usage from time to time + return await GetReportData(dataListSelectedRequest); + } + + + + + + + ///////////////////////////////////////////////////////////////////// + + }//eoc + + +}//eons + diff --git a/server/AyaNova/models/AyContext.cs b/server/AyaNova/models/AyContext.cs index 06636b91..24a294df 100644 --- a/server/AyaNova/models/AyContext.cs +++ b/server/AyaNova/models/AyContext.cs @@ -122,6 +122,7 @@ namespace AyaNova.Models // public virtual DbSet ServiceBank { get; set; } public virtual DbSet ViewRestockRequired { get; set; } + public virtual DbSet VPartInventoryList { get; set; } diff --git a/server/AyaNova/models/VPartInventoryList.cs b/server/AyaNova/models/VPartInventoryList.cs new file mode 100644 index 00000000..7c21ac9a --- /dev/null +++ b/server/AyaNova/models/VPartInventoryList.cs @@ -0,0 +1,40 @@ +namespace AyaNova.Models +{ + //Note this is how to define a View backed model with no key (id) + + [Microsoft.EntityFrameworkCore.Keyless] + public class VPartInventoryList + { + public long PartId { get; set; } + public string PartNumber { get; set; } + public string PartName { get; set; } + public bool PartActive { get; set; } + public decimal PartCost { get; set; } + public decimal PartRetail { get; set; } + public long PartWarehouseId { get; set; } + public string PartWarehouseName { get; set; } + public long WholesalerId { get; set; } + public string WholesalerName { get; set; } + public long AltWholesalerId { get; set; } + public string AltWholesalerName { get; set; } + public decimal OnHandQty { get; set; } + public decimal OnOrderQty { get; set; } + public decimal OnOrderCommittedQty { get; set; } + public decimal RestockMinQty { get; set; } + public decimal ReOrderQty { get; set; } + public long PartInventoryId { get; set; } + public string PartInventoryDescription { get; set; } + + + }//eoc + +}//eons + +/* +vpartinventorylist AS select apart.id as partid, apart.partnumber, apart.name as partname, apart.active as partactive, apart.cost as partcost, apart.retail as partretail," ++"apartwarehouse.id as partwarehouseid, apartwarehouse.name as partwarehousename, awholesaler.name as wholesalername, awholesaler.id as wholesalerid, " ++"aaltwholesaler.id as altwholesalerid, aaltwholesaler.name as altwholesalername, vpartinventorynow.balance as onhandqty,vpartsonorder.quantityonorder as onorderqty, " ++"vpartsonordercommitted.quantityonordercommitted as onordercommittedqty,apartstocklevel.minimumquantity as restockminqty, " ++"GREATEST( COALESCE(apartstocklevel.minimumquantity, 0) - (COALESCE(vpartinventorynow.balance, 0) + COALESCE(vpartsonorder.quantityonorder, 0) - COALESCE(vpartsonordercommitted.quantityonordercommitted, 0)) ,0) AS reorderqty," ++"vpartinventorynow.id as partinventoryid, vpartinventorynow.description as partinventorydescription " +*/ \ No newline at end of file diff --git a/server/AyaNova/util/AySchema.cs b/server/AyaNova/util/AySchema.cs index 19fce594..a093f01d 100644 --- a/server/AyaNova/util/AySchema.cs +++ b/server/AyaNova/util/AySchema.cs @@ -904,8 +904,8 @@ $BODY$ LANGUAGE PLPGSQL STABLE"); await ExecQueryAsync( "CREATE VIEW vpartinventorylist AS select apart.id as partid, apart.partnumber, apart.name as partname, apart.active as partactive, apart.cost as partcost, apart.retail as partretail," +"apartwarehouse.id as partwarehouseid, apartwarehouse.name as partwarehousename, awholesaler.name as wholesalername, awholesaler.id as wholesalerid, " - +"aaltwholesaler.id as altwholesalerid, aaltwholesaler.name as altwholesalername, vpartinventorynow.balance as onhandqty,vpartsonorder.quantityonorder as onorderqty, " - +"vpartsonordercommitted.quantityonordercommitted as onordercommittedqty,apartstocklevel.minimumquantity as restockminqty, " + +"aaltwholesaler.id as altwholesalerid, aaltwholesaler.name as altwholesalername, vpartinventorynow.balance as onhandqty,COALESCE(vpartsonorder.quantityonorder,0) as onorderqty, " + +"COALESCE(vpartsonordercommitted.quantityonordercommitted,0) as onordercommittedqty,COALESCE(apartstocklevel.minimumquantity,0) as restockminqty, " +"GREATEST( COALESCE(apartstocklevel.minimumquantity, 0) - (COALESCE(vpartinventorynow.balance, 0) + COALESCE(vpartsonorder.quantityonorder, 0) - COALESCE(vpartsonordercommitted.quantityonordercommitted, 0)) ,0) AS reorderqty," +"vpartinventorynow.id as partinventoryid, vpartinventorynow.description as partinventorydescription " +"FROM vpartinventorynow LEFT JOIN vpartsonordercommitted ON "