diff --git a/server/AyaNova/Controllers/PartAssemblyController.cs b/server/AyaNova/Controllers/PartAssemblyController.cs
index f9e280c1..92f7f2fb 100644
--- a/server/AyaNova/Controllers/PartAssemblyController.cs
+++ b/server/AyaNova/Controllers/PartAssemblyController.cs
@@ -8,16 +8,6 @@ using AyaNova.Models;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Biz;
-/*
-Ok, need to figure this one out.
-Is a partassembly posted with all it's collection of parts or are those two separate ops?
-It's obvs more efficient to post it all at once on creation
-From users' point of view it's all one object
-Maybe it should act like it's one big collection as the partassembly is unlikely to have a huge number of parts ever anyway
-
-so the todo is to make the assemblyitem part of a collection under assembly that gets CRUD together
-https://stackoverflow.com/questions/46517584/how-to-add-a-parent-record-with-its-children-records-in-ef-core#46615455
-*/
namespace AyaNova.Api.Controllers
{
@@ -107,7 +97,7 @@ namespace AyaNova.Api.Controllers
return StatusCode(403, new ApiNotAuthorizedResponse());
if (!ModelState.IsValid)
return BadRequest(new ApiErrorResponse(ModelState));
- var o = await biz.GetAsync(id,true,true);
+ var o = await biz.GetAsync(id, true, true);
if (o == null) return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
return Ok(ApiOkResponse.Response(o));
}
diff --git a/server/AyaNova/Controllers/PartController.cs b/server/AyaNova/Controllers/PartController.cs
index 1a0a7cc1..c9f984ec 100644
--- a/server/AyaNova/Controllers/PartController.cs
+++ b/server/AyaNova/Controllers/PartController.cs
@@ -204,6 +204,56 @@ namespace AyaNova.Api.Controllers
return Ok(ApiOkResponse.Response(o));
}
+ ///
+ /// Get stock levels for part
+ ///
+ ///
+ /// Array of part stock levels
+ [HttpGet("stock-levels/{id}")]
+ public async Task GetPartStockLevels([FromRoute] long id)
+ {
+ if (!serverState.IsOpen)
+ return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
+
+ if (!Authorized.HasReadFullRole(HttpContext.Items, AyaType.Part))
+ return StatusCode(403, new ApiNotAuthorizedResponse());
+ if (!ModelState.IsValid)
+ return BadRequest(new ApiErrorResponse(ModelState));
+ var o = await ct.PartStockLevel.AsNoTracking().Where(z => z.PartId == id).OrderBy(z => z.PartWarehouseId).ToListAsync();
+ return Ok(ApiOkResponse.Response(o));
+ }
+
+ ///
+ /// Put (update) stock levels for part
+ ///
+ /// array of part stock levels
+ ///PartId
+ ///
+ [HttpPut("stock-levels/{id}")]
+ public async Task PutPartStockLevels([FromRoute] long id, [FromBody] List partStockLevels)
+ {
+ if (!serverState.IsOpen)
+ return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
+ if (!ModelState.IsValid)
+ return BadRequest(new ApiErrorResponse(ModelState));
+ PartBiz biz = PartBiz.GetBiz(ct, HttpContext);
+ if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType))
+ return StatusCode(403, new ApiNotAuthorizedResponse());
+
+
+ var o = await biz.PutStockLevelsAsync(id, partStockLevels);
+ if (o == null)
+ {
+ if (biz.Errors.Exists(z => z.Code == ApiErrorCode.CONCURRENCY_CONFLICT))
+ return StatusCode(409, new ApiErrorResponse(biz.Errors));
+ else
+ return BadRequest(new ApiErrorResponse(biz.Errors));
+ }
+
+ return Ok(ApiOkResponse.Response(o));
+ }
+
+
//------------
diff --git a/server/AyaNova/biz/PartBiz.cs b/server/AyaNova/biz/PartBiz.cs
index 84bcabfa..ac8bff4e 100644
--- a/server/AyaNova/biz/PartBiz.cs
+++ b/server/AyaNova/biz/PartBiz.cs
@@ -232,6 +232,58 @@ namespace AyaNova.Biz
}
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////
+ //UPDATE STOCK LEVELS
+ //
+ internal async Task> PutStockLevelsAsync(long id, List putPartStockLevels)
+ {
+ //Fixup serials
+
+ int nAdded = 0;
+ int nRemoved = 0;
+ var ExistingStockLevels = await ct.PartStockLevel.Where(z => z.PartId == id).ToListAsync();
+
+ //Remove any that should not be there anymore
+ foreach (PartStockLevel existingPS in ExistingStockLevels)
+ {
+ if (!putPartStockLevels.Any(z => z.PartWarehouseId==existingPS.PartWarehouseId))
+ {
+ //no longer in the collection so ditch it
+ ct.PartStockLevel.Remove(existingPS);
+ nRemoved++;
+ }
+ }
+
+ //Add any new ones
+ foreach (PartStockLevel putPS in putPartStockLevels)
+ {
+ if (!ExistingStockLevels.Any(z => z.PartWarehouseId==putPS.PartWarehouseId))
+ {
+ ct.PartStockLevel.Add(new PartStockLevel() { PartWarehouseId = putPS.PartWarehouseId, PartId = id });
+ nAdded++;
+ }
+ }
+
+
+ try
+ {
+ await ct.SaveChangesAsync();
+ }
+ catch (DbUpdateConcurrencyException)
+ {
+ if (!await ExistsAsync(id))
+ AddError(ApiErrorCode.NOT_FOUND);
+ else
+ AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
+ return null;
+ }
+
+ await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, AyaEvent.Modified, $"LT:PartSerialNumbersAvailable change (+{nAdded}, -{nRemoved})"), ct);
+
+ return await ct.PartSerial.Where(z => z.PartId == id).OrderBy(z => z.Serial).Select(z => z.Serial).ToListAsync();
+ }
+
////////////////////////////////////////////////////////////////////////////////////////////////
//SEARCH
//
diff --git a/server/AyaNova/models/AyContext.cs b/server/AyaNova/models/AyContext.cs
index 3ef081b1..1aed71d4 100644
--- a/server/AyaNova/models/AyContext.cs
+++ b/server/AyaNova/models/AyContext.cs
@@ -42,6 +42,7 @@ namespace AyaNova.Models
public virtual DbSet Part { get; set; }
public virtual DbSet PartInventory { get; set; }
public virtual DbSet PartWarehouse { get; set; }
+ public virtual DbSet PartStockLevel { get; set; }
public virtual DbSet PartSerial { get; set; }
public virtual DbSet PartAssembly { get; set; }
public virtual DbSet PartAssemblyItem { get; set; }
diff --git a/server/AyaNova/models/PartStockLevel.cs b/server/AyaNova/models/PartStockLevel.cs
new file mode 100644
index 00000000..381bb6ef
--- /dev/null
+++ b/server/AyaNova/models/PartStockLevel.cs
@@ -0,0 +1,29 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Newtonsoft.Json;
+
+namespace AyaNova.Models
+{
+ //for specific parts only
+ //if not in this table then a part has no minimum level
+ //NOTE: called this specifically partstocklevel and not restock level because it could be expanded to include reorder, danger, maximum levels
+ //if we want to implement fully inventory management down the road (DTR)
+ /*
+ This article throws light upon the four major types of stock levels of inventory. The types are: 1. Minimum Level 2. Maximum Level 3. Danger Level 4. Average Stock Level.
+ https://www.yourarticlelibrary.com/material-management/inventory-control-material-management/4-major-types-of-stock-levels-of-inventory-with-formula/90394
+ */
+ public class PartStockLevel
+ {
+ public long Id { get; set; }
+ public uint Concurrency { get; set; }
+
+ [Required]
+ public long PartWarehouseId { get; set; }
+ [Required]
+ public long PartId { get; set; }
+ [Required]
+ public decimal MinimumQuantity { get; set; }
+
+ }//eoc
+
+}//eons
\ No newline at end of file
diff --git a/server/AyaNova/resource/de.json b/server/AyaNova/resource/de.json
index e671b0e6..3f7c95c5 100644
--- a/server/AyaNova/resource/de.json
+++ b/server/AyaNova/resource/de.json
@@ -2142,5 +2142,6 @@
"PartInventoryTransactionEntryDate":"Datum",
"PartInventoryTransactionSource":"Transaktionsquelle",
"PartInventoryTransactionQuantity":"Menge",
- "PartInventoryBalance":"Menge zur Hand"
+ "PartInventoryBalance":"Menge zur Hand",
+ "PartStockingLevels":"Mindestbestand an Teilen"
}
\ No newline at end of file
diff --git a/server/AyaNova/resource/en.json b/server/AyaNova/resource/en.json
index 89465865..85746ebd 100644
--- a/server/AyaNova/resource/en.json
+++ b/server/AyaNova/resource/en.json
@@ -2142,6 +2142,7 @@
"PartInventoryTransactionEntryDate":"Date",
"PartInventoryTransactionSource":"Transaction source",
"PartInventoryTransactionQuantity":"Quantity",
- "PartInventoryBalance":"On hand quantity"
+ "PartInventoryBalance":"On hand quantity",
+ "PartStockingLevels":"Part stocking levels"
}
\ No newline at end of file
diff --git a/server/AyaNova/resource/es.json b/server/AyaNova/resource/es.json
index faeb3d3f..88ff3ca7 100644
--- a/server/AyaNova/resource/es.json
+++ b/server/AyaNova/resource/es.json
@@ -2142,5 +2142,6 @@
"PartInventoryTransactionEntryDate":"Fecha",
"PartInventoryTransactionSource":"Origen de la transacción",
"PartInventoryTransactionQuantity":"Cantidad",
- "PartInventoryBalance":"Cantidad en mano"
+ "PartInventoryBalance":"Cantidad en mano",
+ "PartStockingLevels":"Niveles de existencias de piezas"
}
\ No newline at end of file
diff --git a/server/AyaNova/resource/fr.json b/server/AyaNova/resource/fr.json
index 50ee8a8d..4b240ab3 100644
--- a/server/AyaNova/resource/fr.json
+++ b/server/AyaNova/resource/fr.json
@@ -2142,5 +2142,6 @@
"PartInventoryTransactionEntryDate":"Date",
"PartInventoryTransactionSource":"Source de transaction",
"PartInventoryTransactionQuantity":"Quantité",
- "PartInventoryBalance":"Quantité disponible"
+ "PartInventoryBalance":"Quantité disponible",
+ "PartStockingLevels":"Niveaux de stockage des pièces"
}
\ No newline at end of file
diff --git a/server/AyaNova/util/AySchema.cs b/server/AyaNova/util/AySchema.cs
index 92bd295a..c6d91784 100644
--- a/server/AyaNova/util/AySchema.cs
+++ b/server/AyaNova/util/AySchema.cs
@@ -675,15 +675,15 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
//PARTSERIAL
await ExecQueryAsync("CREATE TABLE apartserial (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, serial TEXT NOT NULL, " +
- "partid BIGINT NOT NULL REFERENCES apart on delete cascade, CONSTRAINT unq_partserialpart UNIQUE (partid, serial) )");//ensure not duplicate partid/serial combo
+ "partid BIGINT NOT NULL REFERENCES apart ON DELETE CASCADE, CONSTRAINT unq_partserialpart UNIQUE (partid, serial) )");//ensure not duplicate partid/serial combo
//PARTASSEMBLY
await ExecQueryAsync("CREATE TABLE apartassembly (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL UNIQUE, active BOOL NOT NULL, " +
"notes TEXT, wiki TEXT, customfields TEXT, tags VARCHAR(255) ARRAY )");
//PARTASSEMBLYITEM
- await ExecQueryAsync("CREATE TABLE apartassemblyitem (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, partassemblyid BIGINT NOT NULL REFERENCES apartassembly on delete cascade, " +
- "partid BIGINT NOT NULL REFERENCES apart, quantity DECIMAL(19,4) NOT NULL default 1, " +
+ await ExecQueryAsync("CREATE TABLE apartassemblyitem (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, partassemblyid BIGINT NOT NULL REFERENCES apartassembly ON DELETE CASCADE, " +
+ "partid BIGINT NOT NULL REFERENCES apart ON DELETE CASCADE, quantity DECIMAL(19,4) NOT NULL default 1, " +
"CONSTRAINT unq_partassemblypart UNIQUE (partid, partassemblyid) " +//ensure no duplicate parts in the same assembly
")");
// await ExecQueryAsync("CREATE INDEX idx_apartassemblyitem_partid ON apartassemblyitem(partid)");
@@ -711,6 +711,13 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
//see what if any index tuning will help with a huge db of realistic data doing most common ops
//await ExecQueryAsync("CREATE INDEX idx_PartInventory_SourceId_SourceType ON apartinventory (sourceid, sourcetype);");
+ //PARTSTOCKLEVEL
+ await ExecQueryAsync("CREATE TABLE apartstocklevel (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, partwarehouseid BIGINT NOT NULL REFERENCES apartwarehouse ON DELETE CASCADE, " +
+ "partid BIGINT NOT NULL REFERENCES apart ON DELETE CASCADE, minimumquantity DECIMAL(19,4) NOT NULL default 1, " +
+ "CONSTRAINT unq_partstocklevel_part_warehouse UNIQUE (partid, partwarehouseid) " +//ensure no duplicates
+ ")");
+
+
//PROJECT
await ExecQueryAsync("CREATE TABLE aproject (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL UNIQUE, active BOOL NOT NULL, " +
"notes TEXT, wiki TEXT, customfields TEXT, tags VARCHAR(255) ARRAY, " +
@@ -856,7 +863,7 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
"deliveryaddress TEXT, linkreportid BIGINT NOT NULL, tags VARCHAR(255) ARRAY)");
await ExecQueryAsync("CREATE TABLE anotifyevent (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, created TIMESTAMP NOT NULL, " +
- "ayatype INTEGER NOT NULL, objectid BIGINT NOT NULL, name TEXT NOT NULL, eventtype INTEGER NOT NULL, notifysubscriptionid BIGINT NOT NULL REFERENCES anotifysubscription(id) on delete cascade, " +
+ "ayatype INTEGER NOT NULL, objectid BIGINT NOT NULL, name TEXT NOT NULL, eventtype INTEGER NOT NULL, notifysubscriptionid BIGINT NOT NULL REFERENCES anotifysubscription(id) ON DELETE CASCADE, " +
"userid BIGINT NOT NULL REFERENCES auser (id), eventdate TIMESTAMP NOT NULL, message TEXT)");
//these fields were in here but seem to not be required so commented out for now, see notifyevent model for deets but
//basically remove this comment once certain don't need these fields (close to release or after)
@@ -865,7 +872,7 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
await ExecQueryAsync("CREATE TABLE anotification (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, userid BIGINT NOT NULL REFERENCES auser (id), " +
"created TIMESTAMP NOT NULL, ayatype INTEGER NOT NULL, objectid BIGINT NOT NULL, name TEXT NOT NULL, eventtype INTEGER NOT NULL, " +
- "notifysubscriptionid BIGINT NOT NULL REFERENCES anotifysubscription(id) on delete cascade, message TEXT, fetched BOOL NOT NULL)");
+ "notifysubscriptionid BIGINT NOT NULL REFERENCES anotifysubscription(id) ON DELETE CASCADE, message TEXT, fetched BOOL NOT NULL)");
await ExecQueryAsync("CREATE TABLE anotifydeliverylog (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, processed TIMESTAMP NOT NULL, " +
"ayatype INTEGER NOT NULL, objectid BIGINT NOT NULL, eventtype INTEGER NOT NULL, notifysubscriptionid BIGINT NOT NULL, idvalue BIGINT NOT NULL, " +