From 206a3f2def1ceed6293946b1271a139d5547af0f Mon Sep 17 00:00:00 2001 From: James Kolpack Date: Wed, 21 Sep 2016 11:55:06 -0400 Subject: [PATCH] Export Monthly Inventory --- .gitignore | 1 + .../App_Start/AutoMapperConfig.cs | 5 +- .../InventoryTraker.Web.Tests.csproj | 1 + .../Utilities/MovementReportWriterTests.cs | 75 +++++ InventoryTraker.Web/App_Start/BundleConfig.cs | 1 + .../Controllers/ReportController.cs | 33 ++- .../InventoryTraker.Web.csproj | 4 +- .../Models/InventoryReportItem.cs | 6 - .../Models/InventoryTypeViewModel.cs | 6 + InventoryTraker.Web/Scripts/FileSaver.js | 270 ++++++++++++++++++ InventoryTraker.Web/Scripts/FileSaver.min.js | 2 + .../Utilities/MovementReportWriter.cs | 106 ++++++- .../Views/Report/Movement.cshtml | 5 +- .../js/report/MovementReportController.js | 8 +- InventoryTraker.Web/js/report/reportSvc.js | 7 +- .../js/utility/DownloadService.js | 17 ++ InventoryTraker.Web/packages.config | 1 + 17 files changed, 525 insertions(+), 23 deletions(-) create mode 100644 InventoryTraker.Web.Tests/Utilities/MovementReportWriterTests.cs delete mode 100644 InventoryTraker.Web/Models/InventoryReportItem.cs create mode 100644 InventoryTraker.Web/Scripts/FileSaver.js create mode 100644 InventoryTraker.Web/Scripts/FileSaver.min.js create mode 100644 InventoryTraker.Web/js/utility/DownloadService.js diff --git a/.gitignore b/.gitignore index f13d810..4ac4fa1 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ _ReSharper*/ packages/**/ **/App_Data/* */Logs/* +logs/* diff --git a/InventoryTraker.Web.Tests/App_Start/AutoMapperConfig.cs b/InventoryTraker.Web.Tests/App_Start/AutoMapperConfig.cs index c095557..f69b5e9 100644 --- a/InventoryTraker.Web.Tests/App_Start/AutoMapperConfig.cs +++ b/InventoryTraker.Web.Tests/App_Start/AutoMapperConfig.cs @@ -1,4 +1,5 @@ using Heroic.AutoMapper; +using InventoryTraker.Web.Tests.Utilities; [assembly: WebActivatorEx.PreApplicationStartMethod(typeof(InventoryTraker.Web.Tests.AutoMapperConfig), "Configure")] namespace InventoryTraker.Web.Tests @@ -11,9 +12,9 @@ namespace InventoryTraker.Web.Tests // You can customize this by passing in a lambda to filter the assemblies by name, // like so: //HeroicAutoMapperConfigurator.LoadMapsFromCallerAndReferencedAssemblies(x => x.Name.StartsWith("YourPrefix")); - HeroicAutoMapperConfigurator.LoadMapsFromCallerAndReferencedAssemblies(); + //HeroicAutoMapperConfigurator.LoadMapsFromCallerAndReferencedAssemblies(); //If you run into issues with the maps not being located at runtime, try using this method instead: - //HeroicAutoMapperConfigurator.LoadMapsFromAssemblyContainingTypeAndReferencedAssemblies(); + HeroicAutoMapperConfigurator.LoadMapsFromAssemblyContainingTypeAndReferencedAssemblies(); } } } \ No newline at end of file diff --git a/InventoryTraker.Web.Tests/InventoryTraker.Web.Tests.csproj b/InventoryTraker.Web.Tests/InventoryTraker.Web.Tests.csproj index c1fa6d1..cb34cc8 100644 --- a/InventoryTraker.Web.Tests/InventoryTraker.Web.Tests.csproj +++ b/InventoryTraker.Web.Tests/InventoryTraker.Web.Tests.csproj @@ -63,6 +63,7 @@ + diff --git a/InventoryTraker.Web.Tests/Utilities/MovementReportWriterTests.cs b/InventoryTraker.Web.Tests/Utilities/MovementReportWriterTests.cs new file mode 100644 index 0000000..0b3621b --- /dev/null +++ b/InventoryTraker.Web.Tests/Utilities/MovementReportWriterTests.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +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 MovementReportWriterTests + { + [OneTimeSetUp] + public void StartUp() + { + HeroicAutoMapperConfigurator.LoadMapsFromAssemblyContainingTypeAndReferencedAssemblies(); + } + + readonly MovementReport _movementReport = new MovementReport + { + Month = new DateTime(2016, 04, 1), + Items = new[] + { + new MovementReportItem + { + InventoryType = new InventoryTypeViewModel + { + Name = "Beans", + ContainerType = "#300 cans", + Id = 1, + Identifier = "10001", + UnitsPerCase = 24 + }, + BeginningQuantity = 7, + AddedQuantity = 5, + TotalAvailableQuantity = 12, + AdjustmentQuantity = 3, + DistributedQuantity = 1, + EndingQuantity = 8 + }, + new MovementReportItem + { + InventoryType = new InventoryTypeViewModel + { + Name = "Peanut Butter", + ContainerType = "16oz jars", + Id = 2, + Identifier = "20001", + UnitsPerCase = 12 + }, + BeginningQuantity = 5, + AddedQuantity = 11, + TotalAvailableQuantity = 16, + AdjustmentQuantity = 0, + DistributedQuantity = 2, + EndingQuantity = 14 + } + } + }; + + [Test, Explicit] + public void Write() + { + using + (var outputFile + = new StreamWriter(Path.Combine(@"c:\temp", "MovementReport.xlsx"))) + { + var writer = new MovementReportWriter(); + writer.WriteStream(_movementReport, outputFile.BaseStream); + } + } + } +} diff --git a/InventoryTraker.Web/App_Start/BundleConfig.cs b/InventoryTraker.Web/App_Start/BundleConfig.cs index 0b8e3de..de6e6b1 100644 --- a/InventoryTraker.Web/App_Start/BundleConfig.cs +++ b/InventoryTraker.Web/App_Start/BundleConfig.cs @@ -23,6 +23,7 @@ namespace InventoryTraker.Web .Include("~/Scripts/angular-strap.js") .Include("~/Scripts/angular-strap.tpl.js") .Include("~/Scripts/ui-grid.js") + .Include("~/Scripts/FileSaver.js") .Include("~/js/app.js") .IncludeDirectory("~/js/", "*.js", true) ); diff --git a/InventoryTraker.Web/Controllers/ReportController.cs b/InventoryTraker.Web/Controllers/ReportController.cs index 93a4381..5e6a39f 100644 --- a/InventoryTraker.Web/Controllers/ReportController.cs +++ b/InventoryTraker.Web/Controllers/ReportController.cs @@ -64,21 +64,42 @@ namespace InventoryTraker.Web.Controllers } public ActionResult Movement(DateTime month) + { + var report = GetMovementReport(month); + + return BetterJson(report); + } + + public ActionResult MovementExcel(DateTime month) + { + var report = GetMovementReport(month); + + var writer = new MovementReportWriter(); + var excel = writer.Write(report); + + var filename = $"MonthlyInventoryReport{report.Month:MMMMyyyy}.xlsx"; + + return + new FileContentResult(excel, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + { + FileDownloadName = filename + }; + } + + private MovementReport GetMovementReport(DateTime month) { var startDate = month; var endDate = startDate.AddMonths(1); - var inventoryTypeReport - = new MovementReport + return + new MovementReport { - Items = GetInventoryTypeReportItems(startDate, endDate), + Items = GetMovementReportItems(startDate, endDate), Month = month }; - - return BetterJson(inventoryTypeReport); } - private IEnumerable GetInventoryTypeReportItems(DateTime startDate, DateTime endDate) + private IEnumerable GetMovementReportItems(DateTime startDate, DateTime endDate) { var transactionsMostRecentBefore = (from transaction in _context.Transactions diff --git a/InventoryTraker.Web/InventoryTraker.Web.csproj b/InventoryTraker.Web/InventoryTraker.Web.csproj index 646f5ad..fde9ebf 100644 --- a/InventoryTraker.Web/InventoryTraker.Web.csproj +++ b/InventoryTraker.Web/InventoryTraker.Web.csproj @@ -317,6 +317,8 @@ + + @@ -332,6 +334,7 @@ + @@ -373,7 +376,6 @@ - diff --git a/InventoryTraker.Web/Models/InventoryReportItem.cs b/InventoryTraker.Web/Models/InventoryReportItem.cs deleted file mode 100644 index 4513ca2..0000000 --- a/InventoryTraker.Web/Models/InventoryReportItem.cs +++ /dev/null @@ -1,6 +0,0 @@ -using InventoryTraker.Web.Core; - -namespace InventoryTraker.Web.Models -{ - -} \ No newline at end of file diff --git a/InventoryTraker.Web/Models/InventoryTypeViewModel.cs b/InventoryTraker.Web/Models/InventoryTypeViewModel.cs index 00e8921..e4d04d6 100644 --- a/InventoryTraker.Web/Models/InventoryTypeViewModel.cs +++ b/InventoryTraker.Web/Models/InventoryTypeViewModel.cs @@ -22,6 +22,12 @@ namespace InventoryTraker.Web.Models [Required] public string ContainerType { get; set; } + [HiddenInput] + public string UnitsPerCaseContainerType + { + get { return $"{UnitsPerCase} / {ContainerType}"; } + } + [Required] public double WeightPerCase { get; set; } diff --git a/InventoryTraker.Web/Scripts/FileSaver.js b/InventoryTraker.Web/Scripts/FileSaver.js new file mode 100644 index 0000000..6f8c060 --- /dev/null +++ b/InventoryTraker.Web/Scripts/FileSaver.js @@ -0,0 +1,270 @@ +/* FileSaver.js + * A saveAs() FileSaver implementation. + * 1.1.20151003 + * + * By Eli Grey, http://eligrey.com + * License: MIT + * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md + */ + +/*global self */ +/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */ + +/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ + +var saveAs = saveAs || (function(view) { + "use strict"; + // IE <10 is explicitly unsupported + if (typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) { + return; + } + var + doc = view.document + // only get URL when necessary in case Blob.js hasn't overridden it yet + , get_URL = function() { + return view.URL || view.webkitURL || view; + } + , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") + , can_use_save_link = "download" in save_link + , click = function(node) { + var event = new MouseEvent("click"); + node.dispatchEvent(event); + } + , is_safari = /Version\/[\d\.]+.*Safari/.test(navigator.userAgent) + , webkit_req_fs = view.webkitRequestFileSystem + , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem + , throw_outside = function(ex) { + (view.setImmediate || view.setTimeout)(function() { + throw ex; + }, 0); + } + , force_saveable_type = "application/octet-stream" + , fs_min_size = 0 + // See https://code.google.com/p/chromium/issues/detail?id=375297#c7 and + // https://github.com/eligrey/FileSaver.js/commit/485930a#commitcomment-8768047 + // for the reasoning behind the timeout and revocation flow + , arbitrary_revoke_timeout = 500 // in ms + , revoke = function(file) { + var revoker = function() { + if (typeof file === "string") { // file is an object URL + get_URL().revokeObjectURL(file); + } else { // file is a File + file.remove(); + } + }; + if (view.chrome) { + revoker(); + } else { + setTimeout(revoker, arbitrary_revoke_timeout); + } + } + , dispatch = function(filesaver, event_types, event) { + event_types = [].concat(event_types); + var i = event_types.length; + while (i--) { + var listener = filesaver["on" + event_types[i]]; + if (typeof listener === "function") { + try { + listener.call(filesaver, event || filesaver); + } catch (ex) { + throw_outside(ex); + } + } + } + } + , auto_bom = function(blob) { + // prepend BOM for UTF-8 XML and text/* types (including HTML) + if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { + return new Blob(["\ufeff", blob], {type: blob.type}); + } + return blob; + } + , FileSaver = function(blob, name, no_auto_bom) { + if (!no_auto_bom) { + blob = auto_bom(blob); + } + // First try a.download, then web filesystem, then object URLs + var + filesaver = this + , type = blob.type + , blob_changed = false + , object_url + , target_view + , dispatch_all = function() { + dispatch(filesaver, "writestart progress write writeend".split(" ")); + } + // on any filesys errors revert to saving with object URLs + , fs_error = function() { + if (target_view && is_safari && typeof FileReader !== "undefined") { + // Safari doesn't allow downloading of blob urls + var reader = new FileReader(); + reader.onloadend = function() { + var base64Data = reader.result; + target_view.location.href = "data:attachment/file" + base64Data.slice(base64Data.search(/[,;]/)); + filesaver.readyState = filesaver.DONE; + dispatch_all(); + }; + reader.readAsDataURL(blob); + filesaver.readyState = filesaver.INIT; + return; + } + // don't create more object URLs than needed + if (blob_changed || !object_url) { + object_url = get_URL().createObjectURL(blob); + } + if (target_view) { + target_view.location.href = object_url; + } else { + var new_tab = view.open(object_url, "_blank"); + if (new_tab == undefined && is_safari) { + //Apple do not allow window.open, see http://bit.ly/1kZffRI + view.location.href = object_url + } + } + filesaver.readyState = filesaver.DONE; + dispatch_all(); + revoke(object_url); + } + , abortable = function(func) { + return function() { + if (filesaver.readyState !== filesaver.DONE) { + return func.apply(this, arguments); + } + }; + } + , create_if_not_found = {create: true, exclusive: false} + , slice + ; + filesaver.readyState = filesaver.INIT; + if (!name) { + name = "download"; + } + if (can_use_save_link) { + object_url = get_URL().createObjectURL(blob); + save_link.href = object_url; + save_link.download = name; + setTimeout(function() { + click(save_link); + dispatch_all(); + revoke(object_url); + filesaver.readyState = filesaver.DONE; + }); + return; + } + // Object and web filesystem URLs have a problem saving in Google Chrome when + // viewed in a tab, so I force save with application/octet-stream + // http://code.google.com/p/chromium/issues/detail?id=91158 + // Update: Google errantly closed 91158, I submitted it again: + // https://code.google.com/p/chromium/issues/detail?id=389642 + if (view.chrome && type && type !== force_saveable_type) { + slice = blob.slice || blob.webkitSlice; + blob = slice.call(blob, 0, blob.size, force_saveable_type); + blob_changed = true; + } + // Since I can't be sure that the guessed media type will trigger a download + // in WebKit, I append .download to the filename. + // https://bugs.webkit.org/show_bug.cgi?id=65440 + if (webkit_req_fs && name !== "download") { + name += ".download"; + } + if (type === force_saveable_type || webkit_req_fs) { + target_view = view; + } + if (!req_fs) { + fs_error(); + return; + } + fs_min_size += blob.size; + req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) { + fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) { + var save = function() { + dir.getFile(name, create_if_not_found, abortable(function(file) { + file.createWriter(abortable(function(writer) { + writer.onwriteend = function(event) { + target_view.location.href = file.toURL(); + filesaver.readyState = filesaver.DONE; + dispatch(filesaver, "writeend", event); + revoke(file); + }; + writer.onerror = function() { + var error = writer.error; + if (error.code !== error.ABORT_ERR) { + fs_error(); + } + }; + "writestart progress write abort".split(" ").forEach(function(event) { + writer["on" + event] = filesaver["on" + event]; + }); + writer.write(blob); + filesaver.abort = function() { + writer.abort(); + filesaver.readyState = filesaver.DONE; + }; + filesaver.readyState = filesaver.WRITING; + }), fs_error); + }), fs_error); + }; + dir.getFile(name, {create: false}, abortable(function(file) { + // delete file if it already exists + file.remove(); + save(); + }), abortable(function(ex) { + if (ex.code === ex.NOT_FOUND_ERR) { + save(); + } else { + fs_error(); + } + })); + }), fs_error); + }), fs_error); + } + , FS_proto = FileSaver.prototype + , saveAs = function(blob, name, no_auto_bom) { + return new FileSaver(blob, name, no_auto_bom); + } + ; + // IE 10+ (native saveAs) + if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) { + return function(blob, name, no_auto_bom) { + if (!no_auto_bom) { + blob = auto_bom(blob); + } + return navigator.msSaveOrOpenBlob(blob, name || "download"); + }; + } + + FS_proto.abort = function() { + var filesaver = this; + filesaver.readyState = filesaver.DONE; + dispatch(filesaver, "abort"); + }; + FS_proto.readyState = FS_proto.INIT = 0; + FS_proto.WRITING = 1; + FS_proto.DONE = 2; + + FS_proto.error = + FS_proto.onwritestart = + FS_proto.onprogress = + FS_proto.onwrite = + FS_proto.onabort = + FS_proto.onerror = + FS_proto.onwriteend = + null; + + return saveAs; +}( + typeof self !== "undefined" && self + || typeof window !== "undefined" && window + || this.content +)); +// `self` is undefined in Firefox for Android content script context +// while `this` is nsIContentFrameMessageManager +// with an attribute `content` that corresponds to the window + +if (typeof module !== "undefined" && module.exports) { + module.exports.saveAs = saveAs; +} else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) { + define([], function() { + return saveAs; + }); +} diff --git a/InventoryTraker.Web/Scripts/FileSaver.min.js b/InventoryTraker.Web/Scripts/FileSaver.min.js new file mode 100644 index 0000000..8bbbf76 --- /dev/null +++ b/InventoryTraker.Web/Scripts/FileSaver.min.js @@ -0,0 +1,2 @@ +/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ +var saveAs=saveAs||function(e){"use strict";if(typeof navigator!=="undefined"&&/MSIE [1-9]\./.test(navigator.userAgent)){return}var t=e.document,n=function(){return e.URL||e.webkitURL||e},r=t.createElementNS("http://www.w3.org/1999/xhtml","a"),i="download"in r,o=function(e){var t=new MouseEvent("click");e.dispatchEvent(t)},a=/Version\/[\d\.]+.*Safari/.test(navigator.userAgent),f=e.webkitRequestFileSystem,u=e.requestFileSystem||f||e.mozRequestFileSystem,s=function(t){(e.setImmediate||e.setTimeout)(function(){throw t},0)},c="application/octet-stream",d=0,l=500,w=function(t){var r=function(){if(typeof t==="string"){n().revokeObjectURL(t)}else{t.remove()}};if(e.chrome){r()}else{setTimeout(r,l)}},p=function(e,t,n){t=[].concat(t);var r=t.length;while(r--){var i=e["on"+t[r]];if(typeof i==="function"){try{i.call(e,n||e)}catch(o){s(o)}}}},v=function(e){if(/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(e.type)){return new Blob(["\ufeff",e],{type:e.type})}return e},y=function(t,s,l){if(!l){t=v(t)}var y=this,m=t.type,S=false,h,R,O=function(){p(y,"writestart progress write writeend".split(" "))},g=function(){if(R&&a&&typeof FileReader!=="undefined"){var r=new FileReader;r.onloadend=function(){var e=r.result;R.location.href="data:attachment/file"+e.slice(e.search(/[,;]/));y.readyState=y.DONE;O()};r.readAsDataURL(t);y.readyState=y.INIT;return}if(S||!h){h=n().createObjectURL(t)}if(R){R.location.href=h}else{var i=e.open(h,"_blank");if(i==undefined&&a){e.location.href=h}}y.readyState=y.DONE;O();w(h)},b=function(e){return function(){if(y.readyState!==y.DONE){return e.apply(this,arguments)}}},E={create:true,exclusive:false},N;y.readyState=y.INIT;if(!s){s="download"}if(i){h=n().createObjectURL(t);r.href=h;r.download=s;setTimeout(function(){o(r);O();w(h);y.readyState=y.DONE});return}if(e.chrome&&m&&m!==c){N=t.slice||t.webkitSlice;t=N.call(t,0,t.size,c);S=true}if(f&&s!=="download"){s+=".download"}if(m===c||f){R=e}if(!u){g();return}d+=t.size;u(e.TEMPORARY,d,b(function(e){e.root.getDirectory("saved",E,b(function(e){var n=function(){e.getFile(s,E,b(function(e){e.createWriter(b(function(n){n.onwriteend=function(t){R.location.href=e.toURL();y.readyState=y.DONE;p(y,"writeend",t);w(e)};n.onerror=function(){var e=n.error;if(e.code!==e.ABORT_ERR){g()}};"writestart progress write abort".split(" ").forEach(function(e){n["on"+e]=y["on"+e]});n.write(t);y.abort=function(){n.abort();y.readyState=y.DONE};y.readyState=y.WRITING}),g)}),g)};e.getFile(s,{create:false},b(function(e){e.remove();n()}),b(function(e){if(e.code===e.NOT_FOUND_ERR){n()}else{g()}}))}),g)}),g)},m=y.prototype,S=function(e,t,n){return new y(e,t,n)};if(typeof navigator!=="undefined"&&navigator.msSaveOrOpenBlob){return function(e,t,n){if(!n){e=v(e)}return navigator.msSaveOrOpenBlob(e,t||"download")}}m.abort=function(){var e=this;e.readyState=e.DONE;p(e,"abort")};m.readyState=m.INIT=0;m.WRITING=1;m.DONE=2;m.error=m.onwritestart=m.onprogress=m.onwrite=m.onabort=m.onerror=m.onwriteend=null;return S}(typeof self!=="undefined"&&self||typeof window!=="undefined"&&window||this.content);if(typeof module!=="undefined"&&module.exports){module.exports.saveAs=saveAs}else if(typeof define!=="undefined"&&define!==null&&define.amd!=null){define([],function(){return saveAs})} \ No newline at end of file diff --git a/InventoryTraker.Web/Utilities/MovementReportWriter.cs b/InventoryTraker.Web/Utilities/MovementReportWriter.cs index cbd87e1..456c199 100644 --- a/InventoryTraker.Web/Utilities/MovementReportWriter.cs +++ b/InventoryTraker.Web/Utilities/MovementReportWriter.cs @@ -1,21 +1,119 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; -using System.Web; +using AutoMapper; +using ClosedXML.Excel; +using CsvHelper; using CsvHelper.Configuration; +using CsvHelper.Excel; +using Heroic.AutoMapper; using InventoryTraker.Web.Models; namespace InventoryTraker.Web.Utilities { public class MovementReportWriter { - public sealed class InventoryTypeReportMap : CsvClassMap + public class MovementReportExportItem : IMapFrom, IHaveCustomMappings { - public InventoryTypeReportMap() - { + public string Name { get; set; } + public string UnitsPerCaseContainerType { get; set; } + public string BeginningQuantity { get; set; } + public string AddedQuantity { get; set; } + public string TotalAvailableQuantity { get; set; } + public string DistributedQuantity { get; set; } + public string AdjustmentQuantity { get; set; } + public string EndingQuantity { get; set; } + public void CreateMappings(IMapperConfiguration configuration) + { + Func formatCases = + (cases, unitsPerCase) => $"{cases * unitsPerCase} ({cases} cs)"; + + Func hideEmptyCases = + (cases, unitsPerCase) => + cases > 0 + ? formatCases(cases, unitsPerCase) + : ""; + + Func negative = + (cases, unitsPerCase) => + cases > 0 + ? "- " + hideEmptyCases(cases, unitsPerCase) + : ""; + + configuration.CreateMap() + .ForMember(d => d.Name, opt => opt.MapFrom(i => i.InventoryType.Name)) + .ForMember(d => d.UnitsPerCaseContainerType, + opt => opt.MapFrom(i => i.InventoryType.UnitsPerCaseContainerType)) + .ForMember(d => d.BeginningQuantity, + opt => opt.MapFrom(i => + formatCases(i.BeginningQuantity, i.InventoryType.UnitsPerCase))) + .ForMember(d => d.AddedQuantity, + opt => opt.MapFrom(i => + hideEmptyCases(i.AddedQuantity, i.InventoryType.UnitsPerCase))) + .ForMember(d => d.TotalAvailableQuantity, + opt => opt.MapFrom(i => + hideEmptyCases(i.TotalAvailableQuantity, i.InventoryType.UnitsPerCase))) + .ForMember(d => d.AdjustmentQuantity, + opt => opt.MapFrom(i => + negative(i.AdjustmentQuantity, i.InventoryType.UnitsPerCase))) + .ForMember(d => d.DistributedQuantity, + opt => opt.MapFrom(i => + hideEmptyCases(i.DistributedQuantity, i.InventoryType.UnitsPerCase))) + .ForMember(d => d.EndingQuantity, + opt => opt.MapFrom(i => + formatCases(i.EndingQuantity, i.InventoryType.UnitsPerCase))); } } + private sealed class MovementReportMap : CsvClassMap + { + public MovementReportMap() + { + Map(m => m.Name).Name("Name of Commodity"); + Map(m => m.UnitsPerCaseContainerType).Name("Pack Size / Units per Case"); + Map(m => m.BeginningQuantity).Name("Beginning Inventory"); + Map(m => m.AddedQuantity).Name("Units Rec'd"); + Map(m => m.TotalAvailableQuantity).Name("Total Units Available"); + Map(m => m.AdjustmentQuantity).Name("Adjustments (show + or -)"); + Map(m => m.DistributedQuantity).Name("Units Distributed"); + Map(m => m.EndingQuantity).Name("Ending Inventory"); + } + } + + public byte[] Write(MovementReport report) + { + using (var stream = new MemoryStream()) + { + WriteStream(report, stream); + return stream.ToArray(); + } + } + + public void WriteStream(MovementReport report, Stream stream) + { + var csvConfiguration = new CsvConfiguration(); + csvConfiguration.RegisterClassMap(); + + using (var workbook = new XLWorkbook(XLEventTracking.Disabled)) + { + var worksheet = workbook.AddWorksheet("Monthly Inventory Report"); + using (var writer = new CsvWriter(new ExcelSerializer(worksheet))) + { + writer.Configuration.RegisterClassMap(new MovementReportMap()); + writer.WriteField("Monthly Inventory Report"); + writer.NextRecord(); + writer.WriteField($"Month: {report.Month:MMMM yyyy}"); + writer.NextRecord(); + var items = + Mapper.Map, IEnumerable> + (report.Items) + .OrderBy(i => i.Name); + writer.WriteRecords(items); + workbook.SaveAs(stream); + } + } + } } } \ No newline at end of file diff --git a/InventoryTraker.Web/Views/Report/Movement.cshtml b/InventoryTraker.Web/Views/Report/Movement.cshtml index 8932382..511fe12 100644 --- a/InventoryTraker.Web/Views/Report/Movement.cshtml +++ b/InventoryTraker.Web/Views/Report/Movement.cshtml @@ -13,13 +13,16 @@
+

{{vm.movementData.month | date:'MMMM yyyy'}}

- +
diff --git a/InventoryTraker.Web/js/report/MovementReportController.js b/InventoryTraker.Web/js/report/MovementReportController.js index ada26ce..799ba73 100644 --- a/InventoryTraker.Web/js/report/MovementReportController.js +++ b/InventoryTraker.Web/js/report/MovementReportController.js @@ -3,10 +3,14 @@ window.app.controller('MovementReportController', MovementReportController); - MovementReportController.$inject = ['$scope', 'reportSvc']; - function MovementReportController($scope, reportSvc) { + MovementReportController.$inject = ['$scope', 'reportSvc', 'downloadSvc']; + function MovementReportController($scope, reportSvc, downloadSvc) { var vm = this; vm.loadData = reportSvc.loadMovementData; vm.movementData = reportSvc.movementData; + vm.export = function(month) { + reportSvc.exportMovementData({month: month}) + .success(downloadSvc.success); + } } })(); \ No newline at end of file diff --git a/InventoryTraker.Web/js/report/reportSvc.js b/InventoryTraker.Web/js/report/reportSvc.js index 618c095..ae3be46 100644 --- a/InventoryTraker.Web/js/report/reportSvc.js +++ b/InventoryTraker.Web/js/report/reportSvc.js @@ -10,7 +10,8 @@ distributionData: distributionData, loadDistributionReport: loadDistributionReport, movementData: movementData, - loadMovementData: loadMovementData + loadMovementData: loadMovementData, + exportMovementData: exportMovementData }; return svc; @@ -28,5 +29,9 @@ angular.copy(data, movementData); }); } + + function exportMovementData(query) { + return $http.post('/Report/MovementExcel', query, { responseType: 'arraybuffer' }); + } } })(); \ No newline at end of file diff --git a/InventoryTraker.Web/js/utility/DownloadService.js b/InventoryTraker.Web/js/utility/DownloadService.js new file mode 100644 index 0000000..6e30c46 --- /dev/null +++ b/InventoryTraker.Web/js/utility/DownloadService.js @@ -0,0 +1,17 @@ +(function () { + window.app.factory('downloadSvc', downloadSvc); + + downloadSvc.$inject = ['$http']; + function downloadSvc($http) { + + var svc = { + success: function (data, status, headers, config) { + var file = new Blob([data], { type: headers('Content-Type') }); + var match = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(headers('Content-Disposition')); + saveAs(file, match[1]); + } + }; + + return svc; + } +})(); \ No newline at end of file diff --git a/InventoryTraker.Web/packages.config b/InventoryTraker.Web/packages.config index 9d7acd4..fc774a7 100644 --- a/InventoryTraker.Web/packages.config +++ b/InventoryTraker.Web/packages.config @@ -13,6 +13,7 @@ +