/// <summary>
        /// Скидка при оплате бонусами
        /// </summary>
        Int64 CalculateDiscountForBonuses(FiscalArticle Article)
        {
            Int64 Result = 0, CurrentDiscount = 0;

              if (!GetDiscountForSpendBonuses(Article, out CurrentDiscount)) // ищем скидку на товар в БД товаров-исключений
            CurrentDiscount = BonusPaymentDiscounts[Card.StatusID];
              if (CurrentDiscount > 0)
              {
            Result = Calc.NormalizePrice(Article.PriceWithoutDiscount * CurrentDiscount);
            RetailScreen.Add("Скидка {0}% за оплату бонусами\n", (int)Calc.ValueToNaturalAmount(CurrentDiscount));
            //PrinterMessage += string.Format("{0}% оплату бонусами\n", Parameters.ValueToStringPercent(CurrentDiscount));
            // max 24 symbols: 123456789012345678901234
              }
              return Result;
        }
        // скидка в счастливые часы
        Int64 CalculateDiscountForHappyHours(FiscalArticle Article)
        {
            Int64 Result = 0, ActionDiscount = 0;
              string HappyDescription = "";
              if (DateInHappyHours(Transaction.TerminalDateTime)) // если время в списке счасливых часов, то
              {
            ActionDiscount = HappyHourDiscounts[Card.StatusID];
            HappyDescription = "счастливые часы";
              }
              else if (DateInHappyDayOfWeek(Transaction.TerminalDateTime)) // если дата в списке счасливых дней недели
              {
            ActionDiscount = HappyDayOfWeekDiscounts[Card.StatusID];
            HappyDescription = "выходные";
              }
              else if (DateInHappyHolidays(Transaction.TerminalDateTime)) // если дата в списке праздников
              {
            ActionDiscount = HappyHolidayDiscounts[Card.StatusID];
            HappyDescription = "праздничные дни";
              }

              if (ActionDiscount > 0)
              {
            Result = Calc.NormalizePrice(Article.PriceWithoutDiscount * ActionDiscount);
            RetailScreen.Add("+{0}% бонусов за покупку в {1}\n", Calc.ValueToStringPercent(ActionDiscount), HappyDescription);
            //PrinterMessage += string.Format("{0}% за счастливые часы\n", Parameters.ValueToStringPercent(ActionDiscount));
            // max 24 symbols: 123456789012345678901234

            /*if (Result != 0)
              Transaction.ActiveActions.Add(new TransactionAction(Actions.BonusesForHappy, new ActionParameterValue(ActionParameters.ApproximateAmountOfBonuses, Calc.ValueToCurrencyAmount(Calc.NormalizePriceQuantity(Result * Article.Quantity)), HappyDescription)));*/
              }
              return Result;
        }
 // бонусы за оплату банковской картой
 Int64 CalculateDiscountForBankingCard(FiscalArticle Article)
 {
     Int64 Result = 0;
       if (Article.PaymentType == PaymentTypes.BankingCard) // если товар оплачивается банковской картой
       {
     Int64 DiscountForPaymentByBankingCard;
     if (DiscountPaymentByBankingCard.TryGetValue(Card.StatusID, out DiscountForPaymentByBankingCard))
       Result = Calc.NormalizePrice(Article.PriceWithoutDiscount * DiscountForPaymentByBankingCard);
     RetailScreen.Add("+{0}% бонусов за покупку банковской картой\n", Calc.ValueToStringPercent(DiscountForPaymentByBankingCard));
       }
       return Result;
 }
 // скидка за день рождения
 Int64 CalculateDiscountForBirthday(FiscalArticle Article)
 {
     Int64 Result = 0;
       if (Card.Contract.LoyaltyClient.Birthday != DateTime.MinValue && Card.Contract.LoyaltyClient.Birthday.Day == Transaction.TerminalDateTime.Day && Card.Contract.LoyaltyClient.Birthday.Month == Transaction.TerminalDateTime.Month)
       {
     Result = Calc.NormalizePrice(Article.PriceWithoutDiscount * BirthdayDiscounts[Card.StatusID]);
     RetailScreen.Add("+{0}% бонусов за покупку в день рождения\n", Calc.ValueToStringPercent(BirthdayDiscounts[Card.StatusID]));
     //PrinterMessage += string.Format("{0}% за день рождения!\n", Parameters.ValueToStringPercent(Parameters.BirthdayDiscount));
     // max 24 symbols: 123456789012345678901234
     /*if (Result != 0)
       Transaction.ActiveActions.Add(new TransactionAction(Actions.BonusesForBirthday, new ActionParameterValue(ActionParameters.ApproximateAmountOfBonuses, Calc.ValueToCurrencyAmount(Calc.NormalizePriceQuantity(Result * Article.Quantity)), Card.Contract.LoyaltyClient.Birthday.ToString())));*/
       }
       return Result;
 }
 /// <summary>Ищет товарную позицию в чеке (сравнение по коду и типу оплаты). Возвращет null, если позиция не найдена</summary>
 public FiscalArticle FindArticle(FiscalArticle ToFind)
 {
     foreach (FiscalArticle Article in Articles)
     if (Article != null && Article.IsAnalog(ToFind))
       return Article;
       return null;
 }
        // скидка за сумму артикула
        Int64 CalculateDiscountForAmount(FiscalArticle Article, Int64 FiscalTotalAmount)
        {
            Int64 Result = 0, ActionLimit = BigPurchaseLimits[Card.StatusID], ActionDiscount = BigPurchaseDiscounts[Card.StatusID];
              if (FiscalTotalAmount >= ActionLimit)
              {
            Result = Calc.NormalizePrice(Article.PriceWithoutDiscount * ActionDiscount);
            RetailScreen.Add("+{0}% бонусов за сумму выше {1} {2}\n", Calc.ValueToStringPercent(ActionDiscount), (int)Calc.ValueToNaturalAmount(ActionLimit), Transaction.Terminal.ServicePoint.Region.Currency.ShortName);
            //PrinterMessage += string.Format("{0}% за сумму > {1}\n", Parameters.ValueToStringPercent(ActionDiscount), (int)Parameters.ValueToCurrencyAmount(ActionLimit));
            // max 24 symbols: 123456789012345678901234

            /*if (Result != 0)
              Transaction.ActiveActions.Add(new TransactionAction(Actions.BonusesForPurchase,
            new ActionParameterValue(ActionParameters.ApproximateAmountOfBonuses, Calc.ValueToCurrencyAmount(Calc.NormalizePriceQuantity(Result * Article.Quantity))),
            new ActionParameterValue(ActionParameters.MainParameter, Calc.ValueToCurrencyAmount(ActionLimit))));*/
              }
              return Result;
        }
 /// <summary>
 /// true, если на товар действует специальная скидка в обход всех акций
 /// </summary>
 public bool GetSpecialDiscountForBonusAwarding(FiscalArticle Article, out Int64 Discount)
 {
     Discount = 0;
       LogicalProductInfo ap = Article.Product;
       if (ProductsBonusAwardDiscount != null && ap != null)
       {
     try
     {
       if (ProductsBonusAwardDiscount.GetPartForCurrent(Transaction).TryGetValue(ap.ID, out Discount))
       {
     Discount = Calc.NormalizePrice(Article.PriceWithoutDiscount * Discount);
     return true;
       }
     }
     catch { } // продукт не найден
       }
       return false;
 }
 /// <summary>Возвращает true, если переданная позиция является аналогом переданной позиции (сравнение по коду и по типу оплаты)</summary>
 public bool IsAnalog(FiscalArticle Article)
 {
     return GoodsCode == Article.GoodsCode && PaymentType.Value == Article.PaymentType.Value;
 }
        /// <summary>
        /// Возвращает количество товара, ограниченное в соотв. с параметрами. На возвращённое количество товара можно начислять бонусы.
        /// В случае изменения количества товара на экран кассира отправляется информация о превышении
        /// </summary>
        private Int64 GetLimitedQuantityForPurchase(FiscalArticle Article)
        {
            Int64 Result = Article.Quantity;

              LogicalProductInfo Product = Article.Product;
              if (Product != null)
              {
            string CounterName = GetCounterNameForLimitedProduct(Product);
            Int64 CurrentCounterValue = Card.Counters[CounterName].CurrentValue;
            Int64 Limit = ProductQuantityDailyLimits[Product.ID];

            if (CurrentCounterValue >= Limit)
              Result = 0;
            else
            {
              if (CurrentCounterValue + Result > Limit)
            Result = Limit - CurrentCounterValue; // в других случаях Quantity не меняем
            }

            if (Result != Article.Quantity)
              RetailScreen.Add("Внимание! Превышен допустимый лимит {0} литров при покупке {1}. Начисление бонусов за {1} сегодня производиться не будет\n", Calc.ValueToNaturalQuantity(Limit), Product.Name);
              }

              return Result;
        }
 /// <summary>Проверяет, входит ли переданный артикул в продукты, потребление которых необходимо ограничить</summary>
 bool IsArticleInLimitedProducts(FiscalArticle Article)
 {
     if (Article.Product == null)
     return false;
       foreach (var kvp in ProductQuantityDailyLimits)
     if (Products.IsCodeInProductID(Article.GoodsCode, RetailSystem.ID, kvp.Key))
       return true;
       return false;
 }
 /// <summary>
 /// Получить для артикула, продаваемого на терминале, специальную скидку при оплате бонусами.
 /// Скидка будет возвращена в Discount
 /// </summary>
 /// <returns>true, если скидка найдена</returns>
 bool GetDiscountForSpendBonuses(FiscalArticle Article, out Int64 Discount)
 {
     LogicalProductInfo Product = Article.Product;
       Discount = 0;
       if (Product != null)
     return ProductsBonusSpendDiscount.GetPartForCurrent(Transaction).TryGetValue(Product.ID, out Discount);
       else
     return false;
 }
        // parse TLV for fiscal receipt
        internal void Deserialize(byte[] Data, LeveledFileLogger LeveledLog)
        {
            var NextLeveledLog = LeveledLog.CloneNextLevel();
              PaymentsReceived = false;

              byte Tag = 0;
              byte[] Value = null;
              TLVReader FiscalReader = new TLVReader(Data);
              while (FiscalReader.NextValue(out Tag, out Value))
            switch ((CMPFiscalReceiptTags)Tag)
            {
              case CMPFiscalReceiptTags.Article: // может быть несколько позиций чека
            FiscalArticle Article = new FiscalArticle(this);
            LeveledLog.Message("Article = {");
            Article.Deserialize(Value, NextLeveledLog); // в артикуле у нас все полученные значения
            LeveledLog.Message("}");
            this.Articles.Add(Article);
            break;
              case CMPFiscalReceiptTags.Flags:
            Flags.Receive((FiscalReceiptFlags)TLVReader.ValueToByte(Value));
            LeveledLog.Message("Flags = {0} ({1})", Flags.Value, (int)Flags.Value);
            break;
              case CMPFiscalReceiptTags.AmountWithoutDiscount:
            AmountWithoutDiscount.Receive(TLVReader.ValueToInt32(Value));
            LeveledLog.Message("AmountWithoutDiscount = {0}", AmountWithoutDiscount.Value);
            break;
              case CMPFiscalReceiptTags.DiscountForAmount:
            DiscountForAmount.Receive(TLVReader.ValueToInt32(Value));
            LeveledLog.Message("DiscountForAmount = {0}", DiscountForAmount.Value);
            break;
              case CMPFiscalReceiptTags.Payments: // performed payments
            LeveledLog.Message("Payments = {");
            Payments.Deserialize(Value, NextLeveledLog);
            LeveledLog.Message("}");
            PaymentsReceived = true;
            break;
              default:
            throw new Exception(string.Format("Unknown tag 0x{0:X} received", Tag));
            }
        }
 /// <summary>ищет среди позиций чека, позицию с GoodsCode и PaymentType совпадающими с переданной и заменяет её в исходной коллекции на клон переданной</summary>
 public void ReplaceArticle(FiscalArticle Article)
 {
     for (int i = 0; i < Articles.Count; i++)
     if (Articles[i].GoodsCode == Article.GoodsCode && Articles[i].PaymentType.Value == Article.PaymentType.Value)
     {
       Articles[i] = Article.Clone(Articles[i].Fiscal);
       return;
     }
       throw new Exception(string.Format("Could not find Article {0} with payment type {1}", Article.GoodsCode, Article.PaymentType.Value));
       //return false;
 }
        // скидка за статус
        Int64 CalculateDiscountForStatus(FiscalArticle Article)
        {
            Int64 ActionDiscount = Article.IsFuel ? StatusDiscountsFuel[Card.StatusID] : StatusDiscountsNonFuel[Card.StatusID];
              RetailScreen.Add("+{0}% бонусов по статусу - за покупку '{1}'\n", Calc.ValueToStringPercent(ActionDiscount), Article.GoodsNameForTerminalPrinter/* (Article.IsFuel ? "топлива" : "товаров")*/);
              //PrinterMessage += string.Format("{0}% по статусу\n", Parameters.ValueToStringPercent(ActionDiscount));
              // max 24 symbols: 123456789012345678901234

              Int64 Result = Calc.NormalizePrice(Article.PriceWithoutDiscount * ActionDiscount);
              /*if (Result != 0)
            Transaction.ActiveActions.Add(new TransactionAction(Actions.BonusesForStatus, new ActionParameterValue(ActionParameters.ApproximateAmountOfBonuses, Calc.ValueToCurrencyAmount(Calc.NormalizePriceQuantity(Result * Article.Quantity)))));*/

              return Result;
        }
 /// <summary>
 /// true, если на товар запрещены начисления бонусов
 /// </summary>
 public bool IsBonusAwardForbidden(FiscalArticle Article)
 {
     Int64 Dummy;
       LogicalProductInfo ap = Article.Product;
       if (ap != null)
     return ProductsBonusAwardForbidden.GetPartForCurrent(Transaction).TryGetValue(ap.ID, out Dummy);
       return false;
 }
        /// <summary>Заменяет параметры артикула this, использующиеся для вычислений, на параметры переданного артикула</summary>
        public bool Combine(FiscalArticle Article)
        {
            if (Article != null)
              {
            if (Article.Quantity.Received) Quantity = new DVInt64(Article.Quantity.Value);
            if (Article.PriceWithoutDiscount.Received) PriceWithoutDiscount = new DVInt64(Article.PriceWithoutDiscount.Value);
            if (Article.DiscountForPrice.Received) DiscountForPrice = new DVInt64(Article.DiscountForPrice.Value);
            if (Article.AmountWithoutDiscount.Received) AmountWithoutDiscount = new DVInt64(Article.AmountWithoutDiscount.Value);
            if (Article.DiscountForAmount.Received) DiscountForAmount = new DVInt64(Article.DiscountForAmount.Value);
            if (Article.Bonuses.Received) Bonuses = new DVInt64(Article.Bonuses.Value);
            if (Article.Flags.Received) Flags = new DVFiscalArticleFlags((FiscalArticleFlags)Article.Flags.Value);
            if (Article.paymentType.Received) paymentType = new DVPaymentTypes(Article.paymentType.Value);
              }

              return Fiscal.ErrorMessage.IsEmpty;
        }
 /// <summary>true, если за товар нельзя платить бонусами</summary>
 public bool IsForbiddenBonusPayment(FiscalArticle Article)
 {
     LogicalProductInfo ap = Article.Product;
       if (ProductsBonusSpendForbidden != null && ap != null)
       {
     try
     {
       Int64 Dummy;
       return ProductsBonusSpendForbidden.GetPartForCurrent(Transaction).TryGetValue(ap.ID, out Dummy);
     }
     catch { } // продукт не найден
       }
       return false;
 }
 internal static FiscalArticle Substract(FiscalReceipt FR, FiscalArticle A, FiscalArticle B)
 {
     var Result = A.Clone(FR);
       Result.Quantity.Value = A.Quantity.Value - B.Quantity.Value;
       Result.PriceWithoutDiscount.Value = A.Price;
       Result.DiscountForPriceCurrent.Value = 0;
       Result.AmountWithoutDiscount.Value = A.Amount - B.Amount;
       Result.DiscountForAmount.Value = 0;
       Result.Bonuses.Value = A.Bonuses.Value - B.Bonuses.Value;
       return Result;
 }
 /// <summary>Добавить запись в список товаров фискального чека</summary>
 public FiscalArticle AddArticle(string GoodsCode, PaymentTypes PaymentType, string GoodsName, Int64 Quantity, Int64 Price, Int64 Amount, Int64 DiscountForPrice, Int64 DiscountForAmount, Int64 Bonuses)
 {
     FiscalArticle Article = new FiscalArticle(this);
       Articles.Add(Article);
       Article.Flags.Value = FiscalArticleFlags.Added;
       Article.GoodsCode = GoodsCode;
       Article.PaymentType = new DVPaymentTypes(PaymentType);
       Article.GoodsName = GoodsName;
       Article.Quantity = new DVInt64(Quantity);
       Article.PriceWithoutDiscount = new DVInt64(Price);
       Article.AmountWithoutDiscount = new DVInt64(Amount);
       Article.DiscountForPrice = new DVInt64(DiscountForPrice);
       Article.DiscountForAmount = new DVInt64(DiscountForAmount);
       Article.Bonuses = new DVInt64(Bonuses);
       return Article;
 }