This commit is contained in:
2021-01-25 19:52:22 +00:00
parent 936452cdb4
commit 887dbe5dd8
10 changed files with 153 additions and 20 deletions

View File

@@ -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));
}

View File

@@ -204,6 +204,56 @@ namespace AyaNova.Api.Controllers
return Ok(ApiOkResponse.Response(o));
}
/// <summary>
/// Get stock levels for part
/// </summary>
/// <param name="id"></param>
/// <returns>Array of part stock levels</returns>
[HttpGet("stock-levels/{id}")]
public async Task<IActionResult> 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));
}
/// <summary>
/// Put (update) stock levels for part
/// </summary>
/// <param name="partStockLevels">array of part stock levels</param>
///<param name="id">PartId</param>
/// <returns></returns>
[HttpPut("stock-levels/{id}")]
public async Task<IActionResult> PutPartStockLevels([FromRoute] long id, [FromBody] List<PartStockLevel> 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));
}
//------------

View File

@@ -232,6 +232,58 @@ namespace AyaNova.Biz
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE STOCK LEVELS
//
internal async Task<List<String>> PutStockLevelsAsync(long id, List<PartStockLevel> 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
//

View File

@@ -42,6 +42,7 @@ namespace AyaNova.Models
public virtual DbSet<Part> Part { get; set; }
public virtual DbSet<PartInventory> PartInventory { get; set; }
public virtual DbSet<PartWarehouse> PartWarehouse { get; set; }
public virtual DbSet<PartStockLevel> PartStockLevel { get; set; }
public virtual DbSet<PartSerial> PartSerial { get; set; }
public virtual DbSet<PartAssembly> PartAssembly { get; set; }
public virtual DbSet<PartAssemblyItem> PartAssemblyItem { get; set; }

View File

@@ -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

View File

@@ -2142,5 +2142,6 @@
"PartInventoryTransactionEntryDate":"Datum",
"PartInventoryTransactionSource":"Transaktionsquelle",
"PartInventoryTransactionQuantity":"Menge",
"PartInventoryBalance":"Menge zur Hand"
"PartInventoryBalance":"Menge zur Hand",
"PartStockingLevels":"Mindestbestand an Teilen"
}

View File

@@ -2142,6 +2142,7 @@
"PartInventoryTransactionEntryDate":"Date",
"PartInventoryTransactionSource":"Transaction source",
"PartInventoryTransactionQuantity":"Quantity",
"PartInventoryBalance":"On hand quantity"
"PartInventoryBalance":"On hand quantity",
"PartStockingLevels":"Part stocking levels"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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, " +