< Summary

Information
Class: Services.ParserServices.FielParsers.ExcelExporter
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ParserServices/FielParsers/ExcelExporter.cs
Line coverage
63%
Covered lines: 141
Uncovered lines: 82
Coverable lines: 223
Total lines: 415
Line coverage: 63.2%
Branch coverage
34%
Covered branches: 60
Total branches: 172
Branch coverage: 34.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()57.14%141498.57%
get_PayoutPercent()100%11100%
get_PayoutLowThreshold()100%11100%
get_PayoutMidThreshold()100%11100%
get_PayoutMidFixed()100%11100%
get_PayoutHighFixed()100%11100%
get_ColumnMappings()100%11100%
CalculateClientPayout(...)37.5%14855.55%
GetClientPayoutExplanation(...)40%151063.63%
HasZasyl(...)50%9875%
GetShipmentWarehouseWithZasyl(...)60%111080%
ExportToExcel(...)61.76%803465.85%
ExportToExcel(...)100%11100%
ExportToExcel(...)0%1190340%
AddOrdersToFileData(...)0%506220%
AddColumnValue(...)0%7280%
RoundNumericValue(...)75%88100%
ApplyColumnFormats(...)50%361657.14%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/ParserServices/FielParsers/ExcelExporter.cs

#LineLine coverage
 1using OfficeOpenXml;
 2using OzonDomains.Models;
 3using System.Drawing;
 4
 5
 6namespace Services.ParserServices.FielParsers
 7{
 8    public class ExcelExporter
 9    {
 910        public Dictionary<string, string> ColumnFormats = new Dictionary<string, string>
 911        {
 912            { "Номер заказа", "@" },
 913            { "Клиент", "@" },
 914            { "Принят в обработку", "dd.MM.yyyy HH:mm" },
 915            { "Дата отгрузки", "dd.MM.yyyy HH:mm" },
 916            { "Срок доставки", @"d' д. 'hh' ч.'mm' мин.'" },
 917            { "Статус клиента", "@" },
 918            { "Статус", "@" },
 919            { "Наименование товара", "@" },
 920            { "Артикул", "@" },
 921            { "ЧистыйАртикул", "@" },
 922            { "Производитель", "@" },
 923            { "Склад отгрузки", "@" },
 924            { "Поставщик", "@" },
 925            { "Номер заказа поставщику", "@" },
 926            { "Цена сайта", "#,##0.00" },
 927            { "Цена", "#,##0.00" },
 928            { "Количество", "#,##0.00" },
 929            { "Сумма отправления", "#,##0.00" },
 930            { "Категория", "@" },
 931            { "Объемный вес", "#,##0.00" },
 932            { "Цена закупки", "#,##0.00" },
 933            { "Оригинальная цена", "#,##0.00" },
 934            { "Себестоимость", "#,##0.00" },
 935            { "Комиссия ОЗОН (мин.)", "#,##0.00" },
 936            { "Комиссия ОЗОН (макс.)", "#,##0.00" },
 937            { "Прибыль (мин.)", "#,##0.00" },
 938            { "Прибыль (макс.)", "#,##0.00" },
 939            { "Наценка % (мин.)", "#,##0.00" },
 940            { "Наценка % (макс.)", "#,##0.00" },
 941            { "Город доставки", "@" },
 942            { "Выплата клиенту", "#,##0.00" },
 943            { "Комментарий выплаты клиенту", "@" }
 944        };
 45
 46        // Параметры расчёта выплаты клиенту (могут переопределяться из формы)
 1247        public decimal PayoutPercent { get; set; } = 0.05m;        // 5%
 1348        public decimal PayoutLowThreshold { get; set; } = 5000m;   // до 5 000 ₽
 949        public decimal PayoutMidThreshold { get; set; } = 20000m;  // до 20 000 ₽
 950        public decimal PayoutMidFixed { get; set; } = 250m;        // фикс до 20 000 ₽
 951        public decimal PayoutHighFixed { get; set; } = 500m;       // фикс свыше 20 000 ₽
 52
 4653        public Dictionary<string, Func<Order, object?>> ColumnMappings { get; }
 54
 955        public ExcelExporter()
 56        {
 957            ColumnMappings = new Dictionary<string, Func<Order, object?>>
 958            {
 359                { "Номер заказа", order => order.ShipmentNumber },
 360                { "Клиент", order => order.OzonClient?.Name },
 161                { "Принят в обработку", order => order.FullFormattedProcessingDate },
 162                { "Дата отгрузки", order => order.FullFormattedShippingDate },
 163                { "Срок доставки", order => order.FormattedDeliveryPeriod },
 164                { "Статус клиента", order => order.Status },
 165                { "Статус", order => order.AppStatus?.Name },
 366                { "Наименование товара", order => order.ProductName },
 367                { "Артикул", order => order.Article },
 168                { "ЧистыйАртикул", order => order.CleanedArticle },
 369                { "Производитель", order => order.EtProducer?.Name },
 070                { "Склад отгрузки", order => GetShipmentWarehouseWithZasyl(order) },
 171                { "Поставщик", order => order.Supplier?.Name },
 172                { "Номер заказа поставщику", order => order.OrderNumberToSupplier },
 173                { "Цена сайта", order => order.ProductInfo?.CurrentPriceWithDiscount },
 174                { "Цена", order => order.Price },
 375                { "Количество", order => order.Quantity },
 376                { "Сумма отправления", order => order.ShipmentAmount },
 177                { "Категория", order => order.ProductInfo?.CommercialCategory },
 178                { "Объемный вес", order => order.ProductInfo?.VolumetricWeight },
 179                { "Цена закупки", order => order.PurchasePrice },
 180                { "Оригинальная цена", order => order.OriginalPurchasePrice },
 181                { "Себестоимость", order => order.CostPrice },
 182                { "Комиссия ОЗОН (мин.)", order => order.MinOzonCommission },
 183                { "Комиссия ОЗОН (макс.)", order => order.MaxOzonCommission },
 184                { "Прибыль (мин.)", order => order.MinProfit },
 185                { "Прибыль (макс.)", order => order.MaxProfit },
 186                { "Наценка % (мин.)", order => order.MinDiscount },
 187                { "Наценка % (макс.)", order => order.MaxDiscount },
 188                { "Город доставки", order => order.DeliveryCity },
 189                { "Выплата клиенту", order => CalculateClientPayout(order) },
 190                { "Комментарий выплаты клиенту", order => GetClientPayoutExplanation(order) }
 991            };
 992        }
 93
 94        private decimal? CalculateClientPayout(Order order)
 95        {
 296            if (order == null) return null;
 97
 298            var price = order.Price ?? 0m;
 299            if (price <= 0m)
 0100                return null;
 101
 2102            if (price <= PayoutLowThreshold)
 2103                return Math.Round(price * PayoutPercent, 2, MidpointRounding.AwayFromZero);
 104
 0105            if (price <= PayoutMidThreshold)
 0106                return PayoutMidFixed;
 107
 0108            return PayoutHighFixed;
 109        }
 110
 111        private string GetClientPayoutExplanation(Order order)
 112        {
 1113            if (order == null) return string.Empty;
 1114            var price = order.Price ?? 0m;
 1115            var payout = CalculateClientPayout(order);
 116
 1117            if (price <= 0m || payout == null)
 0118                return "Цена не указана — выплата не рассчитывается";
 119
 1120            if (price <= PayoutLowThreshold)
 121            {
 1122                var percent = PayoutPercent * 100m;
 1123                return $"{percent:F2}% от {price:F2} ₽ = {payout.Value:F2} ₽ (заказ до {PayoutLowThreshold:F2} ₽)";
 124            }
 125
 0126            if (price <= PayoutMidThreshold)
 127            {
 0128                return $"Фиксированная выплата {PayoutMidFixed:F2} ₽ (стоимость {price:F2} ₽, до {PayoutMidThreshold:F2}
 129            }
 130
 0131            return $"Фиксированная выплата {PayoutHighFixed:F2} ₽ (стоимость {price:F2} ₽, свыше {PayoutMidThreshold:F2}
 132        }
 133
 134        private static bool HasZasyl(Order order, out string? zasylValue)
 135        {
 2136            zasylValue = null;
 2137            if (order == null) return false;
 138
 2139            var zasyl = order.OrderExtensions?
 2140                .FirstOrDefault(e => e.Key == "Засыл")?.Value;
 141
 2142            if (string.IsNullOrWhiteSpace(zasyl))
 2143                return false;
 144
 0145            zasylValue = zasyl;
 0146            return true;
 147        }
 148
 149        private static string GetShipmentWarehouseWithZasyl(Order order)
 150        {
 1151            if (order == null) return string.Empty;
 152
 1153            var baseName = order.ShipmentWarehouse?.Name ?? string.Empty;
 154
 155            // Ищем расширение с ключом "Засыл", как на главной странице.
 156            // Если оно есть — возвращаем ТОЛЬКО его, без названия склада.
 1157            if (HasZasyl(order, out var zasyl) && !string.IsNullOrEmpty(zasyl))
 0158                return zasyl!;
 159
 1160            return baseName;
 161        }
 162
 163        public byte[] ExportToExcel(List<Order> orders, List<string> columnsToExport)
 164        {
 2165            using (var package = new ExcelPackage())
 166            {
 2167                var worksheet = package.Workbook.Worksheets.Add("Orders");
 168
 2169                int colIndex = 1;
 82170                for (int i = 0; i < columnsToExport.Count; i++)
 171                {
 39172                    var columnName = columnsToExport[i];
 173
 39174                    if (columnName == "Комиссия ОЗОН" || columnName == "Прибыль" || columnName == "Наценка %")
 175                    {
 0176                        worksheet.Cells[1, colIndex].Value = columnName + " (мин.)";
 0177                        colIndex++;
 0178                        worksheet.Cells[1, colIndex].Value = columnName + " (макс.)";
 0179                        colIndex++;
 180                    }
 181                    else
 182                    {
 39183                        worksheet.Cells[1, colIndex].Value = columnName;
 39184                        colIndex++;
 185                    }
 186                }
 187
 2188                int row = 2;
 10189                foreach (var order in orders)
 190                {
 3191                    colIndex = 1;
 98192                    foreach (var columnName in columnsToExport)
 193                    {
 46194                        if (columnName == "Комиссия ОЗОН" || columnName == "Прибыль" || columnName == "Наценка %")
 195                        {
 0196                            if (ColumnMappings.TryGetValue(columnName + " (мин.)", out var getMinValue) && getMinValue !
 197                            {
 0198                                worksheet.Cells[row, colIndex].Value = RoundNumericValue(getMinValue(order));
 0199                                colIndex++;
 200                            }
 201
 0202                            if (ColumnMappings.TryGetValue(columnName + " (макс.)",
 0203                                    out var getMaxValue) && getMaxValue != null)
 204                            {
 0205                                worksheet.Cells[row, colIndex].Value = RoundNumericValue(getMaxValue(order));
 0206                                colIndex++;
 207                            }
 208                        }
 46209                        else if (columnName == "Склад отгрузки")
 210                        {
 211                            // Особый случай: если есть "Засыл", печатаем его вместо склада
 212                            // и подсвечиваем ячейку жёлтым цветом.
 1213                            var text = GetShipmentWarehouseWithZasyl(order);
 1214                            worksheet.Cells[row, colIndex].Value = text;
 215
 1216                            if (HasZasyl(order, out _))
 217                            {
 0218                                var cell = worksheet.Cells[row, colIndex];
 0219                                cell.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid;
 0220                                cell.Style.Fill.BackgroundColor.SetColor(Color.Yellow);
 221                            }
 222
 1223                            colIndex++;
 224                        }
 45225                        else if (ColumnMappings.TryGetValue(columnName, out var getValue) && getValue != null)
 226                        {
 45227                            object? value = getValue(order);
 45228                            worksheet.Cells[row, colIndex].Value = RoundNumericValue(value);
 45229                            colIndex++;
 230                        }
 231                    }
 232
 3233                    row++;
 234                }
 235
 2236                ApplyColumnFormats(worksheet, columnsToExport, row - 1);
 2237                worksheet.Cells.AutoFitColumns();
 2238                return package.GetAsByteArray();
 239            }
 2240        }
 241
 242        public byte[] ExportToExcel(List<Order> orders)
 243        {
 1244            List<string> columnsToExport = ColumnMappings.Keys.ToList();
 1245            return ExportToExcel(orders, columnsToExport);
 246        }
 247
 248        public byte[] ExportToExcel(List<Dictionary<string, string>> data)
 249        {
 0250            using (var package = new ExcelPackage())
 251            {
 0252                var worksheet = package.Workbook.Worksheets.Add("Data");
 253
 254                // Если нет данных, возвращаем пустой файл
 0255                if (data == null || data.Count == 0)
 256                {
 0257                    return package.GetAsByteArray();
 258                }
 259
 260                // Получаем все уникальные ключи из всех словарей
 0261                var allKeys = data.SelectMany(d => d.Keys).Distinct().ToList();
 262
 263                // Создаем заголовки столбцов
 0264                int colIndex = 1;
 0265                foreach (var key in allKeys)
 266                {
 267                    // Обработка специальных полей с мин/макс значениями
 0268                    if (key == "Комиссия ОЗОН" || key == "Прибыль" || key == "Наценка %")
 269                    {
 0270                        worksheet.Cells[1, colIndex].Value = key + " (мин.)";
 0271                        colIndex++;
 0272                        worksheet.Cells[1, colIndex].Value = key + " (макс.)";
 0273                        colIndex++;
 274                    }
 275                    else
 276                    {
 0277                        worksheet.Cells[1, colIndex].Value = key;
 0278                        colIndex++;
 279                    }
 280                }
 281
 282                // Заполняем данные
 0283                int row = 2;
 0284                foreach (var item in data)
 285                {
 0286                    colIndex = 1;
 0287                    foreach (var key in allKeys)
 288                    {
 0289                        if (key == "Комиссия ОЗОН" || key == "Прибыль" || key == "Наценка %")
 290                        {
 291                            // Для полей с мин/макс значениями
 0292                            if (item.TryGetValue(key + " (мин.)", out var minValue) && minValue != null)
 293                            {
 0294                                worksheet.Cells[row, colIndex].Value = minValue;
 295                            }
 296
 0297                            colIndex++;
 298
 0299                            if (item.TryGetValue(key + " (макс.)", out var maxValue) && maxValue != null)
 300                            {
 0301                                worksheet.Cells[row, colIndex].Value = maxValue;
 302                            }
 303
 0304                            colIndex++;
 305                        }
 306                        else
 307                        {
 0308                            if (item.TryGetValue(key, out var value) && value != null)
 309                            {
 0310                                worksheet.Cells[row, colIndex].Value = value;
 311                            }
 312
 0313                            colIndex++;
 314                        }
 315                    }
 316
 0317                    row++;
 318                }
 319
 0320                worksheet.Cells.AutoFitColumns();
 0321                return package.GetAsByteArray();
 322            }
 0323        }
 324
 325        public List<Dictionary<string, string>> AddOrdersToFileData(
 326            List<Dictionary<string, string>> standartFileData,
 327            Dictionary<int, Order> indexOrderMap,
 328            List<string> addColumnsList)
 329        {
 0330            if (standartFileData == null || indexOrderMap == null || addColumnsList == null)
 0331                return standartFileData ?? new List<Dictionary<string, string>>();
 332
 0333            foreach (var kvp in indexOrderMap)
 334            {
 0335                var index = kvp.Key;
 0336                var order = kvp.Value;
 337
 0338                if (index < 0 || index >= standartFileData.Count)
 339                    continue;
 340
 0341                var rowDict = standartFileData[index];
 342
 0343                foreach (var column in addColumnsList)
 344                {
 0345                    if (column == "Комиссия ОЗОН" || column == "Прибыль" || column == "Наценка %")
 346                    {
 0347                        AddColumnValue(rowDict, column + " (мин.)", order, ColumnMappings);
 0348                        AddColumnValue(rowDict, column + " (макс.)", order, ColumnMappings);
 349                    }
 350                    else
 351                    {
 0352                        AddColumnValue(rowDict, column, order, ColumnMappings);
 353                    }
 354                }
 355            }
 356
 0357            return standartFileData;
 358        }
 359
 360        private void AddColumnValue(
 361            Dictionary<string, string> rowDict,
 362            string columnName,
 363            Order order,
 364            Dictionary<string, Func<Order, object?>> mappings)
 365        {
 0366            if (mappings.TryGetValue(columnName, out var getValue) && getValue != null)
 367            {
 0368                var value = getValue(order)?.ToString();
 0369                if (!string.IsNullOrEmpty(value))
 370                {
 0371                    rowDict[columnName] = value;
 372                }
 373            }
 0374        }
 375
 376        private static object? RoundNumericValue(object? value)
 377        {
 72378            if (value == null) return null;
 20379            if (value is decimal d) return Math.Round(d, 2);
 16380            if (value is double db) return Math.Round(db, 2);
 16381            if (value is float f) return Math.Round(f, 2);
 16382            return value;
 383        }
 384
 385        private void ApplyColumnFormats(ExcelWorksheet worksheet, List<string> columnsToExport, int lastDataRow)
 386        {
 2387            if (lastDataRow < 2) return;
 2388            int colIndex = 1;
 82389            foreach (var columnName in columnsToExport)
 390            {
 39391                if (columnName == "Комиссия ОЗОН" || columnName == "Прибыль" || columnName == "Наценка %")
 392                {
 0393                    if (ColumnFormats.TryGetValue(columnName + " (мин.)", out var formatMin))
 394                    {
 0395                        worksheet.Cells[2, colIndex, lastDataRow, colIndex].Style.Numberformat.Format = formatMin;
 396                    }
 0397                    colIndex++;
 0398                    if (ColumnFormats.TryGetValue(columnName + " (макс.)", out var formatMax))
 399                    {
 0400                        worksheet.Cells[2, colIndex, lastDataRow, colIndex].Style.Numberformat.Format = formatMax;
 401                    }
 0402                    colIndex++;
 403                }
 404                else
 405                {
 39406                    if (ColumnFormats.TryGetValue(columnName, out var format))
 407                    {
 39408                        worksheet.Cells[2, colIndex, lastDataRow, colIndex].Style.Numberformat.Format = format;
 409                    }
 39410                    colIndex++;
 411                }
 412            }
 2413        }
 414    }
 415}