# -*- coding: utf-8 -*-
import time
import threading
import base64

from threading import Lock

from actions.customer import (
    get_customer_doc,
    validate_required_document,
)
from typing import (
    Any,
    List,
    Optional,
    Union,
)

import cfgtools
import json
import logging

from datetime import datetime
from decimal import Decimal
from xml.etree import cElementTree as eTree
import pyscripts
import persistence
import re
import sys

import helper
import sysactions
from actions.delivery.print_original_delivery import print_original_delivery
from actions.discount.clean_discounts import clean_discounts
from actions.production import get_pick_list_orders, print_pick_list_order
from actions.sale_line.set_custom_properties import update_or_remove_sale_type
from actions.eft.request_eft import request_eft
from systools import (
    sys_log_info,
    sys_log_debug,
    sys_log_exception,
    sys_log_error,
)
from msgbus import (
    TK_EVT_EVENT,
    TK_SYS_NAK,
    TK_SYS_ACK,
    TK_ACCOUNT_GIFTCARDACTIVITY,
    TK_SYS_CHECKFORUPDATES,
    TK_POS_USERLOGIN,
    TK_POS_LISTUSERS,
    TK_I18N_GETLANGS,
    TK_POS_SETLANG,
    TK_CDRAWER_OPEN,
    TK_HV_GLOBALRESTART,
    TK_GENESIS_LISTPKGS,
    TK_GENESIS_APPLYUPDATE,
    FM_STRING,
    FM_PARAM,
    FM_XML,
    MBException,
    SE_NOTFOUND,
    TK_PRN_PRINT,
    TK_DRV_IOCTL,
    MBMessage,
)
from posot import (
    OrderTaker,
    OrderTakerException,
    ClearOptionException,
)
from pyscriptscache import cache as _cache
from sysactions import (
    StopAction,
    AuthenticationFailed,
    action,
    send_message,
    get_model,
    get_podtype,
    get_tender_type,
    get_business_period,
    check_genesis_error,
    get_operator_session,
    get_posot,
    get_cfg,
    get_current_operator,
    has_current_order,
    get_current_order,
    check_drawer,
    check_operator_logged,
    check_current_order,
    show_info_message,
    show_messagebox,
    show_confirmation,
    show_listbox,
    show_keyboard,
    show_order_preview,
    show_text_preview,
    close_asynch_dialog,
    print_text,
    print_report,
    set_custom,
    get_custom,
    format_amount,
    get_last_order,
    get_clearOptionsInfo,
    read_scanner,
    is_valid_date,
    get_poslist,
    get_pricelist,
    is_day_opened,
    get_posfunction,
    is_day_blocked,
    show_custom_dialog,
    authenticate_user,
    get_storewide_config,
    translate_message,
)

from actions.functions.do_store_order import doStoreOrder
from pos_model import TenderType, OrderStatus

from actions.eft import (
    check_eft_transaction,
    finish_eft_transactions,
    get_electronic_payment_component_name,
    get_tender_details,
    has_electronic_payment,
)

from poslistener import get_updated_sale_line_defaults
from bustoken import (
    TK_BKOFFICEUPLOADER_SEND_BKOFFICE_ORDER,
    TK_ELECTRONIC_PAYMENT_CANCEL_PAYMENT,
    TK_FISCALWRAPPER_GET_NF_TYPE,
    TK_FISCALWRAPPER_PROCESS_REQUEST,
    TK_FISCALWRAPPER_REPRINT,
    TK_FISCALWRAPPER_RE_SIGN_XML,
    TK_FISCALWRAPPER_SAT_OPERATIONAL_STATUS_REQUEST,
    TK_FISCALWRAPPER_SAT_PAYMENT_STATUS,
    TK_FISCALWRAPPER_SAT_PROCESS_PAYMENT,
    TK_FISCALWRAPPER_SEARCH_REQUEST,
    TK_FISCALWRAPPER_SEFAZ_CONNECTIVITY,
    TK_FISCALWRAPPER_SITUATION,
    TK_KDSMONITOR_STATUS_REQUEST,
    TK_LOYALTY_GET_LOYALTY_ID,
    TK_MAINTENANCE_RECREATE_FILE_NUMBER,
    TK_MAINTENANCE_RESTART_REQUEST,
    TK_MAINTENANCE_TERMINATE_REQUEST,
    TK_PROD_PURGE,
    TK_RUPTURA_GET_DISABLED,
    TK_RUPTURA_GET_ENABLED,
    TK_RUPTURA_UPDATE_ITEMS,
)
from manager.reports import (
    report_action,
    is_valid_period,
)
from tef import PrintReceipt

from actions.delivery import create_delivery_manual_order_json
from actions.order import create_order
from model.customexception import (
    DeliveryCouponPrintError,
    MaxQuantityException,
    MinQuantityException,
    RequiredDocumentException,
    ElectronicPaymentException,
    MissingTenderParametersException,
)

from core import OrderTaker as NOrderTaker

logger = logging.getLogger("PosActions")

import actions
from actions import *
actions.set_mb_context(pyscripts.mbcontext)


import manager
# noinspection PyUnresolvedReferences
from ui import get_product_nav
# noinspection PyUnresolvedReferences
from manager import (
    openday,
    check_business_day,
)
import manager.reports
# noinspection PyUnresolvedReferences
from manager.reports import speedOfServiceReport, hourlySalesReport, cashReport, checkoutReport
from old_helper import (
    config_logger,
    convert_from_utf_to_localtime,
    round_half_away_from_zero,
)
from mw_helper import (
    show_filtered_list_box_dialog,
	show_message_dialog,
    show_message_options_dialog,
    show_numpad_dialog,
    ModelService,
)
from actions.util import (
    get_authorization,
    get_drawer_amount,
    do_set_drawer_status,
    check_sangria,
    is_sangria_enable,
    get_menu_user_logged,
    set_pos_type,
)
from actions.config import (
    get_keyboard_comments_config,
    get_min_value_to_ask_bordereau_justify_config,
    show_numpad_for_customer_name,
)
from actions.loyalty.open_loyalty import get_loyalty_info
from actions.models import UserLevels
from actions.products.get_products_with_measure_unit import (
    is_a_product_with_measure_unit,
)
from actions.fiscal import check_certificate
from actions.functions.do_recall_order import do_recall_order
from actions.functions.do_back_from_total import do_back_from_total
from actions.void.utils import void_order
from utilconfigurations import get_ask_document_pinpad
from utilfunctions import (
    get_customer_doc_after_paid,
    get_device_service,
    update_custom_properties,
    get_tenders_options_dict,
    get_card_brands,
    options_dict_to_str_list,
    get_custom_property_value,
    get_customer_info_config,
    pad_barcode,
    is_stored_order,
)
from application.interactor import OrderInteractor

from custom_sysactions import (
    block_action_if_pos_is_blocked,
    user_authorized_to_execute_action,
    block_action_if_order_items_not_resolved,
)
from threads import UpdatePosBlockedState

from peripherals.scale import (
    WeightComparer,
    PosScaleEvents,
)
from peripherals.scanner import sell_product_by_barcode


USECS_PER_SEC = 1000000

debug_path = '../python/pycharm-debug.egg'
if os.path.exists(debug_path):
    try:
        sys.path.index(debug_path)
    except ValueError:
        sys.path.append(debug_path)
    # noinspection PyUnresolvedReferences
    try:
        import pydevd
    except NameError:
        pass

manager.mbcontext = pyscripts.mbcontext
manager.reports.mbcontext = pyscripts.mbcontext

sysactions.DATE_FMT = "%d/%m/%Y"
sysactions.get_pricelist.DEFAULT = "EI"

# Tender types
TENDER_CASH = "0"
TENDER_CREDIT_CARD = "2"
TENDER_CREDIT_AMEX = "5"
TENDER_CREDIT_VISA = "9"
TENDER_CREDIT_MASTERCARD = "10"
TENDER_CREDIT_DISCOVER = "11"

SIGNED_IN_USER = "SIGNED_IN_USER"
ASSIGNED_USER = "ASSIGNED_USER"

comment_map = {
    "ONSIDE": "[OnSide]",
    "EXTRA": "[Extra]",
    "LIGHT": "[Light]",
    "$ONSIDE": "[OnSide]",
    "$EXTRA": "[Extra]",
    "$LIGHT": "[Light]"
}

# Message-bus context
mbcontext = pyscripts.mbcontext  # type: MBEasyContext
# Tuple of registered POS
poslist = ()
# Store ID
store_id = None

# NF Type (SAT, NFCE)
nf_type = None

_eft_events = []
# List of pending tandem events
_tandem_events = []
# Indicates that a scanner's automatic sale function is paused for a specific POS, used during
# price lookup, or non-product scans. Key is POS id, value True or False.
scanner_sale_paused = {}
# Set of recall-by-picture listeners
_recall_listeners = set()
# Holds a set of product codes that match the "Subst" tag
subst_products = set()
# Item composition cache
_item_comp_cache = {}

# Default Options Cache
_default_options_cache = {}
# Option Items Cache
_options_cache = {}
# Must Be Modified Cache
_must_modify = {}
# Product list cache, with prices and barcodes

# Barcode to product mapping

comment_mods = []
productPrices = {}
cancela = False

lock_tender = threading.Lock()
tendering = {}

lock_recall = threading.Lock()
recalling = []

# limit values to sangria
sangria_levels = None
is_sangria_enabled_config = True
max_transfer_value = 0

keyboard_comments = {}
customer_info_config = {}
scale_ticket_mode = ''
ask_for_delivery_payment = True

pos_scale_events = PosScaleEvents()
pos_scale_dialogs = []
waiting_scale = False

printer_receipt = None  # type: Optional[PrintReceipt]
key_error = "$ERROR"

barcode_pattern_reading = "2 XXXXXX YYYYY W"

do_sale_lock_dict = Lock()
do_sale_lock = {}

#
# Main function - called by the pyscripts module
#
def main():
    helper.import_pydevd(os.environ["LOADERCFG"], 9123)

    persistence.Driver().open(mbcontext).select("SELECT 1")

    # Subscribes event callbacks
    sysactions.initialize()

    # Loads SQLite cache
    force_sqlite_cache()

    # Check for any genesis error and notify the users
    threading.Timer(5.0, check_genesis_error).start()

    update_pos_blocked_state = UpdatePosBlockedState(poslist)
    update_pos_blocked_state.daemon = True
    update_pos_blocked_state.start()

    config_logger(os.environ["LOADERCFG"], 'PosActions')
    config_logger(os.environ["LOADERCFG"], 'PerformanceActions')
    config_logger(os.environ["LOADERCFG"], 'TableActions')
    config_logger(os.environ["LOADERCFG"], 'EftActions')
    config_logger(os.environ["LOADERCFG"], 'AcceptUpdateBinary')

    from application.interactor import ServicePeriodInteractor
    ServicePeriodInteractor().verify_if_service_periods_exist(poslist)

    global printer_receipt
    printer_receipt = PrintReceipt(mbcontext, logger)

    from threading import Thread
    Thread(target=thread_load_pos_config).start()

def _join(dic1, dic2):
    d = dict(dic1)
    d.update(dic2)
    return d


def _dialog_resp_received(params):
    """Callback called by pyscripts module"""
    xml, subject, p_type = params[:3]
    dialog = eTree.XML(xml).find("Dialog")
    dlg_id = str(dialog.get("id"))
    response = str(dialog.findtext("Response"))
    for eft_evt in _eft_events:
        if str(eft_evt[1]) == dlg_id and response == "0":
            pos_id, order_id = eft_evt[2:4]
            sys_log_debug("Pressionada tecla anula. Cancelando pagamento Sitef no POS %s - Order %s" % (str(pos_id), str(order_id)))
            component_name = get_electronic_payment_component_name(pos_id=pos_id)
            if component_name is None:
                continue
            data = "%s;%s" % (str(pos_id), str(order_id))
            mbcontext.MB_EasySendMessage(
                dest_name=component_name,
                token=TK_ELECTRONIC_PAYMENT_CANCEL_PAYMENT,
                format=FM_PARAM,
                data=data
            )

            close_asynch_dialog(pos_id, dlg_id)

    if dlg_id in pos_scale_dialogs:
        pos_scale_dialogs.remove(dlg_id)

    return None


def _sitef_processing_received(params):
    data, subject, p_type, _, pos_id = params[:5]
    if p_type == "PROCESS_STARTED":
        for eft_evt in _eft_events:
            if eft_evt[2] == pos_id:
                model = get_model(pos_id)
                pod_function = get_posfunction(model) if get_podtype(model) in ("DT", "FC") else get_podtype(model)
                # Cashless processing started for this POS... Change the current dialog box to CANCEL
                if pod_function not in ("TT", "OT"):
                    message = "$READ_QR_CODE" if data == "122" else "$EFT_SWIPE_CARD"
                    dlg_id = show_messagebox(pos_id, message, "$EFT", "swipecard", buttons="$CANCEL", timeout=180000, asynch=True)
                    eft_evt[1] = dlg_id
                break
    if p_type == "PROCESS_ENDED":
        for eft_evt in _eft_events:
            if eft_evt[2] == pos_id and eft_evt[1] is not None:
                close_asynch_dialog(pos_id, eft_evt[1])
                break
    if p_type == "PROCESS_FINISHED":
        for eft_evt in _eft_events:
            _eft_events.remove(eft_evt)
            if eft_evt[2] == pos_id and eft_evt[1] is not None:
                close_asynch_dialog(pos_id, eft_evt[1])
                break
    return None


def populate_combo_taxes(posid):
    BEGIN_TRANSACTION = ["BEGIN TRANSACTION", "UPDATE fiscalinfo.FiscalDEnabled SET Enabled=0"]
    COMMIT_TRANSACTION = ["UPDATE fiscalinfo.FiscalDEnabled SET Enabled=1", "COMMIT TRANSACTION"]

    query = """
        INSERT OR REPLACE INTO taxcalc.ProductTaxCategory(ItemId, TaxCgyId)
        SELECT PI.ItemId, V1.TaxCgyId
            FROM (
                SELECT P.ItemId, P.ProductCode, PTC.TaxCgyId
                FROM sysproddb.ProductItem P
                LEFT JOIN taxcalc.ProductTaxCategory PTC ON PTC.ItemId=P.ItemId
                WHERE P.RecursionLevel=1 AND PTC.TaxCgyId IS NOT NULL
            ) V1
        JOIN sysproddb.ProductItem PI ON PI.ProductCode=V1.ProductCode
        WHERE PI.RecursionLevel>1;
        INSERT OR REPLACE INTO fiscalinfo.FiscalProductCatalogChangesCounter SELECT * FROM productdb.ProductCatalogChangesCounter;

        DELETE FROM fiscalinfo.FiscalProductDB
        WHERE EXISTS (
            SELECT 1 FROM productdb.ProductCatalogChangesCounter A
            LEFT JOIN fiscalinfo.FiscalProductCatalogChangesCounter B
            WHERE A.ChangesCount!=COALESCE(B.ChangesCount,-1) OR A.ChangesDateTime!=COALESCE(B.ChangesDateTime,''));

        INSERT OR REPLACE INTO fiscalinfo.FiscalProductDB(ProductCode,ProductName,UnitPrice,TaxFiscalIndex,FiscalCategory,TaxRate,IAT,IPPT,MeasureUnit)
        SELECT ProductCode,ProductName,UnitPrice,TaxFiscalIndex,FiscalCategory,TaxRate,IAT,IPPT,MeasureUnit
        FROM
            (SELECT
                    Price.ProductCode AS ProductCode,
                    Product.ProductName AS ProductName,
                    Price.DefaultUnitPrice as UnitPrice,
                    TaxRule.TaxFiscalIndex AS TaxFiscalIndex,
                    TaxCategory.TaxCgyDescr AS FiscalCategory,
                    tdscale(TaxRule.TaxRate,2,0) AS TaxRate,
                    COALESCE(IAT.CustomParamValue,'T') AS IAT,
                    COALESCE(IPPT.CustomParamValue,'P') AS IPPT,
                    COALESCE(PKP.MeasureUnit,'UN') AS MeasureUnit
                FROM
                    Price Price
                LEFT JOIN taxcalc.ProductTaxCategory ProductTaxCategory
                    ON ProductTaxCategory.ItemId=(Price.Context || "." || Price.ProductCode)
                LEFT JOIN taxcalc.TaxCategory TaxCategory
                    ON TaxCategory.TaxCgyId=ProductTaxCategory.TaxCgyId
                LEFT JOIN taxcalc.TaxRule TaxRule
                    ON TaxRule.TaxCgyId=ProductTaxCategory.TaxCgyId
                LEFT JOIN Product
                    ON Price.ProductCode=Product.ProductCode
                LEFT JOIN productdb.ProductKernelParams PKP
                    ON PKP.ProductCode=Product.ProductCode
                LEFT JOIN productdb.ProductCustomParams IAT
                    ON IAT.ProductCode=Product.ProductCode AND IAT.CustomParamId='IAT'
                LEFT JOIN productdb.ProductCustomParams IPPT
                    ON IPPT.ProductCode=Product.ProductCode AND IPPT.CustomParamId='IPPT'
                WHERE CURRENT_TIMESTAMP BETWEEN Price.ValidFrom AND Price.ValidThru
            ) FiscalProductsView
        WHERE EXISTS (
            SELECT 1 FROM productdb.ProductCatalogChangesCounter A
            LEFT JOIN fiscalinfo.FiscalProductCatalogChangesCounter B
            WHERE A.ChangesCount!=COALESCE(B.ChangesCount,-1) OR A.ChangesDateTime!=COALESCE(B.ChangesDateTime,'')
        );

        INSERT INTO fiscalinfo.FiscalProductCatalogChangesCounter SELECT * FROM productdb.ProductCatalogChangesCounter;
    """

    queries = [query]
    conn = None
    try:
        conn = persistence.Driver().open(mbcontext, dbname=str(posid))
        queries = BEGIN_TRANSACTION + queries + COMMIT_TRANSACTION
        conn.query("\0".join(queries))
    except Exception as _:
        logger.exception("ERRO CRIANDO FISCALINFO DATABASE INFORMATION")
        sys_log_exception("ERRO CRIANDO FISCALINFO DATABASE INFORMATION")
        if conn:
            conn.query('''ROLLBACK TRANSACTION;''')
    finally:
        if conn:
            conn.close()


def thread_load_pos_config():
    while True:
        try:
            load_pos_config()
            break
        except Exception:
            logger.exception("Error loading POS configuration")



def load_pos_config():
    global poslist
    global store_id
    global sangria_levels
    global is_sangria_enabled_config
    global max_transfer_value
    global set_product_quantity_pre
    global skim_digit_limit
    global picklist_reprint_type
    global customer_info_config
    global keyboard_comments
    global scale_ticket_mode
    global ask_for_delivery_payment
    global always_open_drawer
    global use_customer_doc_on_fiscal_doc
    global ask_document_pinpad
    global barcode_pattern_reading

    config = cfgtools.read(os.environ["LOADERCFG"])

    # Store-wide config
    is_sangria_enabled_config = get_storewide_config("Store.IsSangriaEnabled", defval="true").lower() == "true"
    sangria_levels = get_storewide_config("Store.SangriaLevels", defval="250;500;1000")
    max_transfer_value = float(get_storewide_config("Store.MaxTransfer", defval="0"))

    picklist_reprint_type = config.find_value("Reprint.PicklistReprintType") or "production_pick_list"
    auto_generate_skim_number = (config.find_value("Customizations.AutoGenerateSkimNumber") or "true").lower() == "true"
    keyboard_skim_numeric = (config.find_value("Customizations.KeyboardSkimNumeric") or "true").lower() == "true"
    scale_ticket_mode = (config.find_value("Customizations.ScaleTicketMode") or "price").lower()
    ask_for_delivery_payment = config.find_value("Customizations.DeliveryConfigurations.AskForDeliveryPayment", "true").lower() == "true"
    always_open_drawer = (config.find_value("Customizations.AlwaysOpenDrawer") or 'false').lower() == "true"
    use_customer_doc_on_fiscal_doc = (config.find_value("Customizations.UseCustomerDocumentOnFiscalDocument") or 'true').lower() == "true"
    skim_digit_limit_cfg = get_storewide_config("Store.SkimDigitLimit", defval="4-30")

    skim_digit_limit = {
        "min": int(skim_digit_limit_cfg.split("-")[0]),
        "max": int(skim_digit_limit_cfg.split("-")[1])
    }

    store_id = get_storewide_config("Store.Id")
    manager.reports.store_id = store_id
    set_product_quantity_pre = True

    poslist = get_poslist()
    manager.reports.poslist = poslist
    manager.poslist = poslist
    barcode = config.find_value("Customizations.BarCodePatternReading") or "2XXXXXXYYYYYW"
    barcode_pattern_reading = pad_barcode(barcode=barcode)

    min_value_to_ask_bordereau_justify_config = get_min_value_to_ask_bordereau_justify_config(config)

    actions.set_pos_config(
        PosConfig(
            max_transfer_value,
            skim_digit_limit,
            sangria_levels,
            auto_generate_skim_number,
            store_id,
            min_value_to_ask_bordereau_justify_config,
            keyboard_skim_numeric
        )
    )

    for pos_id in poslist:
        customer_info_config[pos_id] = get_customer_info_config(pos_id)

    keyboard_comments = get_keyboard_comments_config(config)


def force_sqlite_cache():
    conn = None
    try:
        conn = persistence.Driver().open(mbcontext)
        for posid in poslist:
            try:
                conn.set_dbname(str(posid))
                conn.select("SELECT count(1) from sysproddb.ProductItem LIMIT 600000")
            except:
                pass
    except:
        pass
    finally:
        if conn:
            conn.close()


def register_gift_activity(posid, session, activity_type, description, amount, authcode, orderid, cardno):
    payload = "\0".join([session, activity_type, description, amount, authcode, orderid, cardno])
    msg = send_message("account%d" % int(posid, 10), TK_ACCOUNT_GIFTCARDACTIVITY, FM_PARAM, payload)
    return msg.token != TK_SYS_NAK


def handle_electronic_payment(pos_id, model, tender_id, amount, xml_order, tender_type, tender_seq_id=None, id_fila=None):
    session = get_operator_session(model)
    order_id = xml_order.get("orderId")
    electronic_type_id = tender_type["electronicTypeId"]
    logger.debug(electronic_type_id)

    amt = "%.2f" % float(amount)
    tender_id = tender_type["electronicTypeId"]
    eft_xml = request_eft(
        pos_id=pos_id,
        mb_context=mbcontext,
        model=model,
        electronic_type=electronic_type_id,
        amount=float(amt),
        _eft_events=[],
        is_table_service=False
    )
    logger.info("TEF result: {}".format(eft_xml))

    check_eft_transaction(
        model,
        pos_id,
        order_id,
        eft_xml,
        electronic_type_id
    )

    xml = eTree.XML(eft_xml)
    cardno, authcode, approved_amt, media, payment_amt, adq, exp_date, nsu, owner_name, last_digits = map(xml.get, ("CardNumber", "AuthCode", "ApprovedAmount", "Media", "ApprovedAmount", "IdAuth", "ExpirationDate", "NSU", "OwnerName", "LastDigits"))
    if not approved_amt:
        approved_amt = amt
    try:
        exp_date = exp_date or 'Indefinida'
        owner_name = owner_name or 'Indefinida'
        last_digits = last_digits or 'Indefinida'
        register_after = register_eft_after_payment(pos_id, order_id, tender_seq_id, authcode, cardno, owner_name, exp_date, adq, nsu, payment_amt, id_fila, media, last_digits)
        if register_after is None:
            show_messagebox(pos_id, key_error, "$ERROR_REGISTER_PAYMENT_SEFAZ")
        elif "ERROR" in register_after:
            show_messagebox(pos_id, key_error, "$ERROR_REGISTER_PAYMENT_SEFAZ|:%s" % register_after)
    except Exception as ex:
        sys_log_exception("Unexpected error after processing electronic payment - Error %s", str(ex))

    return tender_id, amount, xml


def register_eft_before_payment(posid, orderid, tender_seq_id, payment_amt, order_amt):
    payload = "\0".join(map(str, [posid, orderid, tender_seq_id, payment_amt, order_amt]))
    msg = send_message("FiscalWrapper", TK_FISCALWRAPPER_SAT_PROCESS_PAYMENT, FM_PARAM, payload)
    if msg.token == TK_SYS_ACK:
        return msg.data
    else:
        return None


def register_eft_after_payment(posid, orderid, tender_seq_id, auth_code, card_bin, owner_name, exp_date, adq, nsu, payment_amt, id_fila, brand, last_digits):
    payload = "\0".join(map(str, [posid, orderid, tender_seq_id, auth_code, card_bin, owner_name, exp_date, adq, nsu, payment_amt, id_fila, brand, last_digits]))
    msg = send_message("FiscalWrapper", TK_FISCALWRAPPER_SAT_PAYMENT_STATUS, FM_PARAM, payload)
    if msg.token == TK_SYS_ACK:
        return msg.data
    else:
        return None


@action
def doSale(pos_id, item_id, qty="1", sale_type="EAT_IN", pos_ot=None, seat=None, service_period_json=None):

    with get_pos_lock(pos_id=pos_id):
        model = get_model(pos_id)
        pos_ot = get_posot(model) if pos_ot is None else pos_ot
        pod_type = get_podtype(model)
        period = get_business_period(model)
        session = get_operator_session(model)
        is_new_order = None
        model_service = ModelService(pos_id=pos_id)
        try:
            price_list = model_service.get_price_list_from_model(
                pod_type=pod_type,
                sale_type=sale_type,
            )

            sale_type = "DRIVE_THRU" if (pod_type == "DT") else sale_type

            pos_function = get_posfunction(model)
            is_new_order = not has_current_order(model)

            # Do NOT proceed if there is already an opened Order and not PartCode was provided
            if not is_new_order and item_id is None:
                return

            order_properties_dict = {}
            if is_new_order is True:
                if is_sangria_enable() and pod_type != "TT":
                    set_drawer_status = do_set_drawer_status(pos_id, get_drawer_amount(pos_id, period, session))
                    if set_drawer_status:
                        return
                order_properties_dict = new_order_properties(pos_id, pos_ot)

            order_created = False
            if is_new_order:
                order_created = process_new_order(model, pod_type, pos_function, pos_id, pos_ot, price_list, sale_type)

            if is_new_order is True and order_created is False:
                create_order(pos_ot, pos_id, price_list, sale_type=sale_type)
                set_pos_type(pos_ot, model, sale_type=sale_type, pod_type=pod_type)

            if item_id is None:
                return

            custom_properties = {}
            if seat and seat != '0':
                custom_properties["seat"] = seat

            current_order = None
            if has_current_order(model):
                current_order = get_current_order(model)
                tab_id = current_order.find("CustomOrderProperties/OrderProperty/[@key='TABLE_ID']")
                if tab_id is not None and "TAB" in tab_id.attrib["value"]:
                    custom_properties["tab"] = tab_id.attrib["value"]

                if has_current_order(model):
                    current_order = get_current_order(model)
                    tab_id = current_order.find("CustomOrderProperties/OrderProperty/[@key='TABLE_ID']")
                    if tab_id is not None and "TAB" in tab_id.attrib["value"]:
                        custom_properties["tab"] = tab_id.attrib["value"]

                service_period = json.loads(service_period_json if service_period_json else "{}")
                if service_period:
                    custom_properties["servicePeriod"] = service_period_json

            if sale_type == "DELIVERY":
                delivery_brands = _cache.get_delivery_brands()
                has_brands = len(delivery_brands) > 0
                order_brand = get_custom_property_value(
                    pos_id,
                    "BRAND",
                    order_picture=current_order
                )
                if has_brands and not order_brand:
                    selected_brand = _set_order_delivery_brand(
                        pos_id=pos_id,
                        pos_ot=pos_ot,
                        delivery_brands=delivery_brands,
                    )
                    if selected_brand:
                        custom_properties["brand"] = selected_brand
                elif order_brand:
                    custom_properties["brand"] = order_brand

            if sale_type != "EAT_IN":
                custom_properties["saleType"] = sale_type

            apply_def_opts = True
            if pod_type == 'TT':
                apply_def_opts = False

            order_taker = NOrderTaker(pos_ot, mbcontext)
            sale = order_taker.do_sale(
                pos_id,
                item_id,
                price_list,
                qty,
                True,
                sale_type,
                '0',
                apply_def_opts,
                custom_properties
            )

            if qty != "1":
                doChangeQuantity(int(pos_id), None, '1')

            if sale_type not in ("EAT_IN", "TAKE_OUT", "DRIVE_THRU"):
                pos_ot.updateOrderProperties(pos_id, saletype=sale_type)

            if len(order_properties_dict) > 0:
                pos_ot.setOrderCustomProperties(order_properties_dict)

            return sale
        except StopAction as _:
            return

        except Exception as ex:
            if is_new_order:
                model = get_model(pos_id)
                if has_current_order(model):
                    void_order(
                        pos_id,
                        new_order_status=OrderStatus.SYSTEM_VOIDED.value,
                        abandon=1
                    )

            show_messagebox(pos_id, message="$ERROR_START_SALE|{0}".format(ex.message))
            sys_log_exception('Error in doSale - new order: {} - '.format(is_new_order), ex)

def _set_order_delivery_brand(
    pos_id,             # type: str
    pos_ot,             # type: OrderTaker
    delivery_brands     # type: List
):
    # type: (...) -> str

    delivery_brand = {}
    if len(delivery_brands) == 1:
        delivery_brand = delivery_brands[0]
    elif len(delivery_brands) > 1:
        listbox_options = [item["brand"] for item in delivery_brands]
        option = sysactions.show_listbox(
            pos_id,
            listbox_options,
            message="$SELECT_BRAND"
        )
        if option is None:
            raise StopAction()
        delivery_brand = delivery_brands[option]

    if "brand" in delivery_brand:
        try:
            pos_ot.setOrderCustomProperties({"BRAND": delivery_brand.get("brand")})
            return delivery_brand.get("brand")
        except OrderTakerException as _:
            raise StopAction()


def process_new_order(model, pod_type, pos_function, pos_id, pos_ot, price_list, sale_type):
    logger.debug("--- doSale start new order ---")
    check_new_order(model, pos_id)
    return offer_multi_order(model, pod_type, pos_id, pos_ot, pos_function, price_list, sale_type)


def new_order_properties(pos_id, pos_ot):
    dict_sale = {}
    document = customer_info_config[int(pos_id)]['document']
    name = customer_info_config[int(pos_id)]['name']
    request_customer_info = "onStart" in document or "onStart" in name

    if request_customer_info:
        logger.debug("--- doSale before customer info ---")
        customer_doc = None
        if "onStart" in document:
            customer_doc = get_customer_doc(
                pos_id,
                pos_ot,
                ask_document_pinpad=get_ask_document_pinpad(pos_id)
            )
        customer_name = None
        if "onStart" in name:
            customer_name = get_customer_name(pos_id, pos_ot)
        pre_sale = None

        logger.debug("--- doSale after customer info ---")
        dict_sale = update_custom_properties(customer_doc, customer_name, pre_sale)

    return dict_sale


def check_new_order(model, pos_id):
    check_operator_logged(pos_id, model=model, can_be_blocked=True)

    # check if skim is needed before new order
    check_sangria(model, pos_id)

    # check if the drawer should be closed before starting an order
    logger.debug("--- doSale check_drawer ---")
    if get_cfg(pos_id).key_value("needToCloseDrawer", "true") == "true":
        check_drawer(pos_id, model)


def offer_multi_order(model, pod_type, pos_id, pos_ot, pos_function, price_list, sale_type):
    if pod_type == "DT" and pos_function == "CS" and get_last_order(model):
        buttons = "$SAME_CAR|$NEW_CAR|$CANCEL"
        index = show_messagebox(pos_id, message="$DO_YOU_WANT_SAME_OR_NEW_CAR", title="$SAME_CAR", buttons=buttons)
        if index in (None, 2):
            raise StopAction()  # User cancelled, or timeout

        if index == 1:
            return False

        check_current_order(pos_id, model=model, need_order=False)
        try:
            last_order = get_last_order(model)
            if not last_order:
                show_info_message(pos_id, "$NO_PREVIOUS_ORDER_FOR_MULTIORDER", msgtype="warning")
                raise StopAction()

            logger.debug("--- doSale before posot.createOrder ---")
            order_id = last_order["orderId"]
            create_order(pos_ot, pos_id, price_list, multi_order_id=order_id, sale_type=sale_type)
            set_pos_type(pos_ot, model, sale_type=sale_type, pod_type=pod_type)
            logger.debug("--- doSale after posot.createOrder ---")

        except OrderTakerException as ex:
            # show_info_message(pos_id, "$ERROR_CODE_INFO|%d|%s" % (ex.getErrorCode(), ex.getErrorDescr()),
            #                  msgtype="critical")
            show_messagebox(pos_id, message="$ERROR_CODE_INFO|%d|%s" % (ex.getErrorCode(), ex.getErrorDescr()),
                            icon="error")
            raise StopAction()
        else:
            return True

    return False


def get_and_fill_customer_address(pos_id, pos_ot):
    customer_address = show_keyboard(pos_id, "$ENTER_CLIENT_ADDRESS", defvalue="", title="", numpad=False)
    if customer_address not in (None, ""):
        customer_address = customer_address.translate(None, '-+[]/;.|<>:?{}=_()*&^%$#@!').upper()
    else:
        customer_address = ""

    fill_additional_info(pos_ot, "ADDRESS", customer_address)


def get_customer_name(pos_id, pos_ot, default_name=""):
    if default_name and isinstance(default_name, unicode):
        default_name = default_name.encode("utf-8")

    customer_name = show_keyboard(
        posid=pos_id,
        message="$DLV_CUSTOMER_NAME",
        defvalue=default_name,
        title="",
        nopad=not show_numpad_for_customer_name()
    )
    if customer_name is None:
        return None
    if customer_name.strip() == '':
        return ''

    customer_name = customer_name.translate(None, '-+[]/;.|<>:?{}=_()*&^%$#@!,').upper()
    fill_additional_info(pos_ot, "NAME", customer_name)
    return customer_name


def fill_additional_info(pos_ot, properties, value):
    if pos_ot.additionalInfo:
        pos_ot.additionalInfo += "|"
    else:
        pos_ot.additionalInfo = ""
    pos_ot.additionalInfo += "{0}={1}".format(properties, value)


def customer_doc_is_required(order_xml, pod_function):
    if pod_function == "OT":
        return False, ""

    return _check_custom_properties(order_xml, "CUSTOMER_DOC")


def customer_name_is_required(order_xml):
    return _check_custom_properties(order_xml, "CUSTOMER_NAME")


def _check_custom_properties(order_xml, property_name):
    for prop in order_xml.findall("CustomOrderProperties/OrderProperty"):
        key, value = prop.get("key"), prop.get("value")
        if key == property_name:
            if value == "":
                return True, ""
            return False, value
    return True, ""


@action
def do_start_order(pos_id, sale_type="EAT_IN"):
    doSale(pos_id, None, sale_type=sale_type)


@action
def get_nf_type(*args):
    global nf_type
    if not nf_type:
        ret = mbcontext.MB_EasySendMessage("FiscalWrapper", TK_FISCALWRAPPER_GET_NF_TYPE, format=FM_PARAM, data=None)
        if ret.token == TK_SYS_ACK:
            nf_type = ret.data.upper()
            manager.nf_type = nf_type
    return nf_type


def _get_weight_qty(pos_id):
    weight_qty = get_scale_weight(int(pos_id))
    if weight_qty is None:
        weight_qty = show_keyboard(pos_id, "$TYPE_WEIGHT_QTY", title="", mask="weight", defvalue="0.000", numpad=True)
    if weight_qty in (None, "", "."):
        raise StopAction()
    if weight_qty == "0":
        raise StopAction()

    return str(float(weight_qty))


def get_scale_weight(pos_id):
    scale_name = get_device_service(
        pos_id=pos_id,
        device_type="scale",
    )
    weight = scale_wait_data(pos_id, scale_name)
    return weight


def scale_wait_data(pos_id, service_name, timeout=2000):
    weight_comparer = WeightComparer()
    scale_event = pos_scale_events.create_scale_event(pos_id, service_name)
    dialog_id = None

    try:
        while True:
            if not start_measuring_weight(service_name):
                return None

            if dialog_id is None:
                dialog_id = show_messagebox(pos_id, "$WAIT_SCALE_WEIGHT", buttons="$CANCEL", asynch=True)
                pos_scale_dialogs.append(str(dialog_id))
            elif str(dialog_id) not in pos_scale_dialogs:
                return ""

            scale_event.wait(float(timeout/1000.0))
            if scale_event.weight is None:
                return None

            if scale_event.weight > 0 and weight_comparer.is_weight_equal_to_previous(scale_event.weight):
                close_asynch_dialog(pos_id, dialog_id)
                dialog_id = None

                selected_option = show_messagebox(
                    pos_id,
                    message="$CONFIRM_SCALE_WEIGHT|{0}".format("%.3f" % scale_event.weight),
                    buttons="$OK|$GET_WEIGHT_AGAIN|$CANCEL")

                if selected_option == 0:
                    return scale_event.weight
                elif selected_option == 2:
                    return ""
    finally:
        if pos_id in pos_scale_events.get_wait_pos_id_event_dict():
            pos_scale_events.finish_event(scale_event)

        if dialog_id is not None:
            close_asynch_dialog(pos_id, dialog_id)


def start_measuring_weight(service_name):
    try:
        msg = send_message(service_name, TK_DRV_IOCTL, FM_PARAM, "")
        if msg.token == TK_SYS_ACK:
            return True
        else:
            return False
    except MBException as _:
        sys_log_exception("Erro comunicando com a balança")
        return False


@action
@block_action_if_pos_is_blocked
def doCompleteOption(pos_id, context, part_code, qty="1", line_number="", sale_type="EAT_IN", subst='', seat='',
                     service_period='', pricelist='', comment=None, *args):
    model = get_model(pos_id)
    if _is_changing_order(model) and is_table_service(model):
        show_messagebox(pos_id, message="$CANNOT_ADD_ITEMS_IN_AN_OPENED_ORDER", icon="error")
        raise StopAction()

    global waiting_scale
    if is_a_product_with_measure_unit(part_code):
        if waiting_scale:
            return

        try:
            waiting_scale = True
            qty = _get_weight_qty(pos_id)
        finally:
            waiting_scale = False

    pos_ot = get_posot(model)
    sale_type = sale_type or "EAT_IN"
    sale_xml = None

    try:
        if subst:
            if qty != "0":
                pos_ot.blkopnotify = True
            doClearOptionItem(pos_id, line_number, subst, pos_ot)
            pos_ot.blkopnotify = False

        if qty != "0":
            sale_xml = doOption(pos_id, context, part_code, qty, line_number, sale_type, pricelist, comment, pos_ot, seat, service_period, args)

        if sale_xml is None:
            return

        if qty != "1":
            doChangeQuantity(int(pos_id), None, '1')

        return sale_xml
    finally:
        pos_ot.blkopnotify = False


@action
def doOption(pos_id, context, part_code, qty="", line_number="", sale_type="EAT_IN", pricelist ='',
             comment=None, pos_ot=None, seat=None, service_period=None, *args):
    model = get_model(pos_id)
    item_id = context + "." + part_code
    try:
        try:
            check_operator_logged(pos_id, model=model, can_be_blocked=False)
        except StopAction:
            if get_podtype(model) != 'TT':
                raise
            # should never run this code since kiosk always open order before
            if not has_current_order(model):
                from actions.kiosk import do_start_kiosk_order
                do_start_kiosk_order(pos_id)
        item_qty = qty or "1"

        if context == '1':
            # Options not solved OR there are remaining items to sell
            sale_xml = doSale(pos_id, item_id, item_qty, sale_type, pos_ot, seat, service_period)
            return sale_xml
        elif has_current_order(model):
            pos_ot = get_posot(model) if pos_ot is None else pos_ot

            # Try to resolve an open option
            option_done = pos_ot.doOption(int(pos_id), item_id, item_qty, line_number, '@', True)
            if option_done:
                curr_line = get_order_saleline(pos_id, context, part_code, line_number)
                remove_custom_comment(pos_ot, curr_line)

                level = str(int(option_done[0].get("level")) + 1) if option_done else curr_line[0].get("level")
                if comment not in [None, '']:
                    add_custom_comment(pos_ot, context, part_code, line_number, level, comment_map[comment])
                if seat:
                    custom_property = dict(seat=seat)
                    from table_actions import set_sale_line_custom_property
                    set_sale_line_custom_property(
                        model=model,
                        pos_ot=pos_ot,
                        custom_properties=custom_property,
                        line_number=line_number,
                        item_id=context,
                        level=level,
                        part_code=part_code,
                        set_in_child=False
                    )

                return True

    except OrderTakerException as ex:
        sys_log_exception("Error processing [doOption]")
        show_messagebox(pos_id, message="$ERROR_CODE_INFO|%d|%s" % (ex.getErrorCode(), ex.getErrorDescr()))
    return False


@action
def doClearOption(posid, line_number="", qty="", *args):
    model = get_model(int(posid))
    check_current_order(posid, model=model, need_order=True)
    lines = get_clearOptionsInfo(model, line_number)
    if not lines:
        return  # Nothing to clear
    to_clear = lines[0]
    if len(lines) > 1:
        options = [str(line.get("productName")) for line in lines]
        index = show_listbox(posid, options, message="$SELECT_OPTION_TO_CLEAN")
        if index is None:
            return  # The user cancelled - or timed-out
        to_clear = lines[index]
    # Clear the option
    pos_ot = get_posot(model)
    try:
        item_id = "%s.%s" % (to_clear.get("itemId"), to_clear.get("partCode"))
        pos_ot.clearOption(posid, line_number, qty, item_id)
        return True
    except ClearOptionException as e:
        sys_log_exception("Could not clear option")
        show_messagebox(posid, message="$ERROR|%s" % e, icon="error")
    except OrderTakerException as ex:
        show_messagebox(posid, message="$ERROR_CODE_INFO|%d|%s" % (ex.getErrorCode(), ex.getErrorDescr()), icon="error")


@action
def doChangeSaleType(pos_id, sale_type, table_id=None, *args):
    model = get_model(pos_id)
    pos_ot = get_posot(model)
    check_operator_logged(pos_id, model=model, can_be_blocked=False)

    if has_current_order(model):
        # Set the sale type on the order
        pos_ot.updateOrderProperties(pos_id, saletype=sale_type)
        set_pos_type(pos_ot, model, sale_type=sale_type)
        if not table_id:
            return True

        order_picture = get_current_order(model)
        sale_lines = order_picture.findall(".//SaleLine")
        for sale_line in sale_lines:
            if int(sale_line.get("level")) > 0:
                continue

            update_or_remove_sale_type(pos_ot, sale_line, sale_type)

    return True


def list_open_options(order):
    sale_lines = order.findall("SaleLine")
    sale_lines = [x for x in sale_lines if x.get("qty") != "0"]
    for line in sale_lines:
        option_son_line = _get_direct_son(line, order)
        for son_line in option_son_line:
            is_option = son_line.get("itemType") == "OPTION"
            if not is_option:
                continue

            chosen_quantity = float(son_line.get("chosenQty") or 0)
            default_quantity = float(son_line.get("defaultQty") or 0)
            quantity = float(son_line.get("qty") or 0) * float(son_line.get("multipliedQty") or 0)
            if is_option and quantity > 0 and chosen_quantity < default_quantity:
                return son_line


def _get_direct_son(line, order):
    option_son_line = []

    find = "SaleLine[@lineNumber='{}']".format(line.get("lineNumber"))
    for son_line in order.findall(find):
        direct_son_level = int(son_line.get("level")) == int(line.get("level")) + 1
        direct_son_item_id = son_line.get("itemId") == line.get("itemId") + "." + line.get("partCode")
        if direct_son_level and direct_son_item_id:
            option_son_line.append(son_line)

    return option_son_line


# END list_open_options


@action
@block_action_if_order_items_not_resolved
def doTotal(pos_id, screen_number="", dlg_id=None, is_recall=False, is_kiosk="False", is_local_delivery="False", *args):
    logger.debug("Totalizando Order - POS %s" % pos_id)
    is_kiosk = is_kiosk and is_kiosk.lower() == "true"

    model = get_model(pos_id)
    posot = get_posot(model)

    pod_function = get_posfunction(model) if get_podtype(model) in ("DT", "FC") else get_podtype(model)
    document = customer_info_config[int(pos_id)]['document']
    name = customer_info_config[int(pos_id)]['name']
    request_customer_info = "onTotal" in document or "onTotal" in name

    if get_nf_type(pos_id) != "PAF" and not is_kiosk and not is_recall and request_customer_info:
        get_document = "onTotal" in document
        get_name = "onTotal" in name
        fill_customer_properties(
            pod_function,
            pos_id,
            posot,
            get_doc=get_document,
            get_name=get_name
        )

    if is_day_blocked(model):
        show_messagebox(pos_id, message="$POS_IS_BLOCKED_BY_TIME", icon="error")
        return

    # Waits a maximum of 1,5 seconds for the order to "arrive" at the POS model
    time_limit = (time.time() + 1.0)
    while (time.time() < time_limit) and (not has_current_order(get_model(pos_id))):
        time.sleep(0.1)
    logger.debug("Order Carregada - POS %s" % pos_id)

    check_current_order(pos_id, model=model, need_order=True)
    logger.debug("Order Verificada - Pronta para Totalizar - POS %s" % pos_id)
    try:
        # Totalize the order
        posot.doTotal(int(pos_id))
        logger.debug("Core Total OK - POS %s" % pos_id)

        # Check if Order is already PAID (Previuos tenders are bigger than current due amount)
        xml_order = eTree.XML(posot.orderPicture(pos_id))
        due_amount = float(xml_order.get("dueAmount"))

        if due_amount < 0:
            show_messagebox(pos_id, message="$TENDERS_ARE_BIGGER_THEN_DUE_AMOUNT", icon="warning")
            do_back_from_total(pos_id)
            return "Error"

        if screen_number:
            if pod_function == "OT":
                doStoreOrder(pos_id, "false")
                return

    except StopAction as _:
        raise

    except Exception as ex:
        show_messagebox(pos_id, message="$ERROR_CODE_INFO|%d|%s" % (ex.getErrorCode(), ex.getErrorDescr()), icon="error")
        if is_recall:
            raise ex

    finally:
        logger.debug("Order Totalizada - POS %s" % pos_id)

    return True


def _show_clean_numpad(
    pos_id,         # type: str
    new_value,      # type: str
    last_value,     # type: str
):
    # type: (...) -> None

    if last_value and new_value == '':
        show_messagebox(
            posid=pos_id,
            message="$CLEAN_NUMPAD",
        )


def fill_customer_properties(
    pod_function,           # type: str
    pos_id,                 # type: str
    pos_ot,                 # type: OrderTaker
    get_doc=False,          # type: Optional[bool]
    get_name=False,         # type: Optional[bool]
    force=False,            # type: Optional[bool]
    customer_info=None,     # type: Optional[dict]
    stop_action=True,       # type: Optional[bool]
):
    # type: (...) -> None

    order_xml = eTree.XML(pos_ot.orderPicture(pos_id))

    customer_doc = None
    customer_name = None
    if customer_info is not None:
        if "document" in customer_info and customer_info["document"] is not None:
            customer_doc = customer_info["document"]
        if "name" in customer_info and customer_info["name"] is not None:
            customer_name = customer_info["name"]

    default_doc = None
    if get_doc:
        is_required, default_doc = customer_doc_is_required(order_xml, pod_function)
        if force or is_required:
            customer_doc = get_customer_doc(
                pos_id=pos_id,
                pos_ot=pos_ot,
                default_doc=default_doc,
                ask_document_pinpad=get_ask_document_pinpad(pos_id),
                stop_action=stop_action,
            )
        _show_clean_numpad(
            pos_id=pos_id,
            new_value=customer_doc,
            last_value=default_doc,
        )

    default_name = None
    if get_name:
        is_required, default_name = customer_name_is_required(order_xml)
        if force or is_required:
            customer_name = get_customer_name(pos_id, pos_ot, default_name)

        _show_clean_numpad(
            pos_id=pos_id,
            new_value=customer_name,
            last_value=default_name,
        )

    if (customer_doc == default_doc) and (customer_name == default_name):
        return

    loyalty_info = get_loyalty_info(customer_doc=customer_doc)
    if loyalty_info is not None:
        loyalty_data = json.loads(loyalty_info)
        customer_info = loyalty_data.get("customerInfo", {})
        customer = customer_info.get("customer", {})
        customer_name = customer.get("name", None) or customer_name

        _show_welcome_message(
            pos_id=pos_id,
            customer_name=customer_name,
        )

    order_properties_dict = update_custom_properties(
        customer_doc=customer_doc,
        customer_name=customer_name,
        loyalty_customer_info=loyalty_info,
    )

    for prop in order_properties_dict.keys():
        if order_properties_dict[prop] in [None, ""]:
            pos_ot.setOrderCustomProperty(prop)
            del order_properties_dict[prop]

    if len(order_properties_dict) > 0:
        pos_ot.setOrderCustomProperties(order_properties_dict)


def _show_welcome_message(
    pos_id,                     # type: str
    customer_name,              # type: str
):
    # type: (...) -> None

    from application.interactor import LoyaltyInteractor
    show_customer_welcome_message = LoyaltyInteractor(
        pos_id=pos_id,
    ).get_show_customer_welcome_message()

    if show_customer_welcome_message and customer_name is not None:
        message = "$LOYALTY_WELCOME_MESSAGE_CUSTOMER|{}".format(customer_name)
        show_messagebox(
            posid=pos_id,
            message=message,
            title="$LOYALTY_WELCOME_MESSAGE_CUSTOMER_HEADER",
        )


def get_fiscal_document(
        pos_id,                     # type: str
        model,                      # type: eTree.XML
        customer_document=None,     # type: Optional[str]
):
    # type: (...) -> None

    order = get_current_order(model)
    customer_doc = customer_document
    if customer_doc is None:
        customer_doc = _check_custom_properties(order, 'CUSTOMER_DOC')[1]

    if len(customer_doc) == 0:
        return

    if use_customer_doc_on_fiscal_doc:
        _save_fiscal_document(model, order, customer_doc)
        return

    buttons = "$CONFIRM|$OTHER|$WITHOUT_DOCUMENT"
    message = "$CONFIRM_CPF_CNPJ|{0}".format(helper.format_cpf_cnpj(customer_doc))
    resp = show_message_options_dialog(pos_id, buttons, "$CONFIRMATION", message)
    if resp == 2 or resp is None:
        return

    elif resp == 1:
        pos_ot = sysactions.get_posot(model)
        customer_doc = get_customer_doc(
            pos_id,
            pos_ot,
            ask_document_pinpad=get_ask_document_pinpad(pos_id)
        )

    _save_fiscal_document(model, order, customer_doc)


def _save_fiscal_document(
        model,          # type: eTree.XML
        order,          # type: eTree.XML
        customer_doc    # type: str
):
    pos_ot = sysactions.get_posot(model)
    fiscal_document = {'CUSTOMER_FISCAL_DOCUMENT': customer_doc}
    pos_ot.setOrderCustomProperties(fiscal_document, orderid=order.get('orderId'))


@action
def doTender(pos_id,
             amount,
             tender_type_id="0",
             offline="false",
             need_confirmation="false",
             is_kiosk='false',
             danfe_type='',
             local_payment_data='',
             check_amount=True,
             *args):
    logger.debug("--- doTender START ---")

    payment_data = None
    order_paid = True
    if local_payment_data != "":
        payment_data = json.loads(local_payment_data)

    is_kiosk = (is_kiosk or 'false').lower() == 'true'

    if is_kiosk:
        show_info_message(pos_id, "AGUARDE", msgtype="info")

    # Inline function to Cancel current Order and Delete Payments
    def cancel_sale_and_payments(error_message=""):
        close_asynch_dialog(pos_id, dlgid) if dlgid else None
        translated_error_message = translate_message(model, error_message.replace("$", ""))
        show_message_dialog(pos_id, key_error, "$CANCELED_SALE|{0}".format(translated_error_message))

        order = get_current_order(model)
        all_tenders = order.findall("TenderHistory/Tender")
        order_tender_id = ""
        tender_id = ""
        if all_tenders:
            order_tender_id = tender_id = "0"

        _clear_order_tender(
            pos_id=pos_id,
            order_tender_id=order_tender_id,
            order_id=order.get('orderId'),
            tender_id=tender_id,
        )
        void_type = 'VOID_STORED_ORDER' if is_stored_order(model=model) else 'VOID_CURRENT_ORDER'
        void_order(
            pos_id=pos_id,
            void_reason="UNKNOWN_FISCAL_ERROR",
            void_type=void_type,
        )

    model = get_model(pos_id)
    check_current_order(pos_id, model=model, need_order=True)
    posot = get_posot(model)

    # Inline function to confirm last payment and set order to PAID
    def tender_and_finalize():
        while True:
            dlgid = None
            try:
                model = get_model(pos_id)
                check_current_order(pos_id, model=model, need_order=True)
                xml_order = get_current_order(model)

                logger.debug("--- doTender before 'Processando pagamento' 2 ---")

                dlgid = show_message_dialog(pos_id, "$PLEASE_WAIT", "$PROCESSING_PAYMENT_DATA", "NOCONFIRM", True)


                if check_amount:
                    last_tender_type_id = tender_type_id
                    last_amount = amount
                    last_tender_id = res["tenderId"]
                else:
                    all_tenders = xml_order.findall("TenderHistory/Tender")
                    if all_tenders:
                        last_tender = all_tenders[-1]
                        last_tender_type_id = last_tender.get("tenderType")
                        last_amount = last_tender.get("tenderAmount")
                        last_tender_id = last_tender.get("tenderId")
                    else:
                        raise MissingTenderParametersException()

                logger.debug("--- doTender before posot.doTender 2 ---")
                paid_response = posot.doTender(
                    posid=int(pos_id),
                    tenderid=last_tender_type_id,
                    amount=last_amount,
                    donotclose=False,
                    ordertenderid=last_tender_id
                )
                logger.debug("--- doTender after posot.doTender 2 ---")

                if tender_type["openDrawer"] != 0 or always_open_drawer:
                    doOpenDrawer(pos_id)

                close_asynch_dialog(pos_id, dlgid)

                check_certificate(pos_id)

                break
            except OrderTakerException as ex:
                show_message_dialog(pos_id, key_error, "$ERROR_CODE_INFO|%d|%s" % (ex.getErrorCode(), ex.getErrorDescr()))

                # Erro Fiscal (Normalmente falha de comunicação com a impressora) - PopUp (Tentar Novamente ou Cancelar)
                try_again_response = show_message_options_dialog(pos_id,
                                                                 "$TRY_AGAIN|$CANCEL",
                                                                 key_error,
                                                                 "$ERROR_FINALIZE_SALE")

                # 0 -> Tentar Novamente / 1 -> Cancelar
                if try_again_response != 0:
                    cancel_sale_and_payments("$USER_REQUESTED_CANCELLATION")
                    break
                else:
                    time.sleep(2)
            finally:
                if dlgid:
                    close_asynch_dialog(pos_id, dlgid)
    # END

    global lock_tender
    with lock_tender:
        global tendering
        if int(pos_id) not in tendering:
            tendering[int(pos_id)] = False

        if tendering[int(pos_id)]:
            logger.info("PosId {0} with tender locked".format(pos_id))
            sys_log_error("PosId {0} with tender locked".format(pos_id))
            return
        else:
            tendering[int(pos_id)] = True

    dlgid = None
    try:
        model = get_model(pos_id)
        check_current_order(pos_id, model=model, need_order=True)
        posot = get_posot(model)
        period = get_business_period(model)
        session = get_operator_session(model)
        pod_type = get_podtype(model)

        xml_order = get_current_order(model)
        if xml_order.get("state") != "TOTALED":
            show_message_dialog(pos_id, message="$PAF_ECF_LISTENER_NOT_FOUND")
            return

        due_amount = xml_order.get("dueAmount")

        total_gross = xml_order.get("totalGross")
        order_due = float(due_amount)
        order_id = xml_order.get("orderId")
        order_type = xml_order.get("type")

        tender_type = get_tender_type(pos_id, model, tender_type_id, amount)
        if tender_type is None:
            return

        if amount == "Exact":
            amount = due_amount

        if check_amount and (amount in (None, "", ".") or float(amount) <= 0):
            show_message_dialog(pos_id, message="$AMOUNT_DUE_NOT_INFORMED")
            return

        if need_confirmation == "true":
            message = "$CONFIRM_PAYMENT_RECEIVED|{0}|{1}".format(tender_type['descr'], format_amount(model, amount))
            ret = show_message_options_dialog(pos_id, "$OK|$CANCEL", message=message)
            if ret is None or ret == 1:
                return

        pos_ot = get_posot(model)
        try:
            validate_required_document(
                pos_id,
                pos_ot,
                total_gross,
                amount,
                due_amount,
                ask_document_pinpad=get_ask_document_pinpad(pos_id)
            )
        except RequiredDocumentException:
            raise StopAction()

        decimal_len = abs(Decimal(xml_order.get("dueAmount")).as_tuple().exponent)
        if decimal_len > 2:
            difference = abs(float(xml_order.get("dueAmount")) - round_half_away_from_zero(float(xml_order.get("dueAmount")), 2))
            amount = str(float(amount) + difference)

            # If the tender will close the sale and we have an index pin pad,
        tender_seq_id = len(xml_order.findall("TenderHistory/Tender")) + 1

        ignore_payment = not check_amount
        previous_tender_id = None
        tender_type_id = tender_type["id"]
        if check_amount and float(due_amount) <= 0:
            logger.error("Tentativa de pagamento para pedido ja pago. Tratando situacao")
            previous_tender = xml_order.findall("TenderHistory/Tender")[-1]
            previous_tender_type = previous_tender.get("tenderType")
            if previous_tender_type != tender_type_id:
                cancel_sale_and_payments("$ERROR_PROCESSING_SALE")
                return

            previous_tender_amount = previous_tender.get("tenderAmount")
            if float(previous_tender_amount) >= float(amount) or int(tender_type_id) in (TenderType.credit, TenderType.debit):
                ignore_payment = True
                res = previous_tender.attrib
                amount = previous_tender_amount
                res['dueAmount'] = xml_order.get("dueAmount")
                res['totalAmount'] = xml_order.get("totalAmount")
            else:
                previous_tender_id = int(previous_tender.get("tenderId"))
                order_due = float(previous_tender_amount) + float(due_amount)

        if not ignore_payment:
            change = 0
            if (amount != "") and (float(amount) > order_due):
                if tender_type["electronicTypeId"] != 0:
                    show_message_dialog(pos_id, message="$CARD_PAYMENT_AMOUNT_HIGHER_THAN_DUE")
                    return

                if order_type == "REFUND":
                    show_message_dialog(pos_id, message="$NO_CHANGE_ALLOWED_IN_REFUND")
                    return

                change += float(amount) - order_due
                change_limit = tender_type['changeLimit']

                if change_limit and change > float(change_limit):
                    change_limit = float(change_limit)
                    if change_limit > 0:
                        msg = "$MAXIMUM_CHANGE_ALLOWED|%s|%s" % (format_amount(model, change_limit), format_amount(model, change))
                        show_message_dialog(pos_id, message=msg)
                    else:
                        show_message_dialog(pos_id, message="$AMOUNT_GREATER_THAN_DUE")
                    return

                show_message_dialog(pos_id, message="$MONEY_CHANGE|R$%.2f" % abs(change))

            xml = None
            if payment_data is None and (offline.lower() == "false" and tender_type["electronicTypeId"] not in (0, 9)):
                # Sends message to SAT/MFE/VFP-e to register payment data. If it returns error, the sale should be CANCELLED
                id_fila = register_eft_before_payment(pos_id, order_id, tender_seq_id, amount, total_gross)
                if id_fila is None:
                    show_message_dialog(pos_id, key_error, "$ERROR_STARTING_PAYMENT_INTEGRATOR_SEFAZ")
                    return
                elif "ERROR" in id_fila:
                    show_message_dialog(pos_id, key_error, "$ERROR_STARTING_PAYMENT_INTEGRATOR_SEFAZ|: %s" % id_fila)
                    return
                _, amount, xml = handle_electronic_payment(pos_id, model, tender_type_id, amount, xml_order, tender_type, tender_seq_id, id_fila)

            logger.debug("--- doTender before 'Processando pagamento' ---")

            tender_details = {
                "posId": pos_id,
                "userId": model.find("Operator").get("id"),
                "tenderType": tender_type_id,
                "eletronicType": tender_type["electronicTypeId"],
                "tenderDescr": tender_type["descr"],
                "tenderAmount": float(amount)
            }
            if tender_type["electronicTypeId"] not in (0, 9):
                data = payment_data if payment_data is not None else xml.attrib
                tender_details.update(json.loads(get_tender_details(data)))

            # Do Tender - DO NOT SET ORDER TO PAID
            logger.debug("--- doTender before posot.doTender ---")
            res = posot.doTender(
                int(pos_id),
                tender_type_id,
                amount,
                True,
                previous_tender_id,
                tenderdetail=json.dumps(tender_details)
            )
            logger.debug("--- doTender after posot.doTender ---")

            logger.debug("--- doTender after 'Processando pagamento' ---")

            if float(res["dueAmount"]) > 0:
                return

        document, state = get_customer_doc_after_paid(
            pos_id,
            pos_ot,
            ask_document_pinpad=get_ask_document_pinpad(pos_id)
        )
        if document and state:
            order_properties_dict = update_custom_properties(customer_doc=document)
            if len(order_properties_dict) > 0:
                order = get_current_order(model)
                pos_ot.setOrderCustomProperties(order_properties_dict, orderid=order.get("orderId"))

        get_fiscal_document(pos_id, model, document)

        logger.debug("--- doTender after 'Processando pagamento 2' ---")
        order_fiscalized = False
        while True:
            logger.debug("--- doTender before PROCESSING_FISCAL_DATA ---")
            try:
                dlgid = show_message_dialog(pos_id, "$PROCESSING", "$PROCESSING_FISCAL_DATA", "NOCONFIRM", True)
                if danfe_type == "":
                    data = pos_id + '\0' + order_id
                else:
                    data = '\0'.join([pos_id, order_id, danfe_type])
                ret = mbcontext.MB_EasySendMessage("FiscalWrapper",
                                                   TK_FISCALWRAPPER_PROCESS_REQUEST,
                                                   format=FM_PARAM,
                                                   data=data,
                                                   timeout=180000000)

                if ret.token == TK_SYS_ACK:
                    try:
                        order_fiscalized = True
                        current_printer = get_device_service(
                            pos_id=pos_id,
                            device_type="printer",
                        )
                        msg = mbcontext.MB_EasySendMessage(current_printer,
                                                           TK_PRN_PRINT,
                                                           format=FM_PARAM,
                                                           data=ret.data,
                                                           timeout=20000000)  # type: MBMessage
                        if msg.token != TK_SYS_ACK and not is_kiosk:
                            close_asynch_dialog(pos_id, dlgid) if dlgid else None
                            show_message_dialog(pos_id, "$INFORMATION", "$ERROR_PRINT_NFE|%s" % msg.data)
                    except Exception as ex:
                        if not is_kiosk:
                            close_asynch_dialog(pos_id, dlgid) if dlgid else None
                            show_message_dialog(pos_id, "$INFORMATION", "$ERROR_PRINT_NFE|%s" % repr(ex))

                    if not is_kiosk:
                        close_asynch_dialog(pos_id, dlgid) if dlgid else None

                    # Confirm Tender - Set Order to PAID
                    logger.debug("--- doTender before tender_and_finalize ---")
                    tender_and_finalize()
                    logger.debug("--- doTender after tender_and_finalize ---")

                    break
                else:
                    if ret.data in ("$EXPIRED_STORE_CERTIFICATE", "$CERTIFICATE_EXPIRATION_READING_FAILURE"):
                        cancel_sale_and_payments("$EXPIRED_STORE_CERTIFICATE")
                        break

                    if "$ORDER_NOT_FINALIZED_DUE_TO_MAX_SAT_PAYMENT_GROUPS_REACHED" in ret.data:
                        message = ret.data.split('\0')
                        show_message_dialog(pos_id, "$INFORMATION", message[1])
                        break

                parts = ret.data.split('\0')

                if len(parts) < 2:
                    cancel_sale_and_payments(ret.data)
                    order_paid = False
                    break
                elif len(parts) < 5:
                    cancel_sale_and_payments(parts[1])
                    order_paid = False
                    break

                fiscal_ok, message, orderid, data_fiscal, hora_fiscal = ret.data.split('\0')

                # Fiscal OK - Erro na DANFE - Venda concluida sem volta - Apenas informa o usuário
                if fiscal_ok == "True":
                    if not is_kiosk:
                        show_message_dialog(pos_id, "$INFORMATION", "$ERROR_PRINT_NFE|%s" % msg.data)
                        close_asynch_dialog(pos_id, dlgid) if dlgid else None
                        if float(res["totalAmount"]) > 0:
                            doOpenDrawer(pos_id)

                    # Confirm Tender - Set Order to PAID
                    tender_and_finalize()
                    break

                # Erro Fiscal (Falha ao enviar para SEFAZ) - PopUp (Tentar Novamente ou Cancelar)
                try_again_response = 1
                if not is_kiosk:
                    try_again_response = show_message_options_dialog(pos_id, "$TRY_AGAIN|$CANCEL", key_error, "$ERROR_SEND_NFE|%s" % message)

                # 0 -> Tentar Novamente / 1 -> Cancelar
                if try_again_response != 0:
                    cancel_sale_and_payments("$USER_REQUESTED_CANCELLATION")
                    order_paid = False
                    break
                elif is_kiosk:
                    show_message_dialog(pos_id, message="$ERROR_PROCESSING_TAX_DATA")
                    time.sleep(1)
                    try_again_response += 1
            except Exception:
                logger.exception("Erro processando CUPOM Fiscal")
                cancel_sale_and_payments("$ERROR_PROCESSING_COUPON")
                order_paid = False
                break
            finally:
                close_asynch_dialog(pos_id, dlgid) if dlgid else None
                time.sleep(0.2)
                logger.debug("--- doTender after PROCESSING_FISCAL_DATA ---")

        if is_sangria_enable() and pod_type != "TT":
            do_set_drawer_status(pos_id, get_drawer_amount(pos_id, period, session), xml_order.get("state"), get_custom(model, "sangria_level_1_alert"))

        if order_paid and order_fiscalized:
            printer_receipt.print_tef_receipts(
                pos_id=pos_id,
                fiscalization_type=danfe_type,
                order_id=order_id,
            )

            if _is_delivery_sale_type(xml_order):
                create_delivery_manual_order_json(pos_ot, xml_order)
                print_original_delivery(pos_id, order_id)

    except (DeliveryCouponPrintError, ElectronicPaymentException) as ex:
        sys_log_exception(ex.message)
    except OrderTakerException as ex:
        show_message_dialog(pos_id, key_error, "$ERROR_CODE_INFO|%d|%s" % (ex.getErrorCode(), ex.getErrorDescr()))
    finally:
        with lock_tender:
            tendering[int(pos_id)] = False
            close_asynch_dialog(pos_id, dlgid) if dlgid else None

    from application.interactor import LoyaltyInteractor
    LoyaltyInteractor(pos_id).show_loyalty_customer_final_message()

    logger.debug("--- doTender END ---")


def _clear_order_tender(
    pos_id,             # type: str
    order_tender_id,    # type: str
    order_id,           # type: str
    tender_id="",       # type: Optional[str]
):
    # type: (...) -> bool

    order_interactor = OrderInteractor(pos_id=pos_id)

    try:
        order_interactor.clear_order_tender(
            order_tender_id=order_tender_id,
            order_id=order_id,
            tender_id=tender_id,
        )
    except Exception as _:
        logger.exception("Error to clear order tender")
        return False

    return True


@action
def do_clear_order_tenders(pos_id):
    # type: (str) -> bool

    model = get_model(pos_id)
    order_picture_xml = get_current_order(model)
    tenders = filter(
        lambda t: float(t.get("tenderAmount")) > 0,
        order_picture_xml.findall(".//TenderHistory/Tender")
    )
    total_tender = float(order_picture_xml.get("totalTender"))

    if len(tenders) == 0 or total_tender == 0:
        show_message_dialog(
            pos_id,
            "$INFORMATION",
            "$WITHOUT_TENDER_TO_CLEAR"
        )
        return False

    options_dict, _ = get_tenders_options_dict(
        pos_id,
        model,
        tenders,
        total_tender,
        get_card_brands(),
        group_digital_payments=True
    )

    listbox_options = options_dict_to_str_list(
        model,
        options_dict
    )

    option = sysactions.show_listbox(
        pos_id,
        listbox_options,
        message="$SELECT_TENDER"
    )

    order_id = order_picture_xml.get('orderId')

    result = False
    if option is None:
        return False
    elif option == 0:
        for tender in tenders:
            result = _clear_order_tender(
                pos_id=pos_id,
                order_tender_id=tender.get("tenderId"),
                order_id=order_id,
            )

        if has_electronic_payment(order_picture_xml):
            finish_eft_transactions(
                pos_id,
                order_picture_xml,
                "0"
            )
    elif option == 1 and "tenders" in options_dict[1]:
        for tender_id in options_dict[1]["tenders"]:
            result = _clear_order_tender(
                pos_id=pos_id,
                order_tender_id=tender_id,
                order_id=order_id,
            )
        if has_electronic_payment(order_picture_xml):
            finish_eft_transactions(
                pos_id,
                order_picture_xml,
                "0"
            )
    else:
        tender_index = options_dict[option]["tender_index"]
        tender = tenders[tender_index]
        result = _clear_order_tender(
            pos_id=pos_id,
            order_tender_id=tender.get("tenderId"),
            order_id=order_id,
        )

    return result


def _is_delivery_sale_type(xml_order):
    return xml_order.get("saleType", "") == "3"


def _get_tender_type(pos_id, tender_type_id):
    all_tender_types = get_tender_types()
    tender_types = [tender for tender in all_tender_types if (tender["parentId"] is not None and tender["parentId"] == int(tender_type_id))]
    if len(tender_types) != 0:
        if len(tender_types) == 1:
            return tender_types[0]
        else:
            options = [tender["descr"] for tender in tender_types]
            selected_option = show_filtered_list_box_dialog(pos_id, options, "$SELECT_PAYMENT_TYPE", mask="NOFILTER")
            if selected_option is None:
                return None

            tender_type_id = tender_types[selected_option]["id"]
    else:
        return [tender for tender in all_tender_types if tender["id"] == int(tender_type_id)][0]

    return [tender for tender in tender_types if tender["id"] == tender_type_id][0]


@action
@block_action_if_pos_is_blocked
def doRecallNext(pos_id, screen_number="", order_id="", totalize=False, originator_pos_id=None, order_business_period=None, *args):
    logger.debug("Recall pedido - Order %s - PosId %s" % (order_id, pos_id))

    global lock_recall
    global recalling
    with lock_recall:
        if order_id in recalling:
            show_messagebox(pos_id, message="$ORDER_RETRIEVED_ANOTHER_CASH|{}".format(order_id), icon="critical")
            sys_log_error("PosId {} - Order {} - Already in Recall".format(pos_id, order_id))
            logger.debug("Recall sendo recuperado em outro caixa - Order %s - PosId %s" % (order_id, pos_id))
            return
        else:
            recalling.append(order_id)

    try:
        model = get_model(pos_id)

        # TODO: Read this configuration from loader
        closing_day = False
        if not closing_day and order_business_period is not None:
            current_business_day = datetime.strptime(get_business_period(model), "%Y%m%d")
            order_business_period = datetime.strptime(order_business_period, "%Y%m%d")

            if current_business_day > order_business_period:
                message = "$ORDER_SAVED_ON_PREVIOUS_BUSINESS_DAY."
                if show_confirmation(pos_id, message=message, buttons="$DELETE|$CANCEL"):
                    try:
                        do_recall_order(pos_id, order_id)
                        void_order(
                            pos_id=pos_id,
                            void_reason="RECALL_ORDER_TO_OLD",
                            void_type="VOID_STORED_ORDER",
                        )

                        return True
                    except BaseException as _:
                        sys_log_exception("Erro ao apagar pedido")
                        logger.debug("Erro void order recall pedido - Order %s - PosId %s" % (order_id, pos_id))
                        show_messagebox(pos_id, message="$ERROR_VOIDING_STORED_ORDER", icon="error")
                return

            if order_business_period > current_business_day:
                show_messagebox(pos_id, "$ORDER_SAVED_ON_PREVIOUS_BUSINESS_DAY_OPEN_NEW_DAY")
                logger.debug("Recall pedido foi salvo em um dia de negócio posterior ao atual - Order %s - PosId %s" % (order_id, pos_id))
                return

            check_operator_logged(pos_id, model=model, can_be_blocked=False)
            check_current_order(pos_id, model=model, need_order=False)

        period = get_business_period(model)
        session = get_operator_session(model)

        if is_sangria_enable() and do_set_drawer_status(pos_id, get_drawer_amount(pos_id, period, session)):
            logger.debug("Recall pedido sangria - Order %s - PosId %s" % (order_id, pos_id))
            return

        try:
            pod_function = get_posfunction(model) if get_podtype(model) in ("DT", "FC") else get_podtype(model)
            pos_ot = get_posot(model)

            do_recall_order(pos_id, order_id, originator_pos_id)
            logger.debug("Pedido Totalizado - Order %s" % order_id)
            time.sleep(0.1)  # Kernel delayed time for model update
            model = get_model(pos_id)

            try:
                if _is_delivery_order(model):
                    print_original_delivery(pos_id, order_id)
                    clean_discounts(pos_id)
            except Exception as _:
                logger.error("Error printing partner delivery coupon for order {}".format(str(order_id)))

            if totalize == 'true':
                get_name = "onRecall" in customer_info_config[int(pos_id)]['name']
                get_document = "onRecall" in customer_info_config[int(pos_id)]['document']
                if get_document or get_name:
                    fill_customer_properties(
                        pod_function,
                        pos_id,
                        pos_ot,
                        get_doc=get_document,
                        get_name=get_name
                    )

                doTotal(pos_id, screen_number, None, True, *args)

            logger.debug("Recall finalizado - Order %s - PosId %s" % (order_id, pos_id))

            return True

        except StopAction as _:
            raise

        except OrderTakerException as e:
            show_messagebox(pos_id, message="$ORDER_RETRIEVED_ANOTHER_CASH|{}".format(order_id))
            logger.debug("Recall pedido já recuperado em outro caixa - Order %s - PosId %s" % (order_id, pos_id))

            sys_log_info("$ERROR_CODE_INFO|%d|%s - POS %s" % (e.getErrorCode(), e.getErrorDescr(), pos_id))
        except Exception as ex:
            sys_log_exception("Erro recuperando pedido - %s" % str(ex))
            logger.debug("Recall error - Order %s - PosId %s" % (order_id, pos_id))

    finally:
        with lock_recall:
            if order_id in recalling:
                recalling.remove(order_id)


def _is_delivery_order(model):
    order = model.find("CurrentOrder/Order")
    if not order:
        return False

    return order.find("CustomOrderProperties/OrderProperty[@key='REMOTE_ORDER_ID']") is not None


@action
def activateUser(posid, *args):
    current_op = get_current_operator(get_model(posid))
    current_id = current_op.get("id") if current_op else None
    check_business_day(posid, can_be_blocked=False)

    msg = send_message("POS%d" % int(posid), TK_POS_LISTUSERS, FM_PARAM, posid)
    if msg.token == TK_SYS_NAK:
        show_info_message(posid, "$OPERATION_FAILED", msgtype="error")
        return

    xml = eTree.XML(msg.data)
    opened_users = [tag for tag in xml.findall("User") if (not tag.get("closeTime")) and (tag.get("id") != current_id)]
    if not opened_users:
        return

    opened_users.sort(key=lambda tag: tag.get("id"))
    options = ["%s - %s" % (tag.get("id"), (tag.get("name")).encode("utf-8")) for tag in opened_users]
    index = show_listbox(posid, options, message="$SELECT_OPERATOR_TO_ACTIVATE")
    if index is None:
        return

    userid = opened_users[index].get("id")
    username = (opened_users[index].get("name")).encode("utf-8")
    passwd = show_keyboard(posid, message="$ENTER_PASSWORD|%s" % userid, title="$USER_AUTHENTICATION", is_password=True, numpad=True)
    if passwd is None:
        return
    if not show_confirmation(posid, message="$CONFIRM_ACTIVE_OPERATOR".format(userid, username)):
        return

    try:
        userinfo = authenticate_user(userid, passwd)
    except AuthenticationFailed as ex:
        show_info_message(posid, "$%s" % ex.message, msgtype="error")
        return

    longusername = userinfo["LongName"]
    msg = send_message("POS%d" % int(posid), TK_POS_USERLOGIN, FM_PARAM, "%s\0%s\0%s" % (posid, userid, longusername))
    if msg.token == TK_SYS_NAK:
        show_info_message(posid, "$OPERATION_FAILED", msgtype="error")
        return
    show_info_message(posid, "$OPERATION_SUCCEEDED", msgtype="success")


@action
def doOpenDrawer(posid, check_oper="true", *args):
    def thread_open_drawer(drawer):
        try:
            time.sleep(1)
            send_message(drawer.get("name"), TK_CDRAWER_OPEN)
            show_info_message(posid, "$OPERATION_SUCCEEDED", msgtype="success")
        except MBException:
            show_info_message(posid, "$ERROR_OPEN_DRAWER", msgtype="warning")

    model = get_model(posid)
    if check_oper == "true":
        check_operator_logged(posid, model=model, can_be_blocked=False)

    drawers = model.findall("CashDrawer")
    for drawer in drawers:
        from threading import Thread
        open_drawer_thread = Thread(target=thread_open_drawer, args=(drawer,))
        open_drawer_thread.daemon = True
        open_drawer_thread.start()

@action
def notImplemented(posid, *args):
    show_messagebox(posid, "$FEATURE_NOT_IMPLEMENTED_YET")


@action
def dayOpenReport(posid, store_wide="false", *args):
    model = get_model(posid)
    period = get_business_period(model)
    if period is None:
        return
    if not is_valid_date(period):
        show_messagebox(posid, message="$INVALID_DATE", icon="error")
        return
    menu_manager_user = get_menu_user_logged.get_menu_manager_authenticated_user(int(posid))
    print_report(posid, model, True, "dayOpen_report", posid, period, store_wide, menu_manager_user)


@action
def request_quantity(
    pos_id,                     # type: str
    sale_line='',               # type: str
    min_quantity=-1,            # type: Optional[int]
    min_quantity_message=None,  # type: Optional[str]
    max_quantity=99,            # type: Optional[int]
    max_quantity_message=None,  # type: Optional[str]
):
    # type: (...) -> Optional[str]

    try:
        return request_quantity_common(
            pos_id=pos_id,
            sale_line=sale_line,
            min_quantity=min_quantity,
            min_quantity_message=min_quantity_message,
            max_quantity=max_quantity,
            max_quantity_message=max_quantity_message
        )
    except Exception as _:
        raise StopAction()


def request_quantity_common(
    pos_id,                     # type: str
    sale_line='',               # type: str
    min_quantity=-1,            # type: Optional[int]
    min_quantity_message=None,  # type: Optional[str]
    max_quantity=99,            # type: Optional[int]
    max_quantity_message=None,  # type: Optional[str]
):
    # type: (...) -> Optional[str]

    while True:
        amount = show_keyboard(pos_id, "$ENTER_THE_AMOUNT", title="", mask="INTEGER", numpad=True)
        if amount and '.' in amount:
            continue

        if amount not in (None, "", "NaN"):
            if 0 < int(max_quantity) < int(amount):
                if max_quantity_message is None:
                    max_quantity_message = "$QUANTITY_OF_ITEMS_EXCEEDED"

                show_messagebox(pos_id, message=max_quantity_message, icon="info")
                raise MaxQuantityException()

            if int(amount) < int(min_quantity):
                if min_quantity_message is None:
                    min_quantity_message = "$MIN_QUANTITY_OF_ITEMS_NOT_REACHED"

                show_messagebox(pos_id, message=min_quantity_message, icon="info")
                raise MinQuantityException()

            model = get_model(pos_id)
            if has_current_order(model):
                doChangeQuantity(pos_id, sale_line, amount, 'true')

        return amount or ""


@action
def request_rupture_quantity(
    pos_id,     # type: str
):
    # type: (...) -> Optional[str]

    try:
        return request_quantity_common(
            pos_id=pos_id,
            min_quantity=1,
            min_quantity_message='$DAILY_DISHES_QUANTITY_ZERO_INSERTED',
            max_quantity=-1
        )
    except MinQuantityException as _:
        return "0"
    except Exception as _:
        raise StopAction()


@action
def storewideRestart(pos_id, *args):
    try:
        selected_pos_id = show_keyboard(pos_id, "$TYPE_REPORT_POS", title="", defvalue="0", mask="INTEGER", numpad=True)

        if selected_pos_id in (None, "", "."):
            return
        if selected_pos_id == "0":
            if not show_confirmation(pos_id, message="$CONFIRM_STOREWIDE_RESTART"):
                return

            show_messagebox(pos_id, "$PLEASE_WAIT_ALL_NODES_RESTART")
            mbcontext.MB_SendMessage(mbcontext.hv_service, TK_HV_GLOBALRESTART)

        if int(selected_pos_id) not in poslist:
            show_info_message(pos_id, "$POS_NUMBER_INVALID", msgtype="critical")
            return

        confirm = show_confirmation(
            pos_id,
            message="$CONFIRM_CLOSE_POS|{}".format(selected_pos_id),
            title="$WARNINGS",
            buttons="$OK|$CANCEL"
        )
        if not confirm:
            return

        try:
            msg = mbcontext.MB_EasySendMessage(
                "Maintenance%02d" % int(selected_pos_id),
                TK_MAINTENANCE_RESTART_REQUEST,
                format=FM_PARAM,
                data=pos_id,
                timeout=10000 * 1000
            )
            if msg.token == TK_SYS_NAK:
                show_messagebox(pos_id, "%s" % msg.data)
        except Exception as ex:
            if "NOTFOUND" in ex.message:
                show_messagebox(pos_id, "$MAINTENANCE_COMPONENT_NOT_FOUND")
            else:
                show_messagebox(pos_id, "$ERROR_RESTARTING_POS|{}".format(selected_pos_id))
    except Exception as _:
        raise StopAction


@action
def doKill(pos_id, show_message=True):
    if show_message:
        confirm = show_confirmation(pos_id,
                                    message="$WANT_TO_CLOSE_THE_SYSTEM",
                                    title="$ALERTS",
                                    buttons="$OK|$CANCEL")
        if not confirm:
            return

    try:
        msg = mbcontext.MB_EasySendMessage("Maintenance%02d" % int(pos_id), TK_MAINTENANCE_TERMINATE_REQUEST, format=FM_PARAM, data=pos_id, timeout=10000 * 1000)
        if msg.token == TK_SYS_NAK:
            show_messagebox(pos_id, "%s" % msg.data)
    except Exception as ex:
        if "NOTFOUND" in ex.message:
            show_messagebox(pos_id, "$MAINTENANCE_COMPONENT_NOT_FOUND".format(pos_id))
        else:
            show_messagebox(pos_id, "$$ERROR_RESTARTING_POS".format(ex.message))


@action
def doOrderPicture(posid, orderid, *args):
    try:
        picture = OrderTaker(posid, mbcontext).orderPicture(orderid=orderid)
        order = eTree.tostring(eTree.XML(picture).find("Order"), encoding="UTF-8")
        return (FM_XML, order)
    except:
        sys_log_exception("Error taking order picture for order %s" % orderid)
        return ""


def get_timestamp():
    """Return the last update timestamp if available."""
    timestamp = None
    try:
        with open("../genesis/.genpkg_timestamp", "r") as f:
            timestamp = f.read()
    except:
        pass

    return timestamp


def get_install_date():
    """Retrieve install date: the creation date of the current working dir <install_dir>/bin."""
    t = time.strptime(time.ctime(os.path.getctime(os.getcwd())))
    return "%04d-%02d-%02d" % (t.tm_year, t.tm_mon, t.tm_mday)


def day_is_closed_in_all_pos(source_pos_id):
    opened_pos = []

    for pos_id in poslist:
        model = get_model(pos_id)
        if sysactions.get_model(pos_id).find("WorkingMode").get("usrCtrlType") == "QSR":
            if sysactions.get_cfg(pos_id).find_values("saleType") == ["DL"]:
                from table_actions import get_delivery_stored_orders
                delivery_stored_orders = json.loads(get_delivery_stored_orders())
                if delivery_stored_orders:
                    for order in delivery_stored_orders:
                        if order['businessPeriod'] == get_business_period(model):
                            show_messagebox(source_pos_id, "$HAS_DELIVERY_ORDERS_IN_PROGRESS")
                            return False
            else:
                if has_current_order(model):
                    show_messagebox(source_pos_id, "$THERE_IS_AN_OPENED_ORDER_ON_POS|{}".format(pos_id))
                    return False

        elif is_day_opened(model):
            opened_pos.append(pos_id)

    if len(opened_pos) > 0:
        show_messagebox(source_pos_id, "$NEED_TO_CLOSE_DAY_IN_ALL_POS|{}".format(opened_pos))
        return False

    return True


@action
def doCheckUpdates(posid, show_msg="yes", *args):
    if not day_is_closed_in_all_pos(posid):
        return

    try:
        # List available update packages on GenesisServer
        msg = send_message("GenesisServer", TK_GENESIS_LISTPKGS, FM_STRING, "updates")
        if msg.token == TK_SYS_NAK:
            show_info_message(posid, "$OPERATION_FAILED", msgtype="error")
            return
    except MBException as e:
        if e.errorcode != SE_NOTFOUND:
            sys_log_exception("Error checking for updates!")
            show_info_message(posid, "$OPERATION_FAILED", msgtype="error")
        return

    # Parse the XML response
    xml = eTree.XML(msg.data)
    qty = len(xml.findall("Package"))

    if show_msg == "yes":
        if qty == 0:
            show_messagebox(posid, message="$NO_UPDATES_FOUND")
            return
        if qty == 1 and not show_confirmation(posid, message="$CONFIRM_SINGLE_UPDATE"):
                return
        if qty > 1 and not show_confirmation(posid, message="$CONFIRM_MULTIPLE_UPDATES|%d" % qty):
                return

    # Begin the update process
    message = "$APPLYING_SINGLE_UPDATE" if qty == 1 else "$APPLYING_MULTIPLE_UPDATES"
    dlgid = show_messagebox(posid, message=message, buttons="", asynch=True, timeout=720000)
    pkgs = [str(pkg.get("path")) for pkg in xml.findall("Package")]

    try:
        # Request the update (12 minutes timeout)
        msg = send_message("GenesisServer", TK_GENESIS_APPLYUPDATE, FM_PARAM, '\0'.join(pkgs), timeout=720000000)
    finally:
        close_asynch_dialog(posid, dlgid)

    if msg.token == TK_SYS_NAK:
        show_messagebox(posid, message="$OPERATION_FAILED", icon="error")
        return

    # register installed version
    timestamp = get_timestamp()
    install_date = get_install_date()

    try:
        updmsg = mbcontext.MB_EasySendMessage("GAECHANNEL", TK_SYS_CHECKFORUPDATES, format=FM_STRING, data=json.dumps({"timestamp": timestamp, "installDate": install_date}), timeout=5000000)
        sys_log_debug("[AutoUpd] TK_SYS_CHECKFORUPDATES: %s" % updmsg.data)
    except:
        sys_log_exception("Failed to acquire updates")

    show_info_message(posid, "$OPERATION_SUCCEEDED", msgtype="success")

    if show_msg == "yes":
        if not show_confirmation(posid, icon="success", message="$SYSTEM_UPDATE_SUCCESS", timeout=300000):
            return

    # Store Wide Restart
    show_info_message(posid, "$PLEASE_WAIT_ALL_NODES_RESTART", msgtype="info")
    mbcontext.MB_SendMessage(mbcontext.hv_service, TK_HV_GLOBALRESTART)
    return True


@action
def doSetCustomerName(pos_id):
    model = get_model(pos_id)
    pod_function = get_posfunction(model)
    pos_ot = get_posot(model)
    fill_customer_properties(
        pod_function,
        pos_id,
        pos_ot,
        get_name=True,
        force=True
    )


@action
def closedOrders(pos_id, request_authorization="true", allpos="false", requestdate="false", danfe_type="", *args):
    model = get_model(pos_id)
    pos_ot = get_posot(model)

    try:
        if request_authorization == "true" and not get_authorization(pos_id, UserLevels.MANAGER.value, model)[0]:
            return False

        if requestdate.lower() == "true":
            date_to_find = show_keyboard(pos_id, "$TYPE_DATE", title="", mask="DATE", numpad=True)
            if date_to_find is None:
                return False
            if not is_valid_date(date_to_find):
                show_messagebox(pos_id, message="$INVALID_DATE", icon="error")
                return False
            if is_valid_period(date_to_find, pos_id) is False:
                return False
            tmp_date = time.strptime(date_to_find, "%Y%m%d")
        else:
            tmp_date = time.localtime()

        originator_id = ""
        if allpos.lower() != "true":
            originator_id = "POS%04d" % int(pos_id)

        since_date = time.strftime("%Y-%m-%dT00:00:00", tmp_date)
        until_date = time.strftime("%Y-%m-%dT23:59:59", tmp_date)

        orders = pos_ot.listOrders(
            state="PAID",
            since=since_date,
            until=until_date,
            showTenders=True,
            instnbr=int(pos_id) if allpos.lower() != "true" else "",
            originatorid=originator_id)

        if not orders:
            show_messagebox(pos_id, message="$NO_ORDERS_FOUND", icon="warning")
            return False

        def _get_order_timestamp(x):
            if 'custom_properties' in x and 'FISCALIZATION_DATE' in x['custom_properties']:
                return x['custom_properties']['FISCALIZATION_DATE']
            return x['createdAt']

        orders = sorted(orders, key=lambda x: map(int, re.findall('\d+', _get_order_timestamp(x))), reverse=True)

        base_url = "/mwapp/services/PosController/POS{}/?token=TK_POS_EXECUTEACTION&format=2&isBase64=true&payload="
        base_url = base_url.format(int(pos_id))

        preview_data = []
        for order in orders:
            order_id = order["orderId"]
            order_pos_id = int(order["originatorId"][3:])
            tenders = order["tender_history"]
            order_gross = sum([float(x.get("tenderAmount")) - float(x.get("changeAmount") or '0.0') for x in tenders])
            order_type = order["podType"]

            properties = order["custom_properties"]
            order_date = properties['FISCALIZATION_DATE'] if "FISCALIZATION_DATE" in properties else None
            if order_date is None:
                order_date = order.get("createdAt")

            order_date = convert_from_utf_to_localtime(datetime.strptime(order_date, "%Y-%m-%dT%H:%M:%S"))
            order_time = order_date.strftime('%H:%M')

            partner = ""
            is_table = False
            if 'custom_properties' in order:
                partner = properties["PARTNER"] if "PARTNER" in properties else ""

            description = "{}{:02} #{:04} {} R${:.2f}"
            description = description.format(order_type, order_pos_id, int(order_id) % 10000, order_time, order_gross)
            if partner != "":
                short_reference = properties["SHORT_REFERENCE"] if "SHORT_REFERENCE" in properties else order_id
                if partner.upper() == "APP":
                    description += " APP-{}".format(short_reference)
                else:
                    description += " DLY-{}".format(short_reference)
            else:
                table_id = properties["TABLE_ID"] if "TABLE_ID" in properties else ""
                table_name = properties["TABLE_NAME"] if "TABLE_NAME" in properties else ""
                table_name = table_name[:10] + "..." if len(table_name) > 10 else table_name
                is_table = table_id != ""
                if is_table:
                    if "TAB-" in table_id:
                        description += " TAB-{}".format(table_name)
                    else:
                        description += " MESA-{}".format(table_name)

            payload = base64.b64encode("\0".join(("doOrderPicture", pos_id, order_id)))
            url = base_url + payload
            preview_data.append((order_id, "{}|{}".format(description, is_table), url))

        if not preview_data:
            show_messagebox(pos_id, message="$NO_ORDERS_FOUND", icon="warning")
            return

        while True:
            selected = show_order_preview(pos_id,
                                          preview_data,
                                          "$REPRINT_ORDER_SELECT",
                                          buttons="CANCEL|PICKLIST|COUPON",
                                          onthefly=True)
            if (selected is None) or (selected[0] == "0"):
                return

            order_type = filter(lambda x: x["orderId"] == selected[1], orders)[0]["podType"]

            can_reprint_pod_types = get_storewide_config("Store.CanReprintPodTypes", defval="FC;DS;KK;DT;OT;TT;DL;TS").split(";")
            reprint_orders_i18n = "$REPRINT_ORDERS"
            if order_type not in can_reprint_pod_types:
                show_messagebox(pos_id, "$REPRINT_FORBIDDEN", title=reprint_orders_i18n)
                continue

            if selected[0] == "1":
                try:
                    _select_and_reprint_order(
                        pos_ot=pos_ot,
                        pos_id=pos_id,
                        order_id=selected[1],
                    )
                except Exception as ex:
                    show_messagebox(pos_id, "$ERROR_REPRINT_PICKLIST|{}".format(ex.message), title=reprint_orders_i18n, icon="error", buttons="$OK")
                continue

            orderid = selected[1]
            try:
                if danfe_type == "":
                    data = pos_id + '\0' + orderid
                else:
                    data = '\0'.join([pos_id, orderid, danfe_type])
                msg = mbcontext.MB_EasySendMessage("FiscalWrapper", TK_FISCALWRAPPER_REPRINT, format=FM_PARAM, data=data)
                if not msg.token == TK_SYS_ACK:
                    if msg.data and "False" not in msg.data:
                        error_msg = "$ERROR_REPRINT_NFE|{}".format(msg.data)
                    else:
                        error_msg = "$OPERATION_FAILURE"
                    show_messagebox(pos_id, message=error_msg, icon="error")
                else:
                    try:
                        current_printer = get_device_service(
                            pos_id=pos_id,
                            device_type="printer",
                        )
                        msg = mbcontext.MB_EasySendMessage(current_printer, TK_PRN_PRINT, format=FM_PARAM, data=msg.data, timeout=10000000)  # type: MBMessage
                        if msg.token != TK_SYS_ACK:
                            show_messagebox(pos_id, "$ERROR_REPRINT_NFE|%s" % msg.data, title=reprint_orders_i18n, icon="error", buttons="$OK")
                        else:
                            show_messagebox(pos_id, message="$OPERATION_SUCCEEDED", icon="success")
                    except Exception as ex:
                        show_messagebox(pos_id, "$ERROR_REPRINT_NFE|%s" % repr(ex), title=reprint_orders_i18n, icon="error", buttons="$OK")
            except Exception as ex:
                sys_log_exception("Could not print Receipt for voided order - Error: %s" % str(ex))

    except OrderTakerException as ex:
        show_messagebox(pos_id, message="$ERROR_CODE_INFO|%d|%s" % (ex.getErrorCode(), ex.getErrorDescr()), icon="error")


def _select_and_reprint_order(
    pos_ot,         # type: OrderTaker
    pos_id,         # type: str
    order_id,       # type: str

):
    # type: (...) -> None

    from msgbusboundary import MsgBusTableService as tableService

    order_pict = eTree.XML(
        text=pos_ot.orderPicture(
            posid="",
            orderid=order_id,
        )
    )
    orders_list = [order_id]
    table_id = order_pict.find(".//OrderProperty/[@key='TABLE_ID']")
    if table_id is not None and table_id.get("value") != "":
        orders = eTree.XML(
            text=tableService().get_table_info(
                pos_id=pos_id,
                table_id=table_id.get("value"),
            )
        )

        orders_list = orders.get("orders").split(",")

    pick_list_orders = get_pick_list_orders(
        pos_id=pos_id,
        orders_list=orders_list,
    )

    if pick_list_orders is None:
        return

    options = "|".join([item for item in pick_list_orders.keys()])

    selected_option = show_filtered_list_box_dialog(
       pos_id=pos_id,
       options=options,
       title="$SELECT_REPRINT_ORDER",
       message="",
       mask="NOFILTER",
    )

    if selected_option is None:
        return

    selected_option = list(pick_list_orders)[selected_option]
    selected_option = pick_list_orders[selected_option]

    printer = get_device_service(
        pos_id=pos_id,
        device_type="printer",
    )

    print_pick_list_order(
        pos_id=pos_id,
        order_id=order_id,
        printer_name=printer,
        selected_option=selected_option,
    )


@action
def do_preview_order(pos_id, order_id):
    model = get_model(pos_id)
    preview_data = []
    pos_ot = get_posot(model)
    order_pict = pos_ot.orderPicture(orderid=order_id)
    order = eTree.XML(order_pict).find("Order")
    order_pos_id = int(order.attrib["originatorId"][3:])
    order_gross = float(order.attrib["totalGross"])
    order_created_date = datetime.strptime(str(order.attrib["createdAt"]), "%Y-%m-%dT%H:%M:%S.%f")
    order_timestamp = order_created_date.strftime('%H:%M')
    order_type = str(order.attrib["podType"])

    description = "{}{:02} #{:04} {} R${:.2f}".format(order_type, order_pos_id, int(order_id) % 10000, order_timestamp, order_gross)

    buttons = "CANCEL"
    if order.find(".//OrderProperty[@key='PARTNER']") is not None:
        buttons = "CANCEL|PRINT_DELIVERY"
        partner = order.find(".//OrderProperty[@key='PARTNER']").get("value").upper()
        if partner != "":
            description += " - DLV [{}]".format(partner)

    base_url = "/mwapp/services/PosController/POS%d/?token=TK_POS_EXECUTEACTION&format=2&isBase64=true&payload=" % int(pos_id)
    payload = base64.b64encode("\0".join(("doOrderPicture", pos_id, order_id)))
    url = base_url + payload
    preview_data.append((order_id, description, url))

    ret = show_order_preview(pos_id, preview_data, "$ORDER".format(order_id), buttons=buttons, onthefly=True)
    if ret and len(ret) > 1:
        print_original_delivery(pos_id, ret[1])


@action
def doReprintTef(pos_id, *args):
    """
    Reprints last TEF coupon paid in this POS
    """
    wait_dlg_id = show_messagebox(pos_id, "$PLEASE_WAIT", "$REPRINTING_RECEIPT", buttons="", asynch=True)
    try:
        model = get_model(pos_id)
        check_operator_logged(pos_id, model=model, can_be_blocked=False)
        check_current_order(pos_id, model, need_order=False)
        drv = persistence.Driver()
        conn = None
        try:
            conn = drv.open(mbcontext, service_name="FiscalPersistence")
            query = """SELECT p1.OrderId, p1.TenderSeqId, p1.ReceiptMerchant
                FROM fiscal.PaymentData p1
                INNER JOIN (
                    SELECT MAX(OrderId) OrderId
                    FROM fiscal.PaymentData
                ) p2 ON p1.OrderId = p2.OrderId
                ORDER BY p1.TenderSeqId desc"""
            cursor = conn.select(query)
            printed = False
            for line in cursor:
                if line.get_entry("ReceiptMerchant"):
                    print_text(pos_id, model, line.get_entry("ReceiptMerchant"))
                    printed = True
                    break
            if printed:
                show_info_message(pos_id, "$OPERATION_SUCCEEDED", msgtype="success")
            else:
                show_messagebox(pos_id, message="$THERE_IS_NO_PROOF_TO_BE_PRINTED", icon="warning")
        finally:
            if conn:
                conn.close()
    except Exception as ex:
        show_messagebox(pos_id, message="$UNABLE_PRINT_COUPON".format(str(ex)), icon="error")
        sys_log_exception("Could not print Receipt for order")
    finally:
        close_asynch_dialog(pos_id, wait_dlg_id)


@action
def doReprintOrder(pos_id, order_id, *args):
    """
    Reprints PICKLIST and Fiscal Receipt for specific order
    """
    model = get_model(pos_id)
    posot = get_posot(model)
    conn = None
    try:
        conn = persistence.Driver().open(mbcontext, str(pos_id))
        conn.query("DELETE FROM CurrentOrderItem WHERE OrderId = {}".format(order_id))
    except Exception as _:
        logger.exception("Erro limpando Current Order DB")
    finally:
        if conn:
            conn.close()

    _reprint_fiscal_coupon(model, order_id, pos_id)

    try:
        order_pict = posot.orderPicture("", order_id)
        # fix order pict to print picklist correctly
        dict_list = []
        order = eTree.XML(order_pict)

        for line in order.findall("Order/SaleLine"):
            line_number = line.get('lineNumber')
            item_id = line.get('itemId')
            part_code = line.get('partCode')
            level = line.get('level')
            get_updated_sale_line_defaults(dict_list, order_id, int(line_number), item_id, part_code, line.get('qty'), int(level), get_podtype(model), model=model)
            for corrected_line in dict_list:
                corrected_line_dict = json.loads(corrected_line)
                if corrected_line_dict['line_number'] == line_number and \
                   corrected_line_dict['item_id'] == item_id and \
                   corrected_line_dict['part_code'] == part_code and \
                   corrected_line_dict['level'] == level:
                    line_orig_qty = line.get('qty')
                    line.set('addedQty', str(max(0, float(line_orig_qty) - float(corrected_line_dict['default_qty']))))
                    line.set('defaultQty', corrected_line_dict['default_qty'])
        data = sysactions.print_report(pos_id, model, False, picklist_reprint_type, eTree.tostring(order))
        if not data:
            sys_log_error("Exception reprinting Picklist for order %s" % order_id)
    except Exception as _:
        sys_log_exception("Exception reprinting Picklist for order %s" % order_id)


def _reprint_fiscal_coupon(model, order_id, pos_id):
    try:
        msg = mbcontext.MB_EasySendMessage("FiscalWrapper", TK_FISCALWRAPPER_REPRINT, format=FM_PARAM,
                                           data=str('\x00'.join(map(str, [pos_id, order_id]))))
        if not msg.token == TK_SYS_ACK:
            sys_log_error("Could not recreate Fiscal Coupon for order %s - Error: %s" % (order_id, str(msg.data)))
            raise StopAction()
        else:
            current_printer = get_device_service(
                pos_id=pos_id,
                device_type="printer",
            )
            msg = mbcontext.MB_EasySendMessage(current_printer, TK_PRN_PRINT, format=FM_PARAM, data=msg.data,
                                               timeout=10000000)  # type: MBMessage
            if msg.token != TK_SYS_ACK:
                sys_log_error("Could not reprint Fiscal Coupon for order %s - Error: %s" % (order_id, str(msg.data)))
                raise StopAction()
    except Exception as _:
        sys_log_exception("Exception recreating Fiscal Coupon for order %s" % order_id)
        raise StopAction()


@action
def doChangeQuantity(pos_id, line_numbers, qty, is_absolute='false'):
    model = get_model(pos_id)
    pos_ot = get_posot(model)
    pod_function = get_posfunction(model) if get_podtype(model) in ("DT", "FC") else get_podtype(model)
    is_absolute = (is_absolute or 'false').lower() == 'true'

    if set_product_quantity_pre and pod_function != "TT":
        set_custom(pos_id, "pre_quantity", qty)
        return True

    check_current_order(pos_id, model=model, need_order=True)
    if not line_numbers:
        return True

    line_numbers = line_numbers.split('|')
    try:
        all_lines = get_current_order(model).findall("SaleLine")
        float_qty = float(qty or 0)
        to_void = []
        # change only first line
        lines = filter(lambda c: c.get("level") == "0" and c.get("lineNumber") in line_numbers, all_lines)
        excess = sum([float(x.get("qty")) for x in lines[1:]]) if is_absolute else 0

        for i, line in enumerate(line_numbers):
            ln_br = line_numbers[i]
            sale_line = filter(lambda c, l=ln_br: c.get("level") == "0" and c.get("lineNumber") == l, all_lines)[0]
            found_qty = float(sale_line.get("qty")) if (not is_absolute or i != 0) else 0
            if found_qty == 0 and (not is_absolute or i != 0):
                continue

            qty_to_change = found_qty + float_qty - excess
            if qty_to_change < 0:
                to_void.append(ln_br)
                excess = 0
                float_qty = qty_to_change
            else:
                pos_ot.changeLineItemQty(pos_id, ln_br, qty_to_change)
                break

        if to_void:
            pos_ot.voidLine(pos_id, "|".join(to_void))
    except OrderTakerException as ex:
        show_messagebox(pos_id, "$ERROR_CODE_INFO|%d|%s" % (ex.getErrorCode(), ex.getErrorDescr()), icon="error")


@action
def doSetCustomerDocument(
    pos_id,             # type: Any[Union[str, int]]
    stop_action=True,   # type: Optional[bool]
):
    # type: (...) -> None

    model = get_model(pos_id)
    pod_function = get_posfunction(model)
    pos_ot = get_posot(model)
    fill_customer_properties(
        pod_function,
        pos_id,
        pos_ot,
        get_doc=True,
        force=True,
        stop_action=stop_action,
    )


@action
def doSearchSatModules(pos_id):
    msg = mbcontext.MB_EasySendMessage("FiscalWrapper", TK_FISCALWRAPPER_SEARCH_REQUEST, format=FM_PARAM, data=pos_id)
    model = get_model(pos_id)
    if msg.token == TK_SYS_ACK:
        print_report(pos_id, model, True, "sats_report", pos_id, msg.data)
    else:
        show_info_message(pos_id, "%s" % msg.data, msgtype="error")


@action
def doGetKDSstatus(pos_id):
    msg = mbcontext.MB_EasySendMessage("KdsMonitor", TK_KDSMONITOR_STATUS_REQUEST, format=FM_PARAM, data=pos_id)
    model = get_model(pos_id)
    if msg.token == TK_SYS_ACK:
        print_report(pos_id, model, True, "kds_report", pos_id, msg.data)
    else:
        show_info_message(pos_id, "%s" % msg.data, msgtype="error")


@action
def purge_kds_orders(pos_id):
    try:
        confirmation = show_confirmation(pos_id, message="$KDS_CLEANUP_MESSAGE")
        if not confirmation:
            return

        msg = mbcontext.MB_EasySendMessage("ProductionSystem", TK_PROD_PURGE, format=FM_PARAM, timeout=5 * 1000000)
        if msg.token == TK_SYS_ACK:
            show_messagebox(pos_id, "$SUCCESSFULLY_CLEANED_KDS")
            return
    except Exception as _:
        pass

    show_messagebox(pos_id, "$ERROR_CLEANING_KDS", title=key_error, icon="error")


@action
def doCheckSEFAZ(pos_id):
    msg = None
    try:
        msg = mbcontext.MB_EasySendMessage("FiscalWrapper", TK_FISCALWRAPPER_SEFAZ_CONNECTIVITY, format=FM_PARAM, data="%s" % pos_id, timeout=10000 * 1000)

        if msg.token == TK_SYS_ACK:
            show_info_message(pos_id, msg.data, msgtype="success")
        else:
            show_info_message(pos_id, msg.data, msgtype="error")

    except:
        if msg is not None:
            show_info_message(pos_id, msg.data, msgtype="error")
        else:
            show_info_message(pos_id, "$FISCAL_COMPONENT_FAILURE", msgtype="error")


@action
def get_sat_status(pos_id, print_informations='true'):
    msg = mbcontext.MB_EasySendMessage("FiscalWrapper",
                                       TK_FISCALWRAPPER_SAT_OPERATIONAL_STATUS_REQUEST,
                                       format=FM_PARAM,
                                       data=pos_id)
    if print_informations.lower() == 'true':
        model = get_model(pos_id)
        if msg.token == TK_SYS_ACK:
            print_report(pos_id, model, True, "sats_op_report", pos_id, msg.data)
        else:
            show_info_message(pos_id, "%s" % msg.data, msgtype="error")
    else:
        return msg.token == TK_SYS_ACK and "ERROR" not in msg.data


@action
def doPurgeStoredOrders(pos_id):
    model = get_model(pos_id)
    podtype = get_podtype(model)
    posot = get_posot(model)

    orders = posot.listOrders(state="STORED", podtype=podtype)

    if not orders:
        if podtype == "DT":
            show_info_message(pos_id, "$THERE_ARE_NO_DRIVE_ORDERS", msgtype="warning")
        else:
            show_info_message(pos_id, "$THERE_ARE_NO_STORE_ORDERS", msgtype="warning")
        return
    try:
        for order in orders:
            order_id = order.get("orderId")
            do_recall_order(pos_id, order_id, check_date=False)
            void_order(
                pos_id=pos_id,
                void_reason="RECALL_ORDER_TO_OLD",
                void_type="VOID_STORED_ORDER",
            )
        show_info_message(pos_id, "$DELETED_SAVED_ORDERS", msgtype="success")
    except Exception as _:
        show_info_message(pos_id, "$ERROR_DELETED_SAVED_ORDERS", msgtype="error")


@action
def doReSignXML(posid):
    try:
        msg = mbcontext.MB_EasySendMessage("FiscalWrapper", TK_FISCALWRAPPER_RE_SIGN_XML, format=FM_PARAM, data=posid)
        if msg.token == TK_SYS_ACK:
            show_info_message(posid, msg.data, msgtype="success")
        else:
            show_info_message(posid, msg.data, msgtype="error")
    except Exception as _:
        show_info_message(posid, key_error, msgtype="error")


@action
def doFixSefazProtocol(pos_id):
    initial_date = show_keyboard(pos_id, "$TYPE_INITIAL_DATE", title="", mask="DATE", numpad=True)
    if initial_date is None:
        return
    if not is_valid_date(initial_date):
        show_info_message(pos_id, "$INVALID_DATE", msgtype="error")
        return None

    final_date = show_keyboard(pos_id, "$TYPE_INITIAL_DATE", title="", mask="DATE", numpad=True)
    if final_date is None:
        return
    if not is_valid_date(final_date):
        show_info_message(pos_id, "$INVALID_DATE", msgtype="error")
        return None

    try:
        msg = mbcontext.MB_EasySendMessage("FiscalWrapper", TK_FISCALWRAPPER_SITUATION, format=FM_PARAM, data='\0'.join([initial_date, final_date]))
        if msg.token == TK_SYS_ACK:
            show_info_message(pos_id, "$OPERATION_SUCCEEDED", msgtype="success")
        else:
            show_info_message(pos_id, "$OPERATION_FAILED", msgtype="error")
    except Exception:
        show_info_message(pos_id, key_error, msgtype="error")


@action
def doRecreateXMLFiscal(posid, *args):
    try:
        number = show_keyboard(posid, "$ENTER_THE_ORDER_ID", title="", mask="INTEGER", numpad=True)
        msg = mbcontext.MB_EasySendMessage("Maintenance%02d" % 0, TK_MAINTENANCE_RECREATE_FILE_NUMBER, format=FM_PARAM, data=str(number), timeout=10000 * 1000)
        if msg.token == TK_SYS_NAK:
            show_info_message(posid, "%s" % msg.data, msgtype="error")
            return

        show_info_message(posid, msg.data, msgtype="success")
    except Exception:
        show_info_message(posid, key_error, msgtype="error")


@action
def doReportBKOffice(pos_id, *args):
    data, print_list, json_data = [], [], []

    model = get_model(pos_id)
    pos_ot = get_posot(model)

    period = show_keyboard(pos_id, "$TYPE_DATE", title="", mask="DATE", numpad=True)
    if period is None:
        return
    if not is_valid_date(period):
        show_info_message(pos_id, "$INVALID_DATE", msgtype="error")
        return

    bk_office_date = '-'.join(i for i in [period[:4], period[4:6], period[6:8]])
    query_transfer = "SELECT PosId, OrderId, datetime(DataNota, 'unixepoch') as date, OrderPicture  FROM fiscal.FiscalData WHERE date(datetime(datanota, 'unixepoch')) == '{0}' AND SentToBKOffice <> 1 AND SentToNfce = 1".format(bk_office_date)

    paid_orders = [int(x['orderId']) for x in pos_ot.listOrders(state="PAID")]

    orders = []
    conn = None
    try:
        conn = persistence.Driver().open(mbcontext, service_name="FiscalPersistence")
        cursor = conn.select(query_transfer)
        for row in cursor:
            pos_id, order_id, date_fiscal, order_picture = map(row.get_entry, ("PosId", "OrderId", "date", "OrderPicture"))
            if int(order_id) in paid_orders:
                orders.append((pos_id, order_id, date_fiscal, order_picture))
    finally:
        if conn:
            conn.close()

    separator = "{}".format("*" * 35)

    if len(orders) == 0:
        show_info_message(pos_id, "$NOT_ORDER_PENDING_FOUND", msgtype="warning")
        return

    for order in orders:
        pos_id, order_id, date_fiscal, order_picture = order
        json_data.append([order_id, {"order_id": order_id}, order_id])

        order_pict = eTree.XML(base64.b64decode(order_picture))

        if order_pict.tag == 'Orders':
            order_pict = order_pict.find('Order')

        value_order = order_pict.attrib['totalGross']

        date = '/'.join(i for i in [date_fiscal[8:10], date_fiscal[5:7], date_fiscal[:4]])
        hour = date_fiscal.split(" ")[1]
        date_formated = "{} {}".format(date, hour)

        title = "Pedido# {0} - {1}".format(order_id, hour)

        formated_msg = "{}\n" \
                       "\nPedido: {}" \
                       "\nPOS...: {}" \
                       "\nData..: {}" \
                       "\nValor.: R${}" \
                       "\n\n{}"\
            .format(separator, order_id, pos_id, date_formated, value_order, separator)

        data.append([order_id, title, formated_msg])
        print_list.append([order_id, formated_msg])

    index = show_text_preview(pos_id, data, title='$SELECT_ITEM', buttons='$SEND|$PRINT|$CANCEL', defvalue='', onthefly=False, timeout=120000)

    if index is None:
        return

    if int(index[0]) == 0:
        bk_office_response = _sending_order_to_bk_office(index, json_data, pos_id)

    if int(index[0]) == 1:
        try:
            message = None

            for bk_office_response in print_list:
                try:
                    if bk_office_response[0] == index[1]:
                        message = str(bk_office_response[1])
                except Exception as _:
                    continue

            if message is not None:
                printer = "printer%s" % pos_id
                bk_office_response = None

                try:
                    bk_office_response = mbcontext.MB_EasySendMessage(printer, TK_PRN_PRINT, FM_PARAM, message)
                except MBException:
                    time.sleep(0.5)
                    bk_office_response = mbcontext.MB_EasySendMessage(printer, TK_PRN_PRINT, FM_PARAM, message)
                finally:
                    if bk_office_response and bk_office_response.token == TK_SYS_ACK:
                        show_info_message(pos_id, "$OPERATION_SUCCEEDED", msgtype="success")
                    else:
                        show_info_message(pos_id, "$OPERATION_FAILED", msgtype="error")
        except Exception as _:
            show_info_message(pos_id, "$ERROR_PRINT_REPORT", msgtype="error")


def _sending_order_to_bk_office(index, json_data, pos_id):
    wait_dlg_id = show_messagebox(pos_id, "$SENDING_ORDER_TO_BKOFFICE", "$WAIT_MESSAGE", buttons="", asynch=True)
    try:
        obj = {}
        for data in json_data:
            try:
                if data[0] == index[1]:
                    obj = data[1]
            except:
                continue

        msg = mbcontext.MB_EasySendMessage("BKOfficeUploader", TK_BKOFFICEUPLOADER_SEND_BKOFFICE_ORDER, data=str(obj),
                                           format=FM_PARAM, timeout=-1)
        msg = eval(msg.data)
        if msg.get('status'):
            show_info_message(pos_id, msg.get('msg').encode('utf-8'), msgtype="success")
        else:
            show_info_message(pos_id, msg.get('msg').encode('utf-8'), msgtype="error")
    except Exception as _:
        show_info_message(pos_id, "$CONNECTION_NOT_FOUND", msgtype="error")
    finally:
        close_asynch_dialog(pos_id, wait_dlg_id)

    return msg


@action
def rupturaItens(pos_id, *args):
    model = get_model(pos_id)
    conn = None
    dialog_timeout = 600000
    try:
        try:
            enabled_data = None
            disabled_data = None

            # get enabled products from ruptura component
            msg = send_message("Ruptura", TK_RUPTURA_GET_ENABLED, FM_STRING, "")
            if msg.token == TK_SYS_NAK:
                raise Exception()
            enabled_data = json.loads(msg.data)
            enabled_ingredients = [{'product_code': k, 'product_name': v} for k, v in enabled_data.iteritems()]

            # get disabled products from ruptura component
            msg = send_message("Ruptura", TK_RUPTURA_GET_DISABLED, FM_STRING, "")
            if msg.token == TK_SYS_NAK:
                raise Exception()
            disabled_data = json.loads(msg.data)
            disabled_ingredients = [{'product_code': k, 'product_name': v} for k, v in disabled_data.iteritems()]
        except BaseException as e:
            logger.exception('[rupturaItens] Exception {}'.format(e))
            show_messagebox(pos_id, "$ERROR_GETTING_DATA", "$INFORMATION", buttons="Sair")
            return

        set_custom(pos_id, "RUPTURE_ENABLED_ITEMS", json.dumps(enabled_ingredients))
        set_custom(pos_id, "RUPTURE_DISABLED_ITEMS", json.dumps(disabled_ingredients))
        time.sleep(0.1)
        ret = show_custom_dialog(pos_id, "rupture_items", custom_dlg_timeout=dialog_timeout)
        while ret:
            wait_dlg_id = show_messagebox(pos_id, "$WAIT_MESSAGE", "$ITENS_BREAKDOWN", buttons="", asynch=True)
            parsed_ret = ret.split("|")
            command = parsed_ret[0]

            if command == "search":
                close_asynch_dialog(pos_id, wait_dlg_id)
                search_filter = show_keyboard(pos_id, "$SEARCH_ITEMS")
                wait_dlg_id = show_messagebox(pos_id, "$WAIT_MESSAGE", "$ITENS_BREAKDOWN", buttons="", asynch=True)
                if search_filter is not None:
                    search_filter = search_filter.lower()
                    set_custom(pos_id, "RUPTURE_SEARCH_VALUE", search_filter)
                    time.sleep(0.1)  # Kernel delayed time for model update
                close_asynch_dialog(pos_id, wait_dlg_id)
                ret = show_custom_dialog(pos_id, "rupture_items", custom_dlg_timeout=dialog_timeout)
            elif command == "search_clean":
                set_custom(pos_id, "RUPTURE_SEARCH_VALUE", "")
                time.sleep(0.1)  # Kernel delayed time for model update
                close_asynch_dialog(pos_id, wait_dlg_id)
                ret = show_custom_dialog(pos_id, "rupture_items", custom_dlg_timeout=dialog_timeout)
            elif command == "enabled":
                for product in enabled_ingredients:
                    if product["product_code"] == parsed_ret[1]:
                        disabled_ingredients.append(product)
                        enabled_ingredients.remove(product)
                        break
                set_custom(pos_id, "RUPTURE_ENABLED_ITEMS", json.dumps(enabled_ingredients))
                set_custom(pos_id, "RUPTURE_DISABLED_ITEMS", json.dumps(disabled_ingredients))
                time.sleep(0.3)  # Kernel delayed time for model update
                close_asynch_dialog(pos_id, wait_dlg_id)
                ret = show_custom_dialog(pos_id, "rupture_items", custom_dlg_timeout=dialog_timeout)
            elif command == "disabled":
                for product in disabled_ingredients:
                    if product["product_code"] == parsed_ret[1]:
                        enabled_ingredients.append(product)
                        disabled_ingredients.remove(product)
                        break
                set_custom(pos_id, "RUPTURE_ENABLED_ITEMS", json.dumps(enabled_ingredients))
                set_custom(pos_id, "RUPTURE_DISABLED_ITEMS", json.dumps(disabled_ingredients))
                time.sleep(0.3)  # Kernel delayed time for model update
                close_asynch_dialog(pos_id, wait_dlg_id)
                ret = show_custom_dialog(pos_id, "rupture_items", custom_dlg_timeout=dialog_timeout)
            elif command == "save":
                try:
                    user_id = '{} / {}'.format(get_custom(model, 'Last Manager ID'), get_operator_session(model))
                    # save rupture data using ruptura component
                    msg = send_message(
                        "Ruptura",
                        TK_RUPTURA_UPDATE_ITEMS,
                        FM_PARAM,
                        '\0'.join(map(str, [json.dumps({'enabled': enabled_ingredients, 'disabled': disabled_ingredients}), user_id, pos_id]))
                    )
                    if msg.token == TK_SYS_NAK:
                        raise Exception(msg.data)

                except BaseException as e:
                    logger.exception('[rupturaItens] Exception {}'.format(e))
                    show_messagebox(pos_id, "$ERROR_SAVING_DATA", "$INFORMATION", buttons="Sair")
                    return
                finally:
                    close_asynch_dialog(pos_id, wait_dlg_id)
                ret = show_custom_dialog(pos_id, "rupture_items", custom_dlg_timeout=dialog_timeout)
            elif command == "cancel":
                close_asynch_dialog(pos_id, wait_dlg_id)
                set_custom(pos_id, "RUPTURE_SEARCH_VALUE", "")
                break
            else:
                close_asynch_dialog(pos_id, wait_dlg_id)
                break
        return
    finally:
        if conn:
            conn.close()


def _get_custom_property_value_by_key(property_key, custom_properties):
    filtered_custom_properties = filter(lambda x: x[0] == property_key, custom_properties.items())
    if len(filtered_custom_properties) == 1:
        return filtered_custom_properties[0][1]
    else:
        return None


@action
def doClearOptionItem(pos_id, linenumber="", item_id="", pos_ot=None, clean_all_options="false"):
    model = get_model(pos_id)
    if _is_changing_order(model) and is_table_service(model):
        show_messagebox(pos_id, message="$CANNOT_REMOVE_ITEMS_IN_AN_OPENED_ORDER", icon="error")
        raise StopAction()

    clean_all_options = clean_all_options.lower() == 'true'
    check_current_order(pos_id, model=model, need_order=True)
    if not linenumber or not item_id:
        show_messagebox(pos_id, message="$SELECT_LINE_TO_CLEAR", icon="error")
        raise StopAction()

    blkopnotify = False
    if pos_ot is None or pos_ot == '':
        pos_ot = get_posot(model)
        pos_ot.blkopnotify = True
        blkopnotify = True

    try:
        item_id_list = [item_id]

        if clean_all_options:
            item_id_list = _clean_all_options(model, pos_ot, item_id)

        for item_id in item_id_list:
            pos_ot.clearOption(pos_id, linenumber, "", item_id)

    except ClearOptionException as e:
        sys_log_exception("Could not clear option")
        show_info_message(pos_id, "$ERROR".format(str(e)), msgtype="error")
    except OrderTakerException as e:
        show_messagebox(pos_id, message="$ERROR_CODE_INFO|%d|%s" % (e.getErrorCode(), e.getErrorDescr()), icon="error")
    finally:
        if blkopnotify:
            pos_ot.blkopnotify = False
            pos_ot.updateOrderProperties(pos_id)


def _clean_all_options(model, pos_ot, item_id):
    # type: (eTree, eTree, str) -> List[str]

    item_id_split = item_id.split('.')
    item_id_split = item_id_split[0] + '.' + item_id_split[1]
    item_id_list = []

    order_id = int(model.find("CurrentOrder/Order").get("orderId"))
    order_pict = pos_ot.orderPicture("", order_id)

    sale_lines = eTree.XML(order_pict).findall(".//SaleLine[@itemType='CANADD'][@qty]")
    for sale_line in sale_lines:
        if sale_line.get('qty') != '0' and sale_line.get('itemId').startswith(item_id_split):
            prod = sale_line.get('itemId')
            item = _get_items_to_clean(prod, order_pict, item_id_list)
            item_id_list = item

    return item_id_list


def _get_items_to_clean(item_id, order_pict, item_id_list):
    try:
        current_item_item_id = ".".join(item_id.split(".")[:-1])
        current_item_part_code = item_id.split(".")[-1:][0]
        current_sale_lines = eTree.XML(order_pict).findall(".//SaleLine[@itemId='{}'][@partCode='{}']".format(
            current_item_item_id, current_item_part_code))[0]

        if current_sale_lines.get("itemType") == 'OPTION':
            parent_item_id = item_id
        else:
            parent_item_id = ".".join(item_id.split(".")[:-1])

        if parent_item_id != "":
            sale_lines = eTree.XML(order_pict).findall(".//SaleLine[@itemId='{}']".format(parent_item_id))
            for sale_line in sale_lines:
                item = parent_item_id + "." + sale_line.get('partCode')
                item_id_list.append(item) if item not in item_id_list else None

        return item_id_list
    except Exception as _:
        return [item_id]


def remove_custom_comment(pos_ot, curr_line):
    comment_keys = comment_map.keys()
    line_exists = curr_line and len(curr_line) > 0

    comments = {}
    if line_exists:
        for comment in curr_line[0].findall("Comment"):
            comments[comment.get("comment")] = comment.get("commentId")

    for comment_key in comment_keys:
        comment_text = comment_map[comment_key]
        if comment_text in comments.keys():
            pos_ot.delComment(comments[comment_text])
            del comments[comment_text]


def add_custom_comment(pos_ot, item_id, part_code, line_number, level, comment):
    pos_ot.addComment(line_number, item_id, level, part_code, comment)


def get_order_saleline(pos_id, item_id, part_code, line_number):
    model = get_model(pos_id)
    def is_same_line(sl):
        return sl.get("itemId") == item_id and sl.get("partCode") == part_code and sl.get("lineNumber") == line_number

    order = model.find("CurrentOrder/Order")
    return filter(lambda sl: is_same_line(sl), order.findall("SaleLine"))


def get_next_level(model, item_id, line_number, level):
    # type: (eTree, str, str, str) -> int

    try:
        part_code = item_id.split(".")[-1]
        item_id = ".".join(item_id.split(".")[:-1])
        order = model.find("CurrentOrder/Order")
        line_filter = "SaleLine[@lineNumber='{}'][@itemId='{}'][@partCode='{}']".format(line_number, item_id, part_code)
        sale_line = order.find(line_filter)
        return int(sale_line.get("level")) + 1 if sale_line is not None else int(level) + 1
    except Exception as _:
        logger.exception("Error to get next level item to add modifier")
        sys_log_exception("Error to get next level item to add modifier")
        return int(level) + 1


@action
def doModifier(pos_id, item_id, level, part_code, qty, line_number, modtype=''):
    model = get_model(pos_id)
    pos_ot = get_posot(model)

    next_level = get_next_level(model, item_id, line_number, level)
    removing = int(qty) == 0 or not modtype
    pos_ot.changeModifier(pos_id, line_number, item_id, next_level, part_code, qty)

    curr_line = get_order_saleline(pos_id, item_id, part_code, line_number)
    line_exists = curr_line and len(curr_line) > 0

    comment_keys = comment_map.keys()
    if modtype in comment_keys or removing:
        remove_custom_comment(pos_ot, curr_line)

        if not removing:
            if not line_exists:
                pos_ot.changeModifier(pos_id, line_number, item_id, next_level, part_code, 0)
                pos_ot.changeModifier(pos_id, line_number, item_id, next_level, part_code, qty)

            add_custom_comment(pos_ot, item_id, part_code, line_number, next_level, comment_map[modtype])

    doChangeQuantity(int(pos_id), None, '1')


@action
def doToggleMirrorScreen(posid):
    model = get_model(posid)
    value = "false" if get_custom(model, "MIRROR_SCREEN", "false") == "true" else "true"
    set_custom(posid, "MIRROR_SCREEN", value, persist=True)


@action
def doChangeLanguage(posid, *args):
    # Retrieve the list of languages from the I18N service
    msg = send_message("I18N", TK_I18N_GETLANGS)
    if msg.token == TK_SYS_ACK:
        options = msg.data.split('\0')
        if options and not options[len(options) - 1]:
            options.pop()  # Last element is empty... remove it!
        # Find out the current POS language
        try:
            model = get_model(posid)
            current_index = options.index(str(model.find("Language").get("name")))
        except:
            current_index = 0  # default value
        index = show_listbox(posid, options, defvalue=current_index)
        if index is None:
            return  # User canceled
        lang = options[index]
        # Set the new language on PosController
        msg = send_message("POS%d" % int(posid), TK_POS_SETLANG, FM_PARAM, "%s\0%s" % (posid, lang))
        if msg.token == TK_SYS_ACK:
            # Wait some time for the UI to reload
            show_info_message(posid, "$OPERATION_SUCCEEDED", msgtype="success")
            return
    # Failed
    show_info_message(posid, "$OPERATION_FAILED", msgtype="error")


@action
def enterTabNumber(posid, *args):

    model = get_model(posid)
    check_current_order(posid, model=model, need_order=True)
    posot = get_posot(model)
    order = get_current_order(model)
    tab = order.find('CustomOrderProperties/OrderProperty[@key="TAB"]')
    if tab is not None:
        saved_tab_number = tab.get('value')
    else:
        saved_tab_number = ''

    tab_number = show_keyboard(posid, "$ENTER_TAB_NUMBER", defvalue=saved_tab_number, mask="INTEGER", numpad=True)
    if tab_number in (None, ""):
        return
    posot.setOrderCustomProperty("TAB", tab_number)


@action
def doUpdateBlacklist(posid):
    mbcontext.MB_EasySendMessage("Blacklist", TK_EVT_EVENT, data='\0UpdateBlacklist')
    show_info_message(posid, "$BLACKLIST_UPDATE_STARTED")


@action
def doSyncDeliveryMenu(posid):
    mbcontext.MB_EasySendMessage("Ruptura", TK_EVT_EVENT, data='\0FullRuptureRequest')
    show_info_message(posid, "$DELIVERY_SYNC_STARTED")


@action
def doRuptureCleanup(posid):
    mbcontext.MB_EasySendMessage("Ruptura", TK_EVT_EVENT, data='\0CleanRupture\0%s' % posid)
    show_info_message(posid, "$RUPTURE_CLEANUP_STARTED")


@action
def isPyscriptsOnline(posid):
    return True


@action
@user_authorized_to_execute_action
def do_overwrite_price(pos_id, line_number, item_price, item_id, part_code, qty):
    if line_number in [None, '0']:
        show_messagebox(pos_id, message="$SELECT_LINE_FIRST", icon="warning")
        return

    if float(item_price or 0) <= 0 or float(qty or 0) <= 0:
        # TODO: Check if Item has Fiscal Parameters (To be able to fiscalize it)
        show_messagebox(pos_id, message="$OVERWRITE_PRICE_WITHOUT_VALUE", icon="warning")
        return

    model = get_model(pos_id)
    pos_ot = get_posot(model)

    message = "$NEW_PRICE_OR_ZERO"
    new_price = show_keyboard(pos_id, message, title="$OVERWRITE_PRICE", mask="CURRENCY", numpad=True, timeout=720000)

    new_price = round(float(new_price), 2) if new_price else None
    if new_price is None:
        return

    try:
        level = len(item_id.split(".")) - 1
        pos_ot.priceOverwrite(pos_id, line_number, new_price, item_id, level, part_code)
    except OrderTakerException as e:
        error_message = "$ERROR_CODE_INFO|%d|%s" % (e.getErrorCode(), e.getErrorDescr())
        show_messagebox(pos_id, error_message, icon="critical")
        raise

    return True


@action
@user_authorized_to_execute_action
def do_comment(pos_id, line, level, item_id, part_code, comment_id="-1", comment=None):
    model = get_model(pos_id)
    pos_ot = get_posot(model)
    comment_id = None if comment_id == "-1" else comment_id

    if not line:
        show_messagebox(pos_id, message="$NO_LINE_SELECTED", icon="critical")
        return

    default_message = ""
    message = "$ADD_COMMENT"
    if comment_id:
        message = "$EDIT_COMMENT"

        model = get_model(pos_id)
        order = model.find("CurrentOrder/Order")
        for sl in order.findall("SaleLine"):
            for line_comment in sl.findall("Comment"):
                if line_comment.get("commentId") == comment_id:
                    default_message = line_comment.get("comment")
                    break

    if comment is None:
        comments = keyboard_comments["do_comment"] if "do_comment" in keyboard_comments else []
        comments = "|".join(comments)
        comment = show_keyboard(pos_id, message, defvalue=default_message, title="", numpad=False, infotext=comments)

    if comment is None:
        return

    if comment_id:
        if comment == "":
            pos_ot.delComment(comment_id)
        else:
            pos_ot.updComment(comment_id, comment)
    elif comment != "":
        pos_ot.addComment(line, item_id, level, part_code, comment)


def _check_if_operator_is_logged(pos_id, model, need_logged=True, can_be_blocked=True):
    if model is None:
        model = get_model(pos_id)
    if need_logged:
        check_business_day(pos_id, model)

    is_logged = _has_operator_logged(model)

    if need_logged and not is_logged:
        show_messagebox(pos_id, message='$NEED_TO_LOGIN_FIRST', icon='warning')
        raise StopAction()
    elif is_logged and not need_logged:
        show_messagebox(pos_id, message='$NEED_TO_LOGOFF_FIRST', icon='warning')
        raise StopAction()
    if not can_be_blocked and is_day_blocked(model):
        show_messagebox(pos_id, message='$POS_IS_BLOCKED_BY_TIME', icon='error')
        raise StopAction()


def _has_operator_logged(model):
    for op in model.findall('Operator'):
        if op.get('state') in ['LOGGEDIN', 'PAUSED']:
            return True

    return False


def get_tender_types():
    all_tender_types = sysactions.get_tender_types()
    tender_types = [tender for tender_id, tender in all_tender_types.items() if tender_id < 200]
    for tender_type in tender_types:
        tender_type["showInFC"] = True if tender_type["id"] < 90 else False
        tender_type["showInDL"] = True if 90 <= tender_type["id"] < 120 else False
        parent_id = "parentId "
        if parent_id in tender_type:
            tender_type["parentId"] = tender_type[parent_id]
            del tender_type[parent_id]

        has_child = False
        for tender_type_aux in tender_types:
            if "parentId" in tender_type_aux and tender_type_aux["parentId"] == tender_type["id"]:
                has_child = True
                break

        tender_type["hasChild"] = has_child

    try:
        tender_types = sorted(tender_types, key=lambda k: k['id'])
    except Exception as _:
        logger.exception("Error ordering tender types")

    return tender_types


def get_tender_type(pos_id, model, tender_type_id, amount):
    tender_type = _get_tender_type(pos_id, tender_type_id)
    if tender_type is None:
        return None

    if tender_type['needAuth'] and not get_authorization(pos_id, min_level=UserLevels.MANAGER.value, model=model)[0]:
        return None

    if amount in ("CashAmount", "EnterAmount"):
        amount = show_numpad_dialog(pos_id, "$LABEL_CASH_AMOUNT_TENDERED", mask="CURRENCY")
        if amount in (None, "", ".") or float(amount) <= 0:
            return

    return tender_type


def _update_blocked_pos_state(pos_id):
    pass


@action
def do_ask_barcode(pos_id):
    barcode = show_keyboard(pos_id, "$TYPE_BARCODE", title="", mask="INTEGER", defvalue="", numpad=True, timeout=1800000)
    if barcode in (None, ''):
        return
    return sell_product_by_barcode(pos_id, barcode, True)


@action
def do_sell_product_by_barcode(pos_id, barcode_number, check_consistency=False, received_qty="1"):
    return sell_product_by_barcode(pos_id, barcode_number, check_consistency, received_qty)


def _is_changing_order(model):
    current_order = get_current_order(model)
    if not current_order:
        return False

    if current_order.get("state") != "IN_PROGRESS":
        return False

    state_history = current_order.findall(".//StateHistory/State")
    for state in state_history:
        if state.get("state") == "STORED":
            return True

    return False


def is_table_service(model):
    order_properties = model.findall(".//CustomOrderProperties/OrderProperty")
    for order_property in order_properties:
        if order_property.get("key") == "TABLE_ID":
            return True


def get_printer_receipt():
    return printer_receipt


@action
def close_order(pos_id):
    return doTender(pos_id, '', check_amount=False)


def get_pos_lock(
    pos_id,     # type: int
):
    # type: (...) -> Lock

    with do_sale_lock_dict:
        if pos_id not in do_sale_lock:
            do_sale_lock[pos_id] = Lock()
        return do_sale_lock[pos_id]
