Архив метки: онлайн касса

Обзор Атол 90Ф. Работа с 1С

Довелось давеча устанавливать и настраивать онлайн кассу Атол 90Ф, проданную одному  предпринимателю Ростелекомом. 

Описание кассы и функциональные возможности, вполне можно нагуглить, потому начнем сразу с описания боли.

Боль первая: касса оказалась с прошивкой от Февраля 2017 (на момент написания статьи — 1 Августа 2018). Как следствие — зарегистрировать кассу при помощи последних версий драйверов не удается — ругаются на «нет необходимых реквизитов». Пришлось ставить древние драйвера версии 8.14, и регистрировать кассу при помощи их.

Боль вторая: как оказалось, работа онлайн кассы для работы со внешним ПО заблокирована заводским кодом защиты №1. В свое время Атол сначала планировал разрешить работу с ПО, затем передумал по маркетинговым соображениям, затем снова передумал (когда Атол 90Ф сняли с производства) и стал раздавать кода защиты всем желающим, затем снова передумал и стал раздавать коды защиты только через некоторые сервисные центры. Обращение за кодом защиты в Ростелеком ничего не дало — мурыжили и тянули время три дня, в итоге сказали «не умеем, не можем». Пришлось обратиться в сторонний сервисный центр АРТОЛ35 и попросить чтоб дали ключ там. Цена вопроса — 500р. В итоге работа со сторонним ПО (читай с 1С) разблокирована.  Для Розницы 2.2 можно ставить или драйвер АТОЛ «С передачей данных», там в списке драйверов АТОЛ90Ф присутствует, или ветку 9х, там нужно выставлять АТОЛ 11Ф.

Боль третья: собственно из-за этой боли возникла как раз боль номер 2. А именно, с клавиатуры самой кассы чеки конечно выбивать можно… Но только со шпаргалкой. Ибо запомнить все последовательности нажатий кнопок для выбивания чека, закрытия — открытия смены, отмены чека и т.п. просто не реально. Интерфейс работы с кассой разрабатывался не для людей. Не знаю для кого. 

Контроль касс Атол в Zabbix

Основная задача скрипта — вывести в консоль информацию об основных параметрах кассы, что бы агент заббикса мог как то распарсить данные.

Пример скрипта:

#!/usr/bin/python3.5
# -*- coding: utf-8 -*-
import base64
import ctypes
import sys
import json
import datetime
import os
import platform
from pprint import pprint

from lib import IFptr
#fptr = IFptr("")

#get extr param
if len(sys.argv)==1:
    print ("Возможные параметры:")
    print ("--info IP - общая информация о ККМ")
    exit(0)
arg=sys.argv[1]
if arg=="--info":
    ip=sys.argv[2];
    
LIBRARY_PATH = os.path.dirname(os.path.abspath(__file__))
fptr = IFptr(os.path.join(LIBRARY_PATH, "libfptr10.so"))
#connect to ATOL
fptr.setSingleSetting(IFptr.LIBFPTR_SETTING_MODEL, str(IFptr.LIBFPTR_MODEL_ATOL_AUTO))
fptr.setSingleSetting(IFptr.LIBFPTR_SETTING_PORT, str(IFptr.LIBFPTR_PORT_TCPIP))
fptr.setSingleSetting(IFptr.LIBFPTR_SETTING_IPADDRESS, ip)
fptr.setSingleSetting(IFptr.LIBFPTR_SETTING_IPPORT,"5555")
fptr.setSingleSetting(IFptr.LIBFPTR_SETTING_ACCESS_PASSWORD,"0")
fptr.setSingleSetting(IFptr.LIBFPTR_SETTING_USER_PASSWORD,"30")

fptr.applySingleSettings()
#открываем соединение
fptr.open()
isOpened = fptr.isOpened()
if isOpened==0:
    print ("9) Доступность:",0)
    exit(1)
    
if arg=="--info":
    fptr.setParam(IFptr.LIBFPTR_PARAM_FN_DATA_TYPE, IFptr.LIBFPTR_FNDT_REG_INFO)
    fptr.fnQueryData()

    taxationTypes               = fptr.getParamInt(1062)
    agentSign                   = fptr.getParamInt(1057)
    ffdVersion                  = fptr.getParamInt(1209)
    print("Версия ФН:",ffdVersion)

    autoModeSign                = fptr.getParamBool(1001)
    offlineModeSign             = fptr.getParamBool(1002)
    encryptionSign              = fptr.getParamBool(1056)
    internetSign                = fptr.getParamBool(1108)
    serviceSign                 = fptr.getParamBool(1109)
    bsoSign                     = fptr.getParamBool(1110)
    lotterySign                 = fptr.getParamBool(1126)
    gamblingSign                = fptr.getParamBool(1193)
    exciseSign                  = fptr.getParamBool(1207)
    machineInstallationSign     = fptr.getParamBool(1221)
    fnsUrl                      = fptr.getParamString(1060)
    organizationVATIN           = fptr.getParamString(1018)
    organizationName            = fptr.getParamString(1048)
    organizationEmail           = fptr.getParamString(1117)
    paymentsAddress             = fptr.getParamString(1187)
    registrationNumber          = fptr.getParamString(1037)
    machineNumber               = fptr.getParamString(1036)
    ofdVATIN                    = fptr.getParamString(1017)
    ofdName                     = fptr.getParamString(1046)
    print("1) Организация:",organizationName)
    print("2) ОФД:",ofdName)
        
    fptr.setParam(IFptr.LIBFPTR_PARAM_DATA_TYPE, IFptr.LIBFPTR_DT_STATUS)
    fptr.queryData()
    serialNumber    = fptr.getParamString(IFptr.LIBFPTR_PARAM_SERIAL_NUMBER)
    modelName       = fptr.getParamString(IFptr.LIBFPTR_PARAM_MODEL_NAME)
    firmwareVersion = fptr.getParamString(IFptr.LIBFPTR_PARAM_UNIT_VERSION)
    print("3) S/N:",serialNumber)
    print("4) Модель:",modelName)
    print("5) Прошивка:",firmwareVersion)
    fptr.setParam(IFptr.LIBFPTR_PARAM_FN_DATA_TYPE, IFptr.LIBFPTR_FNDT_VALIDITY)
    fptr.fnQueryData()
    dateTime            = fptr.getParamDateTime(IFptr.LIBFPTR_PARAM_DATE_TIME)
    print("6) Дата окончания ФН:",dateTime)
    fptr.setParam(IFptr.LIBFPTR_PARAM_FN_DATA_TYPE, IFptr.LIBFPTR_FNDT_LAST_RECEIPT)
    fptr.fnQueryData()
    documentNumber      = fptr.getParamInt(IFptr.LIBFPTR_PARAM_DOCUMENT_NUMBER)
    print("7) Номер чека:",documentNumber)
    # Получение информации о неотправленных документах
    fptr.setParam(IFptr.LIBFPTR_PARAM_FN_DATA_TYPE, IFptr.LIBFPTR_FNDT_OFD_EXCHANGE_STATUS)
    res=fptr.fnQueryData()
    unsentCount = fptr.getParamInt(IFptr.LIBFPTR_PARAM_DOCUMENTS_COUNT)
    if unsentCount>0:
        unsentFirstNumber = fptr.getParamInt(IFptr.LIBFPTR_PARAM_DOCUMENT_NUMBER)
        unsentDateTime = fptr.getParamDateTime(IFptr.LIBFPTR_PARAM_DATE_TIME)
        print ("8) Не фискализировано:",unsentCount)
        fptr.close()    
        del fptr
        exit(1)
    else:
        print ("8) Не фискализировано:",0)
    print ("9) Доступность:",1)

Пример выполнения:

root@pavel-All-Series:~/online_kassa# ./common.py --info 192.168.0.103
Версия ФН: 105
1) Организация: Индивидуальный предприниматель Пупкин Александр Владимирович
2) ОФД: ООО "Компания "Тензор"
3) S/N: 00103452021133752352
4) Модель: АТОЛ 55Ф
5) Прошивка: 1245
6) Дата окончания ФН: 2021-06-07 00:00:00
7) Номер чека: 414
8) Не фискализировано: 0
9) Доступность: 1

Пример оформления триггера в агенте zabbix:

UserParameter=ping_kassa,/home/pavel/online_kassa/common.py --info 192.168.0.103 | grep "9)" | awk '{print $3}'

UserParameter=non_fiskalize,/home/pavel/online_kassa/common.py --info 192.168.0.103 | grep "8)" | awk '{print $3}'

Пример печати чека на ККМ Атол посредством JavaScript

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

Читать далее Пример печати чека на ККМ Атол посредством JavaScript

Костылище при работе с ККМ в Розница 2.2.

С какого-то момента на одном из компьютеров с 1С, перестали печататься чеки на онлайн-кассе. Причем ошибок 1С никаких не показывала.  Причем при открытии/закрытии смен, чеки с Z отчетами выдавались.  Бился два дня с разнообразными настройками и поисками решения. Грешил и на ОС, и на 1С и на ККМ. Сегодня утром уже даже отладчиком 1С стал код шерстить, чтобы понять на каком моменте, что-то не так. Нашел:

&НаКлиенте
Процедура НапечататьЧекКлиент()
	Перем Отказ, ИдентификаторУстройства, ИспользоватьКассуБезПодключенияОборудования, РаспределениеВыручкиПоСекциям;
	
	ПодготовитьИПроверитьПечатьЧека(Отказ, ИдентификаторУстройства, ИспользоватьКассуБезПодключенияОборудования, РаспределениеВыручкиПоСекциям);
	
	Если Отказ Тогда
		Возврат
	КонецЕсли;
!!!!!!!! ошибка здесь!!!!!
	Если НЕ ИспользоватьПодключаемоеОборудование ИЛИ ИспользоватьКассуБезПодключенияОборудования Тогда
		
		Если НомерДокументаКассыККМ[Объект.КассаККМ] <> Неопределено Тогда
			Объект.НомерЧекаККМ  = НомерДокументаКассыККМ[Объект.КассаККМ];
		Иначе
			Объект.НомерЧекаККМ  = ПорядковыйНомерПродажи;
		КонецЕсли;
		Объект.ПробитЧек     = Истина;
		
		Модифицированность = Истина;

ИспользоватьПодключаемоеОборудование = ВСЕГДА = ЛОЖЬ.  Причем только когда работаешь в 1С именно на этом ПК. На других всё ок. Почему? Не понятно. Галочка стоит:

Место, где заполняется значение нашел:

// Проверяет, что включена ф.о "Использовать подключаемое оборудование" и авторизовался пользователь,
// а не внешний пользователь.
Функция ИспользоватьПодключаемоеОборудование() Экспорт
	
	Возврат ПолучитьФункциональнуюОпцию("ИспользоватьПодключаемоеОборудование") И ТипЗнч(Пользователи.АвторизованныйПользователь()) = Тип("СправочникСсылка.Пользователи");
	
КонецФункции

И оно возвращает ИСТИНА! В общем, бился, бился..

Пока прибил всё это костылем:

&НаКлиенте
Процедура НапечататьЧекКлиент()
	Перем Отказ, ИдентификаторУстройства, ИспользоватьКассуБезПодключенияОборудования, РаспределениеВыручкиПоСекциям;
	
	ПодготовитьИПроверитьПечатьЧека(Отказ, ИдентификаторУстройства, ИспользоватьКассуБезПодключенияОборудования, РаспределениеВыручкиПоСекциям);
	
	Если Отказ Тогда
		Возврат
	КонецЕсли;
	// грибов
	// совершенно не понятный глюк
	ИспользоватьПодключаемоеОборудование=истина;
	//
	Если НЕ ИспользоватьПодключаемоеОборудование ИЛИ ИспользоватьКассуБезПодключенияОборудования Тогда

Фискализация чеков через онлайн-кассу АТОЛ 55Ф на ОС Ubuntu, python

Прилетела задача фискализировать поступления платежей через оплату услуг с карточек через интернет, из личного кабинета абонента, оплаты абонента посредством Сбербанк-онлайн и т.п. Из ТУ : операционная система семейства Linux.

Было рассмотрено несколько вариантов решения:

1) Сервис Атол-Онлайн. Плюсы: простота интеграции, очень вменяемое API. Минусы — со второго года — выходит очень дорого по сравнению со «своей железкой»

2)PAYONLINE-01-ФА. Плюсы: прям в «базе» нет принтера, что удешевляет. Минусы: нет толкового описания работы с драйверами, примеров, нет драйверов под LInux

3) Атол-55Ф. Плюсы: драйвера под Linux, вменяемое описание работы с примерами на Java, cpp. Плюс натолкнулся на пример работы https://github.com/parshin/kkt с этой железкой. Минусы — дорого по сравнению с 2) но дешево по сравнению с 1)

Собственно драйвера ставятся стандартно. Далее пример пробития чека на python

#!/usr/bin/python2.7
# -*- coding: utf-8 -*-

# import web
import sys
import os
import json
import logging

from conf import *	    # загружаем настройки
import dto9fptr		    # загружаем заголовки драйвера

def DeviceSetup(driver,params):    
    driver.put_DeviceSingleSetting("Port", params["Port"])
    driver.put_DeviceSingleSetting("IPAddress", params["IPAddress"])
    driver.put_DeviceSingleSetting("IPPort", params["IPPort"])
    driver.put_DeviceSingleSetting("Model", params["Model"])
    driver.put_DeviceSingleSetting("Protocol", params["Protocol"])
    driver.put_DeviceSingleSetting("AccessPassword", params["AccessPassword"])
    driver.put_DeviceSingleSetting("UserPassword", params["UserPassword"])
    driver.put_DeviceSingleSetting("Protocol", params["Protocol"])
    driver.put_DeviceSingleSetting('SearchDir', os.path.dirname(DTO_LIB_NAME))
    driver.ApplySingleSettings()
    driver.put_DeviceEnabled(True)
    return 0

def GetCurrentParamDevice(driver):
    result = driver.GetStatus()    
    if result[0]!=0:
	print t_err+unicode(result[1])+t_off
	logging.info(result[1])
	exit(1)
    result = driver.get_DeviceSettings()
    print t_mess+'Текщие настройки: '+t_off + str(result)
    if str(result)=="None":
	etxt="Драйвер не подключился к устройству"
	print t_err+etxt+t_off
	logging.info(etxt)
	exit(1)
    return result

def print_check(driver, check_data):
    res=0
    mess="check_data: " + str(check_data)
    
    print t_mess+mess+t_off
    logging.info(mess)
    
    result = driver.put_DeviceEnabled(True)
    logging.info("put device enabled: " + repr(result).decode("unicode_escape"))
    result_code = driver.get_ResultCode()
    result_description = driver.get_ResultDescription()
    if result_code != 0:
        logging.error("  result code:" + repr(result_code).decode("unicode_escape"))
        logging.error("  result description:" + repr(result_description).decode("unicode_escape"))
	print t_err+str(result_code)+t_off
	print t_err+result_description+t_off
        exit(1)  
    print t_ok+result_description+t_off		
    
    print t_mess+"- переводим ККМ в режим регистрации"+t_off
    result = driver.put_Mode(1)
    logging.info("put mode 1:" + repr(result).decode("unicode_escape"))
    result_code = driver.get_ResultCode()
    result_description = driver.get_ResultDescription()
    if result_code != 0:
        logging.error("  result code:" + repr(result_code).decode("unicode_escape"))
        logging.error("  result description:" + repr(result_description).decode("unicode_escape"))
	print t_err+str(result_code)+t_off
	print t_err+result_description+t_off
    print t_ok+result_description+t_off					   
	
    print t_mess+"- создаем новый документ"+t_off
    result = driver.NewDocument()	
    logging.info("new document:" + repr(result).decode("unicode_escape"))
    result_code = driver.get_ResultCode()
    result_description = driver.get_ResultDescription()
    if result_code == -3822:
        logging.error("  result code:" + repr(result_code).decode("unicode_escape"))
        logging.error("  result description:" + repr(result_description).decode("unicode_escape"))
        logging.error("  trying close shift...")
	print t_err+str(result_code)+t_off
	print t_err+result_description+t_off
	exit(1)
    print t_ok+result_description+t_off	
    
    print t_mess+"- выставляем что тип чека - продажа"+t_off
    result = driver.put_CheckType(1)
    logging.info("put check type:" + repr(result).decode("unicode_escape"))
    result_code = driver.get_ResultCode()
    result_description = driver.get_ResultDescription()
    if result_code != 0:
        logging.error("  result code:" + repr(result_code).decode("unicode_escape"))
        logging.error("  result description:" + repr(result_description).decode("unicode_escape"))
	print t_err+str(result_code)+t_off
	print t_err+result_description+t_off
	exit(1)
    print t_ok+result_description+t_off	
	
    print t_mess+"- открываем чек"+t_off
    result = driver.OpenCheck()
    logging.info("open check:" + repr(result).decode("unicode_escape"))
    result_code = driver.get_ResultCode()
    result_description = driver.get_ResultDescription()
    if result_code != 0:
        logging.error("  result code:" + repr(result_code).decode("unicode_escape"))
        logging.error("  result description:" + repr(result_description).decode("unicode_escape"))
	print t_err+str(result_code)+t_off
	print t_err+result_description+t_off
	exit(1)
    print t_ok+result_description+t_off	
    
    print t_mess+"- регистрация позиций"+t_off
    logging.info("registering positions...")
    
    for position in check_data:
	print "--- №:"+str(position["num"])
	print "--- Дата:"+str(position["docdate"])
	print "--- Всего:"+str(position["summdoc"])
	for goods in position["goods"]:
	    print "---- Товар:"+str(goods["name"])
	    result = driver.put_Name(goods["name"].decode('utf8'))
	    print "---- Цена:"+str(goods["price"])
	    result = driver.put_Price(goods["price"])
	    print "---- Количество:"+str(goods["quiantity"])
	    result = driver.put_Quantity(goods["quiantity"])
	    result = driver.put_TaxNumber(3) 
	    print "---- Сумма:"+str(goods["summ"])
	    result_code = driver.put_PositionSum(goods["summ"]) 
	    print "---- Регистрация позиции:"
	    result_code = driver.Registration() 	    
	    if result_code[0] != 0:
		logging.error("  result code:" + repr(result_code).decode("unicode_escape"))
		logging.error("  result description:" + repr(result_description).decode("unicode_escape"))	    
		print t_err+str(result_code)+t_off
		print t_err+result_description+t_off
		exit(1)
	

	print t_mess+"- прием оплаты"+t_off
	result = driver.put_TypeClose(1)  # Тип оплаты - Платежная карта
	logging.info("put type close:" + repr(result).decode("unicode_escape"))
	result_code = driver.get_ResultCode()
	result_description = driver.get_ResultDescription()
	if result_code != 0:
	    logging.error("  result code:" + repr(result_code).decode("unicode_escape"))
	    logging.error("  result description:" + repr(result_description).decode("unicode_escape"))	    
	    print t_err+str(result_code)+t_off
	    print t_err+result_description+t_off
	    exit(1)

	print t_mess+"- сумма оплаты"+t_off
	result = driver.put_Summ(position["summdoc"])
	logging.info("put summ:" + repr(result).decode("unicode_escape"))
	result_code = driver.get_ResultCode()
	result_description = driver.get_ResultDescription()
	if result_code != 0:
	    logging.error("  result code:" + repr(result_code).decode("unicode_escape"))
	    logging.error("  result description:" + repr(result_description).decode("unicode_escape"))
	    print t_err+str(result_code)+t_off
	    print t_err+result_description+t_off
	    exit(1)

	print t_mess+"- регистрация платежа"+t_off
	result = driver.Payment()
	logging.info("pyment:" + repr(result).decode("unicode_escape"))
	result_code = driver.get_ResultCode()
	result_description = driver.get_ResultDescription()
	if result_code != 0:
	    logging.error("  result code:" + repr(result_code).decode("unicode_escape"))
	    logging.error("  result description:" + repr(result_description).decode("unicode_escape"))
	    print t_err+str(result_code)+t_off
	    print t_err+result_description+t_off
	    exit(1)

	print t_mess+"- проверка фиксации "+t_off
	result = driver.get_ResultCode()
	logging.info("payment result code:" + repr(result).decode("unicode_escape"))
	result_code = driver.get_ResultCode()
	result_description = driver.get_ResultDescription()
	if result_code != 0:
	    logging.error("  result code:" + repr(result_code).decode("unicode_escape"))
	    logging.error("  result description:" + repr(result_description).decode("unicode_escape"))
	    print t_err+str(result_code)+t_off
	    print t_err+result_description+t_off
	    exit(1)

    print t_mess+"- закрытие чека "+t_off
    result = driver.CloseCheck()
    logging.info("close check:" + repr(result).decode("unicode_escape"))
    result_code = driver.get_ResultCode()
    result_description = driver.get_ResultDescription()
    if result_code != 0:
        logging.error("  result code:" + repr(result_code).decode("unicode_escape"))
        logging.error("  result description:" + repr(result_description).decode("unicode_escape"))
        print t_err+str(result_code)+t_off
        print t_err+result_description+t_off
        exit(1)

    print t_mess+"- получение номера чека "+t_off
    result = driver.get_CheckNumber()
    logging.info("check number:" + repr(result).decode("unicode_escape"))
    result_code = driver.get_ResultCode()
    result_description = driver.get_ResultDescription()
    if result_code != 0:
        logging.error("  result code:" + repr(result_code).decode("unicode_escape"))
        logging.error("  result description:" + repr(result_description).decode("unicode_escape"))
        print t_err+str(result_code)+t_off
        print t_err+result_description+t_off
        exit(1)


    exit(1)
    return res

os.environ['LD_LIBRARY_PATH'] = os.path.dirname(os.path.realpath(__file__))
logging.info(os.path.dirname(os.path.realpath(__file__)))
logging.info(os.environ)

print t_mess+"Фискализация чеков v 1.0 (c) 2018 by Pavel Gribov http://грибовы.рф"+t_off

DTO_LIB_NAME = '/opt/ATOL/drivers9/bin/libfptr.so'
VERSION = 15
driver = dto9fptr.Fptr(DTO_LIB_NAME, VERSION)

print t_mess+"Драйвер:"+t_off+str(driver)
print t_mess+"Параметры:"+t_off+str(params)

# устанавливаем параметры соединения
DeviceSetup(driver,params)
# Читаем текущие настройки
cursettings=GetCurrentParamDevice(driver)
# печатаем чек
check_data=[]

goodsa=[]
goodsa.append({"name":"Телематические услуги связи 1","price":0.01,"quiantity":1,"summ":0.01});
goodsa.append({"name":"Телематические услуги связи 2","price":0.01,"quiantity":1,"summ":0.01});

check_data.append({"num":"1","docdate":"07.05.2018","summdoc":0.02,"goods":goodsa});

print check_data
print_check(driver, check_data)

driver.Beep()


#Закончили работу
driver.put_DeviceEnabled(False)
del driver

conf.py:

params = {
    "Port": "TCPIP",
    "IPAddress": "192.168.0.99",
    "IPPort": "5555",
    "Model": "62",
    "AccessPassword": "0",
    "UserPassword": "30",
    "Protocol": "2",
    "LogFileName": "/var/log/kkt.log"
}
t_mess="\x1b[33m"
t_err="\x1b[31m"
t_ok="\x1b[32m"
t_off="\x1b[0m"

Далее осталась рутина, отключить физическую печать, доделать автоматическое открытие-закрытие смены и т.п. Но в принципе всё уже есть в https://github.com/parshin/kkt

В файле conf.py нужно прописать параметры подключения к ККТ IP адрес, порт, путь к лог файлу и т.д. Параметр «Model» смотрим в Руководстве программиста приложение 7 модели ККМ ???(у меня 47 не подошло, подошло 62, почему не понял).

VERSION = 15 — это для драйверов 9.12.1. Возможно потом нужно будет менять на 16 и т.д.

P.S. Решение применимо и для других аппаратов АТОЛ с минимальными изменениями.