Files
bnhtrade/src/bnhtrade.Core/Logic/Export/AmazonSettlement.cs
Bobbie Hodgetts 91ef9acc78 SP-API stock reconciliation
Amazon had depreciated a number of reports that were used for stock reconciliation. Application now uses the new fba ledger report to reconcile. It is currently untested, as this requires data from Amazon. Methods that require testing will return a 'NotImplementedException'.

Also, removed the depreciated ILMerge and replaced with ILRepack.

Plus much more tidying up, and improvements.
2024-05-07 08:24:00 +01:00

347 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Transactions;
namespace bnhtrade.Core.Logic.Export
{
public class AmazonSettlement
{
private Logic.Log.LogEvent log = new Logic.Log.LogEvent();
private List<string> lineItemCodeList = new List<string>();
public AmazonSettlement()
{
}
public void ToInvoice()
{
log.LogInformation("Starting processing of Amazon settlement data into export invoice table...");
// check settlement reports consistancy
var consistencyCheck = new Data.Database.Consistency.ImportAmazonSettlement().PeriodDateGaps();
if (consistencyCheck == false)
{ return; }
// get list of unprocssed settlement reports to export
var settlementData = new Data.Database.Import.AmazonSettlementRead();
var settlementList = settlementData.AllUnprocessed();
settlementData = null;
if (settlementList == null)
{
log.LogInformation("No new settlements to process, exiting import...");
return;
}
// create list of settlement ids
var settlementIdList = new List<string>();
for (int i = 0; i < settlementList.Count(); i++)
{
settlementIdList.Add(settlementList[i].SettlementId);
}
// test marketplace-name has been sucsessfully entered into settlement table --
// as this is not supplied in the original report from Amazon and has to be inferred from settlement line data
// this is not picked up in validate stage as null value is valid
for (int i = 0; i < settlementList.Count(); i++)
{
if (!settlementList[i].MarketPlaceNameIsSet)
{
log.LogError(
"Action required: Enter market place name for settlelment report id " + settlementList[i].SettlementId + "."
, "Unable to process settlement data from one settlement report '" + settlementList[i].SettlementId +
"'. Report header table requires a market place name, which is not supplied in original " +
"report from Amazon. This is useually inferred from settlement lines. " +
"However, in this case, it was not not possible. Manual edit/entry for database table required."
);
}
}
// validate settlelments
var validate = new Logic.Validate.AmazonSettlement();
for (int i = 0; i < settlementList.Count(); i++)
{
if (!validate.IsValid(settlementList[i]))
{
log.LogError("Error procesing Amazon Settlement data for export.", validate.ValidationResultListToString());
}
}
if (validate.IsValidResult == false) { return; }
// get dictionary of sku-number to taxcodeId
Console.Write("\rBuilding SKU list... ");
var skuList = new List<string>();
foreach (var settlement in settlementList)
{
if (settlement.SettlementLineListIsSet)
{
foreach (var line in settlement.SettlementLineList)
{
if (line.SkuIsSet
&& !string.IsNullOrWhiteSpace(line.Sku))
{
skuList.Add(line.Sku);
}
}
}
}
var taxCodeBySkuNumer = new Logic.Account.GetTaxCodeInfo().GetBySkuNumber(skuList);
// loop through each settlement and build list of invoices to export
Console.Write("\rBuilding invoices to export... ");
var invoiceList = new List<Model.Account.SalesInvoice>();
for (int i = 0; i < settlementList.Count(); i++)
{
// split settlement line list into months
// List<Model.Account.SalesInvoice.InvoiceLine>
var monthList = settlementList[i].SettlementLineList
.GroupBy(x => new DateTime(x.PostDateTime.Year, x.PostDateTime.Month, 1, 0, 0, 0, x.PostDateTime.Kind));
//.GroupBy(x => string.Format("{0}-{1}", x.PostDateTime.Year, x.PostDateTime.Month));
//.GroupBy(x => new { x.PostDateTime.Month, x.PostDateTime.Year });
int monthCount = 0;
foreach (var month in monthList)
{
monthCount++;
var itemCodeTotal = new Dictionary<string, decimal>();
foreach (var line in month)
{
string itemCode = BuildLineItemCode(taxCodeBySkuNumer, line.Sku, line.TransactionType, line.AmountType, line.AmountDescription);
if (itemCodeTotal.ContainsKey(itemCode))
{
itemCodeTotal[itemCode] += line.Amount;
}
else
{
itemCodeTotal.Add(itemCode, line.Amount);
}
}
// create invoice, one for each month
var invoice = new Model.Account.SalesInvoice();
// create invoice lines forsy
invoice.InvoiceLineList = new List<Model.Account.SalesInvoice.InvoiceLine>();
decimal lineNetTotal = 0m;
decimal lineTaxTotal = 0m;
foreach (var item in itemCodeTotal)
{
var line = new Model.Account.SalesInvoice.InvoiceLine(invoice.UnitAmountIsTaxExclusive);
line.ItemCode = item.Key;
line.Quantity = 1;
line.UnitAmount = item.Value;
lineNetTotal += item.Value;
lineTaxTotal += 0;
invoice.InvoiceLineList.Add(line);
}
invoice.ContactName = settlementList[i].MarketPlaceName;
invoice.InvoiceCurrencyCode = settlementList[i].CurrencyCode;
if (monthList.Count() == 1 || monthList.Count() == monthCount)
{ invoice.InvoiceDate = settlementList[i].EndDate; }
else
{ invoice.InvoiceDate = new DateTime(month.Key.Year, month.Key.Month, 1, 0, 0, 0, DateTimeKind.Utc).AddMonths(1).AddDays(-1); }
invoice.InvoiceDueDate = settlementList[i].DepositDate;
invoice.InvoiceReference = settlementList[i].SettlementId;
invoice.InvoiceTotalAmount = lineNetTotal + lineTaxTotal;
if (invoice.InvoiceTotalAmount < 0) { invoice.IsCreditNote = true; }
else { invoice.IsCreditNote = false; }
// invoice complete, add to list
invoiceList.Add(invoice);
}
}
// sort list of invoices
invoiceList = invoiceList.OrderBy(x => x.InvoiceReference).ThenBy(x => x.InvoiceDate).ToList();
// check invoice total against settlement totals
var invoiceTotal = new Dictionary<string, decimal>();
for (int i = 0; i < invoiceList.Count(); i++)
{
if (invoiceTotal.ContainsKey(invoiceList[i].InvoiceReference))
{
invoiceTotal[invoiceList[i].InvoiceReference] += invoiceList[i].InvoiceTotalAmount.GetValueOrDefault();
}
else
{
invoiceTotal.Add(invoiceList[i].InvoiceReference, invoiceList[i].InvoiceTotalAmount.GetValueOrDefault());
}
}
for (int i = 0; i < settlementList.Count(); i++)
{
if (settlementList[i].TotalAmount != invoiceTotal[settlementList[i].SettlementId])
{
throw new Exception("invoice totals does not match settlement total.");
}
}
if (settlementIdList.Count != invoiceTotal.Count())
{
log.LogError("Stopping Settlement export. Not all settlements have been transposed into invoices.");
return;
}
// add invoice item code data to lines
// also clean invoices of any disabled lines (remove lines and possibly invoices)
var getLineItemInfo = new Logic.Account.GetInvoiceLineItem();
getLineItemInfo.InsertNewOnNoMatch = true;
getLineItemInfo.CacheFill(lineItemCodeList);
bool newTypeFound = false;
string newTypeText = null;
for (int i = 0; i < invoiceList.Count(); i++)
{
for (int j = 0; j < invoiceList[i].InvoiceLineList.Count(); j++)
{
var itemCode = getLineItemInfo.ByItemCode(invoiceList[i].InvoiceLineList[j].ItemCode);
// error! itemCode should never be null
if (itemCode == null)
{
throw new Exception("Item code is null");
}
// flag new type and throw exception further on
else if (itemCode.IsNewReviewRequired)
{
newTypeFound = true;
if (string.IsNullOrWhiteSpace(itemCode.ItemCode))
{
newTypeText = itemCode.Name;
}
else
{
newTypeText = itemCode.ItemCode;
}
}
// clean invoices of any disabled lines (remove lines and possibly invoices)
else if (itemCode.InvoiceLineEntryEnabled == false)
{
// remove line
invoiceList[i].InvoiceTotalAmount = invoiceList[i].InvoiceTotalAmount - invoiceList[i].InvoiceLineList[j].LineTotalAmount;
invoiceList[i].InvoiceLineList.RemoveAt(j);
j = j - 1;
// remove invoice?
if (invoiceList[i].InvoiceLineList.Count == 0)
{
invoiceList.RemoveAt(i);
if (i > 0)
{
i = i - 1;
}
}
}
// get here add info to lines
else
{
invoiceList[i].InvoiceLineList[j].AccountCode = itemCode.DefaultAccountCode;
invoiceList[i].InvoiceLineList[j].Description = itemCode.Name;
invoiceList[i].InvoiceLineList[j].ItemCode = itemCode.ItemCode;
invoiceList[i].InvoiceLineList[j].TaxCode = itemCode.DefaultTaxCode;
}
}
}
if (newTypeFound)
{
if (newTypeFound)
{
throw new Exception("Parameters required for Invoice line item code '"+ newTypeText + "'. Set in tblAccountInvoiceLineItem.");
}
return;
}
// postfix invoices references that span multiple months with -n
if (invoiceList.Count > 1)
{
string lastRef = invoiceList[0].InvoiceReference;
int countRef = 1;
for (int i = 1; i < invoiceList.Count(); i++)
{
if (invoiceList[i].InvoiceReference == lastRef)
{
if (countRef == 1)
{
invoiceList[i - 1].InvoiceReference = lastRef + "-" + countRef;
}
invoiceList[i].InvoiceReference = lastRef + "-" + (countRef += 1);
}
else
{
// shouldn't normally be more than 2 date ranges, log and move on.
if (countRef > 2)
{
log.LogError(
countRef + " invoices where created from Amazon Settlement Id" + lastRef + "."
, "Settlement period appears to span more 3 months or more. Whilst this is possible, it is unsual. Confirm his is correct.");
}
lastRef = invoiceList[i].InvoiceReference;
countRef = 1;
}
}
}
Console.Write("\rWriting to database... ");
using (TransactionScope scope = new TransactionScope())
{
try
{
var saveInv = new Logic.Export.SalesInvoice();
// add temp invoice numbers
saveInv.AddTempInvoiceNumber(invoiceList, true);
// write to the database (gets validated there)
saveInv.SaveSalesInvoice(invoiceList);
// set settlements to isprocessed
new Data.Database.Import.AmazonSettlementUpdate().SetIsProcessedTrue(settlementIdList);
scope.Complete();
}
catch (Exception ex)
{
scope.Dispose();
log.LogError("Exeception caught while writing Amazon settlement invoices to DB. Changes were rolled back."
, ex.Message);
return;
}
}
Console.Write("\r");
log.LogInformation("\rFinished processing of Amazon settlement data. " + invoiceList.Count() + " invoices created from " + settlementIdList.Count() + " Amazon settlement reports.");
}
private string BuildLineItemCode(Dictionary<string, Model.Account.TaxCodeInfo> taxCodeBySkuNumer, string skuNumber, string transactionType, string amountType, string amountDescription)
{
// build the match string
// NB special case for global accounting sale and refunds (also note Goodlwill is included) and sku's where tax is included
string match01 = transactionType;
string match02 = amountType;
string match03 = amountDescription;
string matchString = "<AmazonReport><SettlementReportLine><" + match01 + "><" + match02 + "><" + match03 + ">";
// add tax info if required
if ((match01 == "Order" || match01 == "Refund")
&& (match02 == "ItemPrice" || match02 == "Promotion" || match02 == "ItemWithheldTax"))
{
if (taxCodeBySkuNumer.ContainsKey(skuNumber))
{
matchString = matchString + "<TaxCode=" + taxCodeBySkuNumer[skuNumber].TaxCode + ">";
}
else
{
throw new Exception("Sku#" + skuNumber + " tax info not found in dictionary list.");
}
}
// add to list of generated line item codes
lineItemCodeList.Add(matchString);
// return value
return matchString;
}
}
}