From 28a669ec4d5e31241753f9f57bb159485b04ea9f Mon Sep 17 00:00:00 2001 From: serty Date: Tue, 22 Jul 2025 22:39:59 +0000 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20GuestBill.cs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Пречек без процентов скидок-надбавок. Только название и сумма --- GuestBill.cs | 674 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 674 insertions(+) create mode 100644 GuestBill.cs diff --git a/GuestBill.cs b/GuestBill.cs new file mode 100644 index 0000000..85b6b42 --- /dev/null +++ b/GuestBill.cs @@ -0,0 +1,674 @@ +@using System +@using System.Collections.Generic +@using System.Globalization +@using System.Linq +@using Resto.Front.PrintTemplates.Cheques.Razor +@using Resto.Front.PrintTemplates.Cheques.Razor.TemplateModels + +@inherits TemplateBase +@{ + var order = Model.Order; +} + + + @* Header (begin) *@ + + @if (Model.AdditionalServiceChequeInfo == null) + { + if (Model.RepeatBillNumber == 0) + { +
@Resources.BillHeaderTitle
+ } + else + { +
@string.Format(Resources.RepeateBillHeaderTitleFormat, Model.RepeatBillNumber)
+ } + } + + + + @if (Model.AdditionalServiceChequeInfo != null) + { + @string.Format(Resources.AdditionalServiceHeaderOrderItemsAddedPattern, FormatLongDateTime(Model.CommonInfo.CurrentTime)) + } + + @if (!string.IsNullOrWhiteSpace(order.ExternalNumber)) + { + @string.Format(Resources.BillHeaderOrderExternalNumberPattern, order.ExternalNumber) + } + + @string.Format(Resources.BillHeaderWaiterPattern, order.Waiter.GetNameOrEmpty()) + + @foreach (var clientInfo in + from discountItem in order.DiscountItems + where discountItem.CardInfo != null + select discountItem.CardInfo into cardInfo + select string.IsNullOrWhiteSpace(cardInfo.MaskedCard) ? cardInfo.Owner : string.Format("{0} ({1})", cardInfo.Owner, cardInfo.MaskedCard) into clientInfo + where !string.IsNullOrWhiteSpace(clientInfo) + select clientInfo) + { + @string.Format(Resources.ClientFormat, clientInfo) + } + + @if (Model.AdditionalServiceChequeInfo == null) + { + @Raw(string.Join(Environment.NewLine, Model.Extensions.AfterHeader)) + } + + @if (Model.AdditionalServiceChequeInfo != null) + { + if (order.ClientBinding != null && !string.IsNullOrWhiteSpace(order.ClientBinding.CardNumber)) + { + @string.Format(Resources.CardPattern, order.ClientBinding.CardNumber) + } + +
@Resources.AdditionalServiceHeaderTitle
+ } + @* Header (end) *@ + + @* Body (begin) *@ + + + + + + + + + @Guests() + + @Summaries() + +
+ @* Body (end) *@ + + @* Footer (begin) *@ + + @if (Model.AdditionalServiceChequeInfo == null) + { + @Raw(string.Join(Environment.NewLine, Model.Extensions.BeforeFooter)) + } +
@Model.CommonInfo.CafeSetup.BillFooter
+ + + @if (Model.AdditionalServiceChequeInfo == null) + { + @Raw(string.Join(Environment.NewLine, Model.Extensions.AfterFooter)) + } + + @* Footer (end) *@ +
+ +@helper Guests() +{ + var order = Model.Order; + Func orderItemsFilter; + if (Model.AdditionalServiceChequeInfo != null) + { + orderItemsFilter = orderItem => Model.AdditionalServiceChequeInfo.AddedOrderItems.Contains(orderItem); + } + else + { + orderItemsFilter = orderItem => orderItem.DeletionInfo == null; + } + + var guestsWithItems = Model.Order.Table.Section.DisplayGuests + ? order.Guests.Select(guest => new + { + Guest = guest, + Items = guest.Items.Where(item => orderItemsFilter(item) && OrderItemsToPrintFilter(item, order.DiscountItems) && OrderItemsPrechequePrintableFilter(item)) + }) + .Where(guestWithItems => guestWithItems.Items.Any()).ToList() + : EnumerableEx.Return(new + { + Guest = order.Guests.FirstOrDefault(), + Items = order.Guests.SelectMany(g => g.Items.Where(item => orderItemsFilter(item) && OrderItemsToPrintFilter(item, order.DiscountItems) && OrderItemsPrechequePrintableFilter(item))) + }) + .Where(guestWithItems => guestWithItems.Items.Any()).ToList(); + + if (!guestsWithItems.Any()) + { + return; + } + + + @Resources.NameColumnHeader + @Resources.ProductAmount + + @Resources.ResultSum + + + if (guestsWithItems.Count == 1) + { + @SingleGuest(guestsWithItems.Single().Items.ToList()) + } + else + { + @OneOfMultipleGuests(guestsWithItems.First().Guest, guestsWithItems.First().Items.ToList()) + foreach (var guestWithItems in guestsWithItems.Skip(1)) + { + + @OneOfMultipleGuests(guestWithItems.Guest, guestWithItems.Items.ToList()) + } + } +} + +@helper SingleGuest(IEnumerable items) +{ + foreach (var comboGroup in items.OrderBy(i => i.OrderRank).GroupBy(i => i.Combo)) + { + var combo = comboGroup.Key; + var isPartOfCombo = combo != null; + var additionalSpace = isPartOfCombo ? " " : string.Empty; + if (isPartOfCombo) + { + @combo.Name + @FormatAmount(combo.Amount) + + @FormatMoney(combo.Price * combo.Amount) + } + foreach (var orderItemGroup in comboGroup.OrderBy(item => item.OrderRank).GroupBy(_ => _, CreateComparer(AreOrderItemsEqual))) + { + var totalAmount = orderItemGroup.Sum(orderItem => orderItem.Amount); + var totalCost = orderItemGroup.Sum(orderItem => orderItem.Cost); + + var productItem = orderItemGroup.Key as IProductItem; + if (productItem != null && productItem.CompoundsInfo != null && productItem.CompoundsInfo.IsPrimaryComponent) + { + @(additionalSpace + string.Format("{0} {1}", productItem.CompoundsInfo.ModifierSchemaName, productItem.ProductSize == null ? string.Empty : productItem.ProductSize.Name)) + + + // для разделенной пиццы комменты печатаем под схемой + if (Model.Order.Table.Section.PrintProductItemCommentInCheque && productItem.Comment != null && !productItem.Comment.Deleted) + { + + + + + + + + + @(additionalSpace + productItem.Comment.Text) + +
+
+ + } + + // у пиццы не может быть удаленных модификаторов, поэтому берем весь список + foreach (var orderEntry in productItem.ModifierEntries.Where(orderEntry => ModifiersFilter(orderEntry, productItem, true))) + { + var productName = orderEntry.ProductCustomName ?? orderEntry.Product.Name; + @(additionalSpace + " " + productName) + + if (orderEntry.Amount != 1m) + { + @FormatAmount(orderEntry.Amount) + } + else + { + + } + + + if (orderEntry.Cost != 0m) + { + @FormatMoney(orderEntry.Cost) + } + else + { + + } + + @PrintOrderEntryAllergens(orderEntry) + @CategorizedDiscountsForOrderEntryGroup(new[] { orderEntry }, isPartOfCombo) + } + } + + if (productItem != null && productItem.CompoundsInfo != null) + { + @(additionalSpace + " 1/2 " + GetOrderEntryNameWithProductSize(productItem)) + } + else + { + @(additionalSpace + GetOrderEntryNameWithProductSize(orderItemGroup.Key)) + } + + @FormatAmount(totalAmount) + if (!isPartOfCombo) + { + + @FormatMoney(totalCost) + } + else + { + + } + + if (productItem != null) + { + @PrintOrderEntryAllergens(productItem) + } + + @CategorizedDiscountsForOrderEntryGroup(orderItemGroup.ToList(), isPartOfCombo) + + // здесь комменты для обычных блюд и целых пицц + if (Model.Order.Table.Section.PrintProductItemCommentInCheque && productItem != null && productItem.Comment != null && !productItem.Comment.Deleted && productItem.CompoundsInfo == null) + { + + + + + + + + + @(additionalSpace + productItem.Comment.Text) + +
+
+ + } + + foreach (var orderEntry in orderItemGroup.Key.GetNotDeletedChildren().Where(orderEntry => ModifiersFilter(orderEntry, orderItemGroup.Key)).ToList()) + { + var productName = orderEntry.ProductCustomName ?? orderEntry.Product.Name; + @(additionalSpace + " " + productName) + + if (orderEntry.Amount != 1m) + { + @FormatAmount(orderEntry.Amount) + } + else + { + + } + + + if (orderEntry.Cost != 0m) + { + @FormatMoney(orderEntry.Cost) + } + else + { + + } + + @PrintOrderEntryAllergens(orderEntry) + @CategorizedDiscountsForOrderEntryGroup(new[] { orderEntry }, isPartOfCombo) + } + } + } +} + +@helper CategorizedDiscountsForOrderEntryGroup(ICollection entries, bool isPartOfCombo) +{ + var orderEntry = entries.First(); + var additionalSpace = isPartOfCombo ? " " : string.Empty; + if (orderEntry.Cost != 0m) + { + var categorizedDiscounts = from discountItem in Model.Order.DiscountItems + where discountItem.IsCategorized && discountItem.PrintDetailedInPrecheque + let discountSum = entries.Sum(entry => discountItem.GetDiscountSumFor(entry)) + where discountSum != 0m + select new + { + IsDiscount = discountSum > 0m, + Sum = Math.Abs(discountSum), + Percent = Math.Abs(CalculatePercent(entries.Sum(entry => entry.Cost), discountSum)), + Name = discountItem.Type.PrintableName, + discountItem.Type.DiscountBySum + } into discount + orderby discount.IsDiscount descending + select discount; + + foreach (var categorizedDiscount in categorizedDiscounts) + { + + @(additionalSpace + GetFormattedDiscountDescriptionForOrderItem(categorizedDiscount.IsDiscount, categorizedDiscount.Name, categorizedDiscount.DiscountBySum, categorizedDiscount.Percent)) + + @GetFormattedDiscountSum(categorizedDiscount.IsDiscount, categorizedDiscount.Sum) + } + } +} + +@helper OneOfMultipleGuests(IGuest guest, ICollection items) +{ + @guest.Name + @SingleGuest(items) + + + + var includedEntries = items.SelectMany(item => item.ExpandIncludedEntries()).ToList(); + var total = includedEntries.Sum(orderEntry => orderEntry.Cost); + var totalWithoutCategorizedDiscounts = total - (from orderEntry in includedEntries + from discountItem in Model.Order.DiscountItems + where discountItem.IsCategorized + select discountItem.GetDiscountSumFor(orderEntry)).Sum(); + + var totalWithoutDiscounts = totalWithoutCategorizedDiscounts - (from orderEntry in includedEntries + from discountItem in Model.Order.DiscountItems + where !discountItem.IsCategorized + select discountItem.GetDiscountSumFor(orderEntry)).Sum(); + + if (totalWithoutCategorizedDiscounts != totalWithoutDiscounts) + { + @Resources.BillFooterTotalPlain + @FormatMoney(totalWithoutCategorizedDiscounts) + + var nonCategorizedDiscounts = from discountItem in Model.Order.DiscountItems + where !discountItem.IsCategorized + let discountSum = includedEntries.Sum(orderEntry => discountItem.GetDiscountSumFor(orderEntry)) + select new + { + IsDiscount = discountSum > 0m, + Sum = Math.Abs(discountSum), + Percent = Math.Abs(CalculatePercent(includedEntries.Sum(entry => entry.Cost), discountSum)), + Name = discountItem.Type.PrintableName, + discountItem.Type.DiscountBySum + } into discount + orderby discount.IsDiscount descending + select discount; + + foreach (var nonCategorizedDiscount in nonCategorizedDiscounts) + { + + @(nonCategorizedDiscount.DiscountBySum + ? GetFormattedDiscountDescriptionShort(nonCategorizedDiscount.IsDiscount, nonCategorizedDiscount.Name) + : GetFormattedDiscountDescriptionDetailed(nonCategorizedDiscount.IsDiscount, nonCategorizedDiscount.Name, nonCategorizedDiscount.Percent)) + + @GetFormattedDiscountSum(nonCategorizedDiscount.IsDiscount, nonCategorizedDiscount.Sum) + } + } + + @string.Format(Model.AdditionalServiceChequeInfo == null ? Resources.BillFooterTotalGuestPattern : Resources.AdditionalServiceFooterTotalGuestPattern, guest.Name) + @FormatMoney(totalWithoutDiscounts) +} + +@helper Summaries() +{ + var order = Model.Order; + var fullSum = order.GetFullSum() - order.DiscountItems.Where(di => !di.Type.PrintProductItemInPrecheque).Sum(di => di.GetDiscountSum()); + var categorizedDiscountItems = new List(); + var nonCategorizedDiscountItems = new List(); + + foreach (var discountItem in order.DiscountItems.Where(di => di.Type.PrintProductItemInPrecheque && di.DiscountSums.Count > 0)) + { + if (discountItem.IsCategorized) + { + categorizedDiscountItems.Add(discountItem); + } + else + { + nonCategorizedDiscountItems.Add(discountItem); + } + } + + var subTotal = fullSum - categorizedDiscountItems.Sum(di => di.GetDiscountSum()); + var totalWithoutDiscounts = subTotal - nonCategorizedDiscountItems.Sum(di => di.GetDiscountSum()); + var prepay = order.PrePayments.Sum(prepayItem => prepayItem.Sum); + var total = Math.Max(totalWithoutDiscounts + order.GetVatSumExcludedFromPrice() - prepay, 0m); + + if (Model.DiscountMarketingCampaigns != null) + { + total -= Model.DiscountMarketingCampaigns.TotalDiscount; + totalWithoutDiscounts -= Model.DiscountMarketingCampaigns.TotalDiscount; + } + + var vatSumsByVat = (Model.AdditionalServiceChequeInfo == null + ? order.GetIncludedEntries() + : Model.AdditionalServiceChequeInfo.AddedOrderItems.SelectMany(item => item.ExpandIncludedEntries())) + .Where(orderEntry => !orderEntry.VatIncludedInPrice) + .GroupBy(orderEntry => orderEntry.Vat) + .Where(group => group.Key != 0m) + .Select(group => new { Vat = group.Key, Sum = group.Sum(orderEntry => orderEntry.ExcludedVat) }) + .ToList(); + + var vatSum = vatSumsByVat.Sum(vatWithSum => vatWithSum.Sum); + + if ((prepay != 0m || fullSum != total) && Model.AdditionalServiceChequeInfo == null) + { + @Resources.BillFooterFullSum + @FormatMoney(fullSum) + } + + @PrintOrderDiscounts(categorizedDiscountItems, fullSum) + + if (categorizedDiscountItems.Any()) + { + @Resources.BillFooterTotalPlain + @FormatMoney(subTotal) + } + + @PrintOrderDiscounts(nonCategorizedDiscountItems, fullSum) + + if (Model.DiscountMarketingCampaigns != null) + { + foreach (var discountMarketingCampaign in Model.DiscountMarketingCampaigns.Campaigns) + { + @discountMarketingCampaign.Name + @("-" + FormatMoney(discountMarketingCampaign.TotalDiscount)) + } + } + + if (prepay != 0m && (categorizedDiscountItems.Any() || nonCategorizedDiscountItems.Any())) + { + @Resources.BillFooterTotalWithoutDiscounts + @FormatMoney(totalWithoutDiscounts) + } + + if (vatSum != 0m) + { + foreach (var vatWithSum in vatSumsByVat) + { + @string.Format(Resources.VatFormat, vatWithSum.Vat) + @string.Format(FormatMoney(vatWithSum.Sum)) + } + if (vatSumsByVat.Count > 1) + { + @Resources.VatSum + @FormatMoney(vatSum) + } + } + + if (Model.AdditionalServiceChequeInfo != null) + { + @Resources.AdditionalServiceAddedFooterTotalUpper + @FormatMoney(Model.AdditionalServiceChequeInfo.AddedOrderItems.SelectMany(item => item.ExpandIncludedEntries()).Sum(orderEntry => orderEntry.Cost) + vatSum) + } + + if (prepay != 0m) + { + @Resources.Prepay + @FormatMoney(prepay) + } + + @(Model.AdditionalServiceChequeInfo == null ? Resources.BillFooterTotal : Resources.AdditionalServiceFooterTotalUpper) + @FormatMoney(total) + foreach (var rate in order.FixedCurrencyRates) + { + var currencyIsoName = rate.Key.IsoName; + var defaultCurrencyIsoName = Model.CommonInfo.CafeSetup.CurrencyIsoName; + + + @string.Format(Resources.CurrencyRateFormat, currencyIsoName, rate.Value.ToString("f4", CultureInfo.CurrentCulture), defaultCurrencyIsoName) + + + + @string.Format(Resources.CurrencyFormat, currencyIsoName, FormatMoney(GetSumInAdditionalCurrency(rate.Key, rate.Value, total), currencyIsoName)) + } + + if (Model.AdditionalServiceChequeInfo != null && order.ClientBinding != null && order.ClientBinding.PaymentLimit.HasValue) + { + @Resources.AdditionalServiceLimit + @FormatMoney(order.ClientBinding.PaymentLimit.Value - total) + } + + if (Model.DiscountMarketingCampaigns != null) + { + foreach (var discountMarketingCampaign in Model.DiscountMarketingCampaigns.Campaigns.Where(campaign => !string.IsNullOrWhiteSpace(campaign.BillComment))) + { + @discountMarketingCampaign.BillComment + } + } +} + +@helper PrintOrderDiscounts(IEnumerable discountItems, decimal fullSum) +{ + foreach (var discountItem in discountItems.OrderByDescending(discountItem => discountItem.IsDiscount())) + { + + @GetFormattedDiscountDescriptionShort(discountItem.IsDiscount(), discountItem.Type.PrintableName) + + @GetFormattedDiscountSum(discountItem.IsDiscount(), Math.Abs(discountItem.GetDiscountSum())) + } +} + +@helper PrintOrderEntryAllergens(IOrderEntry orderEntry) +{ + var totalAllergens = orderEntry.Allergens.ToArray(); + if (totalAllergens.Any()) + { + @( string.Format(Resources.AllergenGroupsFormat, string.Join(", ", totalAllergens)) ) + } +} + +@functions +{ + /// + /// Отфильтровывает флаерные блюда, для которых нет настройки "Печать в пречеке блюд по флаеру" + /// + private static bool OrderItemsToPrintFilter(IOrderItem orderItem, IEnumerable discountItems) + { + return !(orderItem is IProductItem) || discountItems + .Where(discountItem => discountItem.DiscountSums.ContainsKey(orderItem)) + .All(discountItem => discountItem.Type.PrintProductItemInPrecheque); + } + private static bool OrderItemsPrechequePrintableFilter(IOrderItem orderItem) + { + if (orderItem.Cost != 0m) + return true; + return orderItem.Product.PrechequePrintable; + } + private bool AreOrderItemsEqual(IOrderItem x, IOrderItem y) + { + if (ReferenceEquals(x, y)) + return true; + if (x == null) + return y == null; + if (y == null) + return false; + + var xProductItem = x as IProductItem; + var yProductItem = y as IProductItem; + + if (xProductItem == null || yProductItem == null || !ProductItemCanBeMerged(xProductItem) || !ProductItemCanBeMerged(yProductItem)) + return false; + if (xProductItem.Product.Name != yProductItem.Product.Name) + return false; + if (xProductItem.ProductCustomName != yProductItem.ProductCustomName) + return false; + if (xProductItem.ProductSize == null ^ yProductItem.ProductSize == null) + return false; + if (xProductItem.ProductSize != null && yProductItem.ProductSize != null && xProductItem.ProductSize.Name != yProductItem.ProductSize.Name) + return false; + if (xProductItem.Price != yProductItem.Price) + return false; + if (xProductItem.Price == 0m) + return true; + + var categorizedDiscounts = Model.Order.DiscountItems + .Where(discountItem => discountItem.IsCategorized && discountItem.PrintDetailedInPrecheque && discountItem.DiscountSums.Count > 0) + .ToList(); + + var xCategorizedDiscountItems = categorizedDiscounts.Where(discountItem => discountItem.DiscountSums.ContainsKey(x)); + var yCategorizedDiscountItems = categorizedDiscounts.Where(discountItem => discountItem.DiscountSums.ContainsKey(y)); + + return new HashSet(xCategorizedDiscountItems).SetEquals(yCategorizedDiscountItems); + } + + private bool ProductItemCanBeMerged(IProductItem productItem) + { + return productItem.CompoundsInfo == null && + productItem.Amount - Math.Truncate(productItem.Amount) == 0m && + productItem.GetNotDeletedChildren().Where(orderEntry => ModifiersFilter(orderEntry, productItem)).IsEmpty() && + (productItem.Comment == null || productItem.Comment.Deleted || !Model.Order.Table.Section.PrintProductItemCommentInCheque); + } + + private static bool CommonModifiersFilter(bool isCommonModifier, IProductItem parent, bool onlyCommonModifiers) + { + if (parent.CompoundsInfo != null && parent.CompoundsInfo.IsPrimaryComponent) + { + if (onlyCommonModifiers && !isCommonModifier) + return false; + if (!onlyCommonModifiers && isCommonModifier) + return false; + return true; + } + return true; + } + + private static bool ModifiersFilter(IOrderEntry orderEntry, IOrderItem parent, bool onlyCommonModifiers = false) + { + var parentProductItem = parent as IProductItem; + if (parentProductItem != null && !CommonModifiersFilter(((IModifierEntry)orderEntry).IsCommonModifier, parentProductItem, onlyCommonModifiers)) + return false; + + if (orderEntry.Cost > 0m) + return true; + + if (!orderEntry.Product.PrechequePrintable) + return false; + + var modifierEntry = orderEntry as IModifierEntry; + if (modifierEntry == null) + return true; + + if (modifierEntry.ChildModifier == null) + return true; + + if (!modifierEntry.ChildModifier.HideIfDefaultAmount) + return true; + + var amountPerItem = modifierEntry.ChildModifier.AmountIndependentOfParentAmount + ? modifierEntry.Amount + : modifierEntry.Amount / GetParentAmount(parent, onlyCommonModifiers); + + return amountPerItem != modifierEntry.ChildModifier.DefaultAmount; + } + + private static string GetFormattedDiscountDescriptionForOrderItem(bool isDiscount, string discountName, bool discountBySum, decimal absolutePercent) + { + return string.Format(" {0}", discountName); + } + + private static string GetFormattedDiscountDescriptionShort(bool isDiscount, string discountName) + { + return string.Format(discountName); + } + + private static string GetFormattedDiscountDescriptionDetailed(bool isDiscount, string discountName, decimal absolutePercent) + { + return string.Format(discountName); + } + + private static string GetFormattedDiscountSum(bool isDiscount, decimal absoluteSum) + { + return (isDiscount ? "-" : "+") + FormatMoney(absoluteSum); + } + + private static string GetOrderEntryNameWithProductSize(IOrderEntry orderEntry) + { + var productName = orderEntry.ProductCustomName ?? orderEntry.Product.Name; + var productItem = orderEntry as IProductItem; + return (productItem == null || productItem.ProductSize == null || productItem.CompoundsInfo != null) + ? productName + : string.Format(Resources.ProductNameWithSizeFormat, productName, productItem.ProductSize.Name); + } + + private static decimal GetParentAmount(IOrderItem parent, bool onlyCommonModifiers) + { + return onlyCommonModifiers ? parent.Amount * 2 : parent.Amount; + } +} \ No newline at end of file