gifts2017

Полезные функции для округления копеек в документах

Опубликовал Кирилл Панфилов (PanKir) в раздел Программирование - Практика программирования

Часто, к примеру, при оформлении заказа покупателя или создании чека ККМ сумма документа получается не целой, с копейками. Если товара не много и есть позиции с количеством товара равном 1-2, то проблема округления копеек тут не стоит. А что делать если количество товара в каждой позиции огромно и лишние копейки никак не делятся на это количество? Предлагаю ряд функций для решения этой проблемы.

У меня эта проблема возникла в конфигурации 1С:Комплексная автоматизация 8 на платформе 8.2. Для решения я создал свой общий модуль и прописал в нем следующие функции:

Функция РасчетНОД (Число1, Число2) Экспорт
  Если
Число1 >= Число2 Тогда
    Первое = Число1;
    Второе = Число2;
  Иначе
    Первое = Число2;
    Второе = Число1;
  КонецЕсли;
  Остаток = Первое % Второе;
  Если
Остаток > 0 Тогда
    Первое = Второе;
    Второе = Остаток;
    НОД = РасчетНОД(Первое, Второе);
  Иначе
    НОД = Второе;
  КонецЕсли;
  Возврат
НОД;
КонецФункции

Функция
Эйлера (Число) Экспорт
  ЧислаЭйлера = Новый Массив;
  Количество =0;
  ЧислаЭйлера.Добавить(Количество);
  Для
Номер = 1 По Число Цикл
    Если
РасчетНОД(Число, Номер) = 1 Тогда
      Количество = Количество + 1;
      ЧислаЭйлера.Добавить(?(Номер>Число/2, Номер-Число, Номер));
    КонецЕсли;
  КонецЦикла;
  ЧислаЭйлера[0] = Количество;
  Возврат
ЧислаЭйлера;
КонецФункции

Функция
СтепеньПоМодулю (Основание, Степень, Модуль) Экспорт
  Основание = Основание % Модуль;
  Число = Основание;
  Для
Индекс = 2 По Степень Цикл
    Число = Основание * Число;
    Число = Число % Модуль;
  КонецЦикла;
  Возврат
Число;
КонецФункции

Функция
РешениеСравнения (Коэффициент, Сравнение, Модуль) Экспорт
  НОД = РасчетНОД(Коэффициент, Модуль);
  Если
Сравнение%НОД = 0 Тогда
    СравнениеНОД = Цел(Сравнение/НОД);
    КоэффициентНОД = Цел(Коэффициент/НОД);
    МодульНОД = Цел(Модуль/НОД);
    Степень = Эйлера(МодульНОД)[0] - 1;
    Решение = СравнениеНОД * СтепеньПоМодулю(КоэффициентНОД, Степень, МодульНОД);
    Решение = Решение % МодульНОД;
  КонецЕсли;
  Возврат
Решение;
КонецФункции

Функция
РешениеСравненияЛинейное (Коэффициент1, Коэффициент2, Сравнение, НОД, Модуль) Экспорт
  МГлавный = Новый Массив;
  МОсновной = Новый Массив;
  Коэффициент1НОД = Цел(Коэффициент1/НОД);
  Коэффициент2НОД = Цел(Коэффициент2/НОД);
  МодульНОД = Цел(Модуль/НОД);
  СравнениеНОД = Цел(Сравнение/НОД);
  НОД1 = РасчетНОД(Коэффициент1НОД, МодульНОД);
  НОД2 = РасчетНОД(Коэффициент2НОД, МодульНОД);
  МГлавный = Эйлера(МодульНОД);
  Если
НОД2 > 1 Тогда
    Коэф = РешениеСравнения(НОД1, СравнениеНОД, НОД2);
  Иначе
    Коэф = 1;
  КонецЕсли;
  Счетчик = Цел(МодульНОД/НОД2);
  Для
Номер = 0 ПО Счетчик-1 Цикл
    Коэф2 = МГлавный.Найти(?((Коэф + Номер*НОД2)>МодульНОД/2, (Коэф + Номер*НОД2)-МодульНОД, (Коэф + Номер*НОД2)));
    Если
ТипЗнч(Коэф2)<>Тип("Неопределено") Тогда
      МОсновной.Добавить(МГлавный[Коэф2]);
    КонецЕсли;
  КонецЦикла;
  Ответ = Новый Массив(2);
  Первый = Новый Массив;
  Второй = Новый Массив;
  Результат = Новый Массив;
  Модуль1 = Цел(МодульНОД/НОД1);
  Модуль2 = Цел(МодульНОД/НОД2);
  Степень1 = Эйлера(Модуль1)[0]-1;
  Степень2 = Эйлера(Модуль2)[0]-1;
  Коэффициент1НОД1 = Цел(Коэффициент1НОД/НОД1);
  Коэффициент2НОД2 = Цел(Коэффициент2НОД/НОД2);
  Для
Номер = 0 ПО МОсновной.Количество()-1 Цикл
    Решение1 = СтепеньПоМодулю(Коэффициент1НОД1, Степень1, Модуль1) * МОсновной.Получить(Номер) % Модуль1;
    Решение2 = СтепеньПоМодулю(Коэффициент2НОД2, Степень2, Модуль2) * Цел((СравнениеНОД-НОД1*МОсновной.Получить(Номер))/НОД2) % Модуль2;
    Первый.Добавить(?(Решение1>Модуль/2, Решение1-Модуль, Решение1));
    Второй.Добавить(?(Решение2>Модуль/2, Решение2-Модуль, Решение2));
    Результат.Добавить(Коэффициент1*Первый[Номер] + Коэффициент2*Второй[Номер]);
  КонецЦикла;
  Ответ[0] = Первый[0];
  Ответ[1] = Второй[0];
  Для
Номер = 1 ПО Результат.Количество()-1 Цикл
    Если
Pow(Результат[Номер-1], 2) > Pow(Результат[Номер], 2) Тогда
      Ответ[0] = Первый[Номер];
      Ответ[1] = Второй[Номер];
    КонецЕсли;
  КонецЦикла;
  Возврат
Ответ;
КонецФункции

 

Пояснения:

 

1. Функция РасчетНОД (Число1, Число2).

  • возвращает Наибольший Общий Делитель двух чисел.

2. Функция Эйлера (Число).

  • возвращает функцию Эйлера (количество чисел взаимопростых с данным и меньших его) для числа в следующем виде: Массив[КоличествоЧиселВзаимопростыхСДанным, МножествоЭтихСамыхЧиселВзаимопростыхСДанным], причем эти числа представляются в виде ?(Номер>Число/2, Номер-Число, Номер), где Номер - эти самые числа.

3. Функция СтепеньПоМодулю (Основание, Степень, Модуль).

  • возвращает остаток от деления степени основания на текущий модуль.

4. Функция РешениеСравнения (Коэффициент, Сравнение, Модуль).

  • возвращает решение линейного сравнения с одной переменнойКоэффициент*Решение = Сравнение (mod Модуль).

5. Функция РешениеСравненияЛинейное (Коэффициент1, Коэффициент2, Сравнение, НОД, Модуль).

  • возвращает решение линейного сравнения с двумя переменнымиКоэффициент1*Ответ1+Коэффициент2*Ответ2 = Сравнение (mod Модуль), при этом НОД = НОД(Коэффициент1, Коэффициент2, Модуль) (общий делитель трех чисел).

 

Далее идут процедуры которые вставляются непосредственно в объект, где надо округлять. Первая процедура вешается на созданую кнопку на форме объекта, а вторая - прописывается в модуле объекта (в моем случае это документ ЗаказПокупателя). Выделенное жирным заменяется согласно Вашему объекту.

 

//Получаем общую сумму и если в ней есть копейки, то будем округлять их до рублей.
//
Процедура КоманднаяПанельТоварыДействиеОкруглитьСтоимость(Кнопка)
  Сумма = Товары.Итог("Сумма");
  Если
Сумма*100%100 <> 0 Тогда
    Ответ = Вопрос("Перед округлением цен надо записать документ! Записать?", РежимДиалогаВопрос.ДаНет);
    Если
Ответ = КодВозвратаДиалога.Да Тогда
      ЭтотОбъект.Записать();
      ОкруглитьСуммуЗаказа();
    КонецЕсли;
  КонецЕсли;
КонецПроцедуры

//Для округления будем стараться выбирать товары с наибольшей ценой для уменьшения погрешности разницы округленной цены и текущей.
//Наценку будем брать так, чтобы она по модулю не превышала 50 копеек, опять же для уменьшения погрешности.
//
Процедура ОкруглитьСуммуЗаказа() Экспорт
  Массив = Новый Массив(2);
  Результаты = Новый Массив;
  Запрос = Новый Запрос;
  Текст = "ВЫБРАТЬ
      | ЗаказПокупателяТовары.НомерСтроки КАК НомерСтроки,
      | ЗаказПокупателяТовары.Количество,
      | ЗаказПокупателяТовары.Цена КАК Цена,
      | ЗаказПокупателяТовары.Сумма КАК Сумма
      |ИЗ
      | Документ.ЗаказПокупателя.Товары КАК ЗаказПокупателяТовары
      |ГДЕ
      | ЗаказПокупателяТовары.Ссылка = &Ссылка
      |
      |УПОРЯДОЧИТЬ ПО
      | Цена УБЫВ
      |ИТОГИ
      | КОЛИЧЕСТВО(НомерСтроки),
      | СУММА(Сумма)
      |ПО
      | ОБЩИЕ"
;
  Запрос.Текст = Текст;
  Запрос.УстановитьПараметр("Ссылка", Ссылка);
  Выборка = Запрос.Выполнить().Выбрать(); //Вытаскиваем список всех товаров упорядоченых по убыванию цены
  Если Выборка.Следующий() Тогда
    СуммаИтого = Выборка.Сумма*100%100; //Получаем общую сумму и количество товаров
    КоличествоИтого = Выборка.НомерСтроки;
  КонецЕсли;
  Если
Выборка.Следующий() Тогда
    Количество1 = Выборка.Количество%100; //Получаем первый, то есть самый дорогой товар
    Цена1 = Выборка.Цена*100;
    Строка1 = Выборка.НомерСтроки;
  КонецЕсли;
  НОД1 = АналитическийМодуль.РасчетНОД(Количество1, 100);
  Если
СуммаИтого%НОД1=0 Тогда
    Наценка = АналитическийМодуль.РешениеСравнения(Количество1, СуммаИтого, 100);
    Наценка1 = -(?(Наценка>50, Наценка-100, Наценка));
    //Если количество самого дорогого товара подходит для решения сравнения,
    //то есть его цену можно исправить так, чтобы исчезли копейки в сумме,
    //то нам больше ничего и не надо, иначе - ищем второй подходящий товар
    //(не трудно понять, что такой товар найдется для решения сравнения с двумя переменными,
    //еслиб не нашлось, то текущей суммы бы не получилось при таких данных параметрах позиций товаров):
  Иначе
    Пока
Выборка.Следующий() Цикл
      Количество2 = Выборка.Количество%100;
      НОД2 = АналитическийМодуль.РасчетНОД(Количество2, НОД1);
      Если (
СуммаИтого%НОД2=0) Тогда
        Массив = АналитическийМодуль.РешениеСравненияЛинейное(Количество1, Количество2, СуммаИтого, НОД2, 100);
        Строка2 = Выборка.НомерСтроки;
        Наценка1 = -(?(Массив[0]>50, Массив[0]-100, Массив[0]));
        Наценка2 = -(?(Массив[1]>50, Массив[1]-100, Массив[1]));
        Прервать;
      КонецЕсли;
    КонецЦикла;
  КонецЕсли;
  ТекСтрока = Товары.Получить(Строка1-1);
  ТекСтрока.Цена = ТекСтрока.Цена + Наценка1/100;
  ОбработкаТабличныхЧастей.РассчитатьСуммуТабЧасти(ТекСтрока, ЭтотОбъект);
  Если
ТипЗнч(Строка2) <> Тип("Неопределено") Тогда
    ТекСтрока = Товары.Получить(Строка2-1);
    ТекСтрока.Цена = ТекСтрока.Цена + Наценка2/100;
    ОбработкаТабличныхЧастей.РассчитатьСуммуТабЧасти(ТекСтрока, ЭтотОбъект);
  КонецЕсли;
КонецПроцедуры

 

При проверки погрешность разницы колебалась в пределах не больше чем 1-2% от объщей суммы.
Минусы:
  • данная система округления работает с "чистыми" ценами, без учета ручных и автоматических скидок, проставляемых отдельно в виде %, поэтому если на некоторые товары действует скидка, то приходится в ручную расчитывать цену со скидкой и прописывать её в колонку "Цена", а колонку "Скидка" не трогать.

См. также

Подписаться Добавить вознаграждение
Комментарии
1. Сергей Ожерельев (Поручик) 20.04.11 22:33
2. dvv01 (dvv01) 21.04.11 08:59
сложно то как:
Сумма = Цена*Количество*Скидка;
Дельта = Сумма - Окр(Сумма,0);
Корр = Дельты/Количество/Скидка;
НоваяЦена = Цена - Корр;
получим:
НоваяСумма = (Цена - Корр)*Количество*Скидка = Сумма - Дельта = Окр(Сумма,0)
еще добавить проверки на Количество=0 и Скидку = 0
и проверку Корр-Окр(Корр,-2) > 0.005 (при этом как раз и вылезут дополнительные копейки, а чтобы не заморачиваться с подгонкой проценктов скидок и штук - пересчитать при этом (или всегда) новую цену для 1 шт и при скидках использовать уже пересчитанную цену.

Ньюансы - разойдутся отчеты по суммам продаж "по документам" и "регистру ТоварыВРознице" - в идеале перед каждой продажей надо делать "переоценку товаров в рознице" или доработать движения документа со скидкой,
аналогично надо бы проверить с "Товары принятые" и "Переданные" - оплата комиссионного товара тоже привязана к цене продажи (или сумме? - тогда проблем тут нет)
Поясню:
ЦенаАТТ = 999р
Продали 1шт*999-3% = ... =998,97 - 29,97 = 969,00
Движения по "ТоварыВРознице";
приход: +1шт +999.00р
расход: -1шт -998,97р
остаток: 0шт +0,03р
не смертельно, но хорошего тоже не много, особенно на 10000 продаж. несколько нивелируется, если коррекцию делать "по арифметическому округлению", т.е. в +/-, а не строго "в меньшую сторону", но тут может всплыть уже юридический ньюанс: продажа по чеку ККМ по цене больше заявленной (и доказывай потом, что итоговая со скидкой сумма все равно получилась меньше прайса - нарушение правил торговли, штраф 30000-40000)
3. Алексей Константинов (alexk-is) 21.04.11 09:33
А что делать если количество товара в каждой позиции огромно и лишние копейки никак не делятся на это количество?
Можно на примере показать, откуда беруться лишние копейки?
4. Ийон Тихий (cool.vlad4) 21.04.11 10:11
(3) Такой же вопрос. Надо не со следствием бороться, а с причиной. Пока, что функции не осмыслил, но есть мнение, что они больше полезны для подбора по сумме (когда какой-нибудь товар надо подобрать по сумме).
5. Кирилл Панфилов (PanKir) 21.04.11 13:30
Поручик пишет:
Раскрась код

Я не хакер, я только учусь... В ближайшем будущем обязуюсь покрасить! )
alexk-is пишет:
Можно на примере показать, откуда беруться лишние копейки?

А что тут показывать? Заходите в любой СУПЕРМАРКЕТ и смотрите их цены. Вам долго придется искать цену без копеек, сейчас же модно ставить цену 99.99, а не 100
Конечно они там не заморачиваются на таком, просто не додают сдачу копеечную, но это уже на их совести, и совсем не для этого форума )

Ну а если пример, то:
Себестоимость двух товаров скажем 73 р. и 69 р.
Розница скажем рассчитывается как 57% от себестоимости, то есть 73*1,57 = 114,61 р. и 69*1,57 = 108,33 р.
Берем 3 шт первого и 8 шт второго: 114,61*3+108,33*8 = 1210,47 р.
Как видим ни 47, ни 53 копейки сразу нельзя скинуть ни на один из товаров (не делится ни на 3, ни на 8).
Тут конечно нам повезло, так как для корректировки хватит решения сравнения с одним неизвестным (для первого товара): 3х = 47 (100), решение: х = 49 копеек,
значит новая цена первого товара: 114,61-0,49 = 114,12 р., погрешность: 3*0,49 = 1,47 р., в процентах: 0,12%

Вот примерно так работает этот алгоритм.

На подобе того, что предлагает dvv01, было до меня тут (на нашей базе) решение моей проблемы: выбирался самый дорогой товар и ставилась автоматическая наценка на него так, чтобы его сумма доокруглила объщую сумму до рублей. Но согласитесь что не очень красиво иметь во всей накладной один товар с наценкой в 0,0007456841357% к примеру )))
6. Кирилл Панфилов (PanKir) 21.04.11 13:34
cool.vlad4 пишет:
Надо не со следствием бороться, а с причиной.

К сожалению причины обычно находятся в не зоны компетенции программистов...
За цены отвечает отдел снабжения, а за розничные наценки от себестоимости, равно как и оптовые скидки от розницы - начальство. Так что приходится уже бороться со следствием, как бы этого не хотелось...
7. Ийон Тихий (cool.vlad4) 21.04.11 13:41
(6)пожалуй, да...но функции интересные...
8. Кирилл Панфилов (PanKir) 21.04.11 14:00
(7)с детства увлекался математикой ;)
9. Кирилл Панфилов (PanKir) 21.04.11 18:11
(1) покрасил... долго мучился с "разукрашкой" :evil: , но всётаки разукрасил.
10. Алексей Константинов (alexk-is) 22.04.11 06:34
(5) Хожу в магазин. У меня дисконт - прогрессивный процент. Плачу по Visa. Расчет до копейки. Получаю 3 чека: на товар, на дисконт, на Visa.
Мне кажется здесь проблема больше управленческая - организация процесса продажи.
И ещё про копейки. Может быть будет интересно. Тоже математика :) http://infostart.ru/public/16630/

(9) Это почему "долго мучался"? Что именно вызвало проблемы?
11. Кирилл Панфилов (PanKir) 22.04.11 09:32
(10)
alexk-is пишет:
Это почему "долго мучался"? Что именно вызвало проблемы?

не понял как переносить текст из разукрашки в поле описания.
сначала попробовал просто скопировать ХТМЛ код и вставить в поле - нифига не получилось, на сохранке были все тэги как они прописались в коде...
поставил вариант ББКод, скопировал - тоже самое что и с ХТМЛ (((
зашел тогда в ХТМЛ едитор (кнопочка тут есть над полем, при редактировании статьи) и запастил туда ХТМЛ код, но вот косяк, запастилось оно туда в виде одной строки!!! и очень трудно отредактировать при этом отдельные части текста.
плюс к этому растановка тэга параграфа такая, что при попытке отформатировать код 1С согласно уровням циклов и условий ничего не выходит, он просто двигает ВЕСЬ!!! текст, пришлось ставить неразрывные пробелы.
в принципе через ХТМЛ едитор удобно вставлять, но надо немного подредактировать саму разукрашку, чтобы она не ломала отступы в процедурах и текст вне процедур, функций и коментов не разукрашивал...
12. Алексей Константинов (alexk-is) 22.04.11 19:17
(11) Так. Понятно. Видимо нужна пошаговая инструкция.
13. Кирилл Панфилов (PanKir) 23.04.11 16:58
(12) не помешало бы, а то хелп там какой-то хиленький (
14. Алексей Константинов (alexk-is) 24.04.11 21:46
(13) Для меня пошаговая инструкция выглядит так:
1. Ввести текст
2. Нажать "Раскрасить"
3. Вставить результат

...но видимо не для всех всё так однозначно. Буду делать инструкцию...
15. Игорь Исхаков (Ish_2) 24.04.11 22:38
(14) По старой дружбе вставлю тебе 5 коп.
И я не смог воспользоваться разукрашкой. Не разобрался. Что куда тыкать , что куда потом вставлять.
почему текст при вставке не разукрасился ? Проще в тему вставить скриншот с текстом программы.
16. Кирилл Панфилов (PanKir) 25.04.11 09:23
(14)сделал всё согласно этой пошаговой инструкции, даже в 2 вариантах - с ХТМЛ кодом и ББКОД кодом
и тот и другой просто скопировал все тэги в поле создания статьи и при сохранении нарисовал их как они есть, то есть как текст

хотя... я тут подумал... я когда вставлял неразрывные пробелы, они тоже сначала сохранялись как текст, а при возвращении в редактирование уже прятались как им положено. может так же будет и с ХТМЛ кодом, честно не проверял.

и ещё одно - разукрашка ломает отступы, может надо как-то сделать чтобы она TAB переводила в 2-4 неразрывных пробела?
17. Алексей Константинов (alexk-is) 25.04.11 18:56
18. Эстер Коган (e.kogan) 18.05.11 10:38
Плюс за объём работы ;) у меня всегда было плохо с арифметикой
19. slon slon (slonba) 11.11.11 12:21
а разве нет встроенной функции округления суммы документа?
20. Кирилл Панфилов (PanKir) 11.11.11 13:06
(19) slonba, честно - не нашел. может плохо искал...
у меня КА стоит, если подскажите где там есть такое, то посмотрю.
хотя и раньше нигде не припоминаю такой функции.

а если Вы имеете ввиду стандартное округление до целого числа - то это немного не то, так как СуммаДокумента и Сумма табличной части в этом случае не совпадут.
а если делать округление цены или суммы в каком-нибудь товаре, то тоже плохо, так как изменив количество этого товара нам снова надо искать где бы округлить, да и не всегда возможно округлить копейки в сумме, если они не делятся на количество товара.

З.Ы. как я уже говорил в конце 5 поста - вариант с наценкой/скидкой в 0,000005415674564% совсем не прельщает ни меня, ни моё начальство.
21. Maratimus Arslan (maratimus) 28.04.12 11:29
можно объяснить популярней для тех кто с детства не увлекался прикладной математикой... Просто стало очень интересно, но многое не понятно, приведите пожалуйста пример работы функции.
22. Кирилл Панфилов (PanKir) 28.04.12 11:57
(21) maratimus,
тут скорее всего чисто психологическое приложение данной процедуры округления, для тех кого бесят копейки в суммах документов.
в качестве наглядного примера как могут появиться копейки и как от них избавляется процедура можно посмотреть в коменте № (5), я там описал ситуацию с супермаркетом
ну а что именно делают каждая из функций - см. публикацию, там они описаны.

З.Ы. если не ответил на вопрос, уточните работу какой функции надо разобрать...