Pull to refresh

IceCash 2.0 Web АРМ Кассира и АИС по обмену данными с кассами под Linux на Python

Reading time 15 min
Views 8.3K

Как-то меня спросили: «Зачем писать то, что уже написано многократно и на более профессиональном уровне? То что ты сделаешь будет заведомо хуже и лишено грамотной поддержки». Я тогда ответил просто: «Мне хочется, чтоб под линух и чтоб код свободный. Чтоб драйвера не покупать для кассы».

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


Нафига козе боян?


Вообще, конечно, «зачем?» это очень важный вопрос. Им нужно озадачится заранее. Прежде чем что-то писать, нужно оценить необходимость, полезность, чтобы самому было не обидно потом. Я после, не раз оценивал зачем я делаю этот проект, находил умные объяснения, но на самом деле… просто чтобы под Linux и чтобы код свободный. Ну и, конечно, драйвера для фискальных аппаратов — это слабо понимаемый мной бизнес.

Итак, хотелось сделать некоторую основу для кассового рабочего места, которую можно было бы изменять под свои задачи. Как ни крути, а очень многие дорабатывают кассы «под себя». Касса должна была бы работать под Linux, в веб браузере, а ее серверная часть могла бы располагаться теоретически где угодно. Сейчас фискальные регистраторы уже «почти» обязаны иметь сетевой интерфейс, а значит серверная часть может быть вообще где угодно. Хоть у вас дома в тумбочке на raspberry. Это сейчас уже не новость, касса мутирует. Интерфейс кассы перелезает на планшеты и телефоны.

Вообще, найти почти бесплатное и почти свободное кассовое ПО можно. Что-то будет не полностью свободное, не всегда бесплатное, громоздкое и не отделимое от собственной системы. Так часто бывает, что простой проект усложняется и создает целую свою среду, от которой ее потом не отодрать. Найти нужное ПО именно с веб интерфейсом не получилось. Ну тогда я закатал рукава, а через месяц и глаза… И предстал предо мной кодерский ад рай.

Первая версия. С дикими глазами и маниакальным трудолюбием.


image

Первую версию, я смастерил быстро. Об этом, я писал статью на хабре когда-то. Да там было много ошибок и ужасный код. Но это выполняло на тот момент задачу минимум. В начале это был легкий и слабенький (практически HTML) сайт на PHP и несколько утилит на питоне. База данных в MySQL, транспорт данных перл-скриптами. На бэкофисе тоже сайт для просмотров чеков и производных отчетов. Свободный драйвер для Штрих-М, написанный неким кодером, был прилеплен к моей системе. Хотя, если быть справедливым, то правильнее сказать, что основой послужил этот драйвер и без него ничего бы не вышло. Именно тогда я уяснил основную проблему самописного свободного кассового ПО — свободных драйверов для ФРК нет.

Я постоянно дописывал сайт, как обычно, опаздывая с рефакторингом и код обрастал уже сложным каллокодием. Так бывает, когда не понятно в какую сторону и насколько далеко начнет уходить концепция. А швыряло ее в разные стороны не по детски. Различные акции, бонусы, призовые системы. Сложность возрастала, энтропия съедала проект изнутри. Минусы системы, с которыми больше не было возможности мириться, в конце концов заставили меня все это переписать. Помимо наведения порядка в коде, необходимо было разделить в кассе все подсистемы, которые уже начали склеиваться друг с другом, умножая хаос.

Нам всем пришел ЕГАИС! И не только..




К тому моменту, когда назрело написание второй версии, уже во всю трубили о том, что не долог век обычных фискальных аппаратов. Напомню, что за последние два года были существенные изменения в законодательстве в области учета розничной торговли. И долгое время не меняющееся положение дел в этом секторе нехило тряхнуло рядом нововведений. Первым пришел ЕГАИС. Да, моя организация занималась торговлей пивом и алкогольной продукцией. Мои коллеги из других организаций, также как и мы, с «радостью» встретили единый электронный документооборот. Я не буду ныть по поводу изъянов этой системы. Сейчас, все как-то более или менее нормально. Конечно, момент внедрения это всегда особенно яркие и запоминающиеся дни и ночи… Все было как обычно — сумасшедшие сроки, невозможность нормального тестирования, сложность регистрации, отсутствие техподдержки и Новый год ). Сейчас мне кажется, что все что мы делали, это какая-то нереалистичная комедия, участники которой делают вид что это всерьез.

В общем, на рынке появилось не мало решений, с помощью которых можно было это требование исполнить. Однако нам было необходимо на каждой кассе поставить ключ JaCarta и обеспечить этот документооборот через программу УТМ (Универсальный транспортный модуль). На момент внедрения, как это обычно бывает, ПО было только под Windows. Поэтому, какие бы фантастические сюжеты не рисовали новаторы сего дела, рассказывая нам о том как все будет легко и просто и под любую систему, на деле оказалось мягко говоря не так.

В общем, к сроку, оборудовать торговые точки новыми системами ПО под ЕГАИС мы не успели. Но, честно говоря, менять ПО на пятидесяти объектах, покупая попутно лицензионный Windows и кассовое ПО, не очень-то хотелось. Только на ключи JaCarta пришлось единовременно выкинуть приличные средства. А ведь многие тогда вообще отказались от торговли алкоголем. Почесав репу, мы сделали по старинке. Был быстренько накидан веб-сервис, который взаимодействовал с единственной копией УТМ и десятками аккаунтов на торговых объектах, где осуществлялось подтверждение приходных документов. Связка пятидесяти ключей jaCarta была вручена специально подготовленной сотруднице. Ей был поставлен комп с мастдаем и нужным ПО. Перетыкая каждый день (на протяжении полугода) ключи и тыкая мышкой на сайте в нужные места, новоявленный внештатный провайдер ЕГАИСА осуществлял обмен документами. Думаю мы не единственные в этом идиотизме, некоторые так делают по сей день.

Продажа алкоголя предполагает работу с ЕГАИС в момент продажи каждого чека, а это уже требовало доработки кассового ПО. До момента внедрения было всего полгода. Так что работая на нехитрой времянке по приему документов ЕГАИС, мы быстро быстро начали делать вторую версию нашей кассы.

Пишем по феншую.


Итак полгода для разработки это много. Загвоздка в том, что разработчик по сути один. В помощь ему только сисадмин. Ну и никто нас от текущей работы не освобождал. Но сделать новую версию очень хотелось, поэтому я вновь с жадностью начал прогать. Теперь было уже понимание того из каких независимых частей должен состоять проект и как эти части реализовывать.
Итак, мы остановились на следующей схеме.

СЕРВЕР ДРАЙВЕРОВ


Помимо существующего драйвера для Штрих-М, нужно было реализовать драйвер для FPRINT (Атол) и драйвер термо-принтеров. Все эти драйвера унифицировать одинаковыми методами печати и единым дизайном чека.

Вообще, идея подключить еще и термо-принтеры, коих у нас было навалом, мне показалась удачной. Я же не знал что спустя два года будет возможна печать только фискальных чеков на обновленных ФРК, подтвержденных онлайн у оператора фискальных данных. Хотя впоследствии, вариант, при котором термо-принтер всерьез пытался прикинуться ФРК+ОФД оказался кстати в моменты, когда фискальник был сломан.

Управление фискальными рестораторами и термо-принтерами, поддерживающими протокол ESCPOS должно было осуществляться через отдельный web-сервис. То есть, все что касается взаимодействия с оборудованием отделяем в отдельное независимое приложение, взаимодействие с которым должно осуществляться по XML протоколу.



Все это было написано и оттестировано за пару месяцев. Мне повезло, что я писал подобную задачу на заказ. То есть реализация печати на термо-принтерах по XML была реализована накануне. Оставалось только написать атоловский драйвер и как-то унифицировать весь этот зоо технопарк одинаковой печатью чеков и их копий. В итоге получился отдельный сервис, который я назвал DTPrint. У него тоже был небольшой сайт, для детальной настройки ФРК. Позже я написал консольный скрипт, с помощью которого можно было обращаться к сервису и передавать ему текстовый файл для печати с нужными параметрами. Так у меня на работе завелся свой факс-лог сообщений с сервера, который печатал всякую админскую инфу с сервака. А позже и дублировал сообщения с jabber-клиента pidgin. В общем, я себе ни в чем не отказывал, в процессе творческого прорыва порыва ). Ах да, еще драйвер fprint играл бипером имперский марш после снятия зет-отчета.

САЙТ КАССЫ


Также как и в первой версии, основой должен стать web-сайт. Только теперь без apache и PHP — только Python. Интерфейс кассира должен стать более дружелюбным ). Также необходимо было навернуть админских функций к интерфейсу и сервисных функций по инициализации обмена данными и прочим полезностям.
Веб-касса, понятное дело, должна была взаимодействовать с сервером драйверов, сервисом обмена данными, бонусным сервером и рядом других бекофисных сервисов.

Данные решили также хранить в MySQL. От системы хранения транзакций (как у Штрих-М) я решил уйти. Теперь в БД хранились именно чеки, целиком. Все необходимые дополнительные данные об операциях с чеком оседали в виде значений полей заголовка или содержимого чека. Это я хотел сделать уже давно. Выборки из такой БД были просты и лаконичны. Расчет зет отчетов прозрачен и без заморочек.

Структура программы-сайта должна была быть максимально разделена внутри на отдельные модули, содержащие объекты подзадач. ЕГАИС модуль, бонусный модуль, модуль обработки чека, модуль БД запросов и т.д.

Важным дополнением должна была стать реализация ЕГАИС. Во первых, в части получения и подтверждения приходов, во вторых в части реализации алкоголя. Вообще, ЕГАИС, в дальнейшем оказался большим и нескончаемым потоком различных документов, запросов и обновлений. Сейчас, я бы его выделил в отдельный модуль над УТМ. Сама программа UTM постоянно менялась, менялся и протокол. Недавно (1юля 2017) ввели его вторую версию. Не удобным осталось обновление ключа на токене JaCarta. Хотя сейчас, многие организации предлагают это сделать удаленно, для Linux+JaCarta это пока еще сложновато. Поэтому обновляем мы ключи, подключая их в ноутбук с Windows. Думаю, эту часть можно было бы доработать, а то слишком хлопотно получается.

Я долго колебался, на счет того, как именно реализовать выгрузку чеков. Если сказать проще, то мучил вопрос: «Сколько онлайна нужно кассе?». Будет ли она иметь режимы онлайн, оффлайн, как будет проходить обмен? В итоге касса была реализована отдельно от функции обмена. Сайт откликается на команды выгрузки чеков, зет отчетов или загрузки данных с сервера. Процессы обмена, запускаемые с нужной частотой из crontab не влияют на работу кассы. Нет связи — не беда, как появится, накопленные данные по отбитым чекам уйдут, прайс обновится.

Как показала практика самый сложный узел — это обработка чека. И это не только печать, это, главным образом последовательный расчет чека. Именно здесь всевозможные доработки логики разных акций, бонусов, розыгрышей сделали расчет чека в первой версии абсолютно не понимаемой черной дырой. Поэтому в новой версии я решил все, что касается чека максимально детерминировать и красиво разложить на методы объекта.



Код был написан, как мне кажется, достаточно хорошо. Всегда можно лучше, разумеется. Но того, что было сделано надолго хватало для стабильного усложнения проекта без опаски съехать в гов хаотичный код (G-CODE). Есть, конечно некоторые некрасивости. Так, применяются разные способы взаимодействия с javascript. Началось с разработки шаблонов в серверной части, закончилось json подгрузкой и более сложными функциями javascript на стороне клиента. XML для взаимодействия с сервером драйверов, конечно, лучше было бы заменить на json. Но, перерабатывать это было уже некогда. И, конечно, я сэкономил на дизайне, так как дизайнер во мне умер еще при рождении.

На замечания сисадмина Вовки по поводу ядовитости цветов, я только бурнул, что надо написать несколько разных CSS расцветок. Но в итоге была добавлена еще только одна — серая, она и стала основной. А ядовитая стала админской. Это было полезно для того, чтобы не путаться в сессиях кассы.



Из плюсов было то, что все оказалось разделено на максимально независимые программные модули, любой из которых можно безопасно переписать, не затрагивая основного кода. Вообще, этот подход мне показался очень правильным. При такой мозаике кода, можно собирать разные конструкции, ориентируясь только на протоколы взаимодействия. Однако есть один важный момент — протоколы должны быть изначально хорошо продуманы, чтобы их потом было легко дорабатывать и не менять основу.

Одновременная работа разными пользователями с кассой-сайтом сделало возможным торговлю с различных устройств в торговом зале. Позже я дорабатывал модуль к кассе для того, чтобы можно было работать с заказами в стиле обслуживания очереди кафе быстрого питания. Все оказалось легко дорабатываемо и масштабируемо.

Еще оказалось полезным быстро подключаться к текущему интерфейсу кассира, фактически наблюдая за его действиями онлайн через браузер или набивая себе самому отложенный чек, чтобы не стоять в очереди ).



Вообще скорость администрирования существенно увеличилась. Стало возможно вмешиваться в процесс оплаты, настроек, управление драйвером с браузера в телефоне (через VPN). Хотя VNC и ssh никто не отменял и все инструменты активно использовались. Некоторые настройки были глобальными и кассы сами их забирали с сервера, заодно отсылая информацию о самих себе (существенные параметры настроек, ip-адрес, время подключения, версия ПО). Со временем на кассе образовался богатый crontab и скрипты всякой самодиагностики и отсылки отчетов на сервер. Но тут особо хвалиться нечем, так делают все.

СЕРВЕР УПРАВЛЕНИЯ КАССАМИ


Для эффективного администрирования касс, необходимо было создать, собственно сервер управления кассами. Как минимум, в него должны были собираться все чеки с касс и через него должно было происходить обновление ПО. Как показала практика, это очень важная задача в самописном проекте.

По сути это был еще один web-сервис и сайт. В него должны были заноситься основные справочники: организаций, прайсов, групп товаров., кассы и их тип прайса. Кассы должны были быть разделены по организациям (филиалам), которых может быть любое множество. Позже, сюда добавилась акционная система — это написание и раздача кассам акционных скриптов. Ну и что-то типа своего языка этих самых акций пришлось изобрести.



Первая важная функция этого сервера — принимать и раздавать кассам новые прайсы, с нее я и начал. Прайсы должны были привязываться к кассам. То есть на самом сервере мы указываем для каждой кассы свой прайс из имеющихся. Касса же просто обращается к серверу сообщая свой идентификационный номер. В ответ, сервер дает добро на GET скачивание zip-архива. Если скачивание было успешно, то сервер сам у себя помечает время последнего скачивания для этого клиента и больше ему прайс не дает. Ну, разумеется, пока новый прайс не будет загружен на сам сервер.
А загрузка файла на сервер это тоже обычная PUT HTTP операция, только нужно указать идентификатор филиала и самого прайса. Позже, мой товарищ, которому мы на работе тоже внедряли часть моей системы, на 1С написал нужную обработку для выгрузки прайса сразу на сервер. Заодно им были написаны обработки для загрузки JSON зет-отчетов с этого же сервера.



Вторая важная функция — это сбор данных с касс. Зет отчеты и чеки. Вообще я пришел к тому, что зет-отчеты на кассе стала формировать сама касса. То есть уже готовые, сгруппированные по товарам, с нужными итоговыми параметрами, зет отчеты находятся в БД. И в веб-интерфейсе их можно детально изучить. На ранней стадии написания кассы, я все реализовывал без сервера сбора данных, поэтому касса и сервер содержат идентичные функции по выдаче зет отчетов и чеков по запросу извне. Получается, что при желании можно обойтись без сервера, просто запрашивать данные у кассы напрямую. Итак, касса, по запросу формирует два потока данных в json — поток не выгруженных зет-отчетов и поток не выгруженных чеков. Данные загружаются на сервер, а оттуда по запросу, выгружаются для 1С. Как вариант, были написаны скрипты, которые выгружают зет отчеты в специализированные текстовые файлы, наподобие фалов Штрих-М.



Из самого интерфейса сервера можно было просматривать чеки. Но функционал этот был неудобный и недостаточно мощный. Поэтому, позже, я написал еще один сайт ), через который можно было делать различные выборки из БД чеков. Этот сайт я планировал развить, добавив туда возможность просмотра зет-отчетов и прочие наработки по существующим рабочим веб-приложениям, ориентированным на прошлую версию IceCash. Но увы, на это не хватило времени, это так и остался сайт одного отчета.





Также на сервер была возложена функция обновления кассового ПО. Обновления были двух типов: обновление системы и обновление программы. Любое обновление начинается с отслеживания версий. Версии, которые мы вводим в эксплуатацию (prod), просто устанавливаются глобальными переменными на сервере (update и upgrade). Все серверные переменные автоматически раздаются кассам. Сюда же могут входить и любые интересующие нас глобальные параметры для всех касс, требующие постоянной репликации. Например, у меня так раздается температура воздуха. Каждый филиал может иметь свой набор переменных и значений, так что погода раздается регионально. Это потребовалось не ради забавы. Была такая акция — по погоде, там формула скидки ориентировалась на температуру.

Так, вот, версия предписанная для установки обновления также раздается переменными, регионально. Это удобно, для ступенчатого тестирования. Сначала на одной кассе, затем на маленьком филиале, на следующий день — на большом. Ну это если все по феншую, у нас то по началу были частенько авралы, связанные с непоследовательным накатом версии.

Обновление программы, это фактически скачивание нужной версии и замена старой, с перезапуском сервисов. Версия работающей программы, программа знает из константы в коде. На сервере можно выставить любую бОльшую, чем работающая, версию и касса ее подхватит.
Обновление системы это tbz архив, содержащий нужные файлы и bash скрипт обновления. Эти обновления выполняются последовательно. Необходимы все архивы на сервере, для того чтобы касса могла последовательно их установить, актуализируя свое состояние до требуемого.



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

Кроме этих задач, в итоге была еще изобретена система создания, хранения и раздачи скриптов акций. Современная розница не может без этих акций жить. Я не знаю насколько они действительно нужны, но для программистов розницы есть специальное место в аду — акционная система. Акции постоянно входят в конфликт:

  1. Сами с собой.
  2. С бонусной системой.
  3. Системой ограничений, основанной на прайсах.
  4. Со всеми системами, работающими с чеком.




Акции капризны и коварны. При этом они всегда в почете у менеджеров, а директор обязательно затребует детальный анализ по их эффективности, от которого вскипает мозг. И это еще не все, акции просто не хотят быть однотипными. Каждая не хочет походить на остальных и норовит вырвать лишние часы вашего внимания, чтобы понять и оттестировать эту заразу. И да, важно не забыть выставить актуальный срок ее работы.



В общем, мои попытки автоматизировать процесс создания акций в первой версии не привел ни к чему, кроме растраты времени и нервов. Поэтому во второй версии я напрочь отказался создания веб-форм с миллионом галочек для менеджеров, потому что следующая акция потребует миллион первую галочку. Вместо этого я написал отдельный модуль в котором реализовал мини-язык этих акций. И уже этим языком я последовательно описывал каждую акцию своим скриптом. Условие чека, выборка, группировка, замена и добавление позиций, и т.д.



Иногда приходилось лишь чуть чуть поменять скрипт, иногда дописать команду. Но зато теперь все менеджеры и интерфейсы для них шли лесом. Нервозатраты точно сократились, стало понятнее как это все работает. А на уговоры сделать интерфейсы для менеджеров я не соглашался, но в шутку предлагал изучить язык акций и писать на нем скрипты.

Дополнительные сервисы


У нас уже была разработана своя бонусная система в виде сервиса на Python+Mysql. Поэтому, был лишь дописан модуль для взаимодействия с ним. Также, была система розыгрышей призов. Это что-то типа лотереи на сервере, где каждая касса после оплаты чека инициирует розыгрыш. Для этого тоже был написан отдельный модуль. А призовая система на точке включалась определенной акцией.

Все эти сервисы, включая и основной сервер обмена были завернуты в свою openvpn сеть, это был самый простой способ обезопасить систему. Кроме того, на кассах работал свой фаервол.
OPENVPN ключ для кассы можно использовать один, если нет необходимости точно закрепить ее за конкретным адресом. Весь пул уже сгенерированных ключей, с закрепленными за ними адресами мы трогать не стали. Но в теории, сервер обмена данными всегда знает какая касса, под каким адресом к нему подключилась.

Оператор фискальных данных


Да, мы справились. Но было еще предостаточно пространства для творческого маневра. Нам бы немного времени, чтобы запилить все красиво. Но… Пришла вторая волна изменений. Тем кто знаком с требованием ФЗ-54, объяснять не надо. Сначала запустили страшилки от налоговых органов. Попутно, как обычно, наврали заверили, что в итоге эксплуатация ФРК будет дешевле, чем раньше. Потом обещания от производителей фискальных регистраторов, заверяющие нас, что все будет в срок. Потом, как обычно, оказалось что никто не готов… Организация шабаша была на троечку.

Бизнес медленно и фырча от недовольства сплевывал горький привкус предвкушаемых затрат и пробовал на вкус незрелые плоды от бистро-автоматизаторов. А мы тоскливо посмотрели на сибирские сугробы и криво улыбались — до лета нам было чем скоротать время.

Самое неприятное во всех этих масштабных изменениях, что все плевать хотели на таких разработчиков как мы. Документации ноль, сроки сжаты. К тестированию допущены только белые люди. Пощупать новый ФРК удалось не скоро. Итак, вариантов для нас было несколько:

  1. Новый АТОЛ
  2. Новый Штрих-М
  3. Старый Штрих-М с комплектом доработки.

По идее, производители фискальных аппаратов должны были поменять ЭКЛЗ на Фискальный накопитель и реализовать отправку данных в ОФД, снабдив фискальник возможностью взаимодействовать с каналом интернет связи. Но выполнить одну и туже задачу можно миллионами способов. И, конечно, их будет в итоге хотя-бы несколько. Итак, были фискальники без LAN гнезда. С помощью специального драйвера, можно было создать LAN over USB и радоваться жизни… если ты под windows или если у тебя много времени. Но, у меня этого времени не было. Поэтому от новой дешевой поделки от Штрих-М РИТЭЙЛ-01 пришлось отказаться.

Зато вариант с комплектом доработки вполне себе подошел. Фискальнику делали шунтирование, вставляли новые мозги и LAN выход. Однако, Штрих не работал как сетевой принтер, он подключался двумя хвостами. Одним, как и раньше, к компьютеру, другим (LAN) к роутеру. Таким образом, он самостоятельно отправлял данные в ОФД и работал с драйвером привычным образом. Но драйвер немного пришлось доработать.

У Атола решение было поинтереснее. Принтер вообще можно было сделать сетевым и обращаться с кассы к нему по LAN. Здесь, в драйвере тоже пришлось сделать некоторые доработки.

Надо сказать, что все тестирование и доработки по драйверу совпали у меня со сменой работы и переездом в другой регион. Однако это не стало катастрофой. Я доработал драйвера удаленно и сейчас все это сносно функционирует на пятидесяти объектах. Правда, одна организация, которую я до введения ОФД перевел на IceCash, все таки отказалась от дальнейшего сотрудничества со мной и, наверное, перескочила на другое ПО.

Все прелести внедрения новых фискальных аппаратов меня тоже не коснулись, мой бывший напарник Вовка хлебнул этого счастья сполна. Потому я и не знаю подробностей о регистрации и настройке новых ФРК. Но об этом не мало написано.

Пилите, Шура, мне не жалко.


Так получилось, что сейчас я работаю программистом в крупной компании и нахожусь далеко от региона, где внедрена касса IceCash. Я что-то дописываю, но у меня жуткий напряг со временем. Требования к ПО тоже не стоят на месте, развивается ЕГАИС, возникают новые потоки документов. Жалко будет, если вдруг очередная розничная сеть прекратит свое существование, а вместе с ней и проект. Возможно, кому-то приглянется эта поделка, его часть, модуль, кусок кода. Может, тогда удастся поддержать, развить проект или создать хороший форк.

Я попытался сделать все так, чтобы стороннему разработчику было легко взять любую часть проекта, отделить ее свободно, не увлекая за ней весь зоопарк изобретенного функционала. В общем, пилите кому надо ;)

Ссылки


Здесь выложен код IceCash2 на git GIT ICECASH2
Сервер обмена GIT ICESERV
Вики, которую я не дописал: WIKI
Tags:
Hubs:
+13
Comments 13
Comments Comments 13

Articles