Остатки нельзя обороты

Прожив уже почти полвека,
Тьму перепробовав работ,
Я убежден, что человека
Достоин лишь любовный пот.
Вам, наверное, интересно, кто я такой и какого такого черта я тут взялся вам рассказывать свои бесценные мысли.
В данный момент я считаю себя инженером по автоматизации и интеграции API.
Однако было время, когда я был финансовым аналитиком, а до этого был кредитным инспектором в банке… Однако, кем бы я ни был, меня постоянно заводила идея автоматизации.
Было в ней какое‑то такое удивительное, знакомое с детства волшебство. Ну вы помните вот эту картинку с Емелей и щукой, глядящей на него из ведра:
«По щучьему велению, по моему хотению... А ну‑ка, ведра, шагайте сами домой!!!»
В бытность мою финансовым аналитиком, моим невинным хобби было превращать какие‑нибудь длинные неструктурированные таблички в красивые графики, построенные в электронных таблицах.
Глядя на полученный график, я вспоминал лекции майора Пиманкина на военной кафедре своей альма‑матер, про то, чем информация отличается от данных. Так вот же, невооруженным глазом видно, чем. Вот таблица с текстом забитая мусором, где и числа‑то не всегда числа, и десятичные точки иногда запятые, а вместо нуля иногда стоит красуется буква «о», а вместо единички вытянулась стройная латинская «l», или числовые данные зачем‑то обернуты в ненужные слова. Это и есть данные. А мой график — это настоящая информация, потому что он несет в себе какой‑то смысл. Можно показать его шефу или коллеге со словами: «Вот ты споришь, а ведь тенденция‑то налицо!!!»
Однажды в интервью какому‑то телеканалу Борис Ефимович Немцов хвастался, что ему известна «формула доллара», с помощью которой он может прогнозировать реакцию российской экономики на изменения цены нефти. Однако делиться формулой с телезрителями политик отказался, сославшись на то, что формулу эту ему по секрету рассказали важные парни то ли из банка «JPMorgan‑Chase», то ли из министерства финансов США.
— Хм! — хмыкнул я и открыл сайт cbr.ru, откуда скачал себе в электронную таблицу данные курса доллара к рублю с 1991 года.
— «JPMorgan» говоришь! — пробурчал я и открыл сайт Investing.com, откуда забрал данные о ценах ближайшего контракта Urals с момента начала торгов.
— По секрету значит! — ворчал я, нажимая «Создать график» и выбирая «диаграмму рассеяния».
— Кстати, Борис Ефимович, вас обманули!!! Формула‑то не одна! — не унимался я, разглядывая, как точки на диаграмме рассеяния кластеризуются в три или даже четыре отдельные плотные группы. Кластеризация точек на диаграмме рассеяния означала смену режима зависимости.
— Если вам рассказали формулу до января 2006, можете выбросить ее в мусор — она больше не работает! — обращался я к воображаемому собеседнику.
Быстро накинув на график несколько линий тенденции и выбрав галочку «построить уравнение регрессии», я переписал в блокнотик три формулы, по одной для каждого периода времени. После чего я довольный собой закрыл крышку ноутбука и пошел ужинать, напевая себе под нос «Альма‑матерь» Ивасей:
— Парадокс демократизма... чудо‑университет, Мы глядим без фанатизма на любой авторитет...
Что говорить, мне дико нравилось автоматизировать процесс добывания информации. Сначала это были просто трехэтажные формулы в Excel, потом я освоил и скрипты VBA.
Наверное, самой показательной была такая вот история. Как‑то раз я предложил коллеге сделать кофе‑брейк, но она отказалась, сославшись на аврал. Шеф поручил ей вытащить ФИО и телефоны контрагентов из текстового файла с перепиской за несколько лет. Я попросил показать файл и, посмотрев на него, предложил ей, как теперь модно говорить, сделку:
— Давай я делаю тебе таблицу со всеми именами и телефонами, а ты покупаешь мне кофе с тортиком!
Я заметил, что каждое сообщение в файле снабжено стандартным заголовком IMAP‑сервера, то есть имя отправителя, электронная почта и телефон снабжены тегами. Быстро набросав простенький VBA‑скрипт, я клацнул F5.
— Электронная почта тебе в виде бонуса! Пошли, я уже знаю, какой тортик хочу!
Коллега, хлопая ресницами, смотрела на три аккуратных столбца данных, выросших как по мановению волшебной палочки на странице. Точно так же, как я сам смотрел в детстве на емелины ведра, весело шагающие от проруби домой.
— Как же полезно уметь программировать! — думал я, мурлыкая от удовольствия, запивая «Наполеон» горячим кофе.
Справедливости ради, мои знания программирования ограничивались школьной информатикой и курсом «численных методов» университета, где нас учили решать дифференциальные уравнения и задачи оптимизации, загружая программки на фортране в терминалы мейнфрейма IBM, загадочно мигавшего индикаторами и занимавшего целую соседнюю комнату. Никаким программистом я, конечно же, не был и фраза «чистый кот» для меня имела куда больше смысла, чем термин «чистый код». Скрипты на VBA оставались этаким полезным хобби, помогающим собирать и «причесывать» нужные мне данные.
Однако мой арсенал рос и развивался. В какой‑то момент я наткнулся на обучающий ролик с многообещающим названием «Web‑scrapping with VBA».
— А что, так можно было?! — в сердцах воскликнул я, глядя на то, как автор ролика ловко выдергивал данные прямо из html‑кода.
Через пару дней я уже в общих чертах знал html‑верстку, классы и селекторы CSS, и даже осваивал Python. В общем, когда в один далеко не прекрасный день в новостях прозвучало: «Вы хотите декоммунизации?...», а в ответ на попытку открыть свой личный профиль, практически родной мне сайт cfainstitute.org выдал:
Sorry, you have been blocked
психологически я уже вполне созрел для того, чтобы попробовать свои силы в автоматизации теперь уже вполне профессионально.
Я, конечно же, понимал, что с моим резюме бессмысленно даже заикаться о поиске позиции разработчика или системного аналитика. Да и перспектива ездить каждое утро в офис и до вечера строчить код «для дяди» меня совсем не вдохновляла. Нет!!! К черту офисы и начальство… Меня манило словечко «фриланс»…
Просматривая список заказов на одной из уцелевших в России фриланс‑бирж, я наткнулся на интересную задачку. Заказчики хотели систему учета и планирования складских операций. Составив длиннющий перечень своих «компетенций» в области управленческого учета (что было стопроцентной правдой), я добавил про опыт разработки на Django и Python (что было правдой процентов на пятнадцать), после чего отослал отклик и сел ждать ответа.
К моему удивлению, заказчик ответил. Уж не знаю, обратил ли он внимание на мой богатый аналитический «бэкграунд» или решающую роль сыграл мой «лютый» демпинг. Так или иначе, вскоре, к собственному ужасу, я получил заказ.
— Тоже мне, «rocket science»! — пробубнил я, клацнув «приступить к выполнению».
Бороться за заказ — одно, а вот сделать его так, чтобы все, сам заказчик и я сам в первую очередь были довольны…
Однако это уже совсем другая история.
Один на один с заказом
Задачка, в общем, была несложной...
Кто в детстве любил играть с конструктором, тот меня поймет. Django — это та самая заветная коробочка с удивительными детальками. Детальки стандартные, а вот количество моделей, которые можно из них собрать, ограничено только вашей фантазией. Вот это вам «список объектов», а вот это «детальное представление объекта», а это обработчик формы для ввода и редактирования данных, а вот эта хитрая деталюшка с кнопкой поможет вам удалить любой объект из базы данных навсегда. А весь набор называется загадочной аббревиатурой CRUD. И все они, как бусины, нанизываются на структуру базы данных, как на скелет. Django потому и называют «data‑driven‑фреймворком», что в основе приложения лежит структура базы данных. Вот за эту структуру я первым делом и принялся.
— Всего делов‑то! — бубнил я себе под нос, рисуя схемы данных.
Мне представлялось, что система учета должна быть основана на нескольких взаимосвязанных таблицах:
«товары»,
«склады»,
«закупки»,
«поставки».
Каждая закупка увеличивает остаток конкретного товара, поставка уменьшает.
Соответственно…
— Стоп… Чего‑то не хватает! А ну да…
И я дорисовал еще один прямоугольник:
«операции».
Эта таблица должна была стать самой главной таблицей учета. Именно в нее предполагалось записывать приходы от закупок и выбытия от поставок каждого артикула.
— Ну вот, дело в шляпе…
Быстро прорисовав связи, отметив, какая из них должна быть «многие‑к‑многим», а какая «многие‑к‑одной», я взялся за представления. Заказчик точно обозначил в ТЗ, что хочет интерфейс «а‑ля» электронная таблица, где все артикулы слева по вертикали, а остатки «в пути» и на складах — это отдельные столбики по горизонтали.
— Отлично. Нужен запрос списка артикулов, а для вычисления остатков нужно агрегировать транзакции. Для каждого артикула в полученном списке вычислить остаток по конкретному складу и в пути…
Вычислить остаток! Легко сказать!!! Чтобы вычислить остаток, нужно просуммировать все операции с соответствующим знаком — поступление на склад — плюс, выбытие со склада — минус. Просуммировать сотни, если не тысячи операций, для каждого артикула и для каждого склада отдельно.
Если у нас, скажем, 100 артикулов и 10 складов, и по каждому было, скажем, по 10 операций, то это 30 000 операций суммирования. А ведь прежде, чем суммировать, эти операции нужно корректно найти и отфильтровать…
А если 1000 артикулов и 20 складов (а у WB гораздо больше) и по 1000 операций, то это уже 20 миллионов операций. И это не считая остатков в пути, в закупках и в поставках… А еще есть планируемые, в сборке. Даже если каждая отдельная операция занимает всего микросекунду, все вместе они займут… 20 секунд?
— Серьезно? Страница будет открываться 20 секунд?
И это только запрос к базе, а ведь будет еще рендеринг странички в браузере. И это еще без учета просьбы заказчика добавить для каждого склада отдельный столбец «Доступные для поставки», в котором надо просуммировать заказы в пути и остатки на складе. И это хорошо, если не нужно менять страницу в процессе ввода данных в форму, а если заказчик попросит динамически обновлять страницу прямо в процессе ввода данных? Это все‑таки «электронная таблица».
— Возможно, надо учитывать остатки. Ну, или считать их как‑то заранее?! Однако если учитывать остатки, то в полный рост встает вопрос инвалидации данных. Уберите одну операцию в прошлом, и вам придется последовательно пересчитать остатки для каждой отдельной операции.
— М‑да‑а‑а‑а… А серверу будет чем заняться на досуге. Похоже, в моем планировании архитектуры наметился тупичок! Тупик! Тупичище!!!
— Если ничего не помогает, значит самое время прочесть инструкцию! — вспомнил я один из законов Мерфи и открыл поисковик. Ведь наверняка кто‑то уже решил эту задачку.
— Как строят системы учета лучшие из лучших? Дайте списать!!! Ну или хотя бы подсмотреть одним глазком. — думал я, запуская поиск в самых разных вариантах на всех языках, которые понимал.
Однако вводимые мной один за другим поисковые запросы возвращали или статьи по бухгалтерскому учету, которые я и так знал назубок, или статьи по проектированию высоконагруженных систем, или статьи про проектирование паттернов ООП — этаких сферических коней в вакууме. Про архитектуру базы данных именно системы учета, что‑то, что помогло бы мне ответить на вопрос: «остатки или транзакции?!?!», ни‑че‑го!!! Ничегошеньки!
Время шло. Счетчик фриланс‑биржи тикал. Нужно было что‑то делать!
В мире 8 миллиардов человек, а списать решение не у кого. Я снова один на один со стихией, как тогда в Испании...
Живя в Испании, я очень любил плавать в море. Каждое утро 500 метров туда и 500 метров обратно. И также вечером. Вокруг было много бассейнов, но плавать каждый день по два километра в бассейне — это то еще скажу я вам удовольствие, от постоянных разворотов голова может закружиться. Поэтому я плавал в море. Каждый день. В любую погоду.
Естественно, мое близкое знакомство с обратным течением было лишь вопросом времени. Обратное течение — это когда огромные волны весом с многотонный грузовик каждая тащат воду к берегу. А обратно все эти тонны воды текут прямо под поверхностью, вдоль дна. И вроде вот он берег — рукой подать... Однако, сколько я ни старался, приблизиться к заветной линии прибоя я не мог. Я отчаянно греб руками, толкался ногами, но все было тщетно, я не приближался к берегу ни на метр. Силы постепенно покидали меня. Плюс ко всему начался отлив и течение воды устремилось прочь из Средиземного моря в Атлантику.
Это в районе турецкой Антальи или даже испанской Барселоны средиземноморские приливы и отливы практически незаметны. А вот на Costa del Sol, в прямой видимости Гибралтара, течения такие, что ого‑го!!! Меня несло прямиком на скалистый волнорез, насыпь из огромных валунов в форме противотанковых ежей, огораживавшую от бушующего моря уютную марину с дорогими яхтами и ресторанчиками.
Что делать?! Продолжать попытки доплыть до берега пока одна из волн с размаху не швырнет меня на эти вот ежи? Или уходить дальше в море и пытаться выйти на берег с другой стороны волнореза? А это еще метров 800 в бушующем море, уже после часа плавания и попыток выйти на берег. По течению конечно, но вдоль скал, когда приближаться к берегу нельзя, но и сильно далеко отплывать тоже не хочется. Кто его знает, это гибралтарское течение!!!
Звать на помощь было некого. Кому придет в голову гулять по волнорезу марины в шторм? Только такому же психу, как я. Все туристы сейчас в гавани, в уютных ресторанчиках… там, за волнорезом. Они меня даже не видят. Я был один на один со стихией и нужно было принимать решение… Счетчик тикал. Заказ нужно было сдать вовремя.
— Нет! Учитывать операции движения товара категорически не годится! — отрезал я и зачеркнул прямоугольник таблицы «операции». Вместо него я нарисовал другой — таблица «остатки». Туда я буду записывать текущие остатки артикулов во всех статусах: в пути, в закупках, в поставках и на складах. Правда придется переопределять те самые стандартные обработчики форм Django, которые тупо меняют данные в базе данных на новые значения из пользовательской формы.
Теперь значение в базе данных можно менять, только если мы формируем поставку или закупку. Если же операция касается склада (приемка закупки или отправка поставки), нужно действовать иначе. Если остаток данного артикула на данном складе уже существует, то значение, введенное пользователем, нужно прибавить к... или вычесть из существующего остатка. А если остаток отсутствует, то его нужно создать.
Если, если, если… Переопределяя обработчик формы, я, как в том известном анекдоте про брокеров, должен был постоянно задавать себе вопрос:
— Стоп... а сейчас мы продаем или покупаем?!
И не дай бог ошибиться со знаком. Вопрос заказчика: «Где товары, Зин?» мне практически гарантирован…
Однако это уже совсем другая история.
Мои университеты
В школе нас учили: «Повторение — мать учения».
Однако мой научный руководитель в университете все время ворчал: «Никакое повторение не мать, а мачеха... злая и нудная... Применение — вот истинная мама учебы».
Выкатив приложение в прод и взявшись за его поддержку на этапе тестирования «до устранения всех косяков», я смог неоднократно убедиться в справедливости слов своего научного руководителя. Вот уж воистину неисчерпаемый источник истины. Мне пришлось общаться с логистами и кладовщиками. И чуть ли не главным вопросом был:
— Здравствуйте! У нас тут остатки не бьются! Что мы делаем не так?
Самое обидное — я сам не знал, что они делают не так…
Мне просто негде было это узнать. Я с пристрастием, как строгий следователь СМЕРШа, расспрашивал их, что и когда они делали, а потом воспроизводил их действия в тестах, пытаясь нащупать, в какой момент что‑то пошло не так.
Забавно, что в моих тестах все работало отлично. Остатки считались как надо. Однако логисты продолжали жаловаться. Остатки не бились.
— Что это! Мой косяк или человеческий фактор? — вновь и вновь задавался вопросом я, буквально собственной шкурой (той, которая на сидячей части спины) впитывая понимание, что учетная система без лога действий пользователя — это «галимое фуфло»!!!
Ох! Как бы мне хотелось, подобно участнику шахматного турнира одновременной игры Остапа Бендера, встать и сказать: «Позвольте, у меня все ходы записаны!»
Однако они не были записаны.
Теперь мне не были нужны книжки или статьи. Теперь я мог сам на пальцах и о‑о‑очень доходчиво объяснить любому яйцеголовому архитектору, что лог операций — это «не роскошь, а средство передвижения». Все операции, все действия пользователя нужно аккуратно и дотошно заносить в базу данных.
— Мда… похоже мое решение строить систему на основе учета остатков было ошибкой! Учет операций — наше все!!! Да здравствуйте учет операций!!!
То, что лучший способ выиграть войну — избежать ее, отлично знает каждый, кто стоял на уроках физкультуры в шеренге одноклассников самым последним. Знает это сам, на собственном опыте, без советов мудрейшего Сунь‑цзы. Поэтому, дабы пресечь постоянные обращения в поддержку на корню, я добавил в базу данных таблицу «операции» и дописал еще пятьдесят строк в свой самодельный обработчик, а список операций по складу и артикулу вывел на страницу, открывающуюся при клике на цифру остатка. Теперь сами пользователи могли посмотреть, что же они там сделали не так и почему же у них не бьются остатки.
И хотя обращения в поддержку не пропали, главный вопрос изменился:
— У меня почему‑то задвоилась вчерашняя проводка приемки товара!
— А вы не могли случайно открыть форму второй раз в другой вкладке?
— Да кто же его знает! Я за полночь работала, нужно было срочно оприходовать партию, может и открыла.
— Ну, вот! Скорее всего из‑за этого два раза и записалось. Или по кнопке формы кликнули дважды… Ну, таким двойным щелчком.
— Может быть!
— Я вам сейчас эту лишнюю транзакцию удалю, но вы старайтесь приложение в двух вкладках одновременно не открывать.
Вот такой он, человеческий фактор. Даже если некорректный баланс — это не ваш косяк, а банальный человеческий фактор, виноват все равно архитектор.
Человеческий фактор — это не закон Ньютона. Это когда несчастный пользователь, работая за полночь, сначала отвлекается на телефонный звонок ребенка, пытающегося уснуть в одиночестве, потом идет налить себе кофе, а вернувшись на рабочее место открывает форму приемки в новой вкладке, забивает туда товар, и нажав на кнопку принять, видит еще одну открытую форму и снова нажимает принять. Виноват тут не он, а тот, кто не предусмотрел защиту от повторного открытия формы или двойного клика на кнопку «Принять»…
Однако это уже совсем другая история.
Песнь победителя
Несколько месяцев спустя я обнаружил в ТГ интересное сообщение:
— Здравствуйте. Меня зовут Ирина. Я раньше работала в вашей системе у Дмитрия. Вчера я вышла на работу в другую компанию. Нам тоже нужна такая система учета.
Что ж... это было признанием. Можно даже не говорить, что вторую систему я сразу же строил на основе учета операций. Однако мысль о супертяжелых запросах при формировании сводной таблицы остатков все же не давала мне покоя.
Однажды вечером, читая новости на каком‑то новостном ресурсе, я обратил внимание на интерфейс поиска. Там было две кнопки:
[искать в новостях][искать в архиве]
— Боже!!! Ну почему я не подумал об этом раньше?! Ведь это так просто!!!
В голове мгновенно всплыла формула, которую нам вбивали в память на курсах бухгалтерского учета и финансового анализа:
Исходящий баланс = Входящий + Обороты - Оборотыактивного счета баланс по дебету по кредиту
— Вот же оно! Ведь решение всё это время было у меня в голове.
Чтобы не суммировать миллионы транзакций «от сотворения мира» до даты отчета, записи старых операций нужно просто периодически переносить в архив, а вместо них добавлять одну единственную балансовую запись — входящий баланс на начало нового отчетного периода.
— Отлично!!! Добавляем в таблицу Операции два флага: «архив» и «баланс». Все!
Теперь, периодически запускаемый скрипт должен будет считать текущий баланс на дату архивации, добавлять балансовую запись, а все предыдущие операции помечать флагом «архив».
В приподнятом настроении я сдал систему в прод. Однако мне было любопытно, как быстро вал операций разрастется до такой степени, что это явным образом скажется на открытии страниц. Поэтому я решил повременить с запуском скрипта архивации в работу. И стал наблюдать.
Справедливости ради признаюсь, в этот раз мне было абсолютно все равно, сколько в базе приложения артикулов. Да хоть миллион! Крохотный JS скрипт, зашитый в страницу запрашивал данные крохотными порциями по 20 артикулов. Поэтому скорость агрегации операций влияла скорее на скорость скрола, но не на скорость первоначального открытия страницы. Тем не менее, посмотреть было интересно.
Время шло,страницы открывались бодро, никаких явных задержек не наблюдалось.
Моя идея оказалась до гениального простой, но в то же время изящной и невероятно мощной. Мои страхи «лагающей» агрегации оказались сильно преувеличенными — даже после года непрерывной работы задержки при открытии сводных страницах были незаметны.
Я тихо ликовал. В моей голове фанфарами гремел победный марш. Песнь победителя!!! Однако уже совсем скоро жизнь снова преподала мне маленький урок, в результате которого песнь победителя вполне могла стать лебединой песней…
Однако это уже совсем другая история.
Допработы
Прилетает заказ: «Мы уходим с FBO на FBS и нам нужен расчет себестоимости товара на складах».
— Хм! Решились таки смотреть на экономику… — подумал я.
С тех пор, как я узнал, что один из моих первых заказчиков вышел из бизнеса, я регулярно напоминал остальным клиентам о важности анализа юнит‑экономики.
— Половина ваших товаров продается в минус, но вам все равно, потому что вы все равно не знаете какая, — шутил я в переписке с клиентами. Клиенты обещали подумать, но реальных доработок никто не заказывал.
И вот, первая ласточка. Трудно сказать, сыграли ли в этом роль пожары на складах WB, или просто пришло время считать деньги. Не знаю, да и не важно. Надо — так надо. Наливаю крепкий кофе и сажусь рисовать схемки.
Как работает учет себестоимости и чем ФИФО отличается от ЛИФО, я знал отлично. Однако, компьютер не человек, ему не объяснишь: «Списывай в поставку первую поступившую закупку из оставшихся». Мало отсортировать закупки по дате. Нужно как‑то отмечать, какие партии все еще на складе, а какие уже уехали в поставки.
Напомню, что моя существующая схема подсчета остатков тупо суммировала все операции, выдавая только остаток, но информацию о том, какие закупки все еще на складе, а какие давно уехали взять было просто негде. Нужен был новый алгоритм учета!!!
— Да, проще простого!!! — подумал я...
Раз нужно отмечать оставшиеся не выбранными закупки, я просто добавлю в таблицу «операции» поле остаток. Записали операцию закупки, скажем, 100 штук артикулов — остаток партии равен 100. Поставили 30 штук — остаток уменьшили до 70, и так постепенно до нуля.
По моим оценкам, все работы по добавлению парсера актов FBS при создании новой поставки, а также внедрению учета себестоимости методом ФИФО займут порядка 17 часов. Отсылаю смету, получаю предоплату. Погнали!
Сижу рисую сценарии.
— А что если пользователь при формировании поставки снова дважды кликнет на кнопку формы? — перед моими глазами всплыло сообщение пользователя:
— У нас тут остатки не бьются!
— Черт! Снова те же грабли! — подумал я. Строить систему на учете только остатков нельзя. Я понял, что нужна дополнительная таблица, скажем «фифо_связи», в которую я буду последовательно записывать связи поставки со всеми использованными закупками, в момент ее создания. Таким образом, у меня образуется табличка, в которой будут храниться М2М связи между всеми поставками и закупками. Такая схема обещала один замечательный бонус, о котором заказчик вообще не говорил, но, скорее всего, сказал бы позже.
Когда я намекнул, что можно на странице движения товара напротив каждой закупки добавить табличку с датами и номерами поставок, в которые данная закупка была списана, заказчик пришел в восторг:
— А так можно? Это было бы просто супер!
— Черт!!! А ведь это дополнительная работа!!! — чертыхнулся я. В смете я это не заложил. Ну и ладно, зато заказчик доволен. Я взялся за исправление отображения страницы остатков.
Итак, я по‑прежнему начинаю со списка артикулов, однако в этот раз я не просто суммирую для каждого артикула все операции.
Первым делом мне нужно отфильтровать из списка операций по данному артикулу только закупки.
Затем, для каждой закупки нужно выбрать все связи с поставками и просуммировав их получить выбранный остаток.
Затем полученный результат нужно вычесть из первоначального объема этой операции. Получится невыбранный баланс.
После этого, нужно дополнительно отфильтровать закупки, оставив только те, у которых этот баланс строго положительный.
Затем нужно сгруппировать полученный список закупок по артикулу.
Наконец нужно агрегировать сумму произведения цены на невыбранный баланс каждой закупки и разделить результат на суммарный невыбранный баланс закупок.
Для тех, кому интересно, код:
prices = Transactions.objects.filter(
article = OuterRef("pk"),
storage = strg,
type ="ord",
).alias(
allocated = Sum("fifo_relations_set__amount")
).alias(
balance = F("amount") — F("allocated")
).filter(
balance__gt = 0
).values("article").alias(
total_cost = Sum(F("price") * F("balance")),
total_balance = Sum("balance")
).annotate(cost = F("total_cost") / F("total_balance"))
articles = Articles.objects.all().annotate(
price = SubQuery(prices.values("cost"))
).Все просто. Тестирую. Дыщ!!! Ошибка. База данных выдает исключение:
Programming error: impossible to calculate
total = Sum('balance')... balance is aggregation.
— О, как!!!
Оказывается Django ОРМ не умеет считать агрегаторы агрегаторов. Будем знать. Раскрываю скобки, агрегирую один раз сразу количество фифо‑связи умноженное на цену, отдельно количество в транзакции умноженное не цену и отдельно невыбранный баланс, а уже потом только складываю и делю. Правда пришлось посчитать невыбранный баланс дважды, чтобы сначала отфильтровать. Снова тестирую.
Вкладка браузера зависла… секунд 10 я смотрел на пустой экран, после чего появилась знакомая табличка с остатками и…. теперь уже с ценами.
— Ура!!! Цены считает… Но 10 секунд — это ни в какие ворота не лезет!!!
— Что делать?
А делать было нечего. Точнее нечего было и думать, чтобы оставлять это представление на стандартных джанговских алиасах и аннотациях. ОРМ не вывозит.
— Мы пойдем другим путем!
Пишу ту же последовательность в виде чистого сырого SQL‑запроса, заворачиваю его вcursor.execute. Запускаю тест. Вжжжжииииххххх… Страница открывается со свистом.
— Да уж!!! Вам Django или ехать?! — пародирую я бородатый анекдот. Конечно, нам ехать.
— Хм!!! А ведь это опять доработка сверх сметы… Ну кто же знал?!...ладно, надо доделать, чтобы все работало, — пробубнил я себе под нос.
Дописываю функцию вычисления себестоимости объекта поставки. Там проще. Снова берем список невыбранных закупок, и просто перебирая его в цикле создаем связи с поставкой, параллельно вычисляя средневзвешенную цену. Все. Дело сделано. Поставки создаются, цена аллокации считается.
— Так. Стоп!!! А почему у меня на сводной странице нулевые остатки? — задумался я.
— Элементарно, Ватсон! — хлопаю я себя ладонью по лбу.
В том самом сыром чистом SQL запросе я сам отказался от расчета остатков старым способом. Чтобы ускорить открытие страницы я решил использовать «total_balance», полученный из таблицы «фифо_связей». Ведь это же и есть суммарный текущий остаток, зачем его считать дважды. Но таблица связей‑то пуста. Нужен скрипт, который пробежит по всей истории операций и свяжет старые поставки со старыми закупками «от сотворения мира» до наших дней.
— Черт!!! Снова непредвиденная допработа!!! Куда же я смотрел, когда планировал?! Мда… будет тебе наука!!
Написал скрипт, вывел его на кнопку на странице настроек. Ну, на тот случай, если нужно будет внести в список операций исправления задним числом. Запустил. Остатки снова не сходятся. Смотрю на страницу движения товара. Напротив каждой закупки красивая табличка со всеми поставками, куда эта закупка разлетелась.
— Черт! А это что такое?
В списке одной из закупок красуется надпись «Списание в брак». То есть, если списание в брак — это такой же расход, как обычная поставка, значит возвраты из брака и разные пересортировки — это такие же поступления, как закупки. Но у них другой тип, а значит фильтр моего скрипта их просто игнорирует.
— Семен Семеныч! — в сердцах восклицаю я! Надо переписать.
Снова правлю код. Меняю фильтр с type="ord", на amount__gt=0.
Снова тестирую.
— Сошлось!!!!
Ну что‑ж. Проект практически закончен! Надо запросить ведомость остатков, чтобы еще раз сверить. Да и исторические цены не помешают.
— Я прикинул время, потраченное на дополнительные работы. Еще, как минимум, 4 часа.
— Вот же! Горе архитектор. Надо как‑то компенсировать... Хм… а не попробовать ли мне… Я уже давно это хотел сделать! — думал я…
Однако это была уже совсем другая история.
Эпилог. Те же и искусственный интеллект...
Фриланс‑биржи давно стали неактуальным источником заказов. Самые интересные проекты приходили через «сарафанное радио» или как доработки от старых заказчиков. Однако я давно думал о том, чтобы найти новых селлеров, которым интересны их собственные системы учета.
— А не написать ли мне статью. Например, на Хабр или в VC?
Признаться, в школе у меня было всего два по‑настоящему нелюбимых предмета. Это «русский язык» и «литература». И все из‑за необходимости писать сочинения. Я ненавидел писать. Преподаватель уже пыталась вразумить меня и так и эдак:
— Я понимаю, что ты будешь заниматься математикой. Но, пойми… грамотная речь и умение излагать свои мысли нужны математикам тоже! — как же она была права. Но тогда я этого не понимал.
— Статью? Я? Я не смогу… Стоп! Секунду! Я же специалист по автоматизации, как‑никак, или нет? — мне в голову снова пришла интересная мысль.
Если я не люблю писать, это для меня рутина, значит надо это автоматизировать. Я открыл поисковик google.com и переключился в режим искусственного интеллекта.
«Ты архитектор‑разработчик систем учета. У тебя есть опыт создания ERM/IMS/WMS систем…»
И я вкратце обрисовал свою историю за последние несколько лет.
«Напиши мне статью для Хабра. Только это должна быть интересная статья. Не скучнющая история успешного успеха или инженерная монография с кодом и заключением, а этакая IT драма в стиле О’Генри. С неожиданными поворотами и непредсказуемой развязкой.»
Я отправил промпт…
— А, ну‑ка, ведра, шагайте сами домой!
Получив ответ, я буквально подвис… хлопая глазами буквально как моя коллега в инвестиционном банке.
ИИ писал:
«Держи IT‑драму в стиле О’Генри в пяти частях с неожиданными поворотами и непредвиденной развязкой.
1. Эпоха „Вчерашних балансов“ (Пакетный учет)
На заре автоматизации (мейнфреймы на магнитных лентах) базы данных не умели в Random Access (произвольный доступ к строке). Чтобы обновить баланс одного клиента, нужно было перемотать всю ленту.
Поэтому системы считали остатки пакетным способом (Batch Processing). Днем транзакции копились на перфокартах, а ночью мейнфрейм запускал тяжелый пересчет, генерируя „снимок остатков на утро“. Днем операционист видел строго вчерашний баланс. И это была событийная модель (Event), ограниченная слабой мощностью процессоров.
2. Кризис реального времени и рождение State‑подхода
В конце 60-х появились жесткие диски и терминалы. Бизнес переходил на ERP системы, которые требовали планировать остатки в реальном времени.
Чтобы обойти слабость процессоров (которые не могли агрегировать тысячи транзакций на лету при каждом клике кассира), архитекторы пошли на сделку с дьяволом. Они изобрели State‑based базы данных.
В память закладывалась готовая цифра остатка, и при каждом действии она просто перезаписывалась (amount += X). Это дало невероятную скорость в реальном времени, но полностью лишило систему истории.
3. Эра „Атак салями“ (Salami Slicing) и уязвимости State‑подхода
Когда системы перешли на State‑based учет, архитекторы получили скорость, но потеряли прозрачность. Если в системе нет жесткого правила, что любое изменение состояния (State) должно быть следствием неизменяемого события (Event), база данных превращается в „черный ящик“. Вы видите, что на счету лежит 100 рублей, но не можете доказать, как они туда попали.
Этот период ознаменовался громкими скандалами, связанными с так называемыми атаками „салями“ (Salami Slicing Attack).
Суть атаки: Это метод совершения финансового мошенничества, при котором от огромного количества транзакций или счетов незаметно „отрезаются“ крошечные доли — например, сотые или тысячные доли копейки/цента.
Механика в State‑системах: При округлении процентов по вкладам или конвертации валют всегда остаются дробные хвосты. Программист‑злоумышленник вносил изменения в код логики округления. Вместо того чтобы округлять сумму по математическим правилам или оставлять остаток банку, система переводила эти „микро‑отрезы“ на один подконтрольный счет.
В чисто State‑ориентированной системе обнаружить это было практически невозможно. На балансовом счете злоумышленника цифра росла (balance += 0.001), а у миллионов клиентов она незначительно уменьшалась или округлялась в меньшую сторону. Поскольку общего лога транзакций (Event Log), который можно было бы пересчитать с нуля и обнаружить расхождение, не существовало, аудит сводился к простой проверке: „Сходится ли дебет с кредитом прямо сейчас?“. И он сходился, ведь деньги не возникали из ниоткуда, они просто перетекали внутри системы.
4. Великий миф, перекочевавший в учебники
Истории атак „салями“ перебирались со страниц новостей сначала в корпоративный фольклор, а потом и в учебники по информационной безопасности. Они были настолько красивыми и кинематографичными, что кочевали, по ходу обрастая придуманными подробностями, из одного учебника по информационной безопасности и проектированию БД в другой на протяжении 80-х, 90-х и 2000-х годов.
Самый известный — миф о „программисте из Ситибанка“, который якобы сколотил состояние на округлениях и попался только потому, что его счет стал слишком большим.
Интересно то, что в реальности большинство этих историй — полумифы или сильно преувеличенные кейсы (в реальности мошенников ловили на банальной невнимательности или обычных переводах). Однако этот „страх перед скрытым изменением состояния“ глубоко укоренился в сознании теоретиков.
Учебники десятилетиями твердили: „Главная проблема State‑подхода — деструктивное обновление (Destructive Update). Когда вы делаете UPDATE accounts SET balance = balance + X, вы уничтожаете прошлое. Вы беззащитны перед инсайдерами и ошибками округления“.
5. Возвращение короля: Как старые учебники продали нам Event Sourcing
И вот мы дожили до 2010–2020-х годов. Процессоры стали невероятно мощными, дисковое пространство — дешевым, а концепции распределенных систем (микросервисов) захватили мир.
И тут авторы современных архитектурных фреймворков открыли те самые старые учебники с байками про атаки „салями“ и заявили: „Посмотрите на классический бухгалтерский учет! Бухгалтеры никогда не стирают ластиком старые записи. Они пишут проводки (События). Если нужно что‑то исправить, они делают сторно (новое Событие). State‑подход — это зло, порождающее уязвимости и лишающее нас аудита!“
Так старые академические аргументы и страшилки про атаки „салями“ стали мощнейшим маркетинговым топливом для нового пришествия Event‑подхода (сегодня мы знаем его как Event Sourcing и CQRS).
Современные апологеты Event‑подхода обещают нам архитектурный рай:
Абсолютный аудит: Больше никакой „салями“ — каждое изменение баланса записано в immutable‑лог (Event Store).
Time Travel: Мы можем „отмотать“ систему на любую секунду в прошлом и посмотреть, как выглядел баланс.
Гибкость: Мы можем пересчитать состояние системы с первого дня, если логика округления изменилась.
Заключение. Ирония судьбы
Мы совершили полный эволюционный виток. Мы ушли от Event‑подхода в 60-х, потому что железо не справлялось с вычислением состояния по цепочке событий. Мы перешли на State‑подход, заработали кучу проблем с консистентностью и аудитом, испугались атак „салями“ со страниц учебников, и теперь, когда железо позволяет, снова топим за Event‑подход, маскируя его под ультрасовременные паттерны проектирования.
Правда, как всегда, оказалась посередине: современный энтерпрайз использует Event‑подход для записи (бизнес‑логика, аудит‑лог) и State‑подход для чтения (Snapshot'ы, read‑модели), пытаясь наконец‑то примирить эти две исторические крайности.»
— Ох, ни фига себе!!!!…
Тут целая история, выходит я прошел тот же путь, что все эти большие парни. Странно, что я этого не нашел. Тут даже названия уже все придуманы. Учет балансов — это State approach, а учет операций — это оказывается Event approach. Это сколько же сил и времени я бы сэкономил, если бы сразу искал байки по кибербезопасности и истории мошенничества, вместо книг по архитектуре учетных систем! Забавно!!!
— Эй! Жулик! А статью! — написал я роботу..
ИИ прислал статью...
— Вкалывают роботы… а не человек... — напевал я себе под нос, исправляя текст полученной статьи.
— Ну, кто так пишет.... Кто так пишет... Я в школе лучше писал! — ворчал я
Через несколько часов текст статьи был основательно пересобран, так что вы, читатели, теперь никогда не догадаетесь, где здесь мои настоящие мысли и истории, а где — чистые галлюцинации Гемини.
Снова перечитав текст, я сидел и думал:
— Инструменты меняются, а задачи автоматизации остаются.
По щучьему велению, по моему хотению… А ну‑ка, ведра, шагайте сами домой!!!
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.