Эта статья написана под влиянием небольшой паники в связи со стремительно приближающимся «днем Х» — очередной переход на онлайн кассы для очередной категории предприятий и организаций. Теперь с 1-го июля онлайн чеки должны выдавать даже те, кто их раньше мог не выдавать — интернет магазины и торговые автоматы.
Как же выдать чек?
Я довольно долго изучаю этот вопрос, так как мне нужно как-то автоматизировать процесс выдачи чеков для интернет магазина. Мне нужно решить эту техническую проблему с минимальными затратами по деньгам и по времени. С кассами ранее дела не имел и вот сейчас пытаюсь это дело изучить и понять и запустить в работу.
Здесь небольшой обзор возможных решений и мои криворукие скрипты на питоне.
Мое решение не коробочный продукт, но возможно у кого-то есть свой напильник и он сможет его довести до ума…
Это возможно подойдет тем, у кого
<?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);
#!/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()
#!/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"] )
{
"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"
}
}
#!/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');
К сожалению, не доступен сервер mySQL