# -*- coding: utf-8 -*-

import base64

import json
import re
import sys
import threading
import time
from datetime import datetime
from decimal import Decimal
from threading import Lock

import cfgtools
import helper
import persistence
import pyscripts
import sysactions
from msgbus import (
    TK_SYS_NAK,
    TK_SYS_ACK,
    TK_POS_USERLOGIN,
    TK_POS_LISTUSERS,
    TK_I18N_GETLANGS,
    TK_POS_SETLANG,
    TK_CDRAWER_OPEN,
    FM_PARAM,
    FM_XML,
    MBException,
    TK_PRN_PRINT,
    TK_DRV_IOCTL,
    MBMessage,
)
from pos_model import TenderType
from posot import (
    OrderTaker,
    OrderTakerException,
    ClearOptionException,
)
from sysactions import (
    AuthenticationFailed,
    StopAction,
    UiOutOfSync,
    action,
    send_message,
    get_model,
    get_podtype,
    get_business_period,
    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,
    close_asynch_dialog,
    print_report,
    set_custom,
    get_custom,
    format_amount,
    get_clearOptionsInfo,
    is_valid_date,
    get_poslist,
    is_day_opened,
    get_posfunction,
    is_day_blocked,
    authenticate_user,
    get_storewide_config,
    translate_message,
)
from systools import (
    sys_log_info,
    sys_log_debug,
    sys_log_exception,
    sys_log_error,
    sys_log_warning,
)
from xml.etree import cElementTree as eTree

import actions
import manager
import manager.reports
from actions import *

from actions.customer import (
    get_customer_doc,
    validate_required_document,
)
from actions.delivery import (
    request_and_save_customer_data,
)
from actions.delivery.delivery_address import handle_customer_data_on_total
from actions.delivery.print_original_delivery import (
    reprint_delivery_coupons,
    print_delivery_coupon,
)
from actions.discount.clean_discounts import clean_discounts
from actions.eft import (
    check_eft_transaction,
    finish_eft_transactions,
    get_electronic_payment_component_name,
    get_tender_details,
    has_electronic_payment,
    set_eft_cache,
)
from actions.eft.cancel_eft_transaction import clear_single_tender
from actions.eft.request_eft import request_eft
from actions.functions.do_store_order import doStoreOrder
from actions.loyalty import (
    get_loyalty_document,
    check_loyalty_totaled_message,
    print_end_of_sale_notes,
)
from actions.coupon.display_coupon import remove_discounts
from bustoken import (
    TK_ELECTRONIC_PAYMENT_CANCEL_PAYMENT,
    TK_FISCALWRAPPER_GET_NF_TYPE,
    TK_FISCALWRAPPER_PROCESS_REQUEST,
    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_MAINTENANCE_RECREATE_FILE_NUMBER,
    TK_MAINTENANCE_RESTART_REQUEST,
    TK_MAINTENANCE_TERMINATE_REQUEST,
    TK_PROD_PURGE,
)
from actions.loyalty.open_loyalty import (
    get_document_from_loyalty_or_customer_response,
    get_order_loyalty_customer_attribute,
    set_loyalty_properties,
)
from actions.order import sync_ui_and_model
from actions.production import get_pick_list_orders, print_pick_list_order
from actions.rupture.rupture_common import get_rupture_user_id_with_validation
from actions.sale_line.set_custom_properties import update_or_remove_sale_type
from actions.util.get_menu_user_logged import get_menu_manager_authenticated_user
from api.nreports import get_nreports_service
from api.order import get_order_service
from api.product import get_product_service
from custom_sysactions import (
    block_action_if_pos_is_blocked,
    user_authorized_to_execute_action,
    block_action_if_order_items_not_resolved,
    user_must_be_logged,
    fix_current_order_in_model,
    release_user_lock_action_after_execution,
)
from manager.reports import is_valid_period
from messagebus import DataType

from actions.delivery import (
    create_delivery_manual_order_json,
    send_remote_order_to_production,
)
from model.customexception import (
    DeliveryCouponPrintError,
    MaxQuantityException,
    MinQuantityException,
    RequiredDocumentException,
    ElectronicPaymentException,
    MissingTenderParametersException,
    ErrorGettingDevice,
)
from order_api.dto import DeliveryInfoDto
from order_api.dto.exception import (
    OrderException,
    ProductInRupture,
    UnavailableProductStock,
)
from peripherals.scanner import sell_product_by_barcode
from pyscriptscache import cache as _cache
from tef import PrintReceipt
from threads import UpdatePosBlockedState
from typing import (
    Any,
    List,
    Union,
)

logger = logging.getLogger("PosActions")

actions.set_mb_context(pyscripts.mbcontext)


# noinspection PyUnresolvedReferences
from manager import (
    openday,
    check_business_day,
)
import manager.reports
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,
    show_listbox_with_disabled_options,
    restart_system,
)
from actions.util import (
    get_drawer_amount,
    get_tender_type,
    do_set_drawer_status,
    check_sangria,
    is_delivery_order,
    is_sangria_enable,
    get_menu_user_logged,
    set_pos_type,
    get_authorization,
    get_user_level,
    check_change_limit_error,
)
from actions.config import (
    get_keyboard_comments_config,
    get_min_value_to_ask_bordereau_justify_config,
    show_numpad_for_customer_name,
    should_ask_pager_number,
    should_generate_automatic_pager_number,
    get_random_pager_number,
)
from actions.fiscal import (
    check_certificate,
    check_can_fiscalize_order_action,
)
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 actions.loyalty import update_tender_details_with_discount_data
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,
    ask_pager_number,
    show_order_tenders_options,
    is_stored_order,
    build_clean_customer_props_dict,
    build_dict,
    get_device_service_error_message,
    get_brand_name,
    show_applied_discount_and_pending_value_to_pay,
    value_is_close_to,
)
from application.interactor import (
    OrderInteractor,
    LoyaltyInteractor,
)

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"

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 = {}
scale_ticket_mode = ''
ask_for_delivery_payment = True
ask_customer_delivery_data = ""

pos_scale_dialogs = []

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

barcode_pattern_reading = "2 XXXXXX YYYYY W"

do_sale_lock_dict = Lock()
do_sale_lock = {}

POS_X = "POS%d"

#
# 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()

    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]
            message = "Pressionada tecla anula. Cancelando pagamento Sitef no POS {} - Order {}".format(
                str(pos_id),
                str(order_id),
            )
            sys_log_debug(message=message)
            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(
                        posid=pos_id,
                        message=message,
                        title="$EFT",
                        icon="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 skim_digit_limit
    global picklist_reprint_type
    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
    global ask_customer_delivery_data

    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(
        path="Customizations.DeliveryConfigurations.AskForDeliveryPayment",
        default="true"
    )
    ask_for_delivery_payment = ask_for_delivery_payment.lower() == "true"
    always_open_drawer = (config.find_value("Customizations.AlwaysOpenDrawer") or 'false').lower() == "true"
    use_customer_doc_on_fiscal_doc = config.find_value(
        path="Customizations.UseCustomerDocumentOnFiscalDocument",
        default="true"
    )
    use_customer_doc_on_fiscal_doc = use_customer_doc_on_fiscal_doc.lower() == "true"
    skim_digit_limit_cfg = get_storewide_config("Store.SkimDigitLimit", defval="5-30")
    ask_customer_delivery_data = config.find_value("Customizations.DeliveryConfigurations.AskCustomerDeliveryData") or "onStart"
    ask_cash_out_justification = config.find_value("Customizations.AskCashOutReason", "false").lower() == "true"

    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

    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=max_transfer_value,
            skim_digit_limit=skim_digit_limit,
            sangria_levels=sangria_levels,
            auto_generate_skim_number=auto_generate_skim_number,
            store_id=store_id,
            min_value_to_ask_bordereau_justify_config=min_value_to_ask_bordereau_justify_config,
            ask_cash_out_justification=ask_cash_out_justification,
            keyboard_skim_numeric=keyboard_skim_numeric,
        )
    )

    keyboard_comments = get_keyboard_comments_config(config)


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

    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=[],
    )
    logger.info("TEF result: {}".format(eft_xml))

    check_eft_transaction(
        pos_id=pos_id,
        eft_xml=eft_xml,
        electronic_type=electronic_type_id
    )

    xml = eTree.XML(eft_xml)
    cardno = xml.get("CardNumber")
    authcode = xml.get("AuthCode")
    media = xml.get("Media")
    payment_amt = xml.get("ApprovedAmount")
    adq = xml.get("IdAuth")
    exp_date = xml.get("ExpirationDate")
    nsu = xml.get("NSU")
    owner_name = xml.get("OwnerName")
    last_digits = xml.get("LastDigits")

    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(
            posid=pos_id,
            orderid=order_id,
            tender_seq_id=tender_seq_id,
            auth_code=authcode,
            card_bin=cardno,
            owner_name=owner_name,
            exp_date=exp_date,
            adq=adq,
            nsu=nsu,
            payment_amt=payment_amt,
            id_fila=id_fila,
            brand=media,
            last_digits=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 _:
        error_msg = "Error after processing electronic payment. {}#{}".format(pos_id, order_id)
        logger.warning(error_msg, exc_info=True)

    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,          # type: str
    orderid,        # type: str
    tender_seq_id,  # type: str
    auth_code,      # type: str
    card_bin,       # type: str
    owner_name,     # type: str
    exp_date,       # type: str
    adq,            # type: str
    nsu,            # type: str
    payment_amt,    # type: str
    id_fila,        # type: str
    brand,          # type: str
    last_digits,    # type: str
):
    # type: (...) -> Optional[str]

    payload = "\0".join(
        [
            str(var) for var in [
                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,                             # type: str
    item_id,                            # type: str
    qty="1",                            # type: Optional[str]
    sale_type="EAT_IN",                 # type: Optional[str]
    pos_ot=None,                        # type: Optional[OrderTaker]
    seat=None,                          # type: Optional[str]
    service_period_json=None,           # type: Optional[str]
    delivery_info_already_asked=False,  # type: Optional[bool]
):
    # type: (...) -> Optional[eTree.Element]


    if item_id in ["", None]:
        return

    auth_user_id = _check_product_authorization(
        pos_id=pos_id,
        item_id=item_id,
    )

    with get_pos_lock(pos_id=int(pos_id)):
        order_service = get_order_service(pos_id=pos_id)
        if not order_service.has_current_order():
            order_xml = do_start_order(
                pos_id=pos_id,
                sale_type=sale_type,
            )
            if order_xml is None:
                return

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

        current_order = order_service.get_order_picture()
        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 auth_user_id:
            custom_properties["authUser"] = auth_user_id

        return order_service.do_sale(
            item_id=item_id,
            qty=qty,
            custom_properties=custom_properties,
        )


def _check_product_authorization(
    pos_id,     # type: str
    item_id,    # type: str
):
    # type: (...) -> Optional[str]

    product_code = item_id.split(".")[-1]
    product_min_level = _cache.get_product_min_level().get(product_code)
    if not product_min_level:
        return

    model = get_model(posid=pos_id)
    operator = model.find("./Operator")
    if operator is None:
        raise StopAction()

    user_id = operator.get("id")
    if get_user_level(user_id=user_id) >= product_min_level:
        return user_id

    status, user_id = get_authorization(
        pos_id=pos_id,
        min_level=product_min_level,
        model=model,
    )
    if not status:
        raise StopAction()

    return user_id


def _set_order_delivery_brand(
    pos_id,             # type: str
    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:
            return delivery_brand.get("brand")
        except OrderTakerException as _:
            raise StopAction()

def get_on_start_customer_info(
    pos_id,     # type: str
):
    # type: (...) -> Dict[str, str]

    dict_sale = {}

    info_config = get_customer_info_config(pos_id)
    document = info_config["document"]
    if "onStart" in document:
        customer_doc = get_customer_doc(
            pos_id=pos_id,
            ask_document_pinpad=get_ask_document_pinpad(pos_id),
        )
        if customer_doc:
            dict_sale["CUSTOMER_DOC"] = customer_doc

    name = info_config["name"]
    if "onStart" in name:
        customer_name = get_customer_name(pos_id=pos_id)
        if customer_name:
            dict_sale["CUSTOMER_NAME"] = customer_name

    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 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,                 # type: str
    pos_ot=None,            # type: OrderTaker
    default_name="",        # type: str
):
    if default_name and isinstance(default_name, unicode):
        default_name = default_name.encode("utf-8")

    customer_name = ""
    while customer_name == "":
        order_picture = get_order_service(pos_id=pos_id).get_order_picture()
        sale_type = order_picture.get("saleTypeDescr") if order_picture else None
        buttons = "$OK|$NOT_INFORM|$PREVIOUS" if sale_type != "DELIVERY" else "$OK|$PREVIOUS"

        customer_name = show_keyboard(
            posid=pos_id,
            message="$DLV_CUSTOMER_NAME",
            buttons=buttons,
            defvalue=default_name,
            title="",
            nopad=not show_numpad_for_customer_name(),
        )

        if customer_name is None:
            return None

        customer_name = customer_name.strip()
        if customer_name == "":
            show_messagebox(
                posid=pos_id,
                message="$INVALID_CUSTOMER_NAME",
            )

    if customer_name == "NOT_INFORM":
        return ""

    customer_name = customer_name.translate(None, '-+[]/;.|<>:?{}=_()*&^%$#@!,').upper()
    if pos_ot is not None:
        fill_additional_info(
            pos_ot=pos_ot,
            properties="NAME",
            value=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
@block_action_if_pos_is_blocked
def do_start_order(
    pos_id,                 # type: str
    sale_type="EAT_IN",     # type: str
    customer_data=None,     # type: Dict[str, Any]
):
    model = get_model(posid=pos_id)
    check_new_order(
        model=model,
        pos_id=pos_id,
    )
    order_service = get_order_service(pos_id=pos_id)
    if order_service.has_current_order():
        sync_ui_and_model(pos_id=pos_id)
        return order_service.get_order_picture()

    custom_properties = get_on_start_customer_info(pos_id=pos_id)
    if any(cp is None for cp in custom_properties.values()):
        raise StopAction()

    delivery_info = None
    if sale_type == "DELIVERY":
        if customer_data is not None:
            custom_properties.update(customer_data)
        elif ask_customer_delivery_data == "onStart":
            customer_data = request_and_save_customer_data(pos_id=pos_id)
            if customer_data is None:
                return None

            if "DELIVERY_STORE_PICKUP" in customer_data and customer_data["DELIVERY_STORE_PICKUP"].lower() == "true":
                custom_properties["PICKUP_TYPE"] = "take_out"
            else:
                custom_properties["PICKUP_TYPE"] = "delivery"

            custom_properties.update(customer_data)

        selected_brand = None
        delivery_brands = _cache.get_delivery_brands()
        if len(delivery_brands) > 0:
            selected_brand = _set_order_delivery_brand(
                pos_id=pos_id,
                delivery_brands=delivery_brands,
            )

        delivery_info = DeliveryInfoDto(
            partner="MANUAL",
            brand=selected_brand,
        )

    service_period_id = None
    service_period_tag = model.find("Custom[@name='SERVICE_PERIOD']")
    if service_period_tag is not None:
        service_period_dict = json.loads(service_period_tag.attrib["value"])
        if "servicePeriodId" in service_period_dict:
            service_period_id = service_period_dict["servicePeriodId"]

    return order_service.create_order(
        sale_type=sale_type,
        service_period_id=service_period_id,
        delivery_info=delivery_info,
        custom_properties=custom_properties,
    )


@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


@action
def doOption(
    pos_id,                 # type: Optional[str]
    context,                # type: Optional[str]
    part_code,              # type: Optional[str]
    qty="",                 # type: Optional[str]
    line_number="",         # type: Optional[str]
    sale_type="EAT_IN",     # type: Optional[str]
    pricelist='',           # type: Optional[str]
    comment=None,           # type: Optional[str]
    pos_ot=None,            # type: Optional[str]
    seat=None,              # type: Optional[str]
    service_period=None,    # type: Optional[str]
    *args                   # type: Any
):
    model = get_model(pos_id)
    item_id = "{}.{}".format(context, part_code)
    try:
        check_operator_logged(pos_id, model=model, can_be_blocked=False)
        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
            order = do_option(
                pos_id=int(pos_id),
                item_id=item_id,
                qty=item_qty,
                line_number=line_number,
            )
            if order is not None:
                curr_line = get_order_sale_line(
                    order=order,
                    item_id=context,
                    part_code=part_code,
                    line_number=line_number,
                )
                handle_comments(
                    comment=comment,
                    context=context,
                    curr_line=curr_line,
                    line_number=line_number,
                    part_code=part_code,
                    pos_ot=pos_ot,
                )

                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,
                        part_code=part_code,
                        set_in_child=False
                    )

                return True
    except UnavailableProductStock as ex:
        logger.warning("[doOption] Product {} has not enough available stock".format(ex.part_code))
        show_messagebox(
            posid=pos_id,
            message="$UNAVAILABLE_PRODUCT_STOCK|{}".format(ex.part_name.encode("utf-8")),
        )
    except ProductInRupture as ex:
        logger.warning("[doOption] Product {} in Rupture".format(ex.part_code))
        show_messagebox(
            posid=pos_id,
            message="$RUPTURED_PRODUCT|{}".format(ex.part_name.encode("utf-8")),
        )
    except OrderException as ex:
        logger.exception("Error processing [doOption]")
        show_messagebox(
            posid=pos_id,
            message="$ERROR_CODE_INFO|%d|%s" % (ex.error_code, ex.error_description),
        )
    return False


def handle_comments(
    context,          # type: str
    curr_line,        # type: List[eTree.Element]
    line_number,      # type: str
    part_code,        # type: str
    pos_ot,           # type: OrderTaker
    comment=None,     # type: Optional[str]
):
    # type: (...) -> None

    remove_custom_comment(
        pos_ot=pos_ot,
        curr_line=curr_line,
    )
    level = curr_line[0].get("level")
    if comment not in [None, '']:
        add_custom_comment(
            pos_ot=pos_ot,
            item_id=context,
            part_code=part_code,
            line_number=line_number,
            level=level,
            comment=comment_map[comment],
        )


def do_option(
    pos_id,         # type: int
    item_id,        # type: str
    qty,            # type: str
    line_number,    # type: str
):
    # type: (...) -> Optional[eTree.Element]

    with get_pos_lock(pos_id=pos_id):
        order_service = get_order_service(pos_id=pos_id)
        return order_service.do_option(
            item_id=item_id,
            qty=qty,
            line_number=line_number,
            apply_default_options=True,
        )


def change_modifier(
    pos_id,         # type: int
    line_number,    # type: int
    item_id,        # type: str
    level,          # type: int
    part_code,      # type: str
    new_qty,        # type: str
):
    # type: (...) -> Optional[eTree]

    with get_pos_lock(pos_id=pos_id):
        order_service = get_order_service(pos_id=pos_id)
        return order_service.change_modifier(
            line_number=line_number,
            item_id=item_id,
            level=level,
            part_code=part_code,
            new_qty=new_qty,
        )


@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
    try:
        item_id = "%s.%s" % (to_clear.get("itemId"), to_clear.get("partCode"))
        order_service = get_order_service(pos_id=posid)
        order_service.clear_option(
            line_number=line_number,
            quantity=qty,
            item_id=item_id,
        )
        return True
    except OrderTakerException as ex:
        logger.exception("Error processing [doClearOption]")
        show_messagebox(
            posid=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)
            quantity = float(son_line.get("qty") or 0) * float(son_line.get("multipliedQty") or 0)

            if quantity == 0:
                continue

            invalid_min_quantity = chosen_quantity < float(son_line.get("minQty") or 0)
            invalid_max_quantity = chosen_quantity > float(son_line.get("maxQty") or 0)

            if invalid_min_quantity or invalid_max_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


def fill_customer_properties_in_event(
    pos_id,                 # type: str
    pos_ot,                 # type: OrderTaker
    pod_function,           # type: str
    key_event,              # type: str
    customer_info=None,     # type: Optional[dict]
    stop_action=True,       # type: Optional[bool]
):
    # type: (...) -> None

    info_config = get_customer_info_config(pos_id)
    events_to_ask_document = info_config["document"]
    events_to_ask_name = info_config["name"]

    get_document = key_event in events_to_ask_document
    get_name = key_event in events_to_ask_name

    if customer_info is not None and customer_info.get("name"):
        get_name = False
    if customer_info is not None and customer_info.get("document"):
        get_document = False

    request_customer_info = key_event in events_to_ask_document or key_event in events_to_ask_name
    if not request_customer_info:
        return

    if get_document:
        loyalty_document = get_document_from_loyalty_or_customer_response(
            pos_id=pos_id,
            pos_ot=pos_ot,
            ask_document_pinpad=get_ask_document_pinpad(pos_id=pos_id),
            stop_action=stop_action,
        )

        if loyalty_document is not None:
            get_document = False
            customer_info = build_dict(
                key="document",
                value=loyalty_document,
            )

    fill_customer_properties(
        pod_function=pod_function,
        pos_id=pos_id,
        pos_ot=pos_ot,
        get_doc=get_document,
        get_name=get_name,
        customer_info=customer_info,
        stop_action=stop_action,
    )


@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)

    handle_customer_data_on_total(pos_id=pos_id)

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

    is_kiosk = is_kiosk and is_kiosk.lower() == "true"
    pod_function = get_posfunction(model) if get_podtype(model) in ("DT", "FC") else get_podtype(model)

    info_config = get_customer_info_config(pos_id)
    document = info_config["document"]
    name = info_config["name"]
    request_customer_info = "onTotal" in document or "onTotal" in name

    if not is_kiosk and not is_recall and request_customer_info:
        fill_customer_properties_in_event(
            pos_id=pos_id,
            pos_ot=posot,
            pod_function=pod_function,
            key_event="onTotal"
        )

    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
        order_service = get_order_service(pos_id=pos_id)
        order_service.total_order()
        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 and pod_function == "OT":
            doStoreOrder(
                pos_id=pos_id,
                totalize="false",
            )
            return

        check_loyalty_totaled_message(pos_id=pos_id)

    except StopAction as _:
        raise

    except Exception as ex:
        show_messagebox(
            posid=pos_id,
            message="$ERROR_CODE_INFO|%d|%s" % (ex.getErrorCode(), ex.getErrorDescr()),
            icon="error"
        )
        order_id = model.find(".//Order").get("orderId")
        fix_current_order_in_model(
            pos_id=pos_id,
            model=model,
            order_taker=posot,
            order_id=order_id,
        )
        if is_recall:
            raise ex

    finally:
        from application.interactor import MessageDialogInteractor
        MessageDialogInteractor(pos_id).clear_pending_messages()
        logger.debug("Order Totalizada - POS %s" % pos_id)

    return True


def _set_loyalty_info_table(
    pos_id,         # type: str
    order_xml,      # type: eTree.XML
    loyalty_id,     # type: str
):
    # type: (...) -> None

    order_custom_property = order_xml.find(".//OrderProperty[@key='TABLE_ID']")
    if order_custom_property is not None:
        table_id = order_custom_property.get("value")
        set_loyalty_properties(
            pos_id=pos_id,
            table_id=table_id,
            order_id="",
            table_seat="table",
            loyalty_id=loyalty_id,
        )


def _get_loyalty_customer_name(
    loyalty_info,   # type: str
):
    # type: (...) -> Optional[str]

    loyalty_data = json.loads(loyalty_info)
    customer_info = loyalty_data.get("customerInfo", {})
    customer = customer_info.get("customer", {})
    return customer.get("name", None)


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 _remove_loyalty_id_table(
    pos_id,       # type: str
    order_xml,    # type: eTree.XML
):
    from msgbusboundary import MsgBusTableService as tableService

    order_custom_property = order_xml.find(".//OrderProperty[@key='TABLE_ID']")
    if order_custom_property is not None:
        table_id = order_custom_property.get("value")
        tableService().set_table_custom_property(
            pos_id=pos_id,
            prop="LOYALTY_ID",
            prop_value="",
            table_id=table_id,
        )


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]
    show_cancel_button=True,  # type: Optional[bool]
):
    # type: (...) -> None

    order_xml = eTree.XML(pos_ot.orderPicture(pos_id))
    table_id_property = order_xml.find(".//OrderProperty/[@key='TABLE_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_cancel_button=show_cancel_button,
            )
        _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,
        )

    customer_name_was_changed = customer_name is not None and customer_name != default_name
    customer_doc_was_changed = customer_doc is not None and customer_doc != default_doc
    if not customer_doc_was_changed and not customer_name_was_changed:
        return

    if table_id_property is not None:
        from table_actions import (
            register_table_properties,
            create_table_modified_event,
        )

        customer_data = {}
        table_id = table_id_property.get("value")
        if get_doc:
            customer_data.update({"CUSTOMER_DOC": customer_doc})
        if get_name:
            customer_data.update({"CUSTOMER_NAME": customer_name})

        table_picture = register_table_properties(
            pos_id=pos_id,
            properties=customer_data,
            table_id=table_id,
        )

        if table_picture is not None:
            create_table_modified_event(
                pos_id=pos_id,
                table_picture=table_picture,
            )

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

    for prop in order_properties_dict.keys():
        if order_properties_dict[prop] is 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

    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.encode("utf-8"))
        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]

    from_loyalty = False
    if customer_doc is None:
        is_valid_document = get_order_loyalty_customer_attribute(
            pos_id=pos_id,
            order_picture=order,
            attribute="isValidDocument",
        )

        if is_valid_document:
            customer_doc = get_order_loyalty_customer_attribute(
                pos_id=pos_id,
                order_picture=order,
            )
            from_loyalty = True
        else:
            return

    if use_customer_doc_on_fiscal_doc and not from_loyalty:
        _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_id,
            pos_ot=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
):
    if customer_doc in (None, 'None', ''):
        return

    pos_ot = sysactions.get_posot(model)
    fiscal_document = {"CUSTOMER_FISCAL_DOCUMENT": customer_doc}
    pos_ot.setOrderCustomProperties(fiscal_document, orderid=order.get("orderId"))


def get_and_save_properties_after_paid(
    pos_id,         # type: str
    pos_ot,         # type: OrderTaker
    model,          # type: eTree.XML
    order_id,       # type: int
):
    # type: (...) -> str

    info_config = get_customer_info_config(pos_id)

    document = get_custom_property_value(pos_id, "CUSTOMER_DOC")
    state = True
    if "afterPaid" in info_config["document"]:
        loyalty_document = get_loyalty_document(
            pos_id=pos_id,
            pos_ot=pos_ot,
            ask_document_pinpad=get_ask_document_pinpad(pos_id=pos_id),
        )

        document = document if loyalty_document is None else loyalty_document
        if loyalty_document is None:
            document, state = get_customer_doc_after_paid(
                pos_id=pos_id,
                pos_ot=pos_ot,
                ask_document_pinpad=get_ask_document_pinpad(pos_id=pos_id),
            )

    order_properties_dict = {}
    if document and state:
        order_properties_dict = update_custom_properties(customer_doc=document)

    _create_page_number(
        pos_id,
        order_id,
        order_properties_dict,
    )

    if len(order_properties_dict) > 0:
        order = get_current_order(model=model)
        pos_ot.setOrderCustomProperties(
            properties=order_properties_dict,
            orderid=order.get("orderId"),
        )

    return document


def _create_page_number(
    pos_id,                     # type: str
    order_id,                   # type: int
    order_properties_dict,      # type: Dict
):
    # type: (...) -> None

    pager_number = None

    if should_ask_pager_number(pos_id=pos_id):
        pager_number = ask_pager_number(pos_id=pos_id)
    elif should_generate_automatic_pager_number(pos_id=pos_id):
        pager_number = get_random_pager_number(order_id=order_id)

    if pager_number:
        order_properties_dict.update({
            "PAGER_NUMBER": pager_number,
        })


@action
@release_user_lock_action_after_execution
def doTender(
    pos_id,                         # type: str
    amount,                         # type: str
    tender_type_id="0",             # type: Optional[str]
    offline="false",                # type: Optional[str]
    need_confirmation="false",      # type: Optional[str]
    is_kiosk="false",               # type: Optional[str]
    danfe_type="",                  # type: Optional[str]
    local_payment_data="",          # type: Optional[str]
    check_amount=True,              # type: Optional[Union[bool, str]]
    applied_discount_info=None,     # type: Optional[str]
    *args
):
    # type: (...) -> None

    logger.debug("doTender START. PosId: {}".format(pos_id))

    if local_payment_data == "":
        local_payment_data = None

    order_paid = True
    is_kiosk = (is_kiosk or "false").lower() == "true"

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

    if local_payment_data:
        local_payment_data = json.loads(local_payment_data)
        tef_processed_amount = str(local_payment_data.get("TefProcessedAmount", ""))
        amount = tef_processed_amount or amount

    # Inline function to Cancel current Order and Delete Payments
    def cancel_sale_and_payments(error_message="", dialog_id=None):
        if dialog_id is not None:
            close_asynch_dialog(pos_id, dialog_id)

        msg_params = error_message.split("|")
        error_message = msg_params[0].replace("$", "")
        error_params = msg_params[1] if len(msg_params) > 1 else ""

        translated_error_message = translate_message(
            model,
            error_message,
            error_params,
        )
        show_message_dialog(pos_id, key_error, "$CANCELED_SALE|{0}".format(translated_error_message))

        order = get_current_order(model)
        log_msg = "Canceling order and payments. {}#{}. Reason: {}"
        logger.error(log_msg.format(pos_id, order.get("orderId"), error_message))

        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,
        )
        remove_discounts(
            pos_id=pos_id,
            coupon_ids=coupon_ids,
        )
        void_type = "VOID_STORED_ORDER" if is_stored_order(pos_id=int(pos_id)) else "VOID_CURRENT_ORDER"
        void_order(
            pos_id=pos_id,
            void_reason="UNKNOWN_FISCAL_ERROR",
            void_type=void_type,
        )

    # Inline function to confirm last payment and set order to PAID
    def tender_and_finalize():
        while True:
            dlg_id = 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 ---")

                dlg_id = 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:
                    tenders = xml_order.findall("TenderHistory/Tender")
                    if tenders:
                        tenders = sorted(tenders, reverse=True)
                        last_tender = next(tender for tender in tenders if float(tender.get("tenderAmount")) > 0)
                        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 ---")
                logger.info("Tender and finalizing order. {}#{}".format(pos_id, xml_order.get("orderId")))

                order_service = get_order_service(pos_id=pos_id)
                order_service.do_tender(
                    tender_id=last_tender_type_id,
                    amount=last_amount,
                    order_tender_id=last_tender_id,
                )
                logger.debug("--- doTender after posot.doTender 2 ---")

                print_end_of_sale_notes(
                    pos_id=int(pos_id),
                    order_id=int(order_id),
                )

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

                if dlg_id is not None and close_asynch_dialog(pos_id, dlg_id):
                    dlg_id = None

                check_certificate(int(pos_id))

                break
            except OrderTakerException as ex:
                logger.excetion("Error processing tender. {}#{}".format(pos_id, order_id))
                show_messagebox(
                    posid=pos_id,
                    message="$ERROR_CODE_INFO|%d|%s" % (ex.getErrorCode(), ex.getErrorDescr()),
                )

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

                # 0 -> Tentar Novamente / 1 -> Cancelar
                if message_response != 0:
                    cancel_sale_and_payments("$USER_REQUESTED_CANCELLATION", dlg_id)
                    break
                else:
                    time.sleep(2)
            finally:
                if dlg_id is not None:
                    close_asynch_dialog(pos_id, dlg_id)

    model = get_model(posid=pos_id)
    check_current_order(
        posid=pos_id,
        model=model,
    )

    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

    dlg_id = None
    order_id = None
    coupon_ids = []
    try:
        model = get_model(posid=pos_id)
        xml_order = get_order_service(pos_id=pos_id).get_order_picture()
        if is_table_service(model=model) or xml_order.get("state") != "TOTALED":
            raise UiOutOfSync(pos_id=pos_id)

        check_current_order(pos_id, model=model, need_order=True)
        period = get_business_period(model)
        session = get_operator_session(model)
        pod_type = get_podtype(model)

        order_id = xml_order.get("orderId")
        due_amount = xml_order.get("dueAmount")

        logger.info("Processing payment. {}#{}: Amount/DueAmount: {}/{}".format(pos_id, order_id, amount, due_amount))
        total_gross = xml_order.get("totalGross")
        order_due_amount = float(due_amount)
        order_type = xml_order.get("type")

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

        if amount == "Exact":
            amount = due_amount

        if check_amount and (amount != "") and (float(amount) > order_due_amount):
            if tender_type["id"] != 0:
                show_message_dialog(
                    pos_id=pos_id,
                    message="$TENDERS_ARE_BIGGER_THEN_DUE_AMOUNT",
                )
                return

        discount_data = json.loads(applied_discount_info or "{}")
        if discount_data:
            coupon_ids.append(discount_data["benefit_key"])

            check_amount = not value_is_close_to(
                value=float(due_amount),
                close_to=0,
            )
            if check_amount and discount_data["new_payment_amount"] <= 0:
                return

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

        if not check_can_fiscalize_order_action(pos_id=pos_id, order=xml_order):
            return

        discount_amount = float((discount_data or {}).get("discount_amount", 0))
        if discount_data and discount_amount > 0:
            show_applied_discount_and_pending_value_to_pay(
                pos_id=pos_id,
                model=model,
                discount_amount=discount_amount,
                pending_value_to_pay=amount,
                tender_type=tender_type["descr"],
            )
        elif need_confirmation == "true":
            formatted_amount = format_amount(
                model=model,
                amount=amount,
            )
            message = "$CONFIRM_PAYMENT_RECEIVED|{0}|{1}".format(
                tender_type["descr"],
                formatted_amount,
            )
            ret = show_message_options_dialog(
                pos_id=pos_id,
                buttons="$OK|$PREVIOUS",
                message=message,
            )
            if ret is None or ret == 1:
                return

        pos_ot = get_posot(model)
        try:
            validate_required_document(
                pos_id=pos_id,
                pos_ot=pos_ot,
                total_gross=total_gross,
                amount=amount,
                due_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:
            rounded_due_amount = round_half_away_from_zero(float(xml_order.get("dueAmount")), 2)
            difference = abs(float(xml_order.get("dueAmount")) - rounded_due_amount)
            amount = str(float(amount or 0) + 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", dlg_id)
                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_amount = float(previous_tender_amount) + float(due_amount)

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

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

                model = get_model(pos_id)
                pos_ot = get_posot(model)
                order_picture = eTree.XML(pos_ot.orderPicture(pos_id))
                due_amount = order_picture.get("dueAmount")

                change += float(amount) - float(due_amount)
                change_limit_error = check_change_limit_error(
                    model=model,
                    change=change,
                    change_limit=float(tender_type["changeLimit"]),
                )
                if change_limit_error:
                    show_message_dialog(
                        pos_id=pos_id,
                        message=change_limit_error,
                    )
                    return

                logger.info("PosId/OrderId: {}/{} Change: {}".format(pos_id, order_id, abs(change)))
                show_message_dialog(pos_id, message="$MONEY_CHANGE|R$%.2f" % abs(change))

            xml = None

            is_electronic_payment = tender_type["electronicTypeId"] not in [0, 9]
            if local_payment_data is None and (offline.lower() == "false" and is_electronic_payment):
                # 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=pos_id,
                    model=model,
                    tender_id=tender_type_id,
                    amount=amount,
                    xml_order=xml_order,
                    tender_type=tender_type,
                    tender_seq_id=tender_seq_id,
                    id_fila=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 or 0.0),
                "tenderTypeDescription": tender_type.get("tenderTypeDescription", ""),
                "tenderChangeAmount": tender_type.get("changeAmount", change),
            }

            update_tender_details_with_discount_data(
                discount_data=discount_data,
                tender_details=tender_details,
            )

            if is_electronic_payment:
                data = local_payment_data or 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 ---")
            order_service = get_order_service(pos_id=pos_id)
            updated_order = order_service.do_tender(
                tender_id=tender_type_id,
                amount=amount,
                do_not_close=True,
                order_tender_id=previous_tender_id,
                tender_detail=tender_details,
            )
            tenders = updated_order.findall("TenderHistory/Tender")
            last_tender_id = int(tenders[-1].get("tenderId")) if tenders else None
            res = {
                "posId": updated_order.get("posId"),
                "orderId": updated_order.get("orderId"),
                "totalTender": updated_order.get("totalTender"),
                "dueAmount": updated_order.get("dueAmount"),
                "totalAmount": updated_order.get("totalGross"),
                "change": updated_order.get("change"),
                "tip": updated_order.get("tip"),
                "tenderId": last_tender_id,
            }
            logger.info("PosId/OrderId: {}/{} Tender details: {}".format(pos_id, order_id, tender_details))

            if local_payment_data:
                set_eft_cache(
                    pos_id=pos_id,
                    payment_data=local_payment_data,
                    tender_id=(previous_tender_id or 0) + 1,
                    order_id=order_id,
                )
            logger.debug("--- doTender after posot.doTender ---")

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

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

        document = get_and_save_properties_after_paid(
            pos_id=pos_id,
            pos_ot=pos_ot,
            model=model,
            order_id=order_id,
        )

        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:
                dlg_id = 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
                        brand_name = get_brand_name(order=xml_order)
                        current_printer = get_device_service(
                            pos_id=pos_id,
                            device_type="printer",
                            brand=brand_name,
                        )
                        print_data = get_nreports_service().end_of_sale(
                            pos_id=pos_id,
                            order_id=order_id,
                            mobile=danfe_type.startswith("APOS"),
                        )

                        msg = mbcontext.MB_EasySendMessage(
                            dest_name=current_printer,
                            token=TK_PRN_PRINT,
                            format=DataType.json,
                            data=print_data,
                            timeout=20000000,
                        )  # type: MBMessage
                        if msg.token != TK_SYS_ACK and not is_kiosk:
                            logger.warning(
                                msg="Error printing DANFE. {}#{}".format(pos_id, order_id),
                                exc_info=True,
                            )
                            if dlg_id is not None and close_asynch_dialog(pos_id, dlg_id):
                                dlg_id = None
                            show_message_dialog(pos_id, "$INFORMATION", "$ERROR_PRINT_NFE|%s" % msg.data)

                    except ErrorGettingDevice as ex:
                        logger.warning(
                            msg="Error getting DANFE. {}#{}".format(pos_id, order_id),
                            exc_info=True,
                        )
                        if not is_kiosk:
                            if dlg_id is not None and close_asynch_dialog(pos_id, dlg_id):
                                dlg_id = None

                            message = get_device_service_error_message(
                                model=model,
                                exception=ex,
                            )
                            show_message_dialog(pos_id, "$INFORMATION", "$ERROR_PRINT_NFE|{}".format(message))
                    except Exception as ex:
                        logger.warning(
                            msg="Error printing DANFE. {}#{}".format(pos_id, order_id),
                            exc_info=True,
                        )
                        if not is_kiosk:
                            if dlg_id is not None and close_asynch_dialog(pos_id, dlg_id):
                                dlg_id = None
                            show_message_dialog(pos_id, "$INFORMATION", "$ERROR_PRINT_NFE|%s" % repr(ex))

                    if not is_kiosk:
                        if dlg_id is not None and close_asynch_dialog(pos_id, dlg_id):
                            dlg_id = 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:
                    logger.error("Error processing fiscal. {}#{}. Response: {}".format(pos_id, order_id, ret.data))

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

                    max_sat_groups = "$ORDER_NOT_FINALIZED_DUE_TO_MAX_SAT_PAYMENT_GROUPS_REACHED" in ret.data
                    if max_sat_groups or "$MAXIMUM_AMOUNT_TO_FISCALIZE" 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, dlg_id)
                    order_paid = False
                    break
                elif len(parts) < 5:
                    cancel_sale_and_payments(parts[1], dlg_id)
                    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)
                        if dlg_id is not None and close_asynch_dialog(pos_id, dlg_id):
                            dlg_id = 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=pos_id,
                        buttons="$TRY_AGAIN|$CANCEL",
                        title=key_error,
                        message="$ERROR_SEND_NFE|%s" % message
                    )

                # 0 -> Tentar Novamente / 1 -> Cancelar
                if try_again_response != 0:
                    cancel_sale_and_payments("$USER_REQUESTED_CANCELLATION", dlg_id)
                    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. {}#{}".format(pos_id, order_id))
                cancel_sale_and_payments("$ERROR_PROCESSING_COUPON", dlg_id)
                order_paid = False
                break
            finally:
                if dlg_id is not None and close_asynch_dialog(pos_id, dlg_id):
                    dlg_id = None
                time.sleep(0.2)
                logger.debug("--- doTender after PROCESSING_FISCAL_DATA ---")

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

        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_delivery_coupon(
                    pos_id=pos_id,
                    order_id=order_id,
                )
                send_remote_order_to_production(order_id=order_id)

    except (DeliveryCouponPrintError, ElectronicPaymentException) as ex:
        if coupon_ids:
            remove_discounts(
                pos_id=pos_id,
                coupon_ids=coupon_ids,
            )
        logger.warning("Ignored Error on Tender Order - OrderId: {}".format(order_id), exc_info=True)
    except OrderTakerException as ex:
        error_msg = "$ERROR_CODE_INFO|%d|%s" % (ex.getErrorCode(), ex.getErrorDescr())
        if coupon_ids:
            remove_discounts(
                pos_id=pos_id,
                coupon_ids=coupon_ids,
            )
        logger.exception("OrderTakerException. {}#{}. Error: {}".format(pos_id, order_id, error_msg))
        show_message_dialog(pos_id, key_error, error_msg)
    except (StopAction, Exception) as ex:
        if coupon_ids:
            remove_discounts(
                pos_id=pos_id,
                coupon_ids=coupon_ids,
            )
        logger.exception("Unexpected Exception on payment. {}#{}. Error: {}".format(pos_id, order_id, ex))
    finally:
        with lock_tender:
            tendering[int(pos_id)] = False
            if dlg_id is not None:
                close_asynch_dialog(pos_id, dlg_id)

    LoyaltyInteractor(pos_id).show_loyalty_customer_final_message()

    logger.debug("doTender END. PosId: {}".format(pos_id))


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 = list(
        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=pos_id,
        model=model,
        tenders=tenders,
        card_brands=get_card_brands(),
        group_digital_payments=True,
        total=total_tender,
    )

    listbox_options = options_dict_to_str_list(
        model,
        options_dict
    )

    option = show_order_tenders_options(
        pos_id=pos_id,
        listbox_options=listbox_options,
        is_disabled=True,
    )

    result = False
    if option is None:
        return False

    operator_id = model.find("Operator").get("id")
    option = option.get("selected_list_index")
    order_interactor = OrderInteractor(pos_id=pos_id)
    if option == 0:
        for tender in tenders:
            clear_single_tender(
                pos_id=pos_id,
                order=order_picture_xml,
                tender=tender,
                order_interactor=order_interactor,
                operator_id=operator_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]:
        filtered_tenders = [tender for tender in tenders if tender.get("tenderId") in options_dict[1]["tenders"]]
        for tender in filtered_tenders:
            clear_single_tender(
                pos_id=pos_id,
                order=order_picture_xml,
                tender=tender,
                order_interactor=order_interactor,
                operator_id=operator_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]
        clear_single_tender(
            pos_id=pos_id,
            order=order_picture_xml,
            tender=tender,
            order_interactor=order_interactor,
            operator_id=operator_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):
    product_service = get_product_service()
    all_tender_types = product_service.get_tenders_list()
    tender_types = []

    for tender in all_tender_types:
        if tender["parentId"] is not None and tender["parentId"] == int(tender_type_id):
            tender_types.append(tender)

    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")
                recall_message = "Recall pedido foi salvo em um dia de negócio posterior ao atual"
                recall_message_destination = "- Order {} - PosId {}".format(order_id, pos_id)
                logger.debug("{} {}".format(recall_message, recall_message_destination))
                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

            try:
                if is_delivery_order(pos_id=pos_id):
                    print_delivery_coupon(
                        pos_id=pos_id,
                        order_id=order_id,
                    )
                    clean_discounts(pos_id)
            except (Exception,):
                logger.error("Error printing partner delivery coupon for order {}".format(str(order_id)))

            if totalize == 'true':
                fill_customer_properties_in_event(
                    pos_id=pos_id,
                    pos_ot=pos_ot,
                    pod_function=pod_function,
                    key_event='onRecall',
                )

                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 _:
            _store_order_in_progress(
                pos_id=pos_id,
            )
            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))
            _store_order_in_progress(
                pos_id=pos_id,
            )

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


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

    order_interactor = OrderInteractor(
        pos_id=pos_id,
    )
    if order_interactor.is_order_in_progress():
        doStoreOrder(
            pos_id=pos_id,
            totalize="false",
            show_popup=False,
        )


@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_X % 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=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_X % 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 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
    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,
            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
    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(
            posid=pos_id,
            message="",
            title="$ENTER_THE_AMOUNT",
            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()

        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

            menu_manager_user = get_menu_manager_authenticated_user(pos_id=int(pos_id))
            restart_system(
                mb_context=mbcontext,
                pos_id=pos_id,
                menu_manager_user=menu_manager_user,
                event_type="MANAGER_RESTART",
            )

        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(
            dest_name="Maintenance%02d" % int(pos_id),
            token=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 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,                         # type: str
    _,                              # type: Optional[str]
    allpos="false",                 # type: Optional[str]
    requestdate="false",            # type: Optional[str]
    danfe_type="",                  # type: Optional[str]
    is_mobile="false",              # type: Optional[str]
    *args
):
    # type: (...) -> Optional[bool]

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

    try:
        if requestdate.lower() == "true":
            date_to_find = show_keyboard(
                posid=pos_id,
                message="$TYPE_DATE",
                title="",
                mask="DATE",
                numpad=True,
                buttons="$OK|$PREVIOUS",
            )
            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)

        business_day = time.strftime("%Y%m%d", tmp_date)

        orders = pos_ot.listOrders(
            state="PAID",
            period=business_day,
            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:])
            order_gross = float(order["totalGross"]) + float(order["tip"])
            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(_parse_date(order_date))
            order_time = order_date.strftime('%H:%M')

            partner = ""
            is_table_order = 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_order = table_id != ""
                if is_table_order:
                    if "TAB-" in table_id:
                        description += " {}-{}".format(
                            actions.util.get_i18n(pos_id=int(pos_id)).internationalize_text(text="$TAB"),
                            table_name,
                        )
                    else:
                        description += " MESA-{}".format(table_name)

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

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

        picklist_button = "PICKLIST" if is_mobile.lower() == "false" else ""

        while True:
            selected = show_order_preview(
                posid=pos_id,
                orders=preview_data,
                title="$REPRINT_ORDER_SELECT",
                buttons="PREVIOUS|{}|COUPON|TEF".format(picklist_button),
                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(
                parameter="Store.CanReprintPodTypes",
                defval="FC;DS;KK;DT;OT;TT;DL;TS"
            )
            can_reprint_pod_types = can_reprint_pod_types.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(
                        posid=pos_id,
                        message="$ERROR_REPRINT_PICKLIST|{}".format(ex.message),
                        title=reprint_orders_i18n,
                        icon="error",
                        buttons="$OK",
                    )
                continue

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

            orderid = selected[1]
            try:
                current_printer = get_device_service(
                    pos_id=pos_id,
                    device_type="printer",
                )
                print_data = get_nreports_service().end_of_sale(
                    pos_id=pos_id,
                    order_id=orderid,
                    mobile=danfe_type.startswith("APOS"),
                )
                msg = mbcontext.MB_EasySendMessage(
                    dest_name=current_printer,
                    token=TK_PRN_PRINT,
                    format=DataType.json,
                    data=print_data,
                    timeout=10000000,
                )  # type: MBMessage

                if msg.token != TK_SYS_ACK:
                    show_messagebox(
                        posid=pos_id,
                        message="$ERROR_REPRINT_NFE|%s" % msg.data,
                        title=reprint_orders_i18n,
                        icon="error",
                    )
                else:
                    show_messagebox(
                        posid=pos_id,
                        message="$OPERATION_SUCCEEDED",
                        icon="success",
                    )
            except Exception as ex:
                show_messagebox(
                    posid=pos_id,
                    message="$ERROR_REPRINT_NFE|%s" % repr(ex),
                    title=reprint_orders_i18n,
                    icon="error",
                )
                sys_log_warning("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 _parse_date(order_date):
    try:
        return datetime.strptime(order_date, "%Y-%m-%dT%H:%M:%S.%f")
    except ValueError:
        return datetime.strptime(order_date, "%Y-%m-%dT%H:%M:%S")


def select_and_reprint_order(
    pos_ot,                     # type: OrderTaker
    pos_id,                     # type: str
    order_id,                   # type: str
    search_in_all_table=True,   # type: Optional[bool]
):
    # type: (...) -> Optional[bool]

    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 search_in_all_table else None
    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 not pick_list_orders:
        show_messagebox(
            posid=pos_id,
            message="$WITHOUT_ORDER_PICKLIST_TO_REPRINT",
        )
        return

    options = "|".join([item.encode("utf-8") 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",
    )

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


def _reprint_tef_coupon_order(
    model,                  # type: eTree
    pos_ot,                 # type: OrderTaker
    pos_id,                 # type: str
    order_id,               # type: str
    fiscalization_type,     # type: str

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

    order_pict = eTree.XML(
        text=pos_ot.orderPicture(
            posid="",
            orderid=order_id,
        )
    )

    tenders = order_pict.findall(".//TenderHistory/Tender")
    initial_card_number = translate_message(
        model=model,
        message_key="INITIAL_CARD_NUMBER",
    )
    final_card_number = translate_message(
        model=model,
        message_key="FINAL_CARD_NUMBER",
    )

    show_list_tenders = {}
    receipts = {}
    index = 0
    for tender in tenders:
        tender_detail = _get_receipt_to_print(
            tender=tender,
        )
        if not tender_detail:
            continue

        receipt_customer = tender_detail.get("ReceiptCustomer", None)
        receipt_merchant = tender_detail.get("ReceiptMerchant", None)
        receipts[index] = [
            receipt_customer.encode("utf-8"),
            receipt_merchant.encode("utf-8")
        ]

        card_number = tender_detail.get("LastDigits")
        if card_number is not None and card_number != "None":
            card_number = _create_card_number_text(
                card_number=card_number,
                prefix=final_card_number
            )
        else:
            card_number = tender_detail.get("CardNumber")
            card_number = _create_card_number_text(
                card_number=card_number,
                prefix=initial_card_number
            )

        tender_description = tender_detail.get("tenderDescr").encode("utf-8")
        tender_description = tender_description.replace("TEF", "").lstrip()

        amount = tender_detail.get("tenderAmount", 0)

        show_list_tenders[index] = "{} - {} - {}  - R${:.2f}".format(
            index + 1,
            card_number,
            tender_description,
            amount,
        )
        index += 1

    if len(show_list_tenders.keys()) == 0:
        return

    _select_and_print_receipt(
        pos_id=pos_id,
        show_list_tenders=show_list_tenders,
        receipts=receipts,
        fiscalization_type=fiscalization_type,
    )


def _create_card_number_text(
    card_number,
    prefix
):
    card_number = "{}: {}".format(
        prefix,
        str(
            card_number[:4] if 'None' not in card_number[:4] else None
        ),
    )
    return card_number


def _get_receipt_to_print(
    tender,     # type: eTree.Element
):
    # type: (...) -> Optional[Dict]

    if int(tender.get("eletronicType")) in (0, 9):
        return None

    tender_detail = json.loads(tender.get("tenderDetail"))
    receipt_customer = tender_detail.get("ReceiptCustomer", None)
    amount = tender_detail.get("tenderAmount", 0)

    if receipt_customer not in ['', None] and amount > 0:
        return tender_detail


def _select_and_print_receipt(
    pos_id,                 # type: str
    show_list_tenders,      # type: Dict
    receipts,               # type: Dict
    fiscalization_type,     # type: str
):
    # type: (...) -> None

    options = [item[1] for item in show_list_tenders.items()]

    model = sysactions.get_model(
        posid=pos_id,
    )

    while True:
        selected_option = show_listbox_with_disabled_options(
            pos_id=pos_id,
            options=options,
            message="$SELECT_TRANSACTION",
            buttons='$PRINT|$CANCEL',
        )

        if selected_option is None:
            return None

        selected_option = selected_option.get("selected_list_index")
        printer_receipt.reprint_receipt(
            pos_id=pos_id,
            model=model,
            receipts=receipts[selected_option],
            fiscalization_type=fiscalization_type,
        )


@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 = "PREVIOUS"
    is_manual_delivery = order.findall("CustomOrderProperties/OrderProperty[@key='MANUAL_DELIVERY_ORDER_JSON']")
    is_manual_delivery = len(is_manual_delivery) > 0
    is_integrated_delivery = len(order.findall("CustomOrderProperties/OrderProperty[@key='REMOTE_ORDER_JSON']")) > 0
    is_delivery = is_manual_delivery or is_integrated_delivery
    if is_delivery:
        buttons = "PREVIOUS|PRINT_DELIVERY"
        partner_attr = order.find(".//OrderProperty[@key='PARTNER']")
        partner = partner_attr.get("value") if partner_attr is not None else "MANUAL"
        if partner != "":
            description += " - DLV [{}]".format(partner.upper())

    pos_token_url = "POS%d/?token=TK_POS_EXECUTEACTION&format=2&isBase64=true&payload=" % int(pos_id)
    base_url = "/mwapp/services/PosController/{}".format(pos_token_url)
    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:
        reprint_delivery_coupons(
            pos_id=pos_id,
            order_id=ret[1],
        )


@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:
        data = {
            "orderIds": []
        }

        by_order_id = show_confirmation(
            posid=pos_id,
            message="$KDS_CLEANUP_MESSAGE_QUESTION",
            buttons="$KDS_CLEANUP_MESSAGE_QUESTION_BY_ORDER_ID|$KDS_CLEANUP_MESSAGE_QUESTION_ALL"
        )
        if by_order_id:
            order_id = show_numpad_dialog(pos_id, "$KDS_CLEANUP_MESSAGE_ORDER_ID")
            data["orderIds"].append(str(order_id))
            if order_id in (None, "", ".") or int(order_id) <= 0:
                raise StopAction()
        else:
            by_order_id = show_confirmation(pos_id, message="$KDS_CLEANUP_MESSAGE")
            if not by_order_id:
                raise StopAction()

        manager_id = get_custom(
            model=get_model(posid=pos_id),
            key="Last Manager ID",
        )
        logger.info("Operation [purge_kds_orders] requested and approved by {}".format(manager_id))

        msg = mbcontext.MB_EasySendMessage(
            dest_name="ProductionSystem",
            token=TK_PROD_PURGE,
            format=FM_PARAM,
            timeout=5 * 1000000,
            data=json.dumps(data)
        )
        if msg.token == TK_SYS_ACK:
            show_messagebox(pos_id, "$SUCCESSFULLY_CLEANED_KDS")
            return
    except Exception as _:
        show_messagebox(pos_id, "$ERROR_CLEANING_KDS", title=key_error, icon="error")


@action
def doCheckSEFAZ(pos_id):
    msg = None
    try:
        msg = mbcontext.MB_EasySendMessage(
            dest_name="FiscalWrapper",
            token=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 purge_store_orders(
    pos_id,     # type: str
):
    # type: (...) -> None

    confirmation = show_confirmation(pos_id, message="$STORED_ORDERS_CLEANUP_MESSAGE")
    if not confirmation:
        raise StopAction()

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

    orders = pos_ot.listOrders(state="STORED", exclppty='TABLE_ID', podtype=pod_type)

    if not orders:
        if pod_type == "DT":
            show_message_dialog(
                pos_id=pos_id,
                message="$THERE_ARE_NO_DRIVE_ORDERS",
            )
        else:
            show_message_dialog(
                pos_id=pos_id,
                message="$THERE_ARE_NO_STORE_ORDERS",
            )
        return
    try:
        for order in orders:
            order_id = order.get("orderId")
            void_order(
                pos_id=pos_id,
                order_id=order_id,
                void_reason="MANAGER_CLEAR_STORED_ORDERS",
                void_type="VOID_STORED_ORDER",
            )
        show_message_dialog(
            pos_id=pos_id,
            message="$DELETED_SAVED_ORDERS",
        )
    except Exception as _:
        show_message_dialog(
            pos_id=pos_id,
            message="$ERROR_DELETED_SAVED_ORDERS",
        )


@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(
        posid=pos_id,
        message="$TYPE_INITIAL_DATE",
        title="",
        mask="DATE",
        numpad=True,
        buttons="$OK|$PREVIOUS",
    )
    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(
        posid=pos_id,
        message="$TYPE_INITIAL_DATE",
        title="",
        mask="DATE",
        numpad=True,
        buttons="$OK|$PREVIOUS",
    )
    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(
            dest_name="FiscalWrapper",
            token=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(
            dest_name="Maintenance%02d" % 0,
            token=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 doClearOptionItem(pos_id, linenumber="", item_id="", pos_ot=None, clean_all_options="false"):
    model = get_model(pos_id)
    if is_changing_order(model=model) and is_table_service(model=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:
            order_service = get_order_service(pos_id=pos_id)
            order_service.clear_option(
                line_number=linenumber,
                item_id=item_id,
                blk_notify=blkopnotify,
            )
    except OrderTakerException as e:
        logger.exception("Error processing [doClearOptionItem]")
        show_messagebox(
            posid=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,      # type: OrderTaker
    curr_line,   # type:List[eTree.Element]
):
    # type: (...) -> None

    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,        # type: OrderTaker
    item_id,       # type: str
    part_code,     # type: str
    line_number,   # type: str
    level,         # type: str
    comment,       # type: str
):
    # type: (...) -> None

    pos_ot.addComment(line_number, item_id, level, part_code, comment)


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 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_X % 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 doRuptureCleanup(
    pos_id,     # type: str
):
    # type: (...) -> None

    from msgbusboundary import Rupture

    user_id = get_rupture_user_id_with_validation(pos_id=pos_id)

    Rupture.update_items(
        user_id=user_id,
        parts_to_enable=[],
        parts_to_disable=[],
    )


@action
def isPyscriptsOnline(posid):
    return True


@action
@user_authorized_to_execute_action
def do_overwrite_price(
    pos_id,                 # type: str
    line_number,            # type: str
    item_price,             # type: str
    item_id,                # type: str
    part_code,              # type: str
    qty,                    # type: str
    original_price=None,    # type: Optional[str]
):
    # type: (...) -> Optional[bool]

    if line_number in [None, '0']:
        show_messagebox(
            posid=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(
            posid=pos_id,
            message="$OVERWRITE_PRICE_WITHOUT_VALUE",
            icon="warning",
        )
        return

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

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

    if new_price is None:
        return

    new_price = round(float(new_price), 2)
    if new_price <= 0.0:
        new_price = round(float(original_price or 0), 2)

    try:
        level = len(item_id.split(".")) - 1
        pos_ot.priceOverwrite(
            posid=pos_id,
            linenumber=line_number,
            unitprice=new_price,
            itemid=item_id,
            level=level,
            partcode=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 != "":
        level = len(item_id.split(".")) - 1
        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


@action
def do_ask_barcode(pos_id):
    barcode = show_keyboard(
        posid=pos_id,
        message="$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)


@action
def do_clear_barcode(
    pos_id,    # type: str
):
    # type: (...) -> str

    resp = show_message_options_dialog(
        pos_id=pos_id,
        buttons="$YES|$NO",
        title="$INFORMATION",
        message="$CLEAR_BARCODE",
    )
    if resp is None or resp == 1:
        return "False"

    return "Clear"


def is_changing_order(
    model,    # type: eTree.XML
):
    # type: (...) -> bool

    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
@user_must_be_logged
def close_order(
    pos_id,             # type: str
    danfe_type="",      # type: Optional[str]
):
    # type: (...) -> None

    doTender(
        pos_id=pos_id,
        amount="",
        danfe_type=danfe_type,
        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]


@action
def show_message_box(
    pos_id,          # type: str
    title,           # type: str
    message,         # type: str
    buttons="$OK",   # type: str
):
    # type: (...) -> int

    return sysactions.show_messagebox(
        posid=pos_id,
        message=message,
        title=title,
        buttons=buttons,
    )


@action
def clean_customer_data(
    pos_id,      # type: str
    order_id,    # type: str
):
    # type: (...) -> None

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

    clean_customer_dict = build_clean_customer_props_dict()

    pos_ot.setOrderCustomProperties(
        properties=clean_customer_dict,
        orderid=order_id,
    )
    show_messagebox(
        posid=pos_id,
        message="$CLEAN_NUMPAD",
    )


def get_order_sale_line(
    order,          # type: eTree.Element
    item_id,        # type: str
    part_code,      # type: str
    line_number,    # type: str
):
    # type: (...) -> List[eTree.Element]

    def _is_same_line(
        l,  # type: eTree.Element
    ):
        # type: (...) -> bool

        return l.get("itemId") == item_id and l.get("partCode") == part_code and l.get("lineNumber") == line_number

    if order is None:
        return []

    return filter(lambda l: _is_same_line(l), order.findall("SaleLine"))
