26 June 2019

Опыт интеграции онлайн кассы Атол с собственной торговой CRM

Development for e-commerceFinance in IT
Tutorial
Вокруг онлайн касс в последнее время дикий ажиотаж, 1 июля 2019 заканчивается последняя отсрочка, поэтому и мне пришлось заняться этим вопросом. Тем, у кого 1С или другая система особо можно не напрягаться, но если у вас собственная самописная система, то на ваши плечи ложится еще и интеграция с онлайн-кассами.

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

Мне на опыты попалась касса Атол 30ф — это такая простая печатная машинка с черным ящиком (ФН), так раз подходит, когда вся логика по составлению заказов лежит на внешнем софте, а не на софте, встроенном в кассу. К тому же, аппараты такого типа относительно недорогие, по сравнению с андроидными аналогами.

Отдельно хочу заметить, что «специалисты» некоторых компаний, занимающиеся поддержкой вообще не в курсе, что у Атол с 10й версии есть встроенный веб-сервер в драйвере, который принимает JSON-задания, более того, этот драйвер можно установить и на linux, судя по количеству готовых решений на малинках, могу предположить что там тоже можно установить, в дистрибутиве 10й версии драйвера установщик для arm присутствует.

Планируемая схема примерно такая — есть CRM, которая крутится на сервере в локальной сети, ее открывают из браузеров, с серверной стороны на PHP через curl будут отправляться чеки и печататься на кассе. А сама касса подключена к любому компу на Windows в этой же сети.

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

Драйвер десятой версии скачиваем вот тут.

Перед установкой нужно установить Java той же разрядности, что и драйвер, иначе галочка web-сервер не будет доступна, если устанавливаете 64 битный драйвер ККТ, то и Java x64.

Вроде бы по логике нужно на 64 битную систему ставить 64 битный драйвер, но некоторый софт 32 битный не сможет с ним работать (вроде и к 1С такое относится, если она 32 битная).



В конце установки есть галочка — конфигурировать веб-сервер, если ее не поставили, то надо зайти в браузере на 127.0.0.1:16732/settings, поставить галочку «активировать сервер» и сохранить.





После этого нужно перезагрузить сервер через ПУСК->АТОЛ->перезапустить…

Еще сразу хочу предупредить, если запустить веб-сервер, то локальные приложения не смогут получить доступ к ККТ, я долго маялся, установил драйвер, запустил тест драйвера ккт, а он мне говорит что порт занят и всё, звонил в техподдержку местного продавца, там сказали не знаем что делать, потом десять раз перегружал комп, переустанавливал драйвер, ничего не помогает.

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

Этот веб-сервис не имеет никакой защиты по паролю, поэтому нужно сразу настроить брандмауэр Windows или другой софт, чтобы по порту 16732 могли обращаться только нужные компы, в моей ситуации это сервер на котором крутится CRM.

Общение с веб-сервисом вообще отдельная тема, очень интересная...

  1. Генерируем уникальный uuid для задания
  2. Отправляем задание методом POST
  3. Долбимся на веб-сервис, ожидая результата задания с нашим UUID, может быть так, что несколько секунд у нашего задания будет висеть статус wait, а может возникнуть error, если в запросе что-то не так сформировали...

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

код на php для примера работы с api (использовать только в учебных целях)
<?php

Class AtolWebDriver
{
	protected $addr="127.0.0.1",$port="16732";
	public $timeout = 30; //таймаут соединений
	public $operator;

	function __construct($addr=false,$port=false)
	{
		if ($addr!==false) $this->addr=$addr; 
		if ($port!==false) $this->port=$port; 
	}

	public function CallAPI($method, $data,$_url="/requests") 
	{
		$url = "http://".$this->addr.":".$this->port.$_url;
		$curl = curl_init($url);
		curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($curl,CURLOPT_TIMEOUT, $this->timeout);
		$headers = ['Content-Type: application/json'];
		curl_setopt($curl,CURLOPT_HTTPHEADER, $headers);
		curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
		curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));

		$resp = curl_exec($curl);
		$data = json_decode($resp,1);
		$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);

		curl_close($curl);

		$res= [$data,$code,$resp];

		print_r($res);
		return $res;
	}

	//получает результат задания
	public function get_res($uuid)
	{
		$ready = false;
		$cnt=0;

		$res_url = '/requests/'.$uuid;

		while (!$ready && ++$cnt<60) 
		{
			usleep(500000); //подождем чуть, прежде чем просить ответ

			list($res,$code,$resp) = $this->CallAPI('GET',[],$res_url);
			$ready = ($res['results'][0]['status'] == 'ready');
			if ($ready) return $res;
		}

		return false; //не удалось получить результата
	}

	//создает задание 
	public function add_req($uuid,$req)
	{
		return $this->CallAPI('POST', ['uuid'=>$uuid,'request'=>$req]);
	}

	//генерирует уникальный id для задания
	public function gen_uuid()
	{
		return exec('uuidgen -r');
	}

	//выполняет задание и возвращает его результат
	public function atol_task($type,$req=[])
	{
		$req['type'] = $type;
		$uuid =  $this->gen_uuid();

		$req = $this->add_req($uuid,$req);
		if ($req[1]!='201') return false; //ошибка добавления

		$res = $this->get_res($uuid);
		//ошибка результата
		if ($res===false || !isset($res['results'][0])) return false; 
		return $res['results'][0];
	}

	/*дальше уже идут конкретные задачи*/

	//статус смены
	public function get_shift_status()
	{
		$res = $this->atol_task('getShiftStatus');
		if ($res===false) return false;
		//closed / opened / expired 
		return $res['result']['shiftStatus']['state'];
	}

	//открытие смены
	public function open_shift()
	{
		$status = $this->get_shift_status();
		//eсли истекла, то надо закрыть
		if ($status=="expired") $this->close_shift(); 
		if ($status=="opened") return "Не могу открыть открытую смену";

		$res = $this->atol_task('openShift',['operator'=>$this->operator]);
	}



	//закрытие смены
	public function close_shift()
	{
		$status = $this->get_shift_status();
		if ($status=="closed") return "Не могу закрыть закрытую смену";

		$res = $this->atol_task('closeShift',['operator'=>$this->operator]);
	}


	public function items_prepare($items)
	{
		$res_items = [];
		$summ = 0;
		while ($item = array_shift($items))
		{
			$res_item = $item;
			if (!isset($item['type'])) 
				$res_item['type']="position";

			if (isset($item['price']) && isset($item['quantity']))
			{
				$res_item['amount'] = $item['price']*$item['quantity'];
				$res_item['tax'] = ['type'=>'none'];
				$summ+=$res_item['amount'];
			}


			$res_items[] = $res_item;
		}

		return [$res_items,$summ];
	}


	//продажа sell, возврат sellReturn
	public function fiskal($type_op="sell",$items,$pay_type="cash")
	{
		$data = [];
		$data['operator'] = $this->operator;
		$data['payments'] = [];
		list($data['items'],$summ) = $this->items_prepare($items);

		//+++тут может быть несколько типов оплаты одновременно
		$data['payments'][] = ['type'=>$pay_type,'sum'=>$summ];

		$res = $this->atol_task($type_op,$data);
	}



}

//тут передается ip где крутится web-сервис драйвера Атол, можно еще и порт передать
$atol = new AtolWebDriver('192.168.100.10');

//тут надо фио кассира
$atol->operator = ['name'=>'сист.администратор'];

//составляем массив товаров с количеством и ценой
$items = [];
$items[] = ['name'=>'Пакет полиэтиленовый','price'=>0.7,'quantity'=>1];
$items[] = ['name'=>'Пакет бумажный','price'=>0.4,'quantity'=>1];


//открываем смену
$atol->open_shift();

//продаем товары
$atol->fiskal("sell",$items); 

sleep(10); //немного подождем, чтобы успеть оторвать чек
//а теперь сделаем возврат всего этого, т.к. это просто проверка 
$atol->fiskal("sellReturn",$items); 

//еще подождем перед огромным чеком
sleep(20);

//закрываем смену, печатаем отчет
$atol->close_shift();



Тут есть такие недоработки, которые я еще поправлю

  1. Округление дробей при подсчете сумм, нужно округлять до копеек, иначе можно получить 1.000000001 или 0.999999999
  2. При правильном написании остальной логики программы такое обычно не возникает, но в ходе тестов я поймал себя на том, что задание вернуло результат error, а я ждал ready

Ну и в процессе внедрения боюсь еще много ошибок поймать, вот к примеру, если задание долго висит в статусе wait, то лучше его удалить из очереди, иначе последующие задания зависнут на несколько минут, я такой глюк поймал один раз, уже не надеялся что распечатает и тут сижу сижу, а оно хоп и напечатало сразу два чека подряд отправленных раннее…

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

Предупреждение, тем, кто невнимательно прочитал статью и не очень компетентен в вопросе безопасности — данный веб-сервис не имеет шифрования (https), не имеет авторизации, даже если это используется только в локальной сети — настройте защиту на доступ к порту.
Tags:онлайн-кассы
Hubs: Development for e-commerce Finance in IT
+7
9.1k 25
Comments 28