Inventory and Type reports

This commit is contained in:
2016-09-22 14:14:12 -04:00
parent 02555eba7e
commit 9f50a4635c
19 changed files with 426 additions and 191 deletions
@@ -0,0 +1,16 @@
using Heroic.AutoMapper;
using InventoryTraker.Web.Core;
using NUnit.Framework;
namespace InventoryTraker.Web.Tests
{
[SetUpFixture]
public class Setup
{
[OneTimeSetUp]
public void S()
{
HeroicAutoMapperConfigurator.LoadMapsFromAssemblyContainingTypeAndReferencedAssemblies<Inventory>();
}
}
}
@@ -63,6 +63,8 @@
<Compile Include="App_Start\AutoMapperConfig.cs" />
<Compile Include="Models\InventoryAddForm.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="App_Start\Setup.cs" />
<Compile Include="Utilities\InventoryReportWriterTests.cs" />
<Compile Include="Utilities\DistributionReportWriterTests.cs" />
<Compile Include="Utilities\MovementReportWriterTests.cs" />
<Compile Include="Utilities\InventoryTypeParserTests.cs" />
@@ -11,12 +11,6 @@ namespace InventoryTraker.Web.Tests.Utilities
[TestFixture]
public class DistributionReportWriterTests
{
[OneTimeSetUp]
public void StartUp()
{
HeroicAutoMapperConfigurator.LoadMapsFromAssemblyContainingTypeAndReferencedAssemblies<Inventory>();
}
private readonly DistributionReport[] _distributionReports =
{
new DistributionReport
@@ -0,0 +1,43 @@
using System;
using System.IO;
using Heroic.AutoMapper;
using InventoryTraker.Web.Core;
using InventoryTraker.Web.Models;
using InventoryTraker.Web.Utilities;
using NUnit.Framework;
namespace InventoryTraker.Web.Tests.Utilities
{
[TestFixture]
public class InventoryReportWriterTests
{
private readonly InventoryViewModel[] _inventory =
{
new InventoryViewModel
{
Name = "Beans",
ContainerType = "#300 cans",
UnitsPerCase = 24,
AddedDate = new DateTime(2015,3,1),
ExpirationDate = new DateTime(2017,4,1),
Quantity = 20,
Memo = "my memo",
PricePerCase = 12.12M,
WeightPerCase = 20.1,
}
};
[Test, Explicit]
public void Write()
{
using
(var outputFile
= new StreamWriter(Path.Combine(@"c:\temp", "InventoryReport.xlsx")))
{
var writer = new InventoryReportWriter();
writer.WriteStream(_inventory, outputFile.BaseStream);
}
}
}
}
@@ -11,12 +11,6 @@ namespace InventoryTraker.Web.Tests.Utilities
[TestFixture]
public class MovementReportWriterTests
{
[OneTimeSetUp]
public void StartUp()
{
HeroicAutoMapperConfigurator.LoadMapsFromAssemblyContainingTypeAndReferencedAssemblies<Inventory>();
}
private readonly MovementReport _movementReport
= new MovementReport
{
@@ -8,6 +8,7 @@ using InventoryTraker.Web.Attributes;
using InventoryTraker.Web.Core;
using InventoryTraker.Web.Data;
using InventoryTraker.Web.Models;
using InventoryTraker.Web.Utilities;
namespace InventoryTraker.Web.Controllers
{
@@ -28,7 +29,7 @@ namespace InventoryTraker.Web.Controllers
public JsonResult All()
{
var viewModels =
AllInventory()
CurrentInventory()
.ProjectTo<InventoryViewModel>()
.ToArray();
@@ -42,7 +43,7 @@ namespace InventoryTraker.Web.Controllers
return BetterJson(viewModel);
}
private IQueryable<Inventory> AllInventory()
private IQueryable<Inventory> CurrentInventory()
{
return _context
.Inventories
@@ -50,6 +51,26 @@ namespace InventoryTraker.Web.Controllers
.OrderBy(x => x.InventoryType.Name);
}
public ActionResult Export()
{
var writer = new InventoryReportWriter();
var viewModels =
CurrentInventory()
.ProjectTo<InventoryViewModel>()
.ToArray();
var excel = writer.Write(viewModels);
var filename = $"Inventory{DateTime.Today:yyyyMMdd}.xlsx";
return
new FileContentResult(excel, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
{
FileDownloadName = filename
};
}
[ActionLog]
public JsonResult Add(InventoryAddForm form)
{
@@ -134,7 +155,7 @@ namespace InventoryTraker.Web.Controllers
_context.SaveChanges();
return BetterJson(AllInventory()
return BetterJson(CurrentInventory()
.ProjectTo<InventoryViewModel>()
.ToArray());
}
@@ -7,6 +7,7 @@ using AutoMapper.QueryableExtensions;
using InventoryTraker.Web.Core;
using InventoryTraker.Web.Data;
using InventoryTraker.Web.Models;
using InventoryTraker.Web.Utilities;
namespace InventoryTraker.Web.Controllers
{
@@ -33,6 +34,27 @@ namespace InventoryTraker.Web.Controllers
return BetterJson(viewModels.ToArray());
}
public ActionResult Export()
{
var writer = new InventoryTypeReportWriter();
var viewModels =
_context.InventoryTypes
.OrderByDescending(x => x.Name)
.ProjectTo<InventoryTypeViewModel>()
.ToArray();
var excel = writer.Write(viewModels);
var filename = $"CommodityTypes{DateTime.Today:yyyyMMdd}.xlsx";
return
new FileContentResult(excel, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
{
FileDownloadName = filename
};
}
public JsonResult Add(InventoryTypeViewModel form)
{
if (!ModelState.IsValid)
@@ -1,23 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using InventoryTraker.Web.Core;
using InventoryTraker.Web.Data;
using InventoryTraker.Web.Models;
using InventoryTraker.Web.Services;
using InventoryTraker.Web.Utilities;
namespace InventoryTraker.Web.Controllers
{
public class ReportController : ControllerBase
{
private readonly AppDbContext _context;
private readonly ReportService _reportService;
public ReportController(AppDbContext context)
public ReportController(ReportService reportService)
{
_context = context;
_reportService = reportService;
}
[HttpGet]
@@ -28,14 +23,14 @@ namespace InventoryTraker.Web.Controllers
public ActionResult Distribution(DateTime startDate, DateTime endDate)
{
var report = GetDistributionReport(startDate, endDate);
var report = _reportService.GetDistributionReport(startDate, endDate);
return BetterJson(report.ToArray());
}
public ActionResult DistributionExcel(DateTime startDate, DateTime endDate)
{
var report = GetDistributionReport(startDate, endDate);
var report = _reportService.GetDistributionReport(startDate, endDate);
var writer = new DistributionReportWriter();
var excel = writer.Write(report);
@@ -49,37 +44,6 @@ namespace InventoryTraker.Web.Controllers
};
}
private DistributionReport[] GetDistributionReport(DateTime startDate, DateTime endDate)
{
var query =
from t in _context.Transactions
where
t.TransactionType == TransactionType.Distributed
&& t.TransactionDate >= startDate
&& t.TransactionDate < endDate
group t by new { t.TransactionDate, t.Destination }
into g
select new
{
Date = g.Key.TransactionDate,
Destination = g.Key.Destination,
Transactions = g.ToList()
};
var report =
from item in query.ToArray()
select new DistributionReport
{
Date = item.Date,
Destination = item.Destination,
Transactions =
Mapper.Map<IList<Transaction>, IList<TransactionViewModel>>
(item.Transactions).ToArray()
};
return report.ToArray();
}
[HttpGet]
public ActionResult Movement()
{
@@ -88,14 +52,14 @@ namespace InventoryTraker.Web.Controllers
public ActionResult Movement(DateTime month)
{
var report = GetMovementReport(month);
var report = _reportService.GetMovementReport(month);
return BetterJson(report);
}
public ActionResult MovementExcel(DateTime month)
{
var report = GetMovementReport(month);
var report = _reportService.GetMovementReport(month);
var writer = new MovementReportWriter();
var excel = writer.Write(report);
@@ -108,127 +72,5 @@ namespace InventoryTraker.Web.Controllers
FileDownloadName = filename
};
}
private MovementReport GetMovementReport(DateTime month)
{
var startDate = month;
var endDate = startDate.AddMonths(1);
return
new MovementReport
{
Items = GetMovementReportItems(startDate, endDate),
Month = month
};
}
private IEnumerable<MovementReportItem> GetMovementReportItems(DateTime startDate, DateTime endDate)
{
var transactionsMostRecentBefore =
(from transaction in _context.Transactions
where
transaction.TransactionDate < startDate
group transaction by transaction.Inventory
into g
let mostRecent =
g.OrderByDescending(t => t.TransactionDate)
.ThenBy(t => t.CurrentQuantity) // for days with multiple, assume it's the smallest
.FirstOrDefault()
where mostRecent.CurrentQuantity > 0
select mostRecent).ToList();
var transactionSums =
(from transaction in _context.Transactions
where
transaction.TransactionDate >= startDate
&& transaction.TransactionDate < endDate
group transaction by transaction.Inventory
into g
let addedQty = g.Sum(t => t.AddedQuantity)
let distributed = g.Where(t => t.TransactionType == TransactionType.Distributed)
let distributedQty = distributed.Any() ? distributed.Sum(t => t.RemovedQuantity) : 0
let adjustment = g.Where(t =>
t.TransactionType == TransactionType.Expired
|| t.TransactionType == TransactionType.Loss)
let adjustmentQty = adjustment.Any() ? adjustment.Sum(t => t.RemovedQuantity) : 0
let endingQty =
g
.OrderByDescending(t => t.TransactionDate)
.ThenBy(t => t.CurrentQuantity)
.FirstOrDefault().CurrentQuantity
select new
{
Inventory = g.Key,
addedQty,
adjustmentQty,
distributedQty,
endingQty
}).ToList();
var inventoryReportItems =
transactionsMostRecentBefore.FullOuterJoin( // source
transactionSums, // inner
before => before.Inventory.Id, // fk
sums => sums.Inventory.Id, // pk
(before, sums, r) =>
{
var item = new MovementReportInventoryItem();
if (before != null)
{
item.Inventory = before.Inventory;
item.BeginningQuantity = before.CurrentQuantity;
if (sums != null)
{
item.AddedQuantity = sums.addedQty;
item.DistributedQuantity = sums.distributedQty;
item.AdjustmentQuantity = sums.adjustmentQty;
item.EndingQuantity = sums.endingQty;
}
else // no change
{
item.EndingQuantity = item.BeginningQuantity;
}
}
else if (sums != null) // item was added in this time period
{
item.Inventory = sums.Inventory;
item.AddedQuantity = sums.addedQty;
item.DistributedQuantity = sums.distributedQty;
item.AdjustmentQuantity = sums.adjustmentQty;
item.EndingQuantity = sums.endingQty;
}
item.TotalAvailableQuantity = item.BeginningQuantity + item.AddedQuantity;
return item;
}).ToArray();
// group by inventory type
var inventoryTypeReportItems =
from item in inventoryReportItems
group item by item.Inventory.InventoryType
into grp
select new MovementReportItem
{
InventoryType = Mapper.Map<InventoryTypeViewModel>(grp.Key),
BeginningQuantity = grp.Sum(g => g.BeginningQuantity),
AddedQuantity = grp.Sum(g => g.AddedQuantity),
TotalAvailableQuantity = grp.Sum(g => g.TotalAvailableQuantity),
DistributedQuantity = grp.Sum(g => g.DistributedQuantity),
AdjustmentQuantity = grp.Sum(g => g.AdjustmentQuantity),
EndingQuantity = grp.Sum(g => g.EndingQuantity)
};
return inventoryTypeReportItems;
}
private class MovementReportInventoryItem
{
public Inventory Inventory { get; set; }
public int BeginningQuantity { get; set; }
public int AddedQuantity { get; set; }
public int TotalAvailableQuantity { get; set; }
public int DistributedQuantity { get; set; }
public int AdjustmentQuantity { get; set; }
public int EndingQuantity { get; set; }
}
}
}
@@ -384,7 +384,10 @@
<Compile Include="Models\ProfileForm.cs" />
<Compile Include="Models\TransactionViewModel.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\ReportService.cs" />
<Compile Include="Utilities\ControllerContextExtensions.cs" />
<Compile Include="Utilities\InventoryTypeReportWriter.cs" />
<Compile Include="Utilities\InventoryReportWriter.cs" />
<Compile Include="Utilities\ExcelParserBase.cs" />
<Compile Include="Utilities\IEnumerableExtensions.cs" />
<Compile Include="Utilities\InventoryTypeParser.cs" />
@@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AutoMapper;
using InventoryTraker.Web.Core;
using InventoryTraker.Web.Data;
using InventoryTraker.Web.Models;
using InventoryTraker.Web.Utilities;
namespace InventoryTraker.Web.Services
{
public class ReportService
{
private readonly AppDbContext _context;
public ReportService(AppDbContext context)
{
_context = context;
}
public DistributionReport[] GetDistributionReport(DateTime startDate, DateTime endDate)
{
var query =
from t in _context.Transactions
where
t.TransactionType == TransactionType.Distributed
&& t.TransactionDate >= startDate
&& t.TransactionDate < endDate
group t by new { t.TransactionDate, t.Destination }
into g
select new
{
Date = g.Key.TransactionDate,
Destination = g.Key.Destination,
Transactions = g.ToList()
};
var report =
from item in query.ToArray()
select new DistributionReport
{
Date = item.Date,
Destination = item.Destination,
Transactions =
Mapper.Map<IList<Transaction>, IList<TransactionViewModel>>
(item.Transactions).ToArray()
};
return report.ToArray();
}
public MovementReport GetMovementReport(DateTime month)
{
var startDate = month;
var endDate = startDate.AddMonths(1);
return
new MovementReport
{
Items = GetMovementReportItems(startDate, endDate),
Month = month
};
}
private IEnumerable<MovementReportItem> GetMovementReportItems(DateTime startDate, DateTime endDate)
{
var transactionsMostRecentBefore =
(from transaction in _context.Transactions
where
transaction.TransactionDate < startDate
group transaction by transaction.Inventory
into g
let mostRecent =
g.OrderByDescending(t => t.TransactionDate)
.ThenBy(t => t.CurrentQuantity) // for days with multiple, assume it's the smallest
.FirstOrDefault()
where mostRecent.CurrentQuantity > 0
select mostRecent).ToList();
var transactionSums =
(from transaction in _context.Transactions
where
transaction.TransactionDate >= startDate
&& transaction.TransactionDate < endDate
group transaction by transaction.Inventory
into g
let addedQty = g.Sum(t => t.AddedQuantity)
let distributed = g.Where(t => t.TransactionType == TransactionType.Distributed)
let distributedQty = distributed.Any() ? distributed.Sum(t => t.RemovedQuantity) : 0
let adjustment = g.Where(t =>
t.TransactionType == TransactionType.Expired
|| t.TransactionType == TransactionType.Loss)
let adjustmentQty = adjustment.Any() ? adjustment.Sum(t => t.RemovedQuantity) : 0
let endingQty =
g
.OrderByDescending(t => t.TransactionDate)
.ThenBy(t => t.CurrentQuantity)
.FirstOrDefault().CurrentQuantity
select new
{
Inventory = g.Key,
addedQty,
adjustmentQty,
distributedQty,
endingQty
}).ToList();
var inventoryReportItems =
transactionsMostRecentBefore.FullOuterJoin( // source
transactionSums, // inner
before => before.Inventory.Id, // fk
sums => sums.Inventory.Id, // pk
(before, sums, r) =>
{
var item = new MovementReportInventoryItem();
if (before != null)
{
item.Inventory = before.Inventory;
item.BeginningQuantity = before.CurrentQuantity;
if (sums != null)
{
item.AddedQuantity = sums.addedQty;
item.DistributedQuantity = sums.distributedQty;
item.AdjustmentQuantity = sums.adjustmentQty;
item.EndingQuantity = sums.endingQty;
}
else // no change
{
item.EndingQuantity = item.BeginningQuantity;
}
}
else if (sums != null) // item was added in this time period
{
item.Inventory = sums.Inventory;
item.AddedQuantity = sums.addedQty;
item.DistributedQuantity = sums.distributedQty;
item.AdjustmentQuantity = sums.adjustmentQty;
item.EndingQuantity = sums.endingQty;
}
item.TotalAvailableQuantity = item.BeginningQuantity + item.AddedQuantity;
return item;
}).ToArray();
// group by inventory type
var inventoryTypeReportItems =
from item in inventoryReportItems
group item by item.Inventory.InventoryType
into grp
select new MovementReportItem
{
InventoryType = Mapper.Map<InventoryTypeViewModel>(grp.Key),
BeginningQuantity = grp.Sum(g => g.BeginningQuantity),
AddedQuantity = grp.Sum(g => g.AddedQuantity),
TotalAvailableQuantity = grp.Sum(g => g.TotalAvailableQuantity),
DistributedQuantity = grp.Sum(g => g.DistributedQuantity),
AdjustmentQuantity = grp.Sum(g => g.AdjustmentQuantity),
EndingQuantity = grp.Sum(g => g.EndingQuantity)
};
return inventoryTypeReportItems;
}
private class MovementReportInventoryItem
{
public Inventory Inventory { get; set; }
public int BeginningQuantity { get; set; }
public int AddedQuantity { get; set; }
public int TotalAvailableQuantity { get; set; }
public int DistributedQuantity { get; set; }
public int AdjustmentQuantity { get; set; }
public int EndingQuantity { get; set; }
}
}
}
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AutoMapper;
@@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ClosedXML.Excel;
using CsvHelper;
using CsvHelper.Configuration;
using CsvHelper.Excel;
using InventoryTraker.Web.Models;
namespace InventoryTraker.Web.Utilities
{
public class InventoryReportWriter
{
private sealed class InventoryViewModelMap : CsvClassMap<InventoryViewModel>
{
public InventoryViewModelMap()
{
Map(m => m.Name).Name("Name of Commodity");
Map(m => m.UnitsPerCase).Name("Units per Case");
Map(m => m.ContainerType).Name("Container Type");
Map(m => m.Quantity).Name("Case Quantity");
Map(m => m.ExpirationDate).Name("Expiration Date");
Map(m => m.AddedDate).Name("Added Date");
Map(m => m.Memo).Name("Memo");
Map(m => m.WeightPerCase).Name("Weight per Case");
Map(m => m.PricePerCase).Name("Price per Case");
}
}
public byte[] Write(IEnumerable<InventoryViewModel> items)
{
using (var stream = new MemoryStream())
{
WriteStream(items, stream);
return stream.ToArray();
}
}
public void WriteStream(IEnumerable<InventoryViewModel> items, Stream stream)
{
using (var workbook = new XLWorkbook(XLEventTracking.Disabled))
{
var worksheet = workbook.AddWorksheet("Current Inventory");
using (var writer = new CsvWriter(new ExcelSerializer(worksheet)))
{
writer.Configuration.RegisterClassMap(new InventoryViewModelMap());
writer.WriteRecords(items.OrderBy(i => i.Name));
workbook.SaveAs(stream);
}
}
}
}
}
@@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ClosedXML.Excel;
using CsvHelper;
using CsvHelper.Configuration;
using CsvHelper.Excel;
using InventoryTraker.Web.Models;
namespace InventoryTraker.Web.Utilities
{
public class InventoryTypeReportWriter
{
private sealed class InventoryTypeViewModelMap : CsvClassMap<InventoryTypeViewModel>
{
public InventoryTypeViewModelMap()
{
Map(m => m.Identifier).Name("Identifier");
Map(m => m.Name).Name("Name of Commodity");
Map(m => m.UnitsPerCase).Name("Units per Case");
Map(m => m.ContainerType).Name("Container Type");
Map(m => m.WeightPerCase).Name("Weight per Case");
Map(m => m.PricePerCase).Name("Price per Case");
}
}
public byte[] Write(IEnumerable<InventoryTypeViewModel> items)
{
using (var stream = new MemoryStream())
{
WriteStream(items, stream);
return stream.ToArray();
}
}
public void WriteStream(IEnumerable<InventoryTypeViewModel> items, Stream stream)
{
using (var workbook = new XLWorkbook(XLEventTracking.Disabled))
{
var worksheet = workbook.AddWorksheet("Commodity Types");
using (var writer = new CsvWriter(new ExcelSerializer(worksheet)))
{
writer.Configuration.RegisterClassMap(new InventoryTypeViewModelMap());
writer.WriteRecords(items.OrderBy(i => i.Name));
workbook.SaveAs(stream);
}
}
}
}
}
@@ -10,6 +10,7 @@
<div class="pull-left">
<a class="btn btn-default" href="" ng-click="vm.add()"><i class="fa fa-plus-circle"></i> Arrival</a>
<a class="btn btn-default" href="" ng-click="vm.distribute()"><i class="fa fa-share-square"></i> Distribute</a>
<a class="btn btn-default" href="" ng-click="vm.export()"><i class="fa fa-file-excel-o"></i> Export</a>
</div>
<div class="pull-right">
<a class="btn btn-default" href="/InventoryType"><i class="fa fa-cube"></i> Commodity Types</a>
@@ -10,7 +10,8 @@
</h1>
<div class="pull-left">
<a class="btn btn-default" href="" ng-click="vm.add()"><i class="fa fa-plus-circle"></i> Add</a>
</div>
<a class="btn btn-default" href="" ng-click="vm.export()"><i class="fa fa-file-excel-o"></i> Export</a>
</div>
<inventory-type-list inventory-types="vm.inventoryTypes"></inventory-type-list>
</div>
@@ -3,14 +3,14 @@
window.app.controller('InventoryListController', InventoryListController);
InventoryListController.$inject = ['$uibModal', 'inventorySvc'];
function InventoryListController($uibModal, inventorySvc) {
InventoryListController.$inject = ['$uibModal', 'inventorySvc', 'downloadSvc'];
function InventoryListController($uibModal, inventorySvc, downloadSvc) {
var vm = this;
vm.add = add;
vm.distribute = distribute;
vm.inventories = inventorySvc.inventories;
function add() {
$uibModal.open({
template: '<inventory-add />',
@@ -24,5 +24,10 @@
backdrop: 'static'
});
}
vm.export = function () {
inventorySvc.exportInventory()
.success(downloadSvc.success);
}
}
})();
@@ -15,7 +15,8 @@
inventories: inventories,
get: get,
refresh: refresh,
find: find
find: find,
exportInventory: exportInventory
};
return svc;
@@ -27,6 +28,10 @@
});
}
function exportInventory() {
return $http.post('/Inventory/Export', {}, { responseType: 'arraybuffer' });
}
function add(inventory) {
return $http.post('/Inventory/Add', inventory)
.success(function(inventory) {
@@ -3,8 +3,8 @@
window.app.controller('InventoryTypeController', InventoryTypeController);
InventoryTypeController.$inject = ['$uibModal', 'inventoryTypeSvc'];
function InventoryTypeController($uibModal, inventoryTypeSvc) {
InventoryTypeController.$inject = ['$uibModal', 'inventoryTypeSvc', 'downloadSvc'];
function InventoryTypeController($uibModal, inventoryTypeSvc, downloadSvc) {
var vm = this;
vm.add = add;
@@ -24,5 +24,10 @@
backdrop: 'static'
});
}
vm.export = function () {
inventoryTypeSvc.exportInventoryTypes()
.success(downloadSvc.success);
}
}
})();
@@ -10,7 +10,8 @@
var svc = {
add: add,
update: update,
inventoryTypes: inventoryTypes
inventoryTypes: inventoryTypes,
exportInventoryTypes: exportInventoryTypes
};
return svc;
@@ -22,6 +23,10 @@
});
}
function exportInventoryTypes() {
return $http.post('/InventoryType/Export', {}, { responseType: 'arraybuffer' });
}
function add(inventoryType) {
return $http.post('/InventoryType/Add', inventoryType)
.success(function (inventoryType) {