https://habr.com/post/413385/- Разработка под e-commerce
- Платежные системы
- Python
- Joomla
- CMS
Эта статья написана под влиянием небольшой паники в связи со стремительно приближающимся «днем Х» — очередной переход на онлайн кассы для очередной категории предприятий и организаций. Теперь с 1-го июля онлайн чеки должны выдавать даже те, кто их раньше мог не выдавать — интернет магазины и торговые автоматы.
Как же выдать чек?
Я довольно долго изучаю этот вопрос, так как мне нужно как-то автоматизировать процесс выдачи чеков для интернет магазина. Мне нужно решить эту техническую проблему с минимальными затратами по деньгам и по времени. С кассами ранее дела не имел и вот сейчас пытаюсь это дело изучить и понять и запустить в работу.
Здесь небольшой обзор возможных решений и мои криворукие скрипты на питоне.
Мое решение не коробочный продукт, но возможно у кого-то есть свой напильник и он сможет его довести до ума…
Это возможно подойдет тем, у кого
- редкие заказы в магазине
- небольшой ассортимент продаваемых штучных товаров
К сожалению, все это очень хрупкое и далеко не идеальное… увы.
В принципе, готовые решения есть. Наверняка я знаком не со всеми, но мне показалось, что большинство предложений по внедрению онлайн касс для интернет магазинов — это предложения с абонентской платой. Такие решения подразумевают либо размещение физической кассы в облаке, либо размещение кассы у заказчика, но интеграция с магазином происходит скажем через Яндекс.Кассу и далее облачного оператора (который берет абонентскую плату). Я вполне допускаю, что абонентская плата не является проблемой для интернет магазинов с большим оборотом. А вот если оборот значительно меньше миллиона рублей в год, то внедрение онлайн кассы с абон платой вполне может пошатнуть бизнес. Предлагаю не обсуждать вопрос «зачем вообще запускать такой интернет магазин у которого и оборота нет». Сегодня пока нет, а завтра возможно будет. К тому же интернет магазин может быть просто попутным бизнесом, который не столько продает товар, сколько рекламирует компанию.
Внедрение онлайн касс — это несомненно увеличение расходов магазина. Я не считаю первоначальные вложения, такие как приобретение собственно кассы, получение ключа для кабинета налоговой для регистрации кассы. Кстати, если кто-то скажет, что стоимость касс компенсируется налоговым вычетом — увы не всем, а только ИП.
Обязательные операционные расходы — это
- фискальный накопитель, примерно 7 тысяч рублей в год
- договор с ОФД, примерно 3 тысячи рублей в год
- абонентская плата за онлайн кассу по подписке или в облаке примерно 1-3 тысячи в месяц
Конечно, тут числами можно немного «поиграть», купить фискальный накопитель не на год, а на три, и с ОФД так же… Нашел только один ОФД, который предлагает микротариф 999 рублей за год. На этом кажется действительно можно немного сэкономить. А вот абонентская плата за онлайн кассу, какая бы она не была, вот этого хотелось бы избежать…
Цены на облачный сервис онлайн касс примерно вот такие:
kassa.yandex.ru/54fz.html
Там по ссылке все тарифы около 30 тысяч в год, только «Бизнес.ру Онлайн-Чеки» вроде бы за «смешные» 3600 рублей в год. Но переходишь по ссылке далее и там уже другие числа. Вот такие дела.
Из всех онлайн касс для интернет магазинов я для себя особо выделил вот эти:
1) касса «micropay on-line»
www.micropay-fas.ru
Но тут как-то нет технических подробностей, описание API есть, но какое-то жиденькое…
И стоит 15 тыр. А почему собственно касса без дисплея, клавиш и принтера стоит дороже, чем некоторые другие кассы с дисплеем, кнопками и принтером… Ценообразование не понятно.
2) ККТ РП-Система 1 ФС
online-kassa.pro/oborudovanie/proizvoditeli/kkt-starrus.html
Эта штука я так понял только в облаке может стоять, то есть от абонентской платы не уйти. И кажется эта штука не продается на руки (но это не точно — я нашел рекламу, где оно продается, но непосредственно производитель говорит, что нигде не купить).
3) Дримкас Пульс
dreamkas.ru/54fz/online-kassy/dreamkas-pulse
На момент написания этой статьи только предзаказ.
Пока не известно, что за зверь, описания нет, цены нет.
Получается, что собственно решений для чистого интернет магазина как-то не очень и много.
Правда… ну есть некие вполне достойные промежуточные решения. Например, как я понял, кассовый аппарат Дримкас Ф вполне можно заставить работать для интернет магазина бесплатно без абонентской платы.
Но, с некоторыми условиями.
Если магазин на Wordpress или OpenCart или 1C или еще некоторые, то можно сделать следующее: в интернет магазин устанавливается готовый компонент/модуль от Дримкас. Этот модуль передает информацию о покупках в интернет Кабинет Дримкас. Сама касса Дримкас Ф стоит в офисе владельца интернет магазина и периодически (кажется раз в минуту) опрашивает Кабинет Дримкас и смотрит может нужно напечатать чек и при необходимости печатает его. Работа с Кабинетом Дримкас и его API бесплатна.
К сожалению, у меня магазин построен на JoomShopping. Готового модуля интеграции нет.
Могу ли я его сам написать? Теоретически да, могу. Правда я не очень умею в PHP, но не это главное… Да, Дримкас дает описание API своего кабинета. Есть модуль Дримкаса скажем для Wordpress — его исходники легко посмотреть и хотя бы понять что там и как там.
В принципе, я с этого и начал. Взял исходники компонента для Wordpress и стал в них ковыряться. Цель была сделать запрос к Кабинету Дримкас, и чтобы он мне уверенно сказал, чек готов к печати, но сперва подключите кассу. Если кто хочет посмотреть код, он вот:
PHP скрипт, который пытается отправить чек Кабинету Дримкас<?php
<?php
/*
Plugin Name: Дримкас
Description: Позволяет фискализировать заказы магазина через обычную кассу от Дримкас (Дримкас-Ф).
Plugin URI: http://wordpress.org/plugins/dreamkas/
Author: Alt-Team
Version: 1.0.0
Author URI: http://alt-team.ru/
*/
//use WC_Payment_Gateways;
function get_option( $name )
{
if( $name=='dreamkas_access_token' )
return '53b32765-XXXX-XXXX-XXXX-93737750bdfc';
if( $name=='dreamkas_payments_ids' )
return 'Yandex.Kassa';
if( $name=='dreamkas_tax_mode')
return 'SIMPLE';
if( $name=='dreamkas_tax_type' )
return 'NDS_NO_TAX';
if( $name=='dreamkas_device_id' )
return '29XXXX';
}
final class xorder {
public function get_status()
{
return 'Payed';
}
public function get_payment_method()
{
return 'Yandex.Kassa';
}
public function get_items()
{
$product1 = array(
"product_id" => 123,
"name" => "Book",
"price" => 500,
"quantity" => 2,
"total" => 1000,
"total_tax" => 0
);
$product2 = array(
"product_id" => 124,
"name" => "Toy",
"price" => 150,
"quantity" => 1,
"total" => 1000,
"total_tax" => 0
);
$items = [$product1,$product2];
return $items;
}
public function get_billing_email()
{
return "nck.kovach@gmail.com";
}
public function get_billing_phone()
{
return "";
}
public function get_total()
{
return 1150;
}
}
function wc_get_order($order_id)
{
return new xorder();
}
final class Dreamkas {
public $version = '1.0.0';
const DEFAULT_QUEUE_NAME = 'default';
const DISCOUNT_NOT_AVAILABLE = 0;
private static $_instance = null;
public static function instance() {
if (is_null(self::$_instance) ) {
self::$_instance = new self();
}
return self::$_instance;
}
public function __construct()
{
//$this->define('DREAMKAS_ABSPATH', plugin_dir_path( __FILE__));
//$this->define('DREAMKAS_ABSPATH_VIEWS', plugin_dir_path( __FILE__) . 'includes/views/');
//$this->define('DREAMKAS_BASENAME', plugin_basename( __FILE__ ));
$this->define('DREAMKAS_ABSPATH', ".");
$this->define('DREAMKAS_ABSPATH_VIEWS', "." . 'includes/views/');
$this->define('DREAMKAS_BASENAME', ".");
$this->includes();
$this->hooks();
$this->wp_hooks();
$this->wp_endpoints();
$this->load_options();
$this->init();
}
public function wp_hooks()
{
//register_activation_hook( __FILE__, array('Dreamkas_Install', 'activation'));
//add_action('woocommerce_order_status_' . get_option('dreamkas_fiscalize_on_order_status'), array($this, 'fiscalize'));
}
public function wp_endpoints()
{
//add_filter('query_vars', array($this, 'add_query_vars'), 0);
//add_action('init', array($this, 'add_endpoint'), 0);
//add_action('parse_request', array($this, 'handle_requests'), 0);
}
public function hooks()
{
//add_action('dreamkas_action_success', array($this, 'action_success'));
//add_action('dreamkas_action_fail', array($this, 'action_fail'));
//add_action('dreamkas_report_create', array($this, 'report_create'), 10, 4);
//add_action('dreamkas_report_update', array($this, 'report_update'), 10, 3);
}
public function includes()
{
/*
require_once(DREAMKAS_ABSPATH . 'includes/class-dreamkas-install.php');
//require_once('debug.php');
if (is_admin()) {
require_once(DREAMKAS_ABSPATH . 'includes/class-dreamkas-admin.php');
add_action('init', array( 'Dreamkas_Admin', 'init'));
}
*/
}
private function define($name, $value)
{
if (!defined( $name )) {
define( $name, $value );
}
}
public function load_options() {
$this->access_token = get_option('dreamkas_access_token');
}
public function init()
{
//do_action('before_dreamkas_init');
//do_action('dreamkas_init');
}
public function taxSystems() {
return array(
'DEFAULT' => 'Общая',
'SIMPLE' => 'Упрощенная доход',
'SIMPLE_WO' => 'Упрощенная доход минус расход',
'ENVD' => 'Единый налог на вмененный доход',
'AGRICULT' => 'Единый сельскохозяйственный налог',
'PATENT' => 'Патентная система налогообложения'
);
}
public function taxTypes() {
return array(
'0' => 'Выберите НДС',
'NDS_NO_TAX' => 'Без НДС',
'NDS_0' => 'НДС 0',
'NDS_10' => 'НДС 10',
'NDS_18' => 'НДС 18',
'NDS_10_CALCULATED' => 'НДС 10/110',
'NDS_18_CALCULATED' => 'НДС 18/118'
);
}
public function paymentIds() {
/*
$payments = WC_Payment_Gateways::instance();
$paymentIds = $payments->get_payment_gateway_ids();//WC_Payment_Gateways::get_available_payment_gateways();
foreach ($paymentIds as $key => $code) {
$_paymentIds[$code] = $payments->payment_gateways[$key]->title;
}
return $_paymentIds;
*/
}
public function fiscalize($order_id)
{
echo "Fiscalize was called!\n";
$order = wc_get_order($order_id);
if (!$order) {
return;
}
echo "Order exists\n";
$status = $order->get_status();
$payment = $order->get_payment_method();
$payments_ids = get_option('dreamkas_payments_ids');
if(!in_array($payment, explode(',', $payments_ids))) {
return;
}
$tax_mode = get_option('dreamkas_tax_mode');
$tax_type = get_option('dreamkas_tax_type');
$items = array();
if (sizeof($order->get_items()) > 0 ) {
foreach ($order->get_items('line_item') as $product) {
$product_tax_type = ""; //get_post_meta( $product['product_id'], 'dk_tax_type', true );
$price = intval(($product['total']+$product['total_tax'])/$product['quantity']*100);
if($price>0) {
$items[] = array(
"name"=> $product['name'], //->get_name(),
"type"=> "COUNTABLE",
"quantity"=> $product['quantity'],
"price"=> $price,
"priceSum"=> ($product['total']+$product['total_tax'])*100,
"tax"=> empty($product_tax_type)?$tax_type:$product_tax_type,//"$tax_type",
"taxSum"=> 0//$product['total_tax']*100
);
}
}
}
// shipping
foreach ($order->get_items('shipping') as $item) {
$price = round(($item['total']+$item['total_tax'])*100);
if($price>0) {
$items[] = array(
"name"=> 'Доставка',//$item->get_name(),
"type"=> "COUNTABLE",
"quantity"=> 1,
"price"=> $price,
"priceSum"=> round(($item['total']+$item['total_tax'])*100),
"tax"=> "$tax_type",
"taxSum"=> 0//round($item['total_tax']*100)
);
}
}
//fn_write_die($items);
if(!empty($items)) {
$request = array(
"deviceId" => get_option('dreamkas_device_id'),
"type" => "SALE",
"timeout" => 180,
"taxMode" => get_option('dreamkas_tax_mode'),
"positions" => $items,
"payments" => array(
array(
"sum" => $order->get_total()*100,
"type" => "CASHLESS"
)
),
"attributes" => array(
"email" => $order->get_billing_email(),
"phone" => $order->get_billing_phone(),
),
"total" => array(
"priceSum" => $order->get_total()*100
)
);
//fnd($request,$order->get_items('line_item'), $order);
$ch = curl_init();
$access_token = get_option('dreamkas_access_token');
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
"Content-Type: application/json",
"Authorization: Bearer $access_token"
));
curl_setopt($ch, CURLOPT_URL, "https://kabinet.dreamkas.ru/api/receipts");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($request));
$response = curl_exec($ch);
curl_close($ch);
}
if(!empty($response)) {
$response = json_decode($response, true);
var_dump( $response );
return;
/*$response = json_decode('{
"id": "5956889136fdd7733f19cfe6",
"createdAt": "2017-06-20 12:01:47.990Z",
"status": "PENDING"
}', true);*/
global $wpdb;
$table_name = $wpdb->prefix . 'dreamkas';
$exist_order_id = $wpdb->get_row( $wpdb->prepare( "SELECT order_id FROM {$table_name} WHERE `order_id` = %d LIMIT 1;", $order_id ) );
if((substr($response['status'], 0, 1)==4)) {
$dk_date = time();
if(empty($exist_order_id)) {
//$wpdb->query( "INSERT INTO $table_name ( `order_id` , `dk_id` , `dk_date`, `dk_status` ) VALUES ( '{$wpdb->blogid}', '{$wp_db_version}', NOW());" );
$wpdb->query("INSERT INTO `" . $wpdb->prefix . "dreamkas` SET "
. "`order_id` = '" . (int)$order_id . "', "
//. "`dk_id` = '".$response['id']."', "
. "`dk_date` ='".$dk_date."', "
. "`dk_status` = '" . $response['status']. "', "
. "`dk_message` = '" .$response['code'].':'.$response['message']. "' "
. "");
} else {
$wpdb->query("UPDATE `" . $wpdb->prefix . "dreamkas` SET "
//. "`order_id` = '" . (int)$order_id . "', "
//. "`dk_id` = '".$response['id']."', "
. "`dk_date` ='".$dk_date."', "
. "`dk_status` = '" .$response['status']. "', "
. "`dk_message` = '" .$response['code'].':'.$response['message']. "' "
. " WHERE order_id = '" . (int)$order_id. "'");
}
//$this->log->write('Dreamkas debug: ' . json_encode($response));
//fnd($exist_order_id, $response, $response['status']);
} else {
$dk_date = empty($response['createdAt'])?$response['completedAt']:$response['createdAt'];
$message = empty($response['message'])?'':$response['code'].':'.$response['message'];
if(empty($exist_order_id)) {
//$wpdb->query( "INSERT INTO $table_name ( `order_id` , `dk_id` , `dk_date`, `dk_status` ) VALUES ( '{$wpdb->blogid}', '{$wp_db_version}', NOW());" );
$wpdb->query("INSERT INTO `" . $wpdb->prefix . "dreamkas` SET `order_id` = '" . (int)$order_id . "', `dk_id` = '".$response['id']."', `dk_date` ='".$dk_date."', `dk_status` = '" . $response['status']. "', ". "`dk_message` = '" .$message. "' ");
} else {
$wpdb->query("UPDATE `" . $wpdb->prefix . "dreamkas` SET `order_id` = '" . (int)$order_id . "', `dk_id` = '".$response['id']."', `dk_date` ='".$dk_date."', `dk_status` = '" .$response['status']. "', `dk_message` = '" .$message. "' WHERE order_id = '" . (int)$order_id. "'");
}
}
//fnd($request, $response);
}
}
public function add_query_vars($vars) {
$vars[] = 'dreamkas';
return $vars;
}
public static function add_endpoint() {
add_rewrite_endpoint('dreamkas', EP_ALL);
}
public function handle_requests() {
global $wp;
if (empty($wp->query_vars['dreamkas'])) {
return;
}
$dreamkas_action = strtolower(wc_clean( $wp->query_vars['dreamkas']));
do_action('dreamkas_action_' . $dreamkas_action);
die(-1);
}
public function action_success()
{
$this->handle_action('success');
}
public function action_fail()
{
//$this->handle_action('fail');
}
public function handle_action($action) {
//do_action('dreamkas_report_update', intval($data['external_id']), $data['state'], $data);
}
/*
public function report_create($order_id, $request_check_data, $response_data, $error="")
{
$this->report->create($order_id, $request_check_data, $response_data, $error);
}
*/
/*
public function report_update($order_id, $state, $report_data)
{
$this->report->update($order_id, $state, $report_data);
}
*
*/
}
$inst = Dreamkas::instance();
$inst->fiscalize(1);
Это PHP скрипт из компонета для Wordpress. Я там закомментировал все запросы к базам данных и иммитирую передачу ордера в функцию fiscalize. Вызывать скрипт можно просто из консоли.терминала.
Я почему-то думал, что я смогу в Кабинете Дримкас увидеть что-то вроде очереди моих тестовых чеков, которые напечатаются, когда моя физическая касса выйдет онлайн (у меня же ведь могут быть и перебои с питанием и какой нибудь экскаватор теоретически может оборвать мой кабель к интернет провайдеру). Саму кассу еще пока не купил, так как не уверен в выборе.
Этот код на PHP выше в принципе как-то начинает работать. Там нужно в начале прописать access_token который берется из Кабинета Дримкас и еще device_id — это я так понял ID физической кассы. Таковой у меня пока нет. Я установил бесплатную кассовую программу «Дримкас Старт», зарегистрировал ее в кабинете Дримкас и таким образом получил ID кассы.
При запуске PHP скрипта я получаю ответ от Кабинета Дримкас:
Вот и думай, что с этим дальше делать. Видимо подключить «Дримкас Старт» таким способом нельзя? Только Дримкас Ф подходит? Вразумительного ответа пока не нашел.
И вообще, мне не очень понятен сам ход разработки. Чтобы API работало должен быть подключен боевой кассовый аппарат с реальным фискальным накопителем. Сколько чеков для бухгалтерии я испорчу, но отправлю в ОФД пока проведу свою разработку? В тех поддержке Дримкас мне сказали, что для разработки я должен использовать эмулятор фискального накопителя МГМ, который сам стоит как кассовый аппарат. Мне это не нравится.
Я бы честно говоря обрадовался, если бы Дримкас сделал не готовый компонент к какой-то CMS, а просто скрипт «функцию» fiscalize() проверенную и рекомендованную к использованию. А так я что-то как-то исправляю и если не работает я не понимаю, толи я наисправлял так и испортил, толи у них на стороне сервера что-то случилось. У меня же нет ни логов сервера, ничего — черный ящик.
Поскольку я уже в своих опытах начал использовать бесплатную кассовую программу «Дримкас Старт», то подумал, что вообще-то можно не делать автоматизацию вообще… Теоретически я могу в офисе, на сервере виртуальных машин создать еще одну виртуалку специально под кассу. В виртуалку нужно пробросить USB от кассового аппарата Вики Принт (с ними работает «Дримкас Старт»). В виртуалке запустить эту кассу. При поступлении заказа (это у меня не часто пока) мне приходит уведомление по почте. Я могу, где бы я ни был, подключиться удаленно к виртуалке и напечать чек вручную. Да плохо и не удобно, но что делать?
А потом я подумал… Постойте. А можно ли автоматизировать клики в кассовой программе?
Идея такая. На почту из интернет магазина приходит уведомление о новом заказе. Выглядит почтовое уведомление из магазина JoomShopping вот так:
Я пишу скрипт на питоне, который будет периодически опрашивать почтовый сервер и смотреть пришли письма или нет. Письма нужно отсортировать и найти действительно заказ с нужным статусом «Оплачено» (пока это поле не проверяю):
Скрип на питоне, который периодически проверяет почту и читает заказы из интернет магазина#!/usr/bin/env python
import sys
import imaplib
import getpass
import email
import email.header
import datetime
import time
import base64
import codecs
import os
from order import *
from kassa import *
#print("Hello Kassa!")
#time.sleep(15)
#print("Lets start!")
#file = codecs.open("order.txt", "r", "utf-8")
#test_order_txt=file.read()
#order=parse_order_from_email(test_order_txt)
#print_order(order)
#make_check(order)
EMAIL_POLL_INTERVAL = 30
EMAIL_FROM_ACCEPTED = "info@supershop.ru"
EMAIL_ACCOUNT = "supershop.ru@gmail.com"
EMAIL_P = "r23fsdf^&G%(HOI"
# Use 'INBOX' to read inbox. Note that whatever folder is specified,
# after successfully running this script all emails in that folder
# will be marked as read.
EMAIL_FOLDER = "INBOX"
def process_mailbox(M):
"""
Do something with emails messages in the folder.
For the sake of this example, print some headers.
"""
rv, data = M.search(None, "UNSEEN")
if rv != 'OK':
print("No messages found!")
return
for num in data[0].split():
rv, data = M.fetch(num, '(RFC822)')
if rv != 'OK':
print("ERROR getting message", num)
return
msg = email.message_from_bytes(data[0][1])
#print(msg)
print("-------------------------")
hdr = email.header.make_header(email.header.decode_header(msg['Subject']))
subject = str(hdr)
print('Message %s: %s' % (num, subject))
print('Raw Date:', msg['Date'])
# Now convert to local date-time
date_tuple = email.utils.parsedate_tz(msg['Date'])
if date_tuple:
local_date = datetime.datetime.fromtimestamp(
email.utils.mktime_tz(date_tuple))
print ("Local Date:", \
local_date.strftime("%a, %d %b %Y %H:%M:%S"))
print("From: ",msg['From'])
if EMAIL_FROM_ACCEPTED not in msg['From']:
continue
if msg.is_multipart():
part = msg.get_payload(0)
payload=part.get_payload(decode=True).decode('utf-8')
else:
payload = msg.get_payload(decode=True)
#print("Text: ",payload)
#break
order=parse_order_from_email(payload)
print_order(order)
make_check(order)
M = imaplib.IMAP4_SSL('imap.gmail.com')
try:
rv, data = M.login(EMAIL_ACCOUNT, EMAIL_P)
except imaplib.IMAP4.error:
print ("LOGIN FAILED!!! ")
sys.exit(1)
print(rv, data)
rv, mailboxes = M.list()
num_email_reads=0
if rv == 'OK':
#print("Mailboxes:")
#print(mailboxes)
while 1:
rv, data = M.select(EMAIL_FOLDER)
if rv == 'OK':
#update console title to see that requests to email go periodically
os.popen("title " + "Emails2Kassa "+str(num_email_reads))
num_email_reads=num_email_reads+1
#print("Processing mailbox...\n")
process_mailbox(M)
time.sleep(EMAIL_POLL_INTERVAL)
M.close()
else:
print("ERROR: Unable to open mailbox ", rv)
M.logout()
Скрипт написан с использованием метода Google-Stackoverflow-Copy-Paste программирования.
Как только письмо с заказом получено, нужно произвести его разбор и извлечь нужные поля из него. Хорошо, что заказ приходит в HTML. Я исправил файл шаблона магазина JoomShopping components/com_jshopping/templates/my_def_div/checkout/orderemail.php и добавил важным полям дополнительный аттрибут data-order="?????":
По этому атрибуту другой питоновский скрипт может разобрать ордер и извлечь из HTML файла все, что нужно. Осторожно! Там регэкспы. Слабонервным не смотреть. Я сам боюсь туда заглядывать.
Разбор письма с ордером из магазина#!/usr/bin/env python
import sys
import re
import codecs
#file = codecs.open("order.txt", "r", "utf-8")
#order_txt=file.read()
def parse_order_from_email(order_txt):
order={}
order_number_raw = re.search(' data-order=\"number\">\s*\d+\s*<\/td>', order_txt).group(0)
order_number = re.search('\d+', order_number_raw).group(0)
order["number"]=order_number
#print (order_number)
order_data_raw = re.search(' data-order=\"data\">\s*\d+\.\d+\.\d+\s*<\/td>', order_txt).group(0)
order_data = re.search('\d+\.\d+\.\d+', order_data_raw).group(0)
#print (order_data)
#print ("Invoice ", order_number, " from ", order_data)
order["data"]=order_data
order_status_raw = re.search(' data-order="status">\D*<\/td>', order_txt).group(0)
order_status = re.search('[\u0400-\u04FF]+\s*[\u0400-\u04FF]*', order_status_raw).group(0)
#print (order_status)
order["status"]=order_status
order_email_raw = re.search(' data-order="email">[^@]+@[^<]*<\/td>', order_txt).group(0)
order_email = re.search('>[^@]+@[^<]*', order_email_raw).group(0)
order_email=order_email[1:]
#print (order_email)
order["email"]=order_email
order["items"]=[]
while 1:
item={}
#where product name starts?
search_str=' data-order="name">'
order_name_ = re.search(search_str, order_txt)
if order_name_==None:
break
order_name_pos = order_name_.start()+len(search_str)
order_txt=order_txt[order_name_pos:]
#skip product <img ..> tag
search_str='>'
order_name_pos = re.search(search_str, order_txt).start()+len(search_str)
order_txt=order_txt[order_name_pos:]
#where <div> attribute starts
search_str='<div'
div_pos = re.search(search_str, order_txt).start()
name=order_txt[:div_pos]
name=name.strip()
#print (name)
item["name"]=name
order_txt=order_txt[div_pos:]
#where <div> attribute ends
search_str='>'
div_pos = re.search(search_str, order_txt).start()+len(search_str)
order_txt=order_txt[div_pos:]
#where attribute ends
search_str='<'
attr_pos = re.search(search_str, order_txt).start()
attr=order_txt[:attr_pos]
attr=attr.strip()
order_txt=order_txt[div_pos:]
item["attr"]=""
if len(attr):
#print (attr)
item["attr"]=attr
order_code_raw_ = re.search('data-order=\"code\">\d*<\/td>', order_txt)
item["code"]=0
if order_code_raw_ :
order_code_raw = order_code_raw_.group(0)
order_code_ = re.search('>\d+',order_code_raw)
if order_code_ :
order_code = order_code_.group(0)
order_code = order_code[1:]
#print ("Code ",order_code)
item["code"]=order_code
order_quantity_raw = re.search('data-order=\"quantity\">\d+<\/td>', order_txt).group(0)
order_quantity = re.search('\d+', order_quantity_raw).group(0)
#print (order_quantity)
item["quantity"]=order_quantity
order_singleprice_raw = re.search(' data-order="singleprice">\s*\d+.', order_txt).group(0)
order_singleprice = re.search('\d+', order_singleprice_raw).group(0)
#print (order_singleprice)
item["singleprice"]=order_singleprice
order_totalprice_raw = re.search(' data-order="totalprice">\s*\d+.', order_txt).group(0)
order_totalprice = re.search('\d+', order_totalprice_raw).group(0)
#print (order_totalprice)
item["totalprice"]=order_totalprice
#print ("---------------------")
order["items"].append(item)
return order
def print_order(order):
#print(order)
print ("---------------------")
print( "Invoice ", order["number"], " from ", order["data"] )
print( "Status ", order["status"] )
print( "Status ", order["email"] )
items_list=order["items"]
for item in items_list :
print( item["code"], ":", item["name"], ":", item["attr"],":",item["singleprice"],":",item["quantity"],":",item["totalprice"] )
Тут нужно заметить, что каждый товар должен иметь «код товара» и передавать его в ордере.
Тогда, после функции parse_order_from_email() получаю результат вот такую структуру (или мап? не знаю как это в питоне называется):
{
«number» => «00010023»,
«data» => «11.03.2018»,
«status» => «Оплачено»,
«email» => «pupkin1994@mail.ru»,
«items» =>
{
«code» => «123»,
«name» => «Нужная штука»,
«attr» => "",
«singleprice» => «500»,
«quantity» => «3»,
«totalprice» => «1500»
},
{
«code» => «125»,
«name» => «Ненужная штука»,
«attr» => "",
«singleprice» => «5000»,
«quantity» => «1»,
«totalprice» => «5000»
}
}
А теперь самое сложное.
Нужно «выполнить этот чек на кассе».
У меня в виртуальной машине вместе с работающими скриптами на питоне работает еще и кассовая программа «Дримкас Старт». Программа автоматизации должна кликать по нужным местам программы так, как написано в чеке.
Для этого попробую использовать pyautogui.
Нужно наделать скриншотов программы «Дримкас Старт» там где находятся кнопки GUI и питоновский pyautogui сможет найти эти изображения на экране с помощью функции pyautogui.locateOnScreen(img). Где нашел изображение кнопки, туда можно подвинуть мышь и сделать клик: pyautogui.moveTo( x, y, 0.5), pyautogui.click().
Скрипт автоматических кликов по программе Дримкас Старт#!/usr/bin/env python
import pyautogui
import time
pyautogui.PAUSE = 3.0
mscale=1.0
def kassa_button_click( img ):
pos = pyautogui.locateOnScreen(img)
if pos!=None :
x=int(pos[0]*mscale)
y=int(pos[1]*mscale)
print("Button ",img," at x=",x," y=",y)
pyautogui.moveTo( x, y, 0.5)
pyautogui.click()
return
print("Button ",img," not found")
def make_check(order):
kassa_button_click('add_buyer.png');
pyautogui.typewrite(order["email"]+"\n", interval=0.1)
items_list=order["items"]
for item in items_list :
code=item["code"]
quantity=int(item["quantity"])
if quantity:
if code:
product_img_name="img\\"+str(code)+".png"
pos = pyautogui.locateOnScreen(product_img_name)
x=int(pos[0]*mscale)
y=int(pos[1]*mscale)
print("Product ",product_img_name," at x=",x," y=",y)
pyautogui.moveTo( x, y, 0.5)
if pos!=None :
i=0
while i<quantity:
i=i+1
pyautogui.click()
kassa_button_click('raschot.png');
kassa_button_click('card.png');
kassa_button_click('gotovo_yellow.png');
kassa_button_click('gotovo_green.png');
При этом (важно!), нужны изображения товарных позиций так же сделать скришотами и сохранить в папку img\ по имени «код товара.png».
Честно говоря вот это место очень ненадежное. PyAutoGUI — это pixel accurate сравнение эталонных картинок с экраном. Во-первых, в виндовсе может стоять scale экрана. Во-вторых, в настройках виндовс может случайно измениться настройка «font smooth». В третьих, если вдруг программа «Дримкас Старт» самообновится, то она может изменить внешний вид программы. Да и сама виндовс может обновиться — это думаю как-то нужно выключить. После любого из этих действий все мои скрипты перестанут работать и все развалится…
Я был бы счастлив, если бы в программе «Дримкас Старт» были бы какие-то keyboard short-cut keys. Все было бы гораздо надежней.
Ну можно еще подключить opencv — смотрел эти примеры, должно работать, даже если изображения не точные.
Итого, вот видео, которое показывает, как система может работать:
Здесь слева браузер в котором клиент оформляет заказ в интернет магазине. Справа виртуальная машина с запущенным скриптом Python, который читает почту, анализирует письма с заказами и по заказу кликает в интерфейсе «Дримкас Старт» программы. Там еще и е-мэйл клиента вбивается.
Все довольно примитивно, но в общем работает.
Конечно, нужно предусмотреть еще массу вещей вроде fail-safe-recovery. Например, упадет питоновский скрипт или программа дримкас, нужно перезапустить их. Еще проблема — касса должна открывать и закрывать смену. Это тоже можно сделать кликами, но я пока не сделал. Дальше хотелось бы добавить оповещение администратора о напечатанном чеке или о какой-то неполадке по почте.
Ну вот как-то так. Далеко не уверен, что такие методы будут позитивно встречены читателями. Но был бы признателен за любые комментарии. Так что критикуйте пожалуйста.
PS: Еще один момент во всей этой истории мне очень не по душе. Это — Кабинеты.
Все решения, которые я видел требуют дополнительных интернет кабинетов.
У меня должен появиться Кабинет Налоговой. Кабинет Яндекс.Кассы (ну он и так уже есть, ладно), Кабинет интегратора, например, Кабинет Дримкас. Ну и наконец ОФД Кабинет.
Здесь в этих кабинетах так много зла скрыто.
Как мне кажется, информация о продажах — это вообще-то коммерческая тайна.
Обидно, что Кабинет Дримкас (да вообще-то любое облачное решение какое найдете требует кабинета в котором будет все храниться и отображаться) получает забесплатно коммерческую тайну и главное нигде не обещают хранить эту тайну. То есть могут анализировать, могут продавать.
То что ОФД накапливает огромные базы — это тоже зло, но зло определенное по закону.
Вот такая вот действительность современной цивилизации.