Ejemplo n.º 1
0
        public Documents RasterizeMany(RasterizationTarget[] targets, string guid, Person uploader,
                                       Organization organization)
        {
            Documents result = new Documents();

            int pageCountTotal   = 0;
            int pageCountRunning = 0;


            foreach (RasterizationTarget target in targets)
            {
                pageCountTotal += GetPageCount(target.FullyQualifiedFileName);
            }


            foreach (RasterizationTarget target in targets)
            {
                int pdfPageCount = GetPageCount(target.FullyQualifiedFileName); // cached

                // Create progress range

                ProgressRange range = new ProgressRange
                {
                    Minimum = pageCountRunning * 100 / pageCountTotal,
                    Maximum = (pageCountRunning + pdfPageCount) * 100 / pageCountTotal
                };

                // Request one conversion

                Documents docs = RasterizeOne(target.FullyQualifiedFileName, target.ClientFileName, guid, uploader,
                                              organization, range);

                // Update pages converted

                pageCountRunning += pdfPageCount;

                // Finally, ask the backend to do the high-res conversions, but now we have the basic, fast ones

                if (docs.Count > 0)
                {
                    RasterizeDocumentHiresOrder backendOrder = new RasterizeDocumentHiresOrder(docs[0]);
                    backendOrder.Create();
                }
            }

            // Set progress to 100% to avoid rounding errors freezing the UI at 99%

            BroadcastProgress(organization, guid, 100);

            return(result);
        }
        private static void ProcessExpensifyUploadThread(object args)
        {
            string       guidProgress = ((ProcessThreadArguments)args).GuidProgress;
            string       guidFiles    = ((ProcessThreadArguments)args).GuidFiles;
            Person       currentUser  = ((ProcessThreadArguments)args).CurrentUser;
            Organization organization = ((ProcessThreadArguments)args).Organization;

            ProgressBarBackend progress = new ProgressBarBackend(guidProgress);

            try
            {
                Documents documents = Documents.RecentFromDescription(guidFiles);
                progress.Set(2); // indicate more life

                if (documents.Count != 1)
                {
                    return; // abort
                }

                Document uploadedDoc = documents[0];

                // TODO: ATTEMPT TO DETERMINE CURRENCY FROM FILE, USING ORIGINAL CURRENCY + ORIGINAL AMOUNT

                string csvEntire;

                using (StreamReader reader = uploadedDoc.GetReader(1252))
                {
                    csvEntire = reader.ReadToEnd();
                }

                GuidCache.Set("ExpensifyRaw-" + guidFiles, csvEntire);

                string[] csvLines   = csvEntire.Split(new char[] { '\r', '\n' });
                string[] fieldNames = csvLines[0].Split(',');

                // Map fields to column indexes

                Dictionary <ExpensifyColumns, int> fieldMap = new Dictionary <ExpensifyColumns, int>();

                for (int loop = 0; loop < fieldNames.Length; loop++)
                {
                    switch (fieldNames[loop].ToLowerInvariant().Trim('\"'))
                    {
                    case "timestamp":
                        fieldMap[ExpensifyColumns.Timestamp] = loop;
                        break;

                    case "amount":
                        fieldMap[ExpensifyColumns.AmountFloat] = loop;
                        break;

                    case "merchant":
                        fieldMap[ExpensifyColumns.Merchant] = loop;
                        break;

                    case "comment":
                        fieldMap[ExpensifyColumns.Comment] = loop;
                        break;

                    case "category":
                        fieldMap[ExpensifyColumns.CategoryCustom] = loop;
                        break;

                    case "mcc":
                        fieldMap[ExpensifyColumns.CategoryStandard] = loop;
                        break;

                    case "vat":
                        fieldMap[ExpensifyColumns.VatFloat] = loop;
                        break;

                    case "original currency":
                        fieldMap[ExpensifyColumns.OriginalCurrency] = loop;
                        break;

                    case "original amount":
                        fieldMap[ExpensifyColumns.OriginalCurrencyAmountFloat] = loop;
                        break;

                    case "receipt":
                        fieldMap[ExpensifyColumns.ReceiptUrl] = loop;
                        break;

                    default:
                        // ignore any unknown fields
                        break;
                    }
                }

                ExpensifyColumns[] requiredData =
                {
                    ExpensifyColumns.AmountFloat,
                    ExpensifyColumns.CategoryCustom,
                    ExpensifyColumns.CategoryStandard,
                    ExpensifyColumns.Comment,
                    ExpensifyColumns.Merchant,
                    ExpensifyColumns.OriginalCurrency,
                    ExpensifyColumns.OriginalCurrencyAmountFloat,
                    ExpensifyColumns.ReceiptUrl,
                    ExpensifyColumns.Timestamp
                };

                foreach (ExpensifyColumns requiredColumn in requiredData)
                {
                    if (!fieldMap.ContainsKey(requiredColumn))
                    {
                        // Abort as invalid file

                        GuidCache.Set("Results-" + guidFiles, new AjaxCallExpensifyUploadResult
                        {
                            Success        = false,
                            ErrorType      = "ERR_INVALIDCSV",
                            DisplayMessage = Resources.Pages.Financial.FileExpenseClaim_Expensify_Error_InvalidCsv
                        });

                        progress.Set(100);     // terminate progress bar, causes retrieval of result

                        documents[0].Delete(); // prevents further processing

                        return;                // terminates thread
                    }
                }

                // TODO: Much more general-case error conditions if not all fields are filled

                bool vatEnabled = organization.VatEnabled;

                if (vatEnabled && !fieldMap.ContainsKey(ExpensifyColumns.VatFloat))
                {
                    // Error: Organization needs a VAT field

                    GuidCache.Set("Results-" + guidFiles, new AjaxCallExpensifyUploadResult
                    {
                        Success        = false,
                        ErrorType      = "ERR_NEEDSVAT",
                        DisplayMessage = Resources.Pages.Financial.FileExpenseClaim_Expensify_Error_NeedsVat
                    });

                    progress.Set(100);     // terminate progress bar, causes retrieval of result

                    documents[0].Delete(); // prevents further processing

                    return;                // terminates thread
                }

                List <ExpensifyRecord> recordList = new List <ExpensifyRecord>();

                CsvHelper.Configuration.Configuration config = new CsvHelper.Configuration.Configuration();
                config.HasHeaderRecord = true;

                using (TextReader textReader = new StringReader(csvEntire))
                {
                    CsvReader csvReader = new CsvReader(textReader, config);
                    csvReader.Read(); // bypass header record -- why isn't this done automatically?

                    while (csvReader.Read())
                    {
                        ExpensifyRecord newRecord            = new ExpensifyRecord();
                        Int64           amountExpensifyCents =
                            newRecord.AmountCents =
                                Formatting.ParseDoubleStringAsCents(
                                    csvReader.GetField(fieldMap[ExpensifyColumns.AmountFloat]),
                                    CultureInfo.InvariantCulture);
                        newRecord.OriginalCurrency =
                            Currency.FromCode(csvReader.GetField(fieldMap[ExpensifyColumns.OriginalCurrency]));
                        newRecord.OriginalAmountCents =
                            Formatting.ParseDoubleStringAsCents(
                                csvReader.GetField(fieldMap[ExpensifyColumns.OriginalCurrencyAmountFloat]),
                                CultureInfo.InvariantCulture);
                        newRecord.Timestamp = DateTime.Parse(csvReader.GetField(fieldMap[ExpensifyColumns.Timestamp]));

                        bool amountNeedsTranslation = false;

                        if (newRecord.OriginalCurrency.Identity != organization.Currency.Identity)
                        {
                            amountNeedsTranslation = true;

                            // May or may not be the same as Expensify calculated

                            newRecord.AmountCents =
                                new Money(newRecord.OriginalAmountCents, newRecord.OriginalCurrency, newRecord.Timestamp)
                                .ToCurrency(organization.Currency).Cents;
                        }

                        newRecord.Description = csvReader.GetField(fieldMap[ExpensifyColumns.Merchant]);

                        string comment = csvReader.GetField(fieldMap[ExpensifyColumns.Comment]).Trim();
                        if (!string.IsNullOrEmpty(comment))
                        {
                            newRecord.Description += " / " + comment;
                        }
                        newRecord.CategoryCustom   = csvReader.GetField(fieldMap[ExpensifyColumns.CategoryCustom]);
                        newRecord.CategoryStandard = csvReader.GetField(fieldMap[ExpensifyColumns.CategoryStandard]);
                        newRecord.ReceiptUrl       = csvReader.GetField(fieldMap[ExpensifyColumns.ReceiptUrl]);

                        newRecord.Guid = Guid.NewGuid().ToString();

                        if (vatEnabled)
                        {
                            Int64 vatOriginalCents =
                                Formatting.ParseDoubleStringAsCents(
                                    csvReader.GetField(fieldMap[ExpensifyColumns.VatFloat]),
                                    CultureInfo.InvariantCulture);

                            if (amountNeedsTranslation)
                            {
                                double vatRatio = vatOriginalCents / (double)amountExpensifyCents;
                                newRecord.VatCents = (Int64)(newRecord.AmountCents * vatRatio);
                            }
                            else
                            {
                                newRecord.VatCents = vatOriginalCents;
                            }
                        }

                        recordList.Add(newRecord);
                    }
                }

                progress.Set(10);

                // We now need to get all the receipt images. This is a little tricky as we don't have the URL
                // of the receipt directly, we only have the URL of a webpage that contains JavaScript code
                // to fetch the receipt image.

                // Get relative date part

                string relativePath = Document.DailyStorageFolder.Substring(Document.StorageRoot.Length);

                // Get all receipts

                for (int loop = 0; loop < recordList.Count; loop++)
                {
                    progress.Set(loop * 90 / recordList.Count + 10);

                    using (WebClient client = new WebClient())
                    {
                        string receiptResource = client.DownloadString(recordList[loop].ReceiptUrl);

                        // We now have the web page which holds information about where the actual receipt is located.

                        Regex regex = new Regex(@"\s*var transaction\s*=\s*(?<jsonTxInfo>{.*});", RegexOptions.Multiline);
                        Match match = regex.Match(receiptResource);
                        if (match.Success)
                        {
                            string  txInfoString = match.Groups["jsonTxInfo"].ToString();
                            JObject txInfo       = JObject.Parse(txInfoString);
                            recordList[loop].ExtendedInfo = txInfoString;

                            string expensifyFileName = (string)txInfo["receiptFilename"];
                            string actualReceiptUrl  = "https://s3.amazonaws.com/receipts.expensify.com/" +
                                                       expensifyFileName;
                            string newGuidString = recordList[loop].Guid;

                            string fullyQualifiedFileName = Document.DailyStorageFolder + newGuidString;
                            string relativeFileName       = relativePath + newGuidString;

                            client.DownloadFile(actualReceiptUrl, fullyQualifiedFileName);
                            recordList[loop].ReceiptFileNameHere = newGuidString;

                            // If original file name ends in PDF, initiate conversion.

                            if (expensifyFileName.ToLowerInvariant().EndsWith(".pdf"))
                            {
                                // Convert low resolution

                                Documents docs = new PdfProcessor().RasterizeOne(fullyQualifiedFileName,
                                                                                 recordList[loop].Description, newGuidString, currentUser, organization);

                                recordList[loop].Documents = docs;

                                // Ask backend for high-res conversion

                                RasterizeDocumentHiresOrder backendOrder =
                                    new RasterizeDocumentHiresOrder(docs[0]);
                                backendOrder.Create();
                            }
                            else
                            {
                                Document doc = Document.Create(relativePath + newGuidString, expensifyFileName, 0,
                                                               newGuidString, null,
                                                               currentUser);

                                recordList[loop].Documents = Documents.FromSingle(doc);
                            }
                        }
                    }
                }



                // We now have the individual expenses and all accompanying receipts.
                // Create the expense claim group, then the individual expense records,
                // and assign the Documents to the records and the records to the Group,
                // so the user can review all of it.


                // TODO: Suggest initial budgets

                List <ExpensifyOutputRecord> outputRecords = new List <ExpensifyOutputRecord>();

                string docString =
                    "<a href='/Pages/v5/Support/StreamUpload.aspx?DocId={0}&hq=1' data-caption=\"{1}\" class='FancyBox_Gallery' data-fancybox='{2}'>";

                string documentsAll = String.Empty;

                foreach (ExpensifyRecord record in recordList)
                {
                    foreach (Document document in record.Documents)
                    {
                        documentsAll += String.Format(docString, document.Identity,
                                                      document.ClientFileName.Replace("\"", "'"),
                                                      "D" + record.Documents[0].Identity.ToString(CultureInfo.InvariantCulture));
                    }
                }

                AjaxCallExpensifyUploadResult result = new AjaxCallExpensifyUploadResult
                {
                    Success   = true,
                    Data      = FormatExpensifyOutputRecords(recordList),
                    Footer    = FormatExpensifyFooter(recordList),
                    Documents = documentsAll
                };

                GuidCache.Set("Results-" + guidFiles, result);
                GuidCache.Set("ExpensifyData-" + guidFiles, recordList);
            }
            catch (Exception exception)
            {
                // Abort as exception

                GuidCache.Set("Results-" + guidFiles, new AjaxCallExpensifyUploadResult
                {
                    Success        = false,
                    ErrorType      = "ERR_EXCEPTION",
                    DisplayMessage = exception.ToString()
                });
            }
            finally
            {
                progress.Set(100); // terminate progress bar, causes retrieval of result
            }
        }