# -*- coding: utf-8 -*-
import os
import json

import iso8601
import pyscripts
import cfgtools
import sysactions
import logging

from actions.config import (
    get_hold_time_config,
    should_ask_pager_number,
    should_ask_delivery_type,
    get_default_delivery_type,
    get_pre_account_with_discount,
    get_pre_account_allow_printing,
    can_cancel_tef_payment_on_error,
    can_create_table_parallel_order,
    considers_tef_on_bordereau,
)
from actions.printer import nreports_preview_and_print
from actions.customer import (
    validate_required_document,
    get_document_required_value,
)
from actions.discount.clean_discounts import clean_orders_discounts
from actions.eft.cache_eft import reprint_receipt
from actions.login.common_login import common_login
from actions.loyalty import (
    migrate_order_loyalty_info_to_table,
    check_loyalty_totaled_message,
    update_tender_details_with_discount_data,
)
from actions.order import update_tenders
from actions.production import get_courses
from actions.table import (
    create_table_order,
    validate_if_table_opened_another_pos,
)
from actions.util import (
    get_user_level,
    get_all_tender_types,
    get_tender_type,
)
from api.order import get_order_service
from cfgtools import Configuration

from api.order import get_order_service
from api.nreports import get_nreports_service
from manager.reports import justification_report
from messagebus import DataType
from model.customexception import (
    RequiredDocumentException,
    InvalidTipError,
    ErrorGettingDevice,
    PrinterError,
    SystemUserAccessException,
)
from posot import (
    OrderTaker,
)

from posactions import get_printer_receipt

from typing import (
    Any,
    Dict,
    List,
    Optional,
    Tuple,
    Union,
)

from datetime import date, datetime, timedelta
from actions.models import (
    UserLevels,
    SaleTypes,
    Course,
)
from actions.fiscal import (
    check_certificate,
    check_can_fiscalize_order_action,
)
from actions.models.mw_sys_errors import MwSysErrors
from actions.util import (
    check_sangria,
    do_set_drawer_status,
    get_authorization,
    get_drawer_amount,
    get_working_type,
    is_sangria_enable,
)
from application.domain import Table
from application.domain.config import StaticConfig, StaticConfigJsonEncoder
from application.interactor import (
    DoChangeTipInteractor,
    StartServiceInteractor,
    ReopenTableInteractor,
    CloseTableInteractor,
    SetTableAvailableInteractor,
    ChooseLineSeatInteractor,
    ServicePeriodInteractor,
    OpenDayInteractor,
    UserInteractor,
    TableInteractor,
    LoyaltyInteractor,
    PrinterInteractor,
    OrderInteractor,
)

from application.interactor.service.impl import TableReleaserImpl
from cfgtools import Group, Key
from actions.login.unpause_user import unpause_user
from msgbusboundary import MsgBusTableService as tableService
from msgbusboundary.ui import MsgBusScreenChanger
from old_helper import round_half_away_from_zero, convert_from_utf_to_localtime

from actions.loyalty import get_loyalty_document
from utilconfigurations import (
    get_ask_document_pinpad,
    must_require_void_reason,
    read_int_value_from_loader,
)
from utilfunctions import (
    get_sector_name,
    get_device_service,
    is_valid_string,
    get_seconds_to_renew_cache,
    reached_seconds_to_renew_cache,
    get_customer_doc_after_paid,
    update_custom_properties,
    get_tenders_options_dict,
    get_card_brands,
    options_dict_to_str_list,
    is_electronic_payment,
    get_customer_info_config,
    send_order_canceled_event,
    get_order_tender,
    show_order_tenders_options,
    get_custom_property_value,
    is_stored_order,
    tender_has_receipt_card,
    get_translated_exception_message,
    get_device_service_error_message,
    get_sale_line_to_tag,
    get_user_id_from_session,
    show_applied_discount_and_pending_value_to_pay,
    value_is_close_to,
)
from actions.void.utils import (
    do_confirm_void,
    get_void_reason_id,
    get_fixed_void_reason,
    get_canceled_by,
)
from sysactions import (
    action,
    get_posot,
    get_model,
    change_screen,
    show_messagebox,
    AuthenticationFailed,
    get_user_information,
    print_report,
    send_message,
    get_current_order,
    format_amount,
    get_custom,
    get_poslist,
    translate_message,
    get_current_operator,
    get_business_period,
    can_void_line,
    get_storewide_config,
    get_posfunction,
    get_podtype,
    get_operator_session,
    has_current_order,
    StopAction,
    show_listbox,
)
from mw_helper import ModelService

from msgbus import (
    FM_PARAM,
    TK_SYS_ACK,
    TK_PRN_PRINT,
    MBMessage,
    TK_POS_BUSINESSBEGIN,
    TK_PERSIST_SEQUENCER_RESET,
    MBTimeout,
    MBException,
    FM_STRING,
)
from systools import (
    sys_log_error,
    sys_log_exception,
)
from tablemgrapi import get_pos_ts, TableServiceException
from xml.etree import cElementTree as eTree
from bustoken import (
    TK_FISCALWRAPPER_PROCESS_REQUEST,
    TK_REMOTE_ORDER_GET_STORED_ORDERS,
    TK_REMOTE_ORDER_GET_VOIDED_ORDERS,
    TK_REMOTE_ORDER_GET_CONFIRMED_ORDERS,
    TK_SLCTRL_OMGR_SET_ORDERS_ITEMS_PROPERTY,
)
from helper import remove_accents
from mw_helper import (
    get_modifier_prefixes,
    show_message_options_dialog,
    show_message_dialog,
    show_numpad_dialog,
    show_filtered_list_box_dialog,
    show_tab_customer_dialog,
    show_keyboard,
    split_and_translate_message,
)
from pos_model import (
    TableStatus,
    TableOperationMode,
    OrderStatus,
)
from actions.login.utils import save_last_authorization
from actions.eft import (
    request_eft,
    has_electronic_payment,
    cancel_eft_transaction,
    set_eft_cache,
)
from actions.eft.util_eft import (
    get_tender_details,
    check_eft_transaction,
    get_tender_id,
    save_cancel_receipt,
)
from actions.util.get_menu_user_logged import (
    clear_menu_manager_authenticated_user,
    get_menu_manager_authenticated_user,
    set_menu_manager_authenticated_user,
)
from actions.coupon.display_coupon import remove_discounts
from actions.void.utils import void_order
from actions.util.list_users import list_all_opened_users
from actions.discount.apply_discount import apply_discount
from actions.coupon.display_coupon import (
    display_coupons,
)
from actions.delivery import get_remote_order_status
from actions.service_sector.operator_by_sector import get_operator_sector_info
from custom_sysactions import (
    block_action_if_order_items_not_resolved,
    block_action_if_pos_is_blocked,
    initialize_manager_authorizations,
    user_authorized_to_execute_action,
    user_must_be_logged,
    release_user_lock_action_after_execution,
)
from threading import Lock

from pos_model import ProductType

DELIVERY_STORED_ORDERS = {}
DELIVERY_VOIDED_ORDERS = {}
selected_tables = {}
max_number_of_seats = 99
max_number_of_tab_digits = None  # type: Union[None, int]
set_table_available_automatically = None
tip_rate = None
table_operation_mode = None     # type: Union[None, str]
can_see_table_from_another_operator = None
enable_tab_buttons = None
fetch_stored_orders_timeout = None  # type: Union[None, float]
print_operator_copy = None
print_tef_customer_receipt = True
print_tef_merchant_receipt = False
sat_info = {}
print_pre_account_after = []
can_close_operator_with_opened_table = False
min_level_needed_to_see_all_tables = None  # type: Union[None, int]
transfer_table_allowed_status = None
transfer_order_table_allowed_status = None
join_table_allowed_status = None
ask_change_as_tip = None
table_service = None
last_cache_time = None
max_seats_reached = ""
table_service_mode = ""
customer_info_tab = []
item_division_limit = None  # type: Optional[int]
pos_static_config = {}

lock = Lock()
mbcontext = pyscripts.mbcontext
common_login.mbcontext = mbcontext
logger = logging.getLogger("TableActions")

HAS_REMOTE_ORDER = mbcontext.MB_LocateService(mbcontext.hv_service, 'RemoteOrder', maxretries=1)

AUTH_LEVEL_KEY = 'Authorization Level'

YES_NO_BUTTONS = "$YES|$NO"
OK_CANCEL_BUTTONS = "$OK|$CANCEL"
CONFIRMATION_TITLE = "$CONFIRMATION"



def main():
    try:
        _get_config()

        # This forces a POS Model update with current Store Tables layout
        for pos_id in get_poslist():
            model = get_model(pos_id)
            if has_table_service():
                posts = get_pos_ts(model)
                posts.storePicture(force_update=True)

    except TableServiceException as e:
        sys_log_error("Error initializing list of tables: %s" % str(e))


def _get_config():
    global max_number_of_seats
    global max_number_of_tab_digits
    global set_table_available_automatically
    global tip_rate
    global table_operation_mode
    global can_see_table_from_another_operator
    global fetch_stored_orders_timeout
    global print_operator_copy
    global print_tef_merchant_receipt
    global print_tef_customer_receipt
    global can_close_operator_with_opened_table
    global min_level_needed_to_see_all_tables
    global transfer_table_allowed_status
    global transfer_order_table_allowed_status
    global join_table_allowed_status
    global ask_change_as_tip
    global max_seats_reached
    global table_service_mode
    global customer_info_tab
    global item_division_limit
    global print_pre_account_after

    config = cfgtools.read(os.environ["LOADERCFG"])
    min_level_needed_to_see_all_tables = int(config.find_value("Customizations/MinLevelNeededToSeeAllTables") or 20)
    max_number_of_seats = int(config.find_value("Customizations.MaxNumberOfSeats") or 99)
    max_number_of_tab_digits = int(config.find_value("Customizations.MaxNumberOfTabDigits") or 13)

    set_table_available_automatically = \
        (config.find_value("Customizations.SetTableAvailableAutomatically") or "true").lower() == "true"
    tip_rate = get_storewide_config("Store.TipRate", defval="10")
    table_operation_mode = \
        (config.find_value("Customizations.TableOperationMode") or TableOperationMode.INDIVIDUAL).upper()
    can_see_table_from_another_operator = \
        (config.find_value("Customizations.CanSeeTableFromAnotherOperator") or "true").lower() == "true"
    print_operator_copy = (config.find_value("Customizations.PrintOperatorCopy") or "false").lower() == "true"

    print_tef_customer_receipt = config.find_value("Customizations.PrintTefCustomerReceipt", "false").lower() == "true"
    print_tef_merchant_receipt = config.find_value("Customizations.PrintTefMerchantReceipt", "false").lower() == "true"

    print_pre_account_after = config.find_values("Customizations.PrintPreAccountAfter") or []
    print_pre_account_after = [item.upper() for item in print_pre_account_after]
    can_close_operator_with_opened_table = config.find_value("Customizations.CanCloseOperatorWithOpenedTable",
                                                             "false").lower() == "true"

    table_status_for_action = "Customizations/TableStatusForAction/"
    transfer_table_allowed_status = config.find_values(table_status_for_action + "do_transfer_table") or []
    transfer_order_table_allowed_status = config.find_values(table_status_for_action + "do_transfer_order") or []
    join_table_allowed_status = config.find_values(table_status_for_action + "do_join_table") or []

    ask_change_as_tip = (config.find_value("Customizations.AskChangeAsTip") or "false").lower() == "true"
    max_seats_reached = config.find_value("Customizations.MaxSeatsReached", "")

    initialize_manager_authorizations(config)

    table_service_mode = config.find_value("Customizations.SelectedTableServiceMode", "seats")

    customer_info_tab = _get_customer_info_tab(config)

    item_division_limit = int(config.find_value(
        "Customizations.ItemDivisionLimit",
        default="50",
    ))


@action
def get_static_config(pos_id):
    global enable_tab_buttons

    if pos_id in pos_static_config:
        return pos_static_config[pos_id]

    config = cfgtools.read(os.environ["LOADERCFG"])
    enable_tab_buttons = (config.find_value("Customizations.EnableTabBtns") or "true").lower() == "true"
    enabled_tags = _get_enabled_tags(config)
    price_override_enabled = _get_enable_price_override(pos_id)

    show_in_dashboard = []
    if sysactions.get_cfg(pos_id).key("ShowInDashboard") is not None:
        show_in_dashboard = sysactions.get_cfg(pos_id).key("ShowInDashboard").values

    available_sale_types = _get_available_sale_types(pos_id)
    idle_alert_time, warning_alert_time, table_opened_alert_time, delivery_alert_time = get_alert_times_config(config)
    cancel_timeout_window, screen_timeout = get_screen_timeout_config(pos_id)
    products_screen_dimensions = get_products_screen_dimensions_config(pos_id)
    spinner_config = get_spinner_config(config)
    ask_pager_number = should_ask_pager_number(pos_id=pos_id)
    pager_mandatory = _is_pager_mandatory(pos_id)

    tender_types = get_all_tender_types()

    can_edit_order = (config.find_value("Customizations.CanEditOrder") or "true").lower() == "true"
    get_sat_config(pos_id, config)
    navigation_config = get_navigation_config(config, pos_id)

    show_cash_in_and_cash_out = config.find_value("Customizations.ShowCashInAndCashOut", "true").lower() == "true"
    show_void_closed_order = config.find_value("Customizations.ShowVoidClosedOrder", "true").lower() == "true"
    cash_payment_enabled = config.find_value("Customizations.CashPaymentEnabled", "true").lower() == "true"
    coupons_enabled = config.find_value("Customizations.CouponsEnabled", "true").lower() == "true"
    discounts_enabled = config.find_value("Customizations.DiscountsEnabled", "true").lower() == "true"
    bill_payment_enabled = config.find_value("Customizations.BillPaymentEnabled", "true").lower() == "true"

    totem_config = _get_totem_config(config, pos_id)
    recall_button = _get_recall_button_config(config)
    operator_button = _get_operator_button_config(config)
    special_modifiers = _get_special_modifiers(config)
    modifier_prefixes = get_modifier_prefixes()
    show_ruptured_products = _get_show_ruptured_products(pos_id)
    delivery_sound = _get_delivery_sound(pos_id)
    delivery_address = _get_delivery_address(config)

    _get_fetch_stored_orders_timeout(pos_id)
    pos_navigation = _get_pos_navigation(pos_id)
    remote_order_status_config = _get_remote_order_status_config(config)
    chat_enabled = config.find_value("Customizations.ChatWithSAC", "false").lower() == "true"
    services_period = _get_services_period()
    menu_test = _get_menu_test_enabled(pos_id)
    sort_table_order_desc = (config.find_value("Customizations.SortTableOrderDesc") or "false").lower() == "true"
    main_navigation = _get_main_navigation_after_login(pos_id)
    has_own_delivery_men = True if UserInteractor().get_own_delivery_men() else False
    scramble_password_num_pad = config.find_value("Customizations.ScramblePasswordNumPad", "false").lower() == "true"
    show_daily_goals_on = _get_daily_goals_on(pos_id)

    customer_info = get_customer_info_config(pos_id)

    store_id = get_storewide_config("Store.Id")
    min_pos_mobile_version = get_storewide_config("Store.MinPOSMobileVersion", "1.7.0.0")

    available_recovery_tabs = _get_recovery_tabs_available(
        pos_id=pos_id,
    )

    pos_config = sysactions.get_cfg(posid=pos_id)

    show_not_required_options = pos_config.find_value(
        "ShowNotRequiredOptions",
        default="true",
    ) == "true"

    show_customer_welcome_message = pos_config.find_value(
        "ShowCustomerWelcomeMessage",
        default="false"
    ) == "true"

    show_customer_final_message = pos_config.find_value(
        "ShowCustomerFinalMessage",
        default="false"
    ) == "true"

    loyalty_interactor = LoyaltyInteractor(pos_id=str(pos_id))
    loyalty_interactor.set_show_customer_welcome_message(
        show=show_customer_welcome_message
    )
    loyalty_interactor.set_show_customer_final_message(
        show=show_customer_final_message
    )

    show_ifood_embeddables, ifood_merchant_ids = _read_embeddalbles_config(config)
    ask_customer_delivery_data = config.find_value("Customizations.DeliveryConfigurations.AskCustomerDeliveryData") or "onStart"

    static_config = StaticConfig(
        time_to_alert_table_opened=table_opened_alert_time,
        time_to_alert_table_is_idle=idle_alert_time,
        time_to_alert_table_is_idle_warning=warning_alert_time,
        time_to_alert_recall_delivery_is_idle=delivery_alert_time,
        recall_button=recall_button,
        operator_button=operator_button,
        enabled_tags=enabled_tags,
        screen_timeout=screen_timeout,
        cancel_timeout_window=cancel_timeout_window,
        show_in_dashboard=show_in_dashboard,
        products_screen_dimensions=products_screen_dimensions,
        spinner_config=spinner_config,
        enable_tab_btns=enable_tab_buttons,
        available_sale_types=available_sale_types,
        fetch_stored_orders_timeout=fetch_stored_orders_timeout,
        tender_types=tender_types,
        price_override_enabled=price_override_enabled,
        table_operation_mode=table_operation_mode,
        can_edit_order=can_edit_order,
        sat_info=sat_info,
        navigation_config=navigation_config,
        show_cash_in_and_cash_out=show_cash_in_and_cash_out,
        show_void_closed_order=show_void_closed_order,
        min_level_needed_to_see_all_tables=min_level_needed_to_see_all_tables,
        totem_config=totem_config,
        cash_payment_enabled=cash_payment_enabled,
        coupons_enabled=coupons_enabled,
        discounts_enabled=discounts_enabled,
        bill_payment_enabled=bill_payment_enabled,
        special_modifiers=special_modifiers,
        modifier_prefixes=modifier_prefixes,
        show_ruptured_products=show_ruptured_products,
        pos_navigation=pos_navigation,
        remote_order_status=remote_order_status_config,
        delivery_sound=delivery_sound,
        delivery_address=delivery_address,
        chat_enabled=chat_enabled,
        services_period=services_period,
        menu_test=menu_test,
        pre_account_with_discount=get_pre_account_with_discount(),
        main_navigation=main_navigation,
        sort_table_order_desc=sort_table_order_desc,
        has_own_delivery_men=has_own_delivery_men,
        customer_info=customer_info,
        table_service_mode=table_service_mode,
        customer_info_tab=customer_info_tab,
        scramble_password_num_pad=scramble_password_num_pad,
        store_id=store_id,
        show_daily_goals_on=show_daily_goals_on,
        min_pos_mobile_version=min_pos_mobile_version,
        available_recovery_tabs=available_recovery_tabs,
        show_not_required_options=show_not_required_options,
        show_ifood_embeddables=show_ifood_embeddables,
        ifood_merchant_ids=ifood_merchant_ids,
        ask_pager_number=ask_pager_number,
        considers_tef_on_bordereau=considers_tef_on_bordereau(),
        ask_customer_delivery_data=ask_customer_delivery_data,
        ask_delivery_type=should_ask_delivery_type(),
        default_delivery_type=get_default_delivery_type(),
        pager_mandatory=pager_mandatory
    )

    pos_static_config[pos_id] = json.dumps(static_config, cls=StaticConfigJsonEncoder)
    return pos_static_config[pos_id]


def _read_embeddalbles_config(config):
    show_ifood_embeddables = (config.find_value("IfoodEmbeddables.ShowEmbeddable") or "false").lower() == "true"

    ifood_merchant_ids = []
    if config.find_values("IfoodEmbeddables.MerchantIds") is not None:
        ifood_merchant_ids = config.find_values("IfoodEmbeddables.MerchantIds")

    return show_ifood_embeddables, ifood_merchant_ids


def _get_show_ruptured_products(pos_id):
    return (sysactions.get_cfg(pos_id).key_value("ShowRupturedProducts") or "true").lower() == "true"


def _get_enable_price_override(pos_id):
    return (sysactions.get_cfg(pos_id).key_value("EnablePriceOverride") or "false").lower() == "true"


def _get_pos_navigation(pos_id):
    return sysactions.get_cfg(pos_id).key_value("Navigation") or ""


def _get_daily_goals_on(pos_id):
    return sysactions.get_cfg(pos_id).find_values("ShowDailyGoalsOn") or []


def _get_enabled_tags(config):
    enabled_tags = []
    enabled_tags_group = config.find_group("Customizations.EnabledTags")  # type: Group
    if enabled_tags_group is not None and enabled_tags_group.keys is not None:
        for key in enabled_tags_group.keys:  # type: Key
            enabled_tags.append(key.name)
    return enabled_tags


def _get_available_sale_types(pos_id):
    # type: (int) -> Dict[str, List]

    pos_sale_types = ["saleTypeQuick", "saleTypeTable"]
    group_sale_type = sysactions.get_cfg(pos_id).find_group("saleType")

    available_sale_types = {}

    for pos_sale_type in pos_sale_types:
        available_sale_types[pos_sale_type] = []
        loaded_sale_types = group_sale_type.find_values(pos_sale_type) or [] if group_sale_type else []
        for sale_type in loaded_sale_types:
            if '|' in sale_type:
                available_sale_types[pos_sale_type].append([SaleTypes.get_name(s) for s in sale_type.split('|')])
            else:
                available_sale_types[pos_sale_type].append([SaleTypes.get_name(sale_type)])

    return available_sale_types


def _get_main_navigation_after_login(pos_id):
    # type: (str) -> str

    default_main_screen = "quick" if get_working_type(pos_id) != "TS" else "table"
    main_screen = _get_main_screen(pos_id, default_main_screen)

    try:
        pod_type = sysactions.get_cfg(pos_id).key_value("pod") or 'FL'
        is_quick_service = pod_type in ["FC", "FL"]
        is_table_service = pod_type in ["TS", "FL"]

        if main_screen == "ORDER":
            return main_screen if is_quick_service else "TABLE"

        if main_screen == "TABLE":
            return main_screen if is_table_service else "ORDER"

        return main_screen
    except (Exception,):
        return main_screen


def _get_main_screen(pos_id, default_main_screen):
    # type: (str, str) -> str

    navigations = {"quick": 'ORDER', "table": 'TABLE', "recall": "RECALL"}
    try:
        navigate = sysactions.get_cfg(pos_id).find_value("mainScreen") or default_main_screen
        return navigations[navigate.lower()]
    except (Exception,):
        return navigations[default_main_screen]


def get_sat_config(pos_id, config):
    global sat_info
    from posactions import get_nf_type

    sat_info["enabled"] = False
    sat_info["timeout"] = 0
    sat_info["display"] = False

    if get_nf_type(pos_id) in ['SAT', 'MFE']:
        sat_info["display"] = True
        sat_info["enabled"] = True
        sat_info["timeout"] = int(config.find_value("Customizations/UpdateSatInfoInterval") or 60)


CACHED_STORED_ORDERS = []
LAST_STORED_ORDER_CACHE = None


def get_orders_grouped_by_sale_type(
    length_only,            # type: bool
    delivery_order_ids,     # type: bool
):
    # type: (...) -> str

    global CACHED_STORED_ORDERS
    global DELIVERY_STORED_ORDERS
    global DELIVERY_VOIDED_ORDERS

    orders_grouped_by_sale_type = {}
    if delivery_order_ids:
        orders_grouped_by_sale_type['DeliveryIds'] = []
        orders_grouped_by_sale_type['DELIVERY_ERROR'] = 0

        for order in DELIVERY_STORED_ORDERS:
            if order['id'] not in orders_grouped_by_sale_type['DeliveryIds']:
                orders_grouped_by_sale_type['DeliveryIds'].append(order['id'])

        for order in DELIVERY_VOIDED_ORDERS:
            if order['id'] not in orders_grouped_by_sale_type['DeliveryIds']:
                orders_grouped_by_sale_type['DeliveryIds'].append(order['id'])
                orders_grouped_by_sale_type['DELIVERY_ERROR'] += 1
    if length_only:
        for order in CACHED_STORED_ORDERS:
            order_sale_type = order.get("saleType")
            if not order_sale_type:
                continue

            if order_sale_type not in orders_grouped_by_sale_type:
                orders_grouped_by_sale_type[order['saleType']] = 1
            else:
                orders_grouped_by_sale_type[order['saleType']] += 1
        return json.dumps(orders_grouped_by_sale_type)
    return json.dumps(CACHED_STORED_ORDERS)


@action
def get_stored_orders(
    pos_id,                         # type: str
    length_only='false',            # type: Optional[str]
    update_cache='false',           # type: Optional[str]
    delivery_order_ids='false',     # type: Optional[str]
):
    # type: (...) -> str

    global LAST_STORED_ORDER_CACHE
    global CACHED_STORED_ORDERS

    length_only = length_only != 'false'
    update_cache = update_cache != 'false'
    delivery_order_ids = delivery_order_ids != 'false'

    try:
        datetime_now = datetime.now()
        if LAST_STORED_ORDER_CACHE is None:
            LAST_STORED_ORDER_CACHE = datetime_now

        if fetch_stored_orders_timeout is None:
            _get_fetch_stored_orders_timeout(pos_id)

        cache_datetime_limit = LAST_STORED_ORDER_CACHE + timedelta(seconds=fetch_stored_orders_timeout)
        if update_cache or (not CACHED_STORED_ORDERS or datetime_now > cache_datetime_limit):
            renewed_stored_orders = _get_renewed_stored_order(pos_id=pos_id)

            CACHED_STORED_ORDERS = renewed_stored_orders
            LAST_STORED_ORDER_CACHE = datetime_now

    except (Exception,):
        message = "Error getting list of stored orders"
        logger.exception(message)
        sys_log_exception(message)

    grouped_orders_response = get_orders_grouped_by_sale_type(
        length_only=length_only,
        delivery_order_ids=delivery_order_ids,
    )
    return grouped_orders_response


@action
def get_delivery_stored_orders(_=None, return_json=True):
    try:
        return_paid_orders = True
        include_items = False
        data = str(return_paid_orders) + "\0" + str(include_items)
        msg = mbcontext.MB_EasySendMessage("RemoteOrder",
                                           TK_REMOTE_ORDER_GET_STORED_ORDERS,
                                           FM_PARAM,
                                           data)
        if msg.token != TK_SYS_ACK:
            logger.error("Error retrieving stored orders: " + msg.data)
            return ""

        return get_delivery_orders(msg, return_json, True)
    except (Exception,):
        return


@action
def get_delivery_errors_orders(_, return_json=True):
    try:
        msg = mbcontext.MB_EasySendMessage("RemoteOrder", TK_REMOTE_ORDER_GET_VOIDED_ORDERS)
        if msg.token != TK_SYS_ACK:
            logger.error("Error retrieving error orders: " + msg.data)
            return ""

        return get_delivery_orders(msg, return_json, renew_voided_global=True)
    except (Exception,):
        return


@action
def get_delivery_confirmed_orders(_, return_json=True):
    try:
        msg = mbcontext.MB_EasySendMessage("RemoteOrder", TK_REMOTE_ORDER_GET_CONFIRMED_ORDERS)
        if msg.token != TK_SYS_ACK:
            logger.error("Error retrieving confirmed orders: " + msg.data)
            return ""

        return get_delivery_orders(msg, return_json)
    except (Exception,):
        return


def _get_renewed_stored_order(
    pos_id,     # type str
):
    # type: (...) -> List

    global DELIVERY_STORED_ORDERS
    global DELIVERY_VOIDED_ORDERS

    renewed_stored_orders = []
    model = get_model(pos_id)
    pos_ot = get_posot(model)

    limit_datetime = (datetime.now() - timedelta(hours=48)).strftime("%Y-%m-%dT%H:%M:%S")
    all_stored_orders = pos_ot.listOrders("STORED", since=limit_datetime, exclppty='TABLE_ID')
    for order in all_stored_orders:
        if order["podType"] in ["FC", "FL", "TT"] and order["priceList"] != "DL":
            renewed_stored_orders.append(order)

    if HAS_REMOTE_ORDER:
        get_delivery_stored_orders(pos_id, False)
        get_delivery_errors_orders(pos_id, False)
    for order in DELIVERY_STORED_ORDERS:
        if order['id'] not in [int(x['orderId']) for x in renewed_stored_orders if 'orderId' in x]:
            renewed_stored_orders.append(order)
    for order in DELIVERY_VOIDED_ORDERS:
        if order['id'] not in [int(x['orderId']) for x in renewed_stored_orders if 'orderId' in x]:
            renewed_stored_orders.append(order)

    return renewed_stored_orders


def get_delivery_orders(msg, return_json, renew_stored_global=False, renew_voided_global=False):
    global DELIVERY_STORED_ORDERS
    global DELIVERY_VOIDED_ORDERS
    orders = []
    if msg.data:
        orders = [order for order in json.loads(msg.data)]
        for order in orders[:]:

            out_of_schedule_time = False
            if "SCHEDULE_TIME" in order["customProps"]:
                schedule_time = iso8601.parse_date(order["customProps"]["SCHEDULE_TIME"])
                now = convert_from_utf_to_localtime(datetime.utcnow())
                schedule_time = convert_from_utf_to_localtime(schedule_time)
                delta = schedule_time - now
                out_of_schedule_time = delta > timedelta(hours=1)
                order["customProps"]["SCHEDULE_TIME"] = schedule_time.strftime("%H:%M:%S")

            if out_of_schedule_time:
                orders.remove(order)

            if 'SHORT_REFERENCE' in order['customProps']:
                order['idDescription'] = str(order['id']) + " / " + order['customProps']['SHORT_REFERENCE']
            else:
                order['idDescription'] = str(order['id']) + " / "
            order['remoteId'] = str(order['remoteId'])

            if order['receiveTime'] is not None:
                formatted_date = datetime.strptime(order['receiveTime'], "%Y-%m-%dT%H:%M:%SZ")
                receive_time = convert_from_utf_to_localtime(formatted_date)
                order['receiveTime'] = receive_time.strftime("%d/%m/%Y %H:%M:%S")
            order['saleType'] = "DELIVERY"

            if "DISPLAY_PARTNER" in order["customProps"]:
                order['partner'] = order["customProps"]["DISPLAY_PARTNER"]

            if 'DELIVERY_ERROR_TYPE' in order['customProps']:
                order['deliveryErrorType'] = order['customProps']['DELIVERY_ERROR_TYPE']
    if renew_stored_global:
        DELIVERY_STORED_ORDERS = orders
    if renew_voided_global:
        DELIVERY_VOIDED_ORDERS = orders
    if not return_json:
        return orders
    return json.dumps(orders)


def get_alert_times_config(config):
    time_to_alert_table_opened = read_int_value_from_loader(
        loader=config,
        key="Customizations.TimeToAlertAfterTableOpened",
        default_value=99,
    )
    time_to_alert_table_is_idle = read_int_value_from_loader(
        loader=config,
        key="Customizations.TimeToAlertTableIsIdle",
        default_value=99,
    )
    time_to_alert_table_is_idle_warning = read_int_value_from_loader(
        loader=config,
        key="Customizations.TimeToAlertTableIsIdleWarning",
        default_value=99,
    )
    time_to_alert_recall_delivery_is_idle = read_int_value_from_loader(
        loader=config,
        key="Customizations.TimeToAlertRecallDeliveryIsIdle",
        default_value=99,
    )

    return (
        time_to_alert_table_is_idle,
        time_to_alert_table_is_idle_warning,
        time_to_alert_table_opened,
        time_to_alert_recall_delivery_is_idle
    )


def get_screen_timeout_config(pos_id):
    screen_timeout = -1
    cancel_timeout_window = -1

    time_out_group = sysactions.get_cfg(pos_id).find_group("TimeoutConfigurations")
    if time_out_group is not None:
        if time_out_group.key_value("ScreenTimeout") is not None:
            screen_timeout = int(time_out_group.key_value("ScreenTimeout"))

        if time_out_group.key_value("CancelTimeoutWindow") is not None:
            cancel_timeout_window = int(time_out_group.key_value("CancelTimeoutWindow"))

    return cancel_timeout_window, screen_timeout


def get_default_navigation_config(pos_id):
    default_navigation = ""
    if sysactions.get_cfg(pos_id).key_value("DefaultNavigation") is not None:
        default_navigation = sysactions.get_cfg(pos_id).key_value("DefaultNavigation")

    return default_navigation


def get_spinner_config(config):
    spinner_enabled = True
    if config.find_value("Customizations/Spinner/Enabled") is not None:
        spinner_enabled = config.find_value("Customizations/Spinner/Enabled").lower() == 'true'

    spinner_type = "defaultSpinner"
    if config.find_value("Customizations/Spinner/Type") is not None:
        spinner_type = config.find_value("Customizations/Spinner/Type")

    spinner_config = {"enabled": spinner_enabled, "type": spinner_type}
    return spinner_config




def get_navigation_config(
        config,      # type: Configuration
        pos_id       # type: int
):
    # type: (...) -> Dict[str, bool]

    show_coupon_screen = False
    if config.find_value("Customizations/NavigationConfig/ShowCouponScreen") is not None:
        show_coupon_screen = config.find_value("Customizations/NavigationConfig/ShowCouponScreen").lower() == 'true'

    show_barcode_screen = False

    show_barcode_screen_pos_ctrl = sysactions.get_cfg(pos_id).find_value("BarcodeScreen/ShowBarcodeScreen")
    show_barcode_screen_py_script = config.find_value("Customizations/NavigationConfig/ShowBarcodeScreen")

    if show_barcode_screen_pos_ctrl is not None:
        show_barcode_screen = show_barcode_screen_pos_ctrl.lower() == 'true'
    elif show_barcode_screen_py_script is not None:
        show_barcode_screen = show_barcode_screen_py_script.lower() == 'true'

    barcoded_is_default_navigation = False
    barcode_screen_default_navigation = sysactions.get_cfg(pos_id).find_value("BarcodeScreen/DefaultNavigation")
    if barcode_screen_default_navigation is not None and show_barcode_screen:
        barcoded_is_default_navigation = barcode_screen_default_navigation.lower() == 'true'

    show_search_screen = False
    if config.find_value("Customizations/NavigationConfig/ShowSearchScreen") is not None:
        show_search_screen = config.find_value("Customizations/NavigationConfig/ShowSearchScreen").lower() == 'true'

    show_customer_info = True
    if sysactions.get_cfg(pos_id).find_value("ShowCustomerInfo") is not None:
        show_customer_info = sysactions.get_cfg(pos_id).find_value("ShowCustomerInfo") == 'true'

    navigation_config = {
        "showCouponScreen": show_coupon_screen,
        "showBarcodeScreen": show_barcode_screen,
        "showSearchScreen": show_search_screen,
        "showCustomerInfo": show_customer_info,
        "BarcodedIsDefaultNavigation": barcoded_is_default_navigation
    }
    return navigation_config

def _get_recall_button_config(config):
    recall_enabled = config.find_value("Customizations/HeaderConfig/RecallButton", "true").lower() == 'true'

    return recall_enabled


def _get_operator_button_config(config):
    operator_enabled = config.find_value("Customizations/HeaderConfig/OperatorButton", "true").lower() == 'true'

    return operator_enabled


def _get_remote_order_status_config(config):
    group_path = "Customizations/DeliveryStatusOnFooter/"
    enabled = (config.find_value(group_path + "Enabled") or "false").lower() == "true"
    fetch_timeout = int(config.find_value(group_path + "FetchRemoteOrderStatusTimeout") or 10)

    remote_order_store_status = json.loads(get_remote_order_status(default=True))

    remote_order_status_config = {
        "enabled": enabled,
        "isOnline": remote_order_store_status.get("isOnline"),
        "isOpened": remote_order_store_status.get("isOpened"),
        "lastExternalContact": remote_order_store_status.get("lastExternalContact"),
        "closedSince": remote_order_store_status.get("closedSince"),
        "fetchTimeout": fetch_timeout
    }

    return remote_order_status_config


def _is_pager_mandatory(
        pos_id       # type: int
):
    # type: (...) -> bool
    return sysactions.get_cfg(pos_id).find_value("PagerMandatory", "false").lower() == "true"


def _get_delivery_sound(pos_id):
    return (sysactions.get_cfg(pos_id).key_value("DeliverySound") or "false").lower() == "true"


def _get_delivery_address(config):
    delivery_address = config.find_value("Customizations/DeliveryAddress", "true").lower() == 'true'

    return delivery_address


def _get_fetch_stored_orders_timeout(pos_id):
    global fetch_stored_orders_timeout
    fetch_stored_orders_timeout = int(sysactions.get_cfg(pos_id).key_value("UpdateStoredOrdersCacheDuration") or 30)


def _get_menu_test_enabled(pos_id):
    menu_test = sysactions.get_cfg(pos_id).key_value("menuTest")
    if menu_test is None:
        return False
    return menu_test.lower() == "true"


def _get_customer_info_tab(config):
    customer_info = config.find_values("Customizations/CustomerInfoTab") \
        if config.find_values("Customizations/CustomerInfoTab") is not None else []

    return customer_info


def _get_special_modifiers(config):
    special_modifiers = config.find_values("Customizations/SpecialModifiers") \
        if config.find_values("Customizations/SpecialModifiers") is not None else []

    return special_modifiers


def _get_totem_config(config, pos_id):
    show_barcode, show_search, show_customer = _get_totem_navigation_config(config)
    show_customer_pos = sysactions.get_cfg(pos_id).find_value("CustomerInfoRequest/ShowCustomerInfo")
    if show_customer_pos is not None:
        show_customer = show_customer_pos.lower() == 'true'

    welcome_bg_format, show_popup = _get_totem_welcome_config(config)
    show_coupon, sale_type_show_image = _get_totem_sale_type_show_image(config)
    horizontal_banner, banner_side = _get_totem_banner_config(config)
    confirmation_timeout = _get_totem_confirmation_config(config)
    show_popup_combo = _get_totem_products_config(config)
    document_required_value = get_document_required_value()
    show_discount = _get_totem_discounts_config(config=config)

    totem_config = {
        "navigation": {
            "showSearchScreen": show_barcode,
            "showBarcodeScreen": show_search,
            "showCustomerInfo": show_customer
        },
        "welcomeScreen": {
            "backgroundFormat": welcome_bg_format,
            "showPopup": show_popup
        },
        "saleType": {
            "showImage": sale_type_show_image,
            "showCouponScreen": show_coupon
        },
        "banner": {
            "horizontal": horizontal_banner,
            "side": banner_side
        },
        "confirmationScreen": {
            "timeout": confirmation_timeout
        },
        "products": {
            "showPopupCombo": show_popup_combo
        },
        "discount": {
            "showDiscount": show_discount
        },
        "customerInfo": {
            "documentRequiredValue": document_required_value
        }
    }

    return totem_config


def _get_totem_navigation_config(config):
    navigation_path = "TotemConfig/Navigation/"
    show_barcode = config.find_value(navigation_path + "ShowBarcodeScreen", "true").lower() == 'true'
    show_search = config.find_value(navigation_path + "ShowSearchScreen", "true").lower() == 'true'
    show_customer = config.find_value(navigation_path + "ShowCustomerInfo", "true").lower() == 'true'

    return show_barcode, show_search, show_customer


def _get_totem_welcome_config(config):
    welcome_path = "TotemConfig/WelcomeScreen/"
    bg_format = config.find_value(welcome_path + "BackgroundFormat")
    show_popup = config.find_value(welcome_path + "ShowPopup", "true").lower() == 'true'

    return bg_format if bg_format is not None else "image", show_popup


def _get_totem_sale_type_show_image(config):
    sale_type_path = "TotemConfig/SaleType/"
    show_coupon = config.find_value(sale_type_path + "ShowCouponScreen", "true").lower() == 'true'
    show_image = config.find_value(sale_type_path + "ShowImage", "false").lower() == 'true'

    return show_coupon, show_image


def _get_totem_banner_config(config):
    banner_path = "TotemConfig/Banner/"
    horizontal = config.find_value(banner_path + "Horizontal", "false").lower() == 'true'
    side = config.find_value(banner_path + "Side")

    return horizontal, side if side is not None else "left"


def _get_totem_confirmation_config(config):
    confirmation_path = "TotemConfig/ConfirmationScreen/"
    timeout = config.find_value(confirmation_path + "Timeout")

    return int(timeout) if timeout is not None else 10


def _get_totem_products_config(config):
    confirmation_path = "TotemConfig/Products/"
    show_popup_combo = config.find_value(confirmation_path + "ShowPopupCombo", "false")

    return show_popup_combo.lower() == 'true'



def _get_totem_discounts_config(
    config,     # type: Configuration
):
    # type: (...) -> bool

    discount_path = "TotemConfig/Discounts/"
    show_discounts = config.find_value(discount_path + "ShowDiscounts", "true")

    return show_discounts.lower() == 'true'


def get_products_screen_dimensions_config(pos_id):
    products_screen_rows = sysactions.get_cfg(pos_id).find_value("ProductsScreenDimension/Rows")
    products_screen_columns = sysactions.get_cfg(pos_id).find_value("ProductsScreenDimension/Columns")
    products_screen_dimensions = {"rows": products_screen_rows, "columns": products_screen_columns}

    return products_screen_dimensions


def _get_services_period():
    # type: () -> List

    return ServicePeriodInteractor().get_services_period()


def _get_table_list(
    all_tables,         # type: List[Table]
    source_table_id,    # type: str
    tab_list=False      # type: Optional[bool]
):
    # type: (...) -> List[tuple]

    if not tab_list and table_service_mode == "tabs":
        return []

    table_list = [
        (
            {str(table.id): table.tab_id} if tab_list else str(table.id),
            int(table.status),
            str(table.name)
        )
        for table in all_tables
        if (
            str(table.status) in transfer_order_table_allowed_status
            and ("TAB" in table.id if tab_list else "TAB" not in table.id)
            and source_table_id != table.id
            and (table.tab_id if tab_list else True)
            and (True if tab_list else table.mode != "tabs")
        )
    ]

    return table_list


def allowed_to_create_table(
    pos_id,         # type: str
    table_id,       # type: str
    is_tab=False,   # type: Optional[bool]
):
    # type: (...) -> bool

    table_info = eTree.XML(tableService().get_table_info(
        pos_id=pos_id,
        table_id=table_id,
    )) if not is_tab else None
    model = get_model(posid=pos_id)

    pos_user_id = int(model.find("Operator").get('id'))
    pos_user_level = get_user_level(user_id=pos_user_id)
    if pos_user_level >= UserLevels.MANAGER.value:
        return True

    if table_operation_mode in [TableOperationMode.INDIVIDUAL_SECTOR, TableOperationMode.SHARED_SECTOR]:
        sectors_info = get_operator_sector_info(pos_id=pos_id)
        all_sector_operators = sectors_info.get("$ALL_SECTORS", [])
        if pos_user_id in all_sector_operators:
            return True

        table_sector = tableService().get_table_sector(
            table_picture=table_info,
            is_tab=is_tab,
        )
        if table_sector not in sectors_info or pos_user_id not in sectors_info.get(table_sector):
            return False

    return True


@action
@block_action_if_pos_is_blocked
@user_must_be_logged
def start_service(pos_id, table_id, service_seats=None):
    mode = table_service_mode

    if not allowed_to_create_table(
        pos_id=pos_id,
        table_id=table_id,
    ):
        show_message_dialog(
            pos_id=pos_id,
            title="$INFORMATION",
            message="$OPERATOR_DOES_NOT_BELONG_TO_TABLE_SECTOR",
        )
        return False

    if table_service_mode == 'mixed':
        buttons = "$SEATS|$TAB|$PREVIOUS"
        resp = show_message_options_dialog(pos_id, buttons, "$INFORMATION", "$TABLE_SERVICE_MODE")
        if resp is None or resp == 2:
            return False
        mode = 'tabs' if resp == 1 else 'seats'

    if service_seats is None and mode != 'tabs':
        service_seats = _get_service_seats_number(pos_id, [table_id])
        if not service_seats:
            deselect_table(pos_id)
            return False

    try:
        table_releaser = TableReleaserImpl(tableService())
        screen_changer = MsgBusScreenChanger()
        interactor = StartServiceInteractor(screen_changer, tableService(), table_releaser)
        table_picture = interactor.execute(pos_id, table_id, service_seats, False, mode=mode)
    except (Exception,):
        logger.exception("Error starting table")
        sys_log_exception("Error starting table")
        show_message_dialog(pos_id, "$INFORMATION", "$TABLE_NOT_AVAILABLE")
        return False

    create_table_modified_event(pos_id, table_picture)
    return True


def create_table_modified_event(pos_id, table_picture):
    event_xml = """<Event subject="POS{}" type="TABLE_MODIFIED">
                           <TABLE_MODIFIED>
                           {}
                       </TABLE_MODIFIED>
                   </Event>""".format(pos_id, table_picture)

    mbcontext.MB_EasyEvtSend("POS{}".format(int(pos_id)), "TableSelected", event_xml)


@action
@block_action_if_order_items_not_resolved
def store_table_order(pos_id, table_id, tab_id=None):
    model = get_model(pos_id)
    order = model.find("CurrentOrder/Order")

    # Check if there is ANY item in the Sale
    sale_lines = order.findall("SaleLine")
    deleted_line_numbers = map(lambda x: x.get("lineNumber"),
                               filter(lambda x: x.get("level") == "0" and x.get("qty") == "0", sale_lines))
    active_sale_lines = filter(lambda x: x.get("lineNumber") not in deleted_line_numbers, sale_lines)
    if not active_sale_lines:
        show_message_dialog(pos_id, "$INFORMATION", "$THERE_NO_ITEMS_IN_ORDER")
        return False

    if tab_id is not None:
        pos_ot = sysactions.get_posot(model)
        pos_ot.setOrderCustomProperty("TAB_ID", value=tab_id, orderid=order.get("orderId"))

        table_picture = eTree.XML(text=tableService().get_table_info(pos_id, table_id))
        if not table_picture.get("sliceParentTableId"):
            _set_tab_additional_info(model, order, pos_id, pos_ot)
        else:
            pos_ot.setOrderCustomProperty(
                key="PARENT_ID",
                value=table_picture.get("sliceParentTableId"),
                orderid=order.get("orderId")
            )

    _set_order_hold_items(active_sale_lines, order, pos_id, table_id)

    try:
        success = tableService().store_service(pos_id, table_id, order.get("orderId"))
        if success:
            create_table_modified_event(pos_id, '0')

            if 'STORED' in print_pre_account_after:
                generate_total_report(
                    pos_id=pos_id,
                    table_id=table_id,
                    force_print_by_table=True,
                    force_print_items_conference=True,
                )
        return success
    except Exception as ex:
        table_picture = tableService().get_table_picture(pos_id, table_id)
        create_table_modified_event(pos_id, table_picture)
        show_message_dialog(pos_id, "$ERROR", ex.message)


def _set_tab_additional_info(model, order, pos_id, pos_ot):
    from posactions import is_changing_order

    if not is_changing_order(model=model):
        additional_info = None
        while not additional_info:
            additional_info = sysactions.show_keyboard(pos_id, "$TAB_ADDITIONAL_INFO", title="$TAB_ADDITIONAL_INFO",
                                                       numpad=False)
            if additional_info is None:
                raise StopAction()
            if additional_info == "":
                response = sysactions.show_confirmation(pos_id, "$TAB_ADDITIONAL_INFO_NONE")
                if response:
                    break
        if additional_info:
            pos_ot.setOrderCustomProperty("TAB_ADD_INFO", value=additional_info, orderid=order.get("orderId"))


def _set_order_hold_items(active_sale_lines, order, pos_id, table_id):
    has_hold_items = False
    order_id = order.get("orderId")
    try:
        hold_orders = tableService().get_table_custom_property(pos_id, "ORDERS_HOLD_ITEMS", table_id=table_id)
        hold_orders = json.loads(hold_orders["ORDERS_HOLD_ITEMS"]) if "ORDERS_HOLD_ITEMS" in hold_orders else []
    except ValueError:
        logger.error("Error parsing ORDERS_HOLD_ITEMS - Table: {}".format(table_id))
        hold_orders = []

    for sale_line in active_sale_lines:
        custom_property_list = []
        try:
            if sale_line.get("customProperties"):
                custom_property_list = json.loads(sale_line.get("customProperties"))
        except ValueError:
            logger.error("Error parsing customProperties - Table: {}".format(table_id))

        hold_value = custom_property_list.get("hold")
        if hold_value == "-1":
            has_hold_items = True

            if order_id not in hold_orders:
                hold_orders.append(order_id)
                if not hold_orders:
                    _set_orders_hold_items(pos_id, table_id)
                else:
                    _set_orders_hold_items(pos_id, table_id, json.dumps(hold_orders))
                break

    if not has_hold_items and order_id in hold_orders:
        hold_orders.remove(order_id)
        if not hold_orders:
            _set_orders_hold_items(pos_id, table_id)
        else:
            _set_orders_hold_items(pos_id, table_id, json.dumps(hold_orders))


def _add_parent_table(
        pos_id,             # type: str
        table_id,           # type: str
        parent_table_id     # type: str
):
    # type: (...) -> None

    tableService().set_parent_table(
        pos_id,
        table_id,
        parent_table_id
    )


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

    tableService().set_parent_table(
        pos_id,
        table_id
    )


@action
@user_authorized_to_execute_action
def edit_table_order(pos_id, order_id, table_id=None):
    tableService().set_current_order(pos_id, order_id, table_id)
    return "0"


@action
@user_authorized_to_execute_action
def cancel_table_order(
        pos_id,
        order_id,
        table_id,
        auth_data=None,
):
    # type: (...) -> bool

    return void_table_order_with_void_reason(
        pos_id=pos_id,
        order_id=order_id,
        table_id=table_id,
        auth_data=auth_data
    )


@action
@user_authorized_to_execute_action
def cancel_current_table_order(
    pos_id,             # type: str
    order_id,           # type: str
    auth_data=None,     # type: Optional[str]
):
    # type: (...) -> bool

    table_id = get_custom_property_value(
        pos_id=pos_id,
        custom_property='TABLE_ID',
        order_id=order_id,
    )
    order_id = ""
    return void_table_order_with_void_reason(
        pos_id=pos_id,
        order_id=order_id,
        table_id=table_id,
        auth_data=auth_data,
    )


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

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

    from posactions import select_and_reprint_order
    picklist_printed = select_and_reprint_order(
        pos_ot=pos_ot,
        pos_id=pos_id,
        order_id=order_id,
        search_in_all_table=False,
    )

    if picklist_printed:
        show_messagebox(
            posid=pos_id,
            message="$ORDER_PICKLIST_SUCCESSFULLY_PRINTED|{}".format(order_id),
        )


def _void_table_order(
    model,          # type: eTree
    void_reason,    # type: Dict
    order_id,       # type: str
    pos_id,         # type: str
    table_id,       # type: str
    canceled_by,    # type: str
):
    # type: (...) -> bool

    pos_ot = get_posot(model)
    pos_ot.setOrderCustomProperties(void_reason, str(order_id))
    order_service = get_order_service(pos_id=pos_id)
    current_order = order_service.get_order_picture(order_id=order_id)
    pre_void_order_state = current_order.get("state")

    try:
        if order_id not in ["", "None", None]:
            tableService().void_service_order(pos_id, table_id, order_id)
        else:
            if not void_order(pos_id, void_reason=void_reason):
                order_service = get_order_service(pos_id=pos_id)
                order_service.store_order()
    except Exception:  # noqa
        clear_void_reason_dict = {
            'VOID_REASON_ID': "clear",
            'VOID_REASON_DESCR': "clear",
            "VOID_AUTH_USER": "clear"
        }
        pos_ot.setOrderCustomProperties(clear_void_reason_dict, str(order_id))
        return False

    justification_type = "VOID_STORED_ORDER" if is_stored_order(pos_id=pos_id) else "VOID_CURRENT_ORDER"
    justification_report(
        pos_id=pos_id,
        justification_type=justification_type,
        justification=void_reason.get('VOID_REASON_DESCR'),
        authorized_by=canceled_by,
        order_id=order_id if order_id not in ["", "None", None] else None
    )

    _clear_hold_items(pos_id, table_id, order_id)

    table_picture = tableService().get_table_picture(pos_id, table_id)
    if can_create_table_parallel_order() and pre_void_order_state == "IN_PROGRESS":
        table_picture = "0"

    create_table_modified_event(
        pos_id=pos_id,
        table_picture=table_picture,
    )
    send_order_canceled_event(
        pos_id=pos_id,
        order_id=order_id,
    )

    return True


def void_table_order_without_void_reason(
    pos_id,             # type: str
    order_id,           # type: str
    table_id,           # type: str
    auth_data=None,     # type: Optional[str]
):
    # type: (...) -> bool

    model = get_model(posid=pos_id)
    auth_user = auth_data if auth_data is not None else None
    canceled_by = get_canceled_by(
        pos_id=pos_id,
        model=model,
        auth_user=auth_user,
    )
    void_type = "VOID_STORED_ORDER" if is_stored_order(pos_id=pos_id) else "VOID_CURRENT_ORDER"

    do_confirm_void(
        pos_id=pos_id,
        ignore_timeout=True
    )
    void_reason = get_fixed_void_reason(
        pos_id=pos_id,
        reason="USER_CANCELLATION",
        canceled_by=canceled_by,
        void_type=void_type,
    )

    return _void_table_order(
        model=model,
        void_reason=void_reason,
        order_id=order_id,
        pos_id=pos_id,
        table_id=table_id,
        canceled_by=canceled_by,
    )


def void_table_order_with_void_reason(
    pos_id,             # type: str
    order_id,           # type: str
    table_id,           # type: str
    auth_data=None,     # type: Optional[str]
):
    # type: (...) -> bool

    model = get_model(posid=pos_id)
    auth_user = auth_data if auth_data is not None else None
    canceled_by_current_operator = get_canceled_by(
        pos_id=pos_id,
        model=model,
        auth_user=auth_user,
    )
    void_type = "VOID_STORED_ORDER" if is_stored_order(pos_id=pos_id) else "VOID_CURRENT_ORDER"
    canceled_by_auth_user = sysactions.get_custom(model=model, key="last_auth_user")
    if must_require_void_reason(void_type):
        void_reason = get_void_reason_id(
            pos_id=pos_id,
            void_type=void_type,
            authentication_user=canceled_by_auth_user,
        )
        if not void_reason:
            return True
    else:
        do_confirm_void(pos_id=pos_id)
        void_reason = get_fixed_void_reason(
            pos_id=pos_id,
            reason="USER_CANCELLATION",
            canceled_by=canceled_by_auth_user,
            void_type=void_type,
        )

    return _void_table_order(
        model=model,
        void_reason=void_reason,
        order_id=order_id,
        pos_id=pos_id,
        table_id=table_id,
        canceled_by=canceled_by_current_operator,
    )


def _clear_hold_items(
        pos_id,     # type: str
        table_id,   # type: str
        order_id    # type: Optional[str]
):
    # type: (...) -> None

    try:
        _set_orders_hold_items(pos_id, table_id)
    except (Exception,):
        order_id = "" if order_id is None else order_id
        message = "Error hold items - table_id: {} order_id: {} pos_id: {}".format(table_id, order_id, pos_id)
        logger.exception(message)


@action
def print_table_order(pos_id, order_id):
    return "0"


def allowed_to_open_table(
    pos_id,                     # type: str
    model,                      # type: eTree
    table,                      # type: Table[eTree]
    is_tab=False,               # type: Optional[bool]
):
    # type: (...) -> bool

    if table_operation_mode == TableOperationMode.ALL:
        return True

    pos_user_id = int(model.find("Operator").get('id'))
    pos_user_level = get_user_level(user_id=pos_user_id)
    if pos_user_level >= UserLevels.MANAGER.value or pos_user_level >= min_level_needed_to_see_all_tables:
        return True

    table_status = table.status
    table_id = table.id
    parent_table_id = table.slice_parent_table_id
    table_user_id = table.user_id

    table_user_id = int(table_user_id) if table_user_id else None
    table_picture = None
    if table_status == TableStatus.LINKED:
        table_picture = eTree.XML(tableService().get_table_info(
            pos_id=pos_id,
            table_id=table_id
        ))
        linked_table_id = table_picture.get("parentLinkedTableId")
        table_picture = eTree.XML(tableService().get_table_info(
            pos_id=pos_id,
            table_id=linked_table_id
        ))
        table_user_id = int(table_picture.get('userId'))
    elif parent_table_id and not table_user_id:
        table_picture = eTree.XML(tableService().get_table_info(
            pos_id=pos_id,
            table_id=parent_table_id,
        ))
        table_user_id = int(table_picture.get('userId'))
        is_tab = False

    table_user_id_level = get_user_level(user_id=table_user_id)
    if table_operation_mode == TableOperationMode.INDIVIDUAL:
        return table_user_id == pos_user_id or pos_user_level > table_user_id_level

    if table and table.sub_sector_name and not table_picture:
        table_sector = table.sub_sector_name
    else:
        if table_picture is None:
            table_picture = eTree.XML(tableService().get_table_info(
                pos_id=pos_id,
                table_id=table.id,
            ))
        table_sector = tableService().get_table_sector(
            table_picture=table_picture,
            is_tab=is_tab,
        )

    sectors_info = get_operator_sector_info(pos_id=pos_id)
    all_sector_operators = sectors_info.get("$ALL_SECTORS", [])
    if pos_user_id not in all_sector_operators and table_sector not in sectors_info:
        return False

    is_table_owner = table_user_id == pos_user_id
    can_open_table = (is_table_owner or pos_user_id in sectors_info.get(table_sector, []))
    if table_operation_mode == TableOperationMode.INDIVIDUAL_SECTOR:
        same_user_or_higher_level = is_table_owner or pos_user_level > table_user_id_level
        return can_open_table and same_user_or_higher_level

    if table_operation_mode == TableOperationMode.SHARED_SECTOR:
        return can_open_table

    return False


@action
@user_must_be_logged
def select_table(pos_id, table_id=None):
    model = get_model(pos_id)
    table_id = _validate_table(model, pos_id, table_id)

    try:
        table_picture = eTree.XML(tableService().get_table_info(pos_id, table_id))
    except TableServiceException:
        logger.exception("Error getting order picture")
        try:
            tableService().recall_service(pos_id, table_id)
        except Exception: # noqa
            logger.error("Error recalling service as expected")
        try:
            deselect_table(pos_id)
        except Exception: # noqa
            logger.exception("Error deselecting table?????")
            raise
        return False

    table_is_opened_another_pos = validate_if_table_opened_another_pos(
        pos_id=pos_id,
        current_table_picture=table_picture,
    )
    if table_is_opened_another_pos:
        return False

    if get_posfunction(model) == "OT" and int(table_picture.get('status')) == TableStatus.TOTALED:
        show_message_dialog(pos_id, "$INFORMATION", "$POS_CANNOT_OPEN_THIS_TABLE")
        return False

    if int(table_picture.get('status')) == TableStatus.AVAILABLE:
        return start_service(pos_id, table_id)

    if int(table_picture.get('status')) in [TableStatus.CLOSED, TableStatus.ABANDONED]:
        ret = _process_closed_table(pos_id, table_id)
        if ret:
            create_table_modified_event(pos_id, ret)
            return True
        return False

    if get_posfunction(model) != "CS":
        is_tab = table_id.find("TAB") != -1
        table = Table(
            id=table_picture.get('id'),
            status=table_picture.get('status'),
            slice_parent_table_id=table_picture.get('sliceParentTableId'),
            user_id=table_picture.get('userId'),
            sub_sector_name=table_picture.get('subSectorName'),
        )
        if not allowed_to_open_table(
            pos_id=pos_id,
            model=model,
            table=table,
            is_tab=is_tab,
        ):
            show_message_dialog(
                pos_id=pos_id,
                title="$INFORMATION",
                message="$WITHOUT_PERMISSION_LEVEL_TO_SEE_TABLE",
            )
            return False

    if int(table_picture.get('status')) == TableStatus.LINKED:
        linked_table_id = table_picture.get("parentLinkedTableId")
        linked_table_name = table_picture.get("parentLinkedTableName")
        message = "$LINKED_TABLE_ALERT|{}".format(linked_table_name or linked_table_id)
        ret = show_message_options_dialog(
            pos_id=pos_id,
            buttons="$GO_TO_LINKED_TABLE|$PREVIOUS",
            title="$INFORMATION",
            message=message,
        )
        if ret == 1:
            return False
        table_id = linked_table_id

    current_operator = get_current_operator(model)
    if current_operator is None:
        show_message_options_dialog(
            pos_id=pos_id,
            buttons="$CANCEL",
            title="$INFORMATION",
            message="$SE_OPERNOTLOGGEDIN",
        )
        return False

    create_parallel_order = False
    if _table_has_order_in_progress(table_picture=table_picture):
        table_properties = table_picture.find("Properties")
        table_mode = table_properties.get("MODE") if table_properties is not None else None
        if table_mode == 'tabs':
            message = "$TABLE_ORDER_IN_PROGRESS|{}".format(table_id)
            show_message_dialog(pos_id, "$INFORMATION", message)
            return False

        create_parallel_order = _ask_user_to_create_parallel_order(pos_id=pos_id)
        if not create_parallel_order:
            return False

    table_picture = tableService().recall_service(pos_id, table_id)
    create_table_modified_event(pos_id, table_picture)
    if create_parallel_order:
        create_table_order(
            pos_id=pos_id,
            table_id=table_id,
        )

    return True


def _ask_user_to_create_parallel_order(
    pos_id,     # type: str
):
    # type: (...) -> bool

    ret = show_message_options_dialog(
        pos_id=pos_id,
        buttons=YES_NO_BUTTONS,
        title="$INFORMATION",
        message="$CREATE_PARALLEL_TABLE_ORDER",
    )
    return ret != 1


def _validate_table(model, pos_id, table_id):
    if table_id is None:
        table_id = _get_table_id(
            pos_id=pos_id,
            model=model,
        )
        if not table_id:
            raise StopAction()

    all_tables = model.findall(".//Tables/Table")
    if not any([x.get("id") == table_id for x in all_tables]):
        show_messagebox(pos_id, "$TABLE_NOT_AVAILABLE")
        raise StopAction()

    return table_id


@action
@user_authorized_to_execute_action
def do_apply_discounts(
    pos_id,
    table_id=None,
    level=None,
):

    if table_id is None:
        return apply_discount(pos_id)

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

    order_service = get_order_service(pos_id=pos_id)
    if order.get('state') == "STORED":
        order_service.recall_order(order_id=order.get("orderId"))

    if order.get('state') != "TOTALED":
        order_service.total_order()

    if apply_discount(pos_id, user_auth_level=(level or -1)):
        process_discount_in_table(pos_id, order, table_id)
        if table_id:
            table_picture = tableService().get_table_picture(pos_id, table_id)
            create_table_modified_event(pos_id, table_picture)

        return True

    return False


@action
@user_authorized_to_execute_action
def do_clean_discounts(
    pos_id,         # type: str
    table_id=None,  # type: Optional[str]
):
    # type: (...) -> bool

    return display_coupons(
        pos_id=pos_id,
        table_id=table_id,
    )


def _deselect_tab_action(
    pos_id,             # type: str
    selected_table,     # type: Table
    table_picture,      # type: eTree.XML
    do_raise,           # type: bool
):
    # type: (...) -> Tuple[bool, Any]

    slice_source_id = table_picture.get("sliceSourceTableId")
    tab_id_name = table_picture.find('Properties').get('TAB_ID')
    can_return_to_original_table = _check_can_return_to_original_table(
        pos_id=pos_id,
        slice_source_id=slice_source_id,
        current_table_picture=table_picture,
    )

    if slice_source_id and not tab_id_name:
        option_back_from_source_table = 0
        if enable_tab_buttons:
            options = "$BACK_FROM_SOURCE_TABLE|$DESELECT_TABLE|$CANCEL"
            option_cancel = 2
        else:
            options = "$BACK_FROM_SOURCE_TABLE|$CANCEL"
            option_cancel = 1

        ret = show_message_options_dialog(pos_id, options, "$INFORMATION", "$CHOOSE_DESELECT_TYPE")
        if ret == option_back_from_source_table:
            return True, do_transfer_table(
                pos_id=pos_id,
                parent_table_id=selected_table.id,
                destination_table_id=slice_source_id if can_return_to_original_table else None,
                select_destination_table=False,
                ignore_totaled_orders=True,
            )

        if ret == option_cancel:
            if not do_raise:
                return True, False
            raise StopAction()

    return False, False


@action
def deselect_table(
    pos_id,                     # type: str
    pre_selected_option=False,  # type: Optional[bool]
    do_raise=True               # type: Optional[bool]
):
    # type: (...) -> bool

    try:
        if not has_table_service():
            return True

        tables = list(tableService().list_tables(pos_id))
        selected_table = tableService().get_selected_table(pos_id, tables)
        if not selected_table:
            create_table_modified_event(pos_id, '0')
            return True

        try:
            table_picture = eTree.XML(tableService().get_table_info(pos_id, selected_table.id))
            is_a_tab = tableService().is_a_tab(pos_id, selected_table.id, table_picture)

            if is_a_tab and not pre_selected_option:
                finish_action, element = _deselect_tab_action(
                    pos_id=pos_id,
                    selected_table=selected_table,
                    table_picture=table_picture,
                    do_raise=do_raise,
                )
                if finish_action:
                    return element

            _check_if_tab_id_is_required(pos_id, selected_table.id, table_picture)

        except TableServiceException:
            logger.exception("Error getting table picture")

        try:
            for table in tables:
                if pos_id == table.current_pos_id:
                    tableService().store_service(pos_id, str(table.id))
        except TableServiceException:
            show_message_dialog(pos_id, "$INFORMATION", "$COULD_NOT_DESELECT_TABLE")
            logger.exception("Error getting table picture")
            return False

        create_table_modified_event(pos_id, '0')
        return True
    except sysactions.StopAction:
        return False


def _check_if_tab_id_is_required(
    pos_id,                 # type: str
    table_id,               # type: str
    table_picture=None,     # type: Optional[eTree.XML]
):
    # type: (...) -> bool

    if not table_picture:
        table_picture = eTree.fromstring(tableService().get_table_info(pos_id, table_id))

    table_description = table_picture.get("typeDescr")

    if table_description == "Tab":
        tab_id = tableService().get_table_custom_property(pos_id, "TAB_ID", table_id=table_id)
        if tab_id:
            return False

        customer_data = _get_tab_info(pos_id)
        if not customer_data:
            raise StopAction()
        table_picture, changed_user = _check_tab_user_sector(
            table_picture=table_picture,
            pos_id=pos_id,
        )
        if table_picture is None:
            raise StopAction()
        tab_id = customer_data.get("TAB_NUMBER", None)
        if not tab_id:
            raise StopAction()

        tableService().set_table_custom_property(pos_id, "TAB_ID", tab_id, table_id)
        current_order_table_id = table_picture.get('currentOrderId')
        if current_order_table_id:
            model = get_model(posid=pos_id)
            pos_ot = get_posot(model=model)
            pos_ot.setOrderCustomProperty(
                key="TABLE_NAME",
                value=tab_id,
                orderid=current_order_table_id,
            )

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

        return changed_user


def _format_customer_data(customer):
    # type: (str) -> Dict

    customer_data = json.loads(customer)
    if 'customer_name' in customer_info_tab:
        customer_data['CUSTOMER_NAME'] = remove_accents(customer_data['CUSTOMER_NAME'])

    customer_data['TAB_NUMBER'] = remove_accents(customer_data['TAB_NUMBER'])

    return customer_data


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

    customer_data = {}
    message = None

    while True:
        has_all_values = False
        if message:
            show_message_dialog(
                pos_id=pos_id,
                title="$INFORMATION",
                message=message
            )

        if customer_info_tab:
            customer = show_tab_customer_dialog(pos_id)
            if customer is None:
                return None

            customer_data = _format_customer_data(customer)

            has_all_values = all([False for value in customer_data.values() if len(value) == 0])
            tab_id = customer_data['TAB_NUMBER']

        else:
            tab_id = show_keyboard(
                posid=pos_id,
                message="$TYPE_TAB_ID",
                defvalue="",
                title="",
                numpad=False,
                buttons="$OK|$PREVIOUS",
            )
            if tab_id is not None and len(tab_id) > 0:
                has_all_values = True
                customer_data['TAB_NUMBER'] = remove_accents(tab_id)

        if tab_id is None:
            return None

        if not has_all_values:
            message = "$INVALID_COSTUMER_INFO"
            continue
        if not is_valid_string(tab_id) or len(tab_id) > max_number_of_tab_digits:
            message = "$INVALID_TAB_NAME|{}".format(max_number_of_tab_digits)
            continue
        if 'customer_phone' in customer_info_tab and len(customer_data['CUSTOMER_PHONE']) < 10:
            message = "$INVALID_CUSTOMER_PHONE"
            continue
        if 'customer_name' in customer_info_tab and not is_valid_string(customer_data['CUSTOMER_NAME']):
            message = "$INVALID_CUSTOMER_NAME"
            continue

        try:
            tableService().tab_id_is_valid(pos_id, tab_id)
        except Exception as ex:
            if ex.message == 'TAB_ALREADY_EXISTS':
                show_message_dialog(pos_id, "$INFORMATION", "$TAB_ALREADY_EXISTS")
                continue
            return None

        break
    return customer_data


def register_table_properties(
    pos_id,                 # type: Any[Union[int, str]]
    properties,             # type: Dict[str, Any]
    table_picture=None,     # type: str
    table_id=None           # type: str
):
    # type: (...) -> str

    if not properties or (not table_picture and not table_id):
        return table_picture

    table_picture = table_picture or tableService().get_table_info(pos_id, table_id)
    service_id = eTree.XML(table_picture).get('serviceId')
    for key, value in properties.iteritems():
        if key != 'TAB_NUMBER':
            tableService().set_table_custom_property(pos_id, key, value, service_id=service_id)

    return tableService().get_table_picture(
            pos_id=pos_id,
            table_id=eTree.XML(table_picture).get('id')
    )


@action
@block_action_if_pos_is_blocked
@user_must_be_logged
def open_tab(pos_id, parent_table_id=None):
    if parent_table_id is None and not allowed_to_create_table(
        pos_id=pos_id,
        table_id="",
        is_tab=True,
    ):
        show_message_dialog(
            pos_id=pos_id,
            title="$INFORMATION",
            message="$OPERATOR_DOES_NOT_BELONG_TO_TABLE_SECTOR",
        )
        return False

    customer_data = _get_tab_info(pos_id)
    if not customer_data:
        return False

    tab_id = customer_data.get("TAB_NUMBER", None)
    if not tab_id:
        return False

    table_releaser = TableReleaserImpl(tableService())
    screen_changer = MsgBusScreenChanger()

    interactor = StartServiceInteractor(screen_changer, tableService(), table_releaser)
    parent_table_id = parent_table_id or ""
    table_picture = interactor.execute(pos_id, with_order=False, tab_id=tab_id, source_table_id=parent_table_id)

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

    create_table_modified_event(pos_id, table_picture)

    return True


@action
def select_table_or_tab(pos_id, table_id):
    return select_table(pos_id, table_id)


@action
def clear_current_authorized_manager(pos_id):
    try:
        clear_menu_manager_authenticated_user(int(pos_id))
    except (Exception,):
        logger.exception("Error cleaning current authorized Manager")


@action
def navigate_to_manager_menu(pos_id):
    if pos_id not in pos_static_config:
        get_static_config(pos_id)

    is_menu_tester = json.loads(pos_static_config[pos_id])["menuTest"]
    if is_menu_tester:
        set_menu_manager_authenticated_user(int(pos_id), "1000")
        sysactions.set_custom(pos_id, AUTH_LEVEL_KEY, "10")
        return True

    try:
        ret, user_id = login_prompt_user(
            pos_id=pos_id,
            min_level=UserLevels.MANAGER.value,
        )
        if ret and user_id:
            set_menu_manager_authenticated_user(int(pos_id), str(user_id))

        return ret
    except SystemUserAccessException:
        _system_user_not_allowed(pos_id)
        return False


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

    try:
        model = get_model(posid=pos_id)
        operator = model.find("Operator")

        if operator is None:
            ret, _ = login_prompt_user(
                pos_id=pos_id,
                min_level=UserLevels.OPERATOR.value,
            )
        else:
            ret = _handle_operator_navigating_to_operator_menu(
                pos_id=pos_id,
                model=model,
                operator=operator,
            )

        return ret
    except SystemUserAccessException:
        _system_user_not_allowed(pos_id)
        return False


def _handle_operator_navigating_to_operator_menu(
    pos_id,      # type: str
    model,       # type: eTree.Element
    operator,    # type: eTree.Element
):
    # type: (...) -> bool

    ret = False
    user_control_type = model.find("WorkingMode").get("usrCtrlType")
    operator_state = operator.get("state")
    user_id = get_user_id_from_session(session_id=operator.get("sessionId"))

    user_level = get_user_level(user_id)
    if int(user_level or 0) == UserLevels.SYSTEM.value:
        raise SystemUserAccessException()

    if operator_state == "LOGGEDIN":
        ret = True
        save_last_authorization(
            pos_id,
            authorized_by=user_id,
            authorized_by_level=user_level,
        )
    elif operator_state == "PAUSED" and user_control_type != "TS":
        ret = unpause_user(
            pos_id=pos_id,
            user_id=user_id,
        )
        if ret:
            save_last_authorization(
                pos_id=pos_id,
                authorized_by=user_id,
                authorized_by_level=user_level,
            )

    elif operator_state == "LOGGEDOUT" or (operator_state == "PAUSED" and user_control_type == "TS"):
        ret, _ = login_prompt_user(
            pos_id=pos_id,
            min_level=UserLevels.OPERATOR.value,
        )

    return ret


def _system_user_not_allowed(pos_id):
    sysactions.clear_custom(pos_id, 'Last Manager ID')
    sysactions.clear_custom(pos_id, AUTH_LEVEL_KEY)
    show_messagebox(pos_id, "$SYSTEM_USER_ACCESS_NOT_ALLOWED")


def login_prompt_user(pos_id, min_level):
    try:
        ret, user_id = get_authorization(pos_id, min_level)
        if not ret:
            return False, None

        user_level = get_user_level(user_id)
        if int(user_level or 0) == UserLevels.SYSTEM.value:
            raise SystemUserAccessException()

        return True, user_id
    except AuthenticationFailed as ex:
        show_messagebox(pos_id, "$%s" % ex.message)
        return False, None


@action
def navigate_to_order(pos_id):
    change_screen(pos_id, "10000")
    return True


def _get_current_customer_info(
    pos_id,     # type: str
    table_id    # type: str
):
    # type: (...) -> Dict[str, Any]

    table_picture = eTree.XML(tableService().get_table_info(
        pos_id=pos_id,
        table_id=table_id
    ))
    order_picture = _get_totaled_service_order(
        pos_id=pos_id,
        table_picture=table_picture
    )

    if order_picture is None:
        return dict(
            document=None,
            name=None
        )

    customer_doc_xml = order_picture.find("CustomOrderProperties/OrderProperty[@key='CUSTOMER_DOC']")
    customer_name_xml = order_picture.find("CustomOrderProperties/OrderProperty[@key='CUSTOMER_NAME']")
    return dict(
        document=customer_doc_xml.get("value") if customer_doc_xml is not None else None,
        name=customer_name_xml.get("value") if customer_name_xml is not None else None
    )

@action
def do_total_table(
    pos_id,                     # type: str
    table_id,                   # type: str
    show_pre_account=False,     # type: Optional[bool]
    order_is_totaled=False,     # type: Optional[bool]
    is_mobile=False,            # type: Optional[bool]
):
    # type: (...) -> bool

    from posactions import fill_customer_properties_in_event

    show_pre_account = _get_bool_value(value=show_pre_account)
    order_is_totaled = _get_bool_value(value=order_is_totaled)
    is_mobile = _get_bool_value(value=is_mobile)

    model = get_model(pos_id)
    order = model.find("CurrentOrder/Order")
    pos_ot = get_posot(model)
    pod_function = get_posfunction(model)

    customer_info = {}
    check_sangria(model, pos_id)
    if not order_is_totaled:
        table_interactor = TableInteractor(pos_id, table_id, tableService())
        related_objects, services_totaled = table_interactor.get_related_services()
        selected_to_total = "all_services"
        if len(related_objects) > 1:
            selected_to_total = table_interactor.get_selected_related_services(
                related_objects=related_objects,
                show_all_services=len(services_totaled) == 0
            )
            if selected_to_total != "all_services":
                deselect_table(pos_id)
                select_table_or_tab(
                    pos_id=pos_id,
                    table_id=selected_to_total
                )
                table_id = selected_to_total

        if table_id in services_totaled:
            return select_table(
                pos_id=pos_id,
                table_id=table_id
            )

        customer_info = _get_current_customer_info(
            pos_id=pos_id,
            table_id=table_id
        )
        table_picture = table_interactor.do_total_table(tip_rate, selected_to_total)

        if get_posfunction(model) == "OT":
            return deselect_table(pos_id)

        if table_picture is None:
            table_picture = tableService().get_table_picture(pos_id, table_id)

        create_table_modified_event(pos_id, table_picture)

        table_picture_xml = eTree.XML(table_picture)
        tip_excess_amount = table_picture_xml.find(
            ".//CustomOrderProperties/OrderProperty[@key='TIP_EXCESS_AMOUNT']"
        )
        if tip_excess_amount is not None:
            message = "$EXCESS_AMOUNT_TURNED_INTO_TIP|{}".format(
                format_amount(
                    model,
                    tip_excess_amount.get("value")
                )
            )
            show_message_dialog(pos_id, "$INFORMATION", message)

    properties = eTree.XML(tableService().get_table_picture(
        pos_id=pos_id,
        table_id=table_id,
    )).find('Properties')

    if properties.get('CUSTOMER_NAME'):
        customer_info['name'] = properties.get('CUSTOMER_NAME')
    else:
        customer_info.pop('name')

    fill_customer_properties_in_event(
        pos_id=pos_id,
        pos_ot=pos_ot,
        pod_function=pod_function,
        key_event='onTotal',
        customer_info=customer_info,
        stop_action=False,
    )

    if 'TOTALED' in print_pre_account_after:
        show_pre_account_dialog(
            pos_id=pos_id,
            table_id=table_id,
            order_is_totaled=True,
            preview=False,
            filter_seat='0',
            order=order,
            is_mobile=is_mobile,
        )
    elif show_pre_account:
        show_pre_account_dialog(
            pos_id=pos_id,
            table_id=table_id,
            order_is_totaled=True,
            order=order,
            is_mobile=is_mobile,
        )

    check_loyalty_totaled_message(
        pos_id=pos_id,
        table_id=table_id,
    )

    from application.interactor import MessageDialogInteractor
    MessageDialogInteractor(pos_id).clear_pending_messages()

    return True


@action
@user_authorized_to_execute_action
def reopen_table(pos_id, table_id):
    changed_user = False
    try:
        table_picture = eTree.XML(tableService().get_table_info(pos_id, table_id))
        is_a_tab = tableService().is_a_tab(
            pos_id=pos_id,
            table_id=table_id,
            table_picture=table_picture,
        )
        slice_source_id = table_picture.get("sliceSourceTableId")
        can_return_to_original_table = _check_can_return_to_original_table(
            pos_id=pos_id,
            slice_source_id=slice_source_id,
            current_table_picture=table_picture,
        )

        tab_id = table_picture.find('Properties').get('TAB_ID')

        if is_a_tab and slice_source_id and not tab_id:
            options = _get_reopen_options()
            ret = show_message_options_dialog(pos_id, options['options'], "$INFORMATION", "$CHOOSE_REOPEN_TYPE")
            if ret == options['cancel']:
                raise StopAction()
            if ret == options['transfer_table']:
                return do_transfer_table(
                    pos_id=pos_id,
                    parent_table_id=table_id,
                    destination_table_id=slice_source_id if can_return_to_original_table else None,
                    select_destination_table=False,
                    ignore_totaled_orders=True,
                )
            if ret == options['create_tab']:
                table_picture, changed_user = _check_tab_user_sector(
                    table_picture=table_picture,
                    pos_id=pos_id,
                    reopen=True,
                )
                if table_picture is None:
                    raise StopAction()

        model = get_model(pos_id)
        order = get_current_order(model)
        clean_orders_discounts(
            pos_id=pos_id,
            orders=[order],
            message="$CLEAR_MANUAL_DISCOUNTS_CONFIRMATION",
        )

        loyalty_id = order.find(".//CustomOrderProperties/OrderProperty[@key='LOYALTY_ID']")
        if loyalty_id is not None:
            migrate_order_loyalty_info_to_table(
                pos_id=pos_id,
                order=order,
                table_id=table_id,
            )

        reopen_table_interactor = ReopenTableInteractor(tableService())
        table_picture = reopen_table_interactor.execute(pos_id, table_id)
        create_table_modified_event(pos_id, table_picture)
        if changed_user:
            deselect_table(
                pos_id=pos_id,
                pre_selected_option=True,
            )
        return True

    except sysactions.StopAction:
        return False


def _check_can_return_to_original_table(
    pos_id,                 # type: str
    slice_source_id,        # type: str
    current_table_picture,  # type: eTree.Element
):
    # type: (...) -> bool

    if not slice_source_id:
        return False

    source_table_picture = eTree.XML(tableService().get_table_picture(
        pos_id=pos_id,
        table_id=slice_source_id
    ))
    status = int(source_table_picture.get('status'))
    if status in [TableStatus.CLOSED, TableStatus.ABANDONED]:
        return False

    if status in [TableStatus.AVAILABLE]:
        return True

    return _check_if_source_table_still_has_the_same_service(
        current_table_picture=current_table_picture,
        source_table_picture=source_table_picture,
    )


def _check_if_source_table_still_has_the_same_service(
    current_table_picture,  # type: eTree.Element
    source_table_picture,   # type: eTree.Element
):
    # type: (...) -> bool

    return current_table_picture.get('sourceServiceId') == source_table_picture.get('serviceId')


def _get_reopen_options():
    # type: (...) -> Dict

    if not enable_tab_buttons:
        options = "$BACK_FROM_SOURCE_TABLE|$CANCEL"
        transfer_table = 0
        create_tab = None
        cancel = 1
    else:
        options = "$BACK_FROM_SOURCE_TABLE|$CREATE_TAB|$CANCEL"
        transfer_table = 0
        create_tab = 1
        cancel = 2

    return dict(
        options=options,
        cancel=cancel,
        transfer_table=transfer_table,
        create_tab=create_tab,
    )


def _check_if_table_have_tenders(
    pos_id,                         # type: Any[Union[int, str]]
    table_picture,                  # type: eTree.XML
    message="$ORDER_HAS_TENDERS"    # type: Optional[str]
):
    # type: (...) -> bool

    tenders = list(
        filter(
            lambda t: float(t.get("tenderAmount")) > 0,
            table_picture.findall(".//ServiceTenders/ServiceTender")
        )
    )

    if len(tenders) > 0:
        show_message_dialog(
            pos_id=pos_id,
            title="$INFORMATION",
            message=message
        )
        return True

    return False


def _get_destination_mode(
    pos_id,                 # type: str
    parent_table_id,        # type: str
    destination_table_id,   # type: str
    is_tab,                 # type: bool
):
    # type: (...) -> str

    table_picture = eTree.XML(
        tableService().get_table_info(
            pos_id=pos_id,
            table_id=parent_table_id
        )
    )
    properties = table_picture.find("Properties")
    parent_mode = (properties.get("MODE") or '') if properties is not None else ''

    if destination_table_id:
        destination_mode = tableService().get_table_custom_property(
            pos_id=pos_id,
            prop="MODE",
            table_id=destination_table_id
        )

        if not is_tab and parent_mode != destination_mode:
            show_message_dialog(pos_id, "$INFORMATION", "$DIFFERENT_TABLE_MODE_IN_SOURCE_AND_DESTINATION")
            raise StopAction()

        return destination_mode.get('MODE', 'seats')

    slice_parent_id = table_picture.get("sliceParentTableId")

    if is_tab and slice_parent_id is not None:
        return "tabs"

    if is_tab and slice_parent_id is None and table_service_mode == "mixed":
        buttons = "$SEATS|$TAB|$CANCEL"
        resp = show_message_options_dialog(
            pos_id=pos_id,
            buttons=buttons,
            title="$INFORMATION",
            message="$DESTINY_TABLE_MODE"
        )
        if resp is None or resp == 2:
            raise StopAction()

        return "tabs" if resp == 1 else "seats"

    return parent_mode or (table_service_mode if table_service_mode != "mixed" else '')


@action
def open_day(pos_id):
    open_day_pos_list = tableService().get_exclusive_pos_list(user_control_type="TS")
    pos_list = open_day_pos_list[:]
    error_dict = {}

    _fill_pos_list_with_can_open_day_pos(
        pos_list=open_day_pos_list,
        error_dict=error_dict,
    )

    model = get_model(pos_id)
    error_messages = _parse_error_messages(model, error_dict)

    if len(error_messages) > 0 and len(open_day_pos_list) == 0:
        show_message_dialog(
            pos_id=pos_id,
            message="$CANOT_OPEN_DAYS|{}".format("\\".join(error_messages)),
        )
        logger.info("Error opening days: {}".format(error_messages))
        raise StopAction()

    if len(open_day_pos_list) == 0:
        show_message_dialog(
            pos_id=pos_id,
            message="$HAS_NO_POS_TO_OPEN",
        )
        return False

    business_period = _get_business_period(
        open_day_pos_list=open_day_pos_list,
        pos_list=pos_list,
    )
    logger.info("Opening business day for POS's: {}. Business period: {}".format(open_day_pos_list, business_period))

    OpenDayInteractor().process_before_open_day(
        pos_id=pos_id,
        mb_context=mbcontext,
        business_period=business_period,
    )

    try:
        error_dict = {}
        successful_pos_id = []

        for pos_no in open_day_pos_list:
            _perform_open_day(
                pos_id=pos_no,
                successful_pos_id=successful_pos_id,
                business_period=business_period,
                error_dict=error_dict,
            )

        if len(error_dict) > 0:
            error_messages = _parse_error_messages(model, error_dict)
            error_msg = "$ERROR_OPENING_DAY|{}".format("\\".join(error_messages))
            show_message_dialog(
                pos_id=pos_id,
                message=error_msg,
            )

            logger.error("Error opening days: {}".format(error_messages))

        if len(successful_pos_id) > 0:
            report_pos_id = "|".join(map(str, successful_pos_id))
            menu_manager_user = get_menu_manager_authenticated_user(int(pos_id))
            report_params = [pos_id, business_period, report_pos_id, "true", menu_manager_user]
            print_report(
                pos_id,
                model,
                True,
                "dayOpen_report",
                *report_params
            )

        send_message("Persistence", TK_PERSIST_SEQUENCER_RESET, format=FM_PARAM, data="TraySetNameGen")
    except sysactions.StopAction:
        return False
    except Exception as ex:
        logger.exception("Error opening business day for POS's: {}".format(open_day_pos_list))
        show_message_dialog(pos_id, "$ERROR", "$ERROR_OPENING_DAY|{}".format(ex.message))


def _get_business_period(
    open_day_pos_list,  # type: List[str]
    pos_list            # type: List[str]
):
    # type: (...) -> str

    already_opened_pos = list(set(pos_list) - set(open_day_pos_list))
    if len(already_opened_pos) > 0:
        first_opened_pos_model = get_model(posid=already_opened_pos[0])
        return get_business_period(model=first_opened_pos_model)

    return date.today().strftime("%Y%m%d")


def _perform_open_day(
    pos_id,             # type: str
    successful_pos_id,  # type: List[str]
    business_period,    # type: str
    error_dict,         # type: Dict[str, List[str]]
):
    # type: (...) -> None

    try:
        logger.info("Opening business day on POS id: [%s]" % pos_id)
        msg = send_message(
            dest_name="POS%d" % int(pos_id),
            token=TK_POS_BUSINESSBEGIN,
            format=FM_PARAM,
            data="%s\0%s" % (str(pos_id), business_period),
            timeout=3600 * 1000000,
        )
        if msg.token == TK_SYS_ACK:
            successful_pos_id.append(pos_id)
        else:
            logger.error("Error opening business day on pos_id: {}. Error: {}".format(pos_id, msg.data))
            errors = msg.data.split('\0')
            if len(errors) > 1:
                error_msg = errors[1]
            else:
                error_msg = errors[0] if len(errors) > 0 else errors

            _fill_error_dict(error_dict, pos_id, error_msg)

    except Exception as ex:
        logger.exception("Error opening business day on pos id: %s" % pos_id)
        _fill_error_dict(error_dict, pos_id, ex.message)


def _parse_error_messages(model, error_dict):
    error_messages = []
    if len(error_dict) > 0:
        for key in error_dict:
            if key in MwSysErrors.list_values():
                msg = translate_message(model, MwSysErrors.get_name(key))
            else:
                msg = str(key)

            error_messages.append("POS {}".format(error_dict[key]) + (" - {}".format(msg)))
    return error_messages


def _fill_error_dict(error_dict, pos_id, error_message):
    error_message = error_message.split("\0")[0]
    if error_message not in error_dict:
        error_dict[error_message] = [pos_id]
    else:
        error_dict[error_message].append(pos_id)
    return error_dict


@action
def change_line_seat(pos_id, sale_line_json, seat_number):
    sale_line_list = json.loads(sale_line_json)

    sale_line_list = [line for line in sale_line_list if int(line['level']) == 0]
    data = {
        "blk_notify": 1,
        "pos_id": int(pos_id),
        "property_key": "seat",
        "property_value": seat_number,
        "orders_data": {},
    }
    for sale_line in sale_line_list:
        order_id = sale_line["orderId"]
        if order_id not in data["orders_data"]:
            data["orders_data"][order_id] = []
        data["orders_data"][order_id].append(sale_line["lineNumber"])

    if sale_line_list:
        msg = send_message(
            dest_name="ORDERMGR{}".format(pos_id),
            token=TK_SLCTRL_OMGR_SET_ORDERS_ITEMS_PROPERTY,
            format=FM_STRING,
            data=json.dumps(data).encode("utf-8"),
            timeout=60 * 1000000,
        )
        if msg.token != TK_SYS_ACK:
            show_message_dialog(
                pos_id=pos_id,
                title="$FATAL_ERROR_CALL_SUPPORT",
                message=msg.data,
            )
            raise StopAction()

        table_id = tableService().get_current_table_id(pos_id=pos_id)
        table_picture = tableService().get_table_picture(
            pos_id=pos_id,
            table_id=table_id,
        )

        create_table_modified_event(
            pos_id=pos_id,
            table_picture=table_picture,
        )

    return True


@action
@user_authorized_to_execute_action
def do_merge_items(pos_id, sale_line_json):
    sale_line_list = json.loads(sale_line_json)
    sale_line_list = filter(lambda x: x.get("level") == 0, sale_line_list)

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

    try:
        merge_line = None
        quantity = 0
        fraction_from = None
        order_id = None
        for sale_line in sale_line_list:
            order_id = sale_line['orderId'] if order_id is None else order_id
            fraction_from = sale_line["fractionFrom"] if fraction_from is None else fraction_from

            if fraction_from != sale_line["fractionFrom"]:
                show_message_dialog(pos_id, "$INFORMATION", "$SELECT_ALL_PARTS")
                return False

            if order_id != sale_line['orderId']:
                show_message_dialog(pos_id, "$INFORMATION", "$CANNOT_MERGE")
                return False

            quantity += float(sale_line["qty"])
            line_number = sale_line["lineNumber"]
            merge_line = sale_line if merge_line is None or line_number < merge_line["lineNumber"] else merge_line
        else:
            if round_half_away_from_zero(number=quantity) != 1 or len(sale_line_list) <= 1:
                show_message_dialog(pos_id, "$INFORMATION", "$SELECT_ALL_PARTS")
                return False

        pos_ot.blkopnotify = True
        order_service.recall_order(
            order_id=merge_line["orderId"],
            blk_notify=pos_ot.blkopnotify,
        )
        pos_ot.revertsFractionatedOrderLine(int(pos_id), merge_line["lineNumber"])

        order_service.store_order(blk_notify=True)
    finally:
        pos_ot.blkopnotify = False

        model = get_model(pos_id)
        if has_current_order(model):
            order_service.store_order()

    table_id = tableService().get_current_table_id(pos_id)
    table_picture = tableService().get_table_picture(pos_id, table_id)

    create_table_modified_event(pos_id, table_picture)
    return True


@action
@user_authorized_to_execute_action
def do_slice_service(
        pos_id,     # type: str
        table_id,   # type: str
        sale_lines  # type: str
):
    # type: (...) -> bool

    try:
        sale_lines = json.loads(sale_lines)
        table_picture = eTree.XML(tableService().get_table_info(
            pos_id,
            table_id
        ))
        table_mode = table_picture.find("Properties").attrib["MODE"]

        if table_mode == "tabs":
            tab_id = sale_lines[0]["@attributes"]["customProperties"]["tab"] if len(sale_lines) > 0 else None
            if tab_id:
                tab_picture = eTree.XML(tableService().get_table_info(
                    pos_id=pos_id,
                    table_id=tab_id,
                ))
                tab_picture, new_operator_id = _check_tab_user_sector(
                    table_picture=tab_picture,
                    pos_id=pos_id,
                )
                if tab_picture is None:
                    raise StopAction()
                _remove_parent_table(
                    pos_id,
                    tab_id
                )
                _notify_table_event(
                    pos_id,
                    table_id
                )
                return True
            else:
                return False
        else:
            from posactions import fill_customer_properties

            orders = []
            for line in sale_lines:
                if line['orderId'] not in orders and line["level"] == 0 and float(line['qty']) > 0:
                    orders.append(line['orderId'])

            if len(orders) == 0:
                show_message_dialog(
                    pos_id,
                    "$INFORMATION",
                    "$NO_ORDER_TO_SLICE"
                )
                return False

            new_setup = []
            for order_id in orders:
                setup = {
                    'originalOrderId': order_id,
                    'lineNumbers': []
                }
                for line in sale_lines:
                    if line['orderId'] == order_id:
                        line_number = line['lineNumber']
                        if line_number not in setup['lineNumbers']:
                            setup['lineNumbers'].append(line_number)
                new_setup.append(setup)

            all_table_sale_lines = eTree.XML(tableService().get_table_picture(
                pos_id=pos_id,
                table_id=table_id,
            )).findall(".//SaleLine")

            seat = str(sale_lines[0].get('seat'))
            has_to_abandon_table = _check_if_table_need_abandon(
                all_table_sale_lines=all_table_sale_lines,
                seat=seat,
            )

            if has_to_abandon_table:
                resp = show_messagebox(
                    posid=pos_id,
                    message="$CLOSE_TABLE_LAST_SEAT",
                    buttons="$OK|$PREVIOUS",
                )
                if resp == 1:
                    return False

            new_table = tableService().slice_service(
                pos_id=pos_id,
                new_setup=json.dumps(new_setup),
                src_table_id=table_id,
                seat=seat,
                abandon_table=has_to_abandon_table,
            )

            _set_table_available_if_possible(
                pos_id=pos_id,
                table_id=table_id,
                has_to_abandon_table=has_to_abandon_table,
            )
            release_all_tables(pos_id)
            tableService().recall_service(
                pos_id,
                new_table['tableId']
            )
            table_picture = tableService().do_total_table(
                pos_id,
                new_table['tableId'],
                tip_rate
            )
            create_table_modified_event(
                pos_id,
                table_picture
            )
            return True

    except Exception as ex:
        sys_log_exception("Error slicing service")
        show_message_dialog(
            pos_id,
            "$ERROR",
            ex.message
        )

    return False


def _set_table_available_if_possible(
    pos_id,                 # type: str
    table_id,               # type: str
    has_to_abandon_table,   # type: bool
):
    # type: (...) -> None

    if has_to_abandon_table:
        table_picture = eTree.XML(tableService().get_table_picture(
            pos_id=pos_id,
            table_id=table_id,
        ))
        valid_status = int(table_picture.get('status')) in [7, 8]
        if valid_status:
            _check_if_table_need_to_be_available_automatically(
                pos_id=pos_id,
                table_id=table_id,
            )


def _check_if_table_need_abandon(
    all_table_sale_lines,   # type: List[eTree.Element]
    seat,                   # type: str
):
    # type: (...) -> bool

    for sale_line in all_table_sale_lines:
        level = float(sale_line.get("level") or 0)
        quantity = float(sale_line.get("qty") or 0)
        if level != 0 or quantity == 0:
            continue

        line_seat = json.loads(sale_line.get("customProperties")).get("seat")
        if line_seat != seat:
            return False

    return True


def release_all_tables(pos_id, tables=None):
    if tables is None:
        tables = tableService().list_tables(pos_id)

    for table in tables:
        if pos_id == table.current_pos_id:
            tableService().store_service(pos_id, str(table.id))


@action
@release_user_lock_action_after_execution
def do_table_tender(
    pos_id,                         # type: str
    tender_type_id,                 # type: str
    amount,                         # type: str
    flag="",                        # type: Optional[str]
    applied_discount_info=None,     # type: Optional[str]
    local_payment_data=None,        # type: Optional[str]
    danfe_type=''                   # type: Optional[str]
):
    # type: (...) -> bool

    if local_payment_data == "":
        local_payment_data = None

    model = get_model(pos_id)
    pos_ot = get_posot(model)
    order = model.find("CurrentOrder/Order")
    order_id = int(order.get("orderId"))

    log_message = "Starting table tender. {}#{}#{}: {}"
    logger.info(log_message.format(pos_id, order_id, tender_type_id, local_payment_data or ''))

    tender_type = get_tender_type(pos_id, model, tender_type_id)
    coupon_ids = []

    if tender_type is None:
        return False

    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

    tender_descr = flag + " - " + tender_type["descr"] if flag else tender_type["descr"]
    tender_type_id = tender_type["id"]

    all_tenders = order.find("TenderHistory").findall("Tender")
    tender_id = get_tender_id(all_tenders, True)

    table_id = tableService().get_current_table_id(pos_id)
    table_picture = tableService().get_table_picture(pos_id, table_id)
    table_picture_xml = eTree.XML(table_picture)

    due_amount = table_picture_xml.get("dueAmount")
    total_tender = table_picture_xml.get("totalTender")

    logger.info("Processing payment. {}#{}: Amount/DueAmount: {}/{}".format(pos_id, order_id, amount, due_amount))

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

    tip_include_change = False
    last_tip = float(table_picture_xml.get("tip", 0))
    user_id = model.find("Operator").get("id")
    custom_properties = {}

    amount_bigger_than_due_amount = float(amount) > float(due_amount)
    tender_value = format_amount(model, amount)
    if ask_change_as_tip and amount_bigger_than_due_amount:
        resp = show_message_options_dialog(pos_id, YES_NO_BUTTONS, CONFIRMATION_TITLE, "$INCLUDE_CHANGE_AS_TIP")
        if resp is not None and resp == 0:
            tip_include_change = True
            current_tip = last_tip + float(amount) - float(due_amount)
            _change_tip(current_tip, pos_id, table_id)
            custom_properties.update({
                "CHANGE_ADDED_TO_TIP_AMOUNT": current_tip - last_tip,
                "CHANGE_ADDED_TO_TIP_BY": user_id,
            })

    if amount_bigger_than_due_amount and tender_type_id != 0 and not tip_include_change:
        log_message = "Tenders are bigger than due amount: {} / {}"
        logger.error(log_message.format(str(amount_bigger_than_due_amount), str(tender_type_id)))
        show_message_dialog(pos_id, "$INFORMATION", "$TENDERS_ARE_BIGGER_THEN_DUE_AMOUNT")
        return False

    discount_data = json.loads(applied_discount_info or "{}")
    if discount_data:
        coupon_ids.append(discount_data["benefit_key"])
        if discount_data["new_payment_amount"] <= 0:
            return True

    if amount in (None, "", ".") or float(amount) <= 0:
        logger.error("Amount to tender not informed")
        show_message_dialog(pos_id, message="$AMOUNT_DUE_NOT_INFORMED")
        return False

    electronic_type = tender_type["electronicType"]
    electronic_type_id = tender_type["electronicTypeId"]
    tender_details = {
        "posId": pos_id,
        "userId": user_id,
        "tableId": table_id,
        "tenderType": tender_type_id,
        "eletronicType": electronic_type,
        "tenderDescr": tender_type["descr"],
        "tenderAmount": float(amount),
        "tenderTypeDescription": tender_type.get("tenderTypeDescription", ""),
        "tenderChangeAmount": tender_type.get("changeAmount", ""),
    }

    update_tender_details_with_discount_data(
        discount_data=discount_data,
        tender_details=tender_details,
    )

    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=float(amount),
            tender_type=tender_type["descr"],
        )

    is_mobile_electronic_payment = local_payment_data is not None
    if not is_mobile_electronic_payment:
        if has_electronic_payment(None, electronic_type):
            message = "Starting process table eft PosId/OrderId/UserId/Amount: {}/{}/{}/{}"
            logger.info(message.format(pos_id, order_id, user_id, amount))

            payment_data = _process_table_eft(
                amount=amount,
                electronic_type_id=electronic_type_id,
                model=model,
                pos_id=pos_id,
                coupon_ids=coupon_ids,
            )
            tender_details.update(json.loads(get_tender_details(payment_data)))

            message = "End process table eft PosId/OrderId/UserId/Amount: {}/{}/{}/{}"
            logger.info(message.format(pos_id, order_id, user_id, amount))
        elif not discount_data or discount_amount == 0:
            message = "$CONFIRM_PAYMENT_RECEIVED|%s|%s" % (tender_descr, tender_value)
            resp = show_message_options_dialog(
                pos_id=pos_id,
                buttons="$OK|$PREVIOUS",
                title=CONFIRMATION_TITLE,
                message=message,
            )
            if resp is None or resp == 1:
                if tip_include_change:
                    _change_tip(last_tip, pos_id, table_id)
                return False
    else:
        tender_details.update(json.loads(get_tender_details(payment_data=local_payment_data)))

        message = "Set payment cache PosId/OrderId/UserId/Amount: {}/{}/{}/{}"
        logger.info(message.format(pos_id, order_id, user_id, amount))

        set_eft_cache(
            pos_id=pos_id,
            payment_data=local_payment_data,
            tender_id=int(tender_id),
            order_id=order_id,
        )

    try:
        message = "Starting register service electronic tender PosId/OrderId/UserId/Amount: {}/{}/{}/{}"
        logger.info(message.format(pos_id, order_id, user_id, amount))

        tender_details_str = json.dumps(tender_details)
        table_picture = tableService().register_service_tender(
            pos_id=pos_id,
            user_id=user_id,
            table_id=table_id,
            tender_id=tender_type_id,
            amount=amount,
            tender_details=tender_details_str,
        )

        logger.info("PosId/OrderId: {}/{} Tender details: {}".format(pos_id, order_id, tender_details_str))
    except (Exception, ) as ex:
        if coupon_ids:
            remove_discounts(
                pos_id=pos_id,
                coupon_ids=coupon_ids,
            )

        logger.exception("Error on table payment for tableId/orderId {}/{}".format(table_id, order_id))
        raise ex

    if has_electronic_payment(None, electronic_type):
        message = "Starting print tef tender receipt PosId/OrderId/UserId/Amount: {}/{}/{}/{}"
        logger.info(message.format(pos_id, order_id, user_id, amount))

        sector_name = get_sector_name(order=order)
        _print_tef_tender_receipt(
            pos_id=pos_id,
            tender_details=tender_details,
            fiscalization_type=danfe_type,
            sector=sector_name,
        )

    logger.info("PosId/OrderId: {}/{} Tender details: {}".format(pos_id, order_id, tender_details_str))

    if tender_type_id == 0:
        change = round_half_away_from_zero(float(amount) - float(due_amount), 2)
        if change > 0 and not tip_include_change:
            logger.info("PosId/OrderId: {}/{} Change: {}".format(pos_id, order_id, change))
            show_message_dialog(pos_id, "$INFORMATION", "$MONEY_CHANGE|%s" % (format_amount(model, change)))

    if _amount_is_bigger_or_equal_than_due_amount(amount=float(amount), due_amount=float(due_amount)):
        _process_before_close_table(
            pos_ot=pos_ot,
            pos_id=pos_id,
            table_id=table_id,
            amount=amount,
            due_amount=due_amount,
            total_tender=total_tender,
        )

        if has_electronic_payment(None, electronic_type) and danfe_type.startswith("APOS"):
            message = "Show confirmation message print fiscal coupon PosId/OrderId/UserId/Amount: {}/{}/{}/{}"
            logger.info(message.format(pos_id, order_id, user_id, amount))
            sysactions.show_confirmation(
                posid=pos_id,
                message="$CONFIRMATION_MESSAGE_PRINT_FISCAL_COUPON",
                buttons="OK",
            )

        logger.info("Order fully paid, closing. {}#{}".format(pos_id, order_id))

        return close_table(
            pos_id=pos_id,
            table_id=table_id,
            custom_properties=custom_properties,
            danfe_type=danfe_type,
            user_id=user_id,
        )

    create_table_modified_event(pos_id, table_picture)
    return True


def _amount_is_bigger_or_equal_than_due_amount(
    amount,         # type: float
    due_amount      # type: float
):
    # type: (...) -> bool

    return amount > due_amount or value_is_close_to(
        value=float(amount),
        close_to=due_amount,
    )


def _process_before_close_table(
    pos_ot,         # type: OrderTaker
    pos_id,         # type: str
    table_id,       # type: str
    amount,         # type: str
    due_amount,     # type: float
    total_tender,   # type: str
):
    # type: (...) -> None

    try:
        total_gross = round_half_away_from_zero(float(due_amount) + float(total_tender), 2)
        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:
        table_picture = tableService().get_table_picture(
            pos_id=pos_id,
            table_id=table_id,
        )
        create_table_modified_event(pos_id, table_picture)
        raise StopAction()

    check_certificate(pos_id=int(pos_id))


def _process_table_eft(
    amount,                 # type: Any[float, str]
    electronic_type_id,     # type: str
    model,                  # type: eTree.XML
    pos_id,                 # type: str
    coupon_ids,             # type: List[str]
):
    # type: (...) -> Dict

    try:
        eft_xml = request_eft(
            pos_id=pos_id,
            mb_context=mbcontext,
            model=model,
            electronic_type=electronic_type_id,
            amount=float(amount),
            _eft_events=[],
            is_table_service=True
        )

        check_eft_transaction(
            pos_id=pos_id,
            eft_xml=eft_xml,
            electronic_type=electronic_type_id,
        )
    except (StopAction, Exception) as _:
        remove_discounts(
            pos_id=pos_id,
            coupon_ids=coupon_ids,
        )
        raise StopAction()

    xml = eTree.XML(eft_xml)
    return xml.attrib


def _print_tef_tender_receipt(
    pos_id,                  # type: str
    tender_details,          # type: Dict
    fiscalization_type="",   # type: Optional[str]
    sector=None,             # type: Optional[str]
):
    # type: (...) -> Optional[bool]

    printer_receipt = get_printer_receipt()
    return printer_receipt.print_tef_tender_receipt(
        pos_id=pos_id,
        tender_detail=tender_details,
        fiscalization_type=fiscalization_type,
        sector=sector,
    )


def _change_tip(current_tip, pos_id, table_id):
    do_change_tip_interactor = DoChangeTipInteractor(tableService())
    table_picture = do_change_tip_interactor.execute(pos_id, table_id, "", str(current_tip))
    create_table_modified_event(pos_id, table_picture)


def _verify_sangria(pos_id):
    model = get_model(pos_id)
    pod_type = get_podtype(model)
    if is_sangria_enable() and pod_type != "TT":
        period = get_business_period(model)
        session = get_operator_session(model)
        xml_order = get_current_order(model)
        do_set_drawer_status(pos_id, get_drawer_amount(pos_id, period, session), xml_order.get("state"),
                             get_custom(model, "sangria_level_1_alert"))


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

    table_picture = tableService().get_table_picture(
        pos_id,
        table_id
    )
    create_table_modified_event(
        pos_id,
        table_picture
    )


def _select_available_table(
        pos_id,             # type: str
        current_table_id,   # type: str
        model=None,         # type: Optional[Any]
        mode="seats"        # type: Optional[str]
):
    # type: (...) -> Optional[Tuple[str, int, str]]

    if not model:
        model = get_model(pos_id)

    all_tables = tableService().list_tables(pos_id)
    table_list = [
        (
            str(table.id),
            int(table.status),
            str(table.name)
        )
        for table in all_tables
        if (
            str(table.status) in transfer_table_allowed_status
            and (not table.mode or table.mode == mode or table.status == TableStatus.AVAILABLE)
            and "TAB" not in table.id
            and current_table_id != table.id
        )
    ]

    return select_table_by_dialog(
        pos_id=pos_id,
        model=model,
        table_list=table_list
    )


def _select_destination_table(
    pos_id,                     # type: Any[Union[str,int]]
    select_destination_table,   # type: bool
    destination_table_id        # type: str
):
    # type: (...) -> None

    release_all_tables(pos_id)

    if select_destination_table is False:
        table_picture = '0'
    else:
        table_picture = tableService().recall_service(
            pos_id=pos_id,
            table_id=destination_table_id
        )

    create_table_modified_event(
        pos_id,
        table_picture
    )


def _transfer_tab(
    pos_id,                     # type: Any[Union[str,int]]
    parent_table_id,            # type: str
    destination_table_id,       # type: str
    select_destination_table,   # type: bool
    destination_mode            # type: str
):
    # type: (...) -> Optional[bool]

    model = get_model(pos_id)
    selected_tab = parent_table_id
    table_picture = eTree.XML(tableService().get_table_picture(
        pos_id,
        parent_table_id
    ))

    if not destination_table_id:
        selected_table = _select_available_table(
            pos_id=pos_id,
            current_table_id=parent_table_id,
            model=model,
            mode=destination_mode or "tabs"
        )

        if selected_table is None:
            return False

        destination_table_id = selected_table[0]

        _validate_destination_table(
            destination_table_id,
            pos_id
        )

    else:
        table_status = int(table_picture.get("status"))
        if table_status == TableStatus.TOTALED:
            if _check_if_table_have_tenders(
                    pos_id,
                    table_picture
            ):
                return False
            reopen_table_interactor = ReopenTableInteractor(tableService())
            reopen_table_interactor.execute(
                pos_id,
                parent_table_id
            )

    tableService().move_child_table(
        pos_id,
        parent_table_id,
        destination_table_id,
        selected_tab,
        "tabs"
    )
    _notify_table_event(
        pos_id,
        parent_table_id
    )
    _select_destination_table(
        pos_id=pos_id,
        select_destination_table=select_destination_table,
        destination_table_id=destination_table_id
    )


def _get_operator_id_to_transfer(
    pos_id,                     # type: str
    available_operators=None,   # type: Optional[List]
    selected_user=None,         # type: Optional[int]
    sector=None,                # type: Optional[str]
):
    # type: (...) -> Tuple[int, bool]

    resp = 0
    if selected_user:
        resp = show_message_options_dialog(
            pos_id=pos_id,
            buttons="$EDIT_ORDER|$KEEP|$PREVIOUS",
            title="$INFORMATION",
            message="$CHANGE_OR_KEEP_OPERATOR",
        )
    if resp == 2:
        return -1, False
    if resp == 0:
        chosen_index, operator_ids = _show_list_operators(
            pos_id=pos_id,
            available_operators=available_operators,
            ignore_user=selected_user,
            sector=sector,
        )
        if chosen_index < 0:
            return -1, False
        return operator_ids[chosen_index], False
    return selected_user, True


def _validate_transfer_table_sector(
    pos_id,                 # type: str
    source_table_picture,   # type: eTree.XML
    destination_table_id,   # type: str
):
    # type: (...) -> (int, bool)

    destination_table_picture = eTree.XML(tableService().get_table_info(
        pos_id=pos_id,
        table_id=destination_table_id,
    ))
    destination_table_user = destination_table_picture.get('userId') \
        if int(destination_table_picture.get('status')) != TableStatus.AVAILABLE.value else None

    if destination_table_user is not None:
        return destination_table_user, True

    source_table_user = int(source_table_picture.get('userId', -1))

    destination_table_users = []
    destination_table_sector = None

    if table_operation_mode in [TableOperationMode.SHARED_SECTOR, TableOperationMode.INDIVIDUAL_SECTOR]:
        sectors = get_operator_sector_info(pos_id=pos_id)
        destination_table_sector = tableService().get_table_sector(
            table_picture=destination_table_picture,
            is_tab=destination_table_picture.get("id").startswith("TAB"),
        )
        if destination_table_sector not in sectors and not _is_any_manager_operator_logged(pos_id=pos_id):
            show_message_dialog(
                pos_id=pos_id,
                title="$INFORMATION",
                message="$TABLE_SECTOR_HAS_NO_REGISTERED_OPERATORS",
            )
            raise StopAction()

        destination_table_users = sectors.get(destination_table_sector, [])
        if source_table_user not in destination_table_users:
            source_table_user = None

    return _get_operator_id_to_transfer(
        pos_id=pos_id,
        available_operators=destination_table_users[:] if destination_table_users else [],
        selected_user=source_table_user,
        sector=destination_table_sector,
    )


def _transfer_table(
    pos_id,                     # type: Any[Union[str,int]]
    parent_table_id,            # type: str
    destination_table_id,       # type: str
    select_destination_table,   # type: bool
    mode,                       # type: str
    ignore_totaled_orders       # type: bool
):
    # type: (...) -> Optional[bool]

    model = get_model(pos_id)
    service_seats = 1
    table_picture = eTree.XML(tableService().get_table_picture(
        pos_id=pos_id,
        table_id=parent_table_id
    ))

    mode = mode or table_picture.find("Properties").attrib["MODE"]
    operator_id = -1

    if not destination_table_id:
        selected_table = _select_available_table(
            pos_id=pos_id,
            current_table_id=parent_table_id,
            model=model,
            mode=mode
        )

        if selected_table is None:
            return False

        tender_amount = sum([float(tender.get("tenderAmount")) for tender in table_picture.findall(".//ServiceTender")])
        destination_table_id = selected_table[0]
        _validate_destination_table(
            destination_table_id,
            pos_id,
            tender_amount
        )

        operator_id, able_to_move = _validate_transfer_table_sector(
            pos_id=pos_id,
            source_table_picture=table_picture,
            destination_table_id=destination_table_id,
        )
        if operator_id < 0:
            return False

        if select_destination_table:
            select_destination_table = able_to_move

        if mode == "seats":
            service_seats = _get_service_seats_number(
                pos_id,
                [destination_table_id]
            )
            if not service_seats:
                return False
    else:
        table_status = int(table_picture.get("status"))
        if table_status == TableStatus.TOTALED:
            if _check_if_table_have_tenders(
                    pos_id,
                    table_picture
            ):
                return False
            reopen_table_interactor = ReopenTableInteractor(tableService())
            reopen_table_interactor.execute(
                pos_id,
                parent_table_id
            )

    pos_ts = get_pos_ts(model)
    user_id = model.find("Operator").get("id") if operator_id < 0 else operator_id
    linked_tables = table_picture.get("linkedTables").split(",") if table_picture.get("linkedTables") else []
    tables_to_set_available = ([parent_table_id] + linked_tables) if set_table_available_automatically else []
    tables_to_set_available_json = json.dumps(tables_to_set_available)
    pos_ts.moveTable(
        user_id=user_id,
        from_table_id=parent_table_id,
        dest_table_id=destination_table_id,
        number_of_seats=service_seats,
        tables_to_set_available=tables_to_set_available_json,
        mode=mode,
        ignore_totaled_orders=ignore_totaled_orders
    )
    _select_destination_table(
        pos_id=pos_id,
        select_destination_table=select_destination_table,
        destination_table_id=destination_table_id
    )


@action
@user_authorized_to_execute_action
def do_transfer_table(
    pos_id,                             # type: str
    parent_table_id,                    # type: str
    destination_table_id=None,          # type: Optional[str]
    select_destination_table=False,     # type: Optional[bool]
    ignore_totaled_orders=False         # type: Optional[bool]
):
    # type: (...) -> bool

    is_tab = parent_table_id.startswith('TAB')

    destination_mode = _get_destination_mode(
        pos_id=pos_id,
        parent_table_id=parent_table_id,
        destination_table_id=destination_table_id,
        is_tab=is_tab
    )

    if not destination_mode:
        show_message_dialog(pos_id, "$INFORMATION", "$INVALID_DESTINATION_TABLE_MODE")
        return False

    _transfer_table(
        pos_id=pos_id,
        parent_table_id=parent_table_id,
        destination_table_id=destination_table_id,
        select_destination_table=select_destination_table,
        mode=destination_mode,
        ignore_totaled_orders=ignore_totaled_orders,
    )

    return True


@action
@user_authorized_to_execute_action
def transfer_table_order(
    pos_id,                     # type: Any[Union[int, str]]
    order_id,                   # type: int
    source_table_id,            # type: str
    destination_table_id=None,  # type: str
):
    # type: (...) -> bool

    model = get_model(pos_id)
    service_seats = ""
    table_picture = eTree.XML(tableService().get_table_picture(
        pos_id=pos_id,
        table_id=source_table_id
    ))

    table_properties = table_picture.find("Properties")
    if table_properties.attrib is not None and table_properties.attrib.get("MODE") == "tabs":
        service_order = table_picture.find("ServiceOrders/ServiceOrder[@orderId='{}']".format(str(order_id)))
        tab_service_id = str(service_order.attrib["tableServiceId"])
        service_child = table_picture.find("ServiceChildren/ServiceChild[@serviceId='{}']".format(tab_service_id))
        tab_id = service_child.attrib["tableId"]
        source_table_id = tab_id

    operator_id = None
    if not destination_table_id:
        all_tables = tableService().list_tables(pos_id)
        table_list = _get_table_list(
            all_tables=all_tables,
            source_table_id=source_table_id,
            tab_list=False
        )

        tab_list = _get_table_list(
            all_tables=all_tables,
            source_table_id=source_table_id,
            tab_list=True
        )

        selected_table = select_table_by_dialog(pos_id, model, table_list=table_list, tab_list=tab_list)
        if selected_table is None:
            return False

        destination_table_id = selected_table[0]
        destination_table_picture = _validate_destination_table(destination_table_id, pos_id)

        operator_id, _ = _validate_transfer_table_sector(
            pos_id=pos_id,
            source_table_picture=table_picture,
            destination_table_id=destination_table_id,
        )
        if operator_id < 0:
            return False

        table_status = int(destination_table_picture.get("status"))
        if table_status == TableStatus.AVAILABLE:
            service_seats = _get_service_seats_number(pos_id, [destination_table_id])
            if not service_seats:
                return False
    else:
        table_status = int(table_picture.get("status"))
        if table_status == TableStatus.TOTALED:
            if _check_if_table_have_tenders(pos_id, table_picture):
                return False
            reopen_table_interactor = ReopenTableInteractor(tableService())
            reopen_table_interactor.execute(pos_id, source_table_id)

    pos_ts = get_pos_ts(model)
    user_id = operator_id or model.find("Operator").get("id")
    pos_ts.moveTableOrder(user_id, source_table_id, destination_table_id, order_id, service_seats)
    release_all_tables(pos_id)

    create_table_modified_event(pos_id, '0')
    return True


def _validate_destination_table(
        destination_table_id,   # type: str
        pos_id,                 # type: int
        partial_payment=0,      # type: Optional[float]
):
    # type: (...) -> eTree.XML

    destination_table_picture = eTree.XML(tableService().get_table_info(pos_id, destination_table_id))
    current_pos_id = destination_table_picture.get('currentPOSId')
    if current_pos_id is not None:
        message = "$TABLE_OPENED_IN_ANOTHER_POS|{}".format(current_pos_id)
        show_messagebox(pos_id, message)
        raise StopAction()

    table_status = int(destination_table_picture.get("status"))
    if partial_payment > 0 and table_status != TableStatus.AVAILABLE:
        show_messagebox(
            posid=pos_id,
            message="$CANNOT_TRANSFER_TABLE_WITH_PARTIAL_AMOUNT",
        )
        raise StopAction()

    return destination_table_picture


@action
@user_authorized_to_execute_action
def do_join_table(
        pos_id,             # type: str
        parent_table_id     # type: str
):
    # type: (...) -> bool

    table_picture = eTree.XML(tableService().get_table_picture(
        pos_id=pos_id,
        table_id=parent_table_id,
    ))
    if _check_if_table_have_tenders(
        pos_id=pos_id,
        table_picture=table_picture,
        message="$TABLE_HAS_TENDERS",
    ):
        return False

    model = get_model(pos_id)
    parent_table = eTree.XML(
        tableService().get_table_info(pos_id, parent_table_id)
    )

    if _is_seat(parent_table) and _has_tabs(parent_table):
        if enable_tab_buttons:
            buttons = "$TABS|$TABLES|$CANCEL"
            order_type_to_show = show_message_options_dialog(
                pos_id,
                buttons,
                message="$CHOOSE_ORDER_TYPE"
            )
            if order_type_to_show in (None, 2):
                return False
        else:
            order_type_to_show = 1

    elif _is_seat(parent_table):
        order_type_to_show = 1

    else:
        show_message_dialog(
            pos_id,
            "$INFORMATION",
            "$TAB_JOIN_NOT_STRAIGHT_ALLOWED"
        )
        return False

    tables_list = tableService().list_tables(pos_id)

    if order_type_to_show == 0:
        tab_list = []
        for tab in tables_list:
            if (str(tab.status) in join_table_allowed_status
                    and tab.id.startswith("TAB")
                    and not tab.slice_parent_table_id
                    and tab.id != parent_table_id):
                tab_list.append((
                    {
                        str(tab.id): tab.tab_id,
                    },
                    tab.status,
                    tab.name
                ))

        if tab_list:
            selected_table = select_table_by_dialog(
                pos_id,
                model,
                tab_list=tab_list,
                title="$SELECT_TAB"
            )
        else:
            show_message_dialog(
                pos_id,
                "$INFORMATION",
                "$NO_OPTIONS_TO_SHOW"
            )
            return False
    else:
        table_list = []
        for table in tables_list:
            conditions = [
                str(table.status) in join_table_allowed_status,
                not table.id.startswith("TAB"),
                table.id != parent_table_id,
            ]

            if table.status != 1:
                if _has_tabs(parent_table):
                    conditions.append(table.mode == "tabs")
                elif _is_seat(parent_table):
                    conditions.append(table.mode != "tabs")

            if all(conditions):
                table_list.append((
                    str(table.id),
                    table.status,
                    table.name
                ))

        selected_table = select_table_by_dialog(
            pos_id,
            model,
            table_list=table_list
        )

    if selected_table is None:
        return False

    joined_table = filter(lambda x: x.id == selected_table[0], tables_list)[0]
    error_message = None

    if joined_table.current_pos_id is not None:
        error_message = "$TABLE_OPENED_IN_ANOTHER_POS|{}".format(joined_table.current_pos_id)
    else:
        table_picture = eTree.XML(tableService().get_table_picture(
            pos_id=pos_id,
            table_id=joined_table.id
        ))
        if _table_has_order_in_progress(table_picture=table_picture):
            error_message = "$TABLE_ORDER_IN_PROGRESS|{}".format(joined_table.id)

    if error_message:
        show_message_dialog(
            pos_id,
            "$INFORMATION",
            error_message
        )
        return False

    if _check_if_table_have_tenders(
        pos_id=pos_id,
        table_picture=table_picture,
        message="$TABLE_HAS_TENDERS"
    ):
        return False

    if order_type_to_show == 0:
        _add_parent_table(
            pos_id,
            joined_table.id,
            parent_table_id
        )
    else:
        if _has_tabs(parent_table):
            service_seats = "1"
        else:
            service_seats = _get_service_seats_number(
                pos_id=pos_id,
                table_ids=[parent_table_id, joined_table.id],
            )
            if not service_seats:
                return False

        user_id = parent_table.get("userId")
        pos_ts = get_pos_ts(model)
        pos_ts.joinTables(
            user_id,
            selected_table[0],
            parent_table_id,
            service_seats
        )

    _notify_table_event(
        pos_id,
        parent_table_id
    )
    return True


@action
def do_abandon_table(
    pos_id,         # type: str
    table_id,       # type: str
    is_mobile,      # type: bool
):
    # type: (...) -> bool

    resp = show_message_options_dialog(
        pos_id=pos_id,
        buttons=YES_NO_BUTTONS,
        title="$INFORMATION",
        message="$ABANDON_TABLE_CONFIRMATION"
    )
    if resp is None or resp == 1:
        return False

    model = get_model(pos_id)
    pos_ts = get_pos_ts(model)
    user_id = model.find("Operator").get("id")
    table_picture = eTree.XML(tableService().get_table_info(pos_id, table_id))
    linked_tables = table_picture.get("linkedTables").split(",") if table_picture.get("linkedTables") else []

    totaled_service_order = _get_totaled_service_order(pos_id, table_picture)
    if totaled_service_order is not None:
        has_payment_to_cancel = _has_payment_to_cancel(tenders=totaled_service_order.findall(".//Tender"))
        if has_payment_to_cancel:
            resp = show_message_options_dialog(
                pos_id=pos_id,
                buttons=YES_NO_BUTTONS,
                title="$INFORMATION",
                message="$PAYMENT_CANCELED_CONFIRMATION"
            )
            if resp is None or resp == 1:
                raise StopAction()

        is_mobile = str(is_mobile).lower() == 'true'
        success = void_totaled_service_order(
            pos_id=pos_id,
            table_id=table_id,
            order_id=totaled_service_order.get('orderId'),
            is_mobile=is_mobile,
        )
        if not success:
            message = "$ERROR_ABANDON_TABLE_BY_REGISTERED_ORDER_PAYMENTS"
            if can_cancel_tef_payment_on_error():
                message = "$ERROR_ABANDON_TABLE"

            show_message_dialog(
                pos_id=pos_id,
                title="$ERROR",
                message=message,
            )
            raise StopAction()

    pos_ts.abandonService(user_id, table_id)
    _check_if_table_need_to_be_available_automatically(pos_id, table_id, table_picture, linked_tables)

    create_table_modified_event(pos_id, '0')
    return True


def _table_has_order_in_progress(
    table_picture,  # type: eTree.XML
):
    # type: (...) -> bool

    table_orders = table_picture.findall(".//ServiceOrder") or []
    orders_in_progress = [order for order in table_orders if order.get("state") == "IN_PROGRESS"]
    return len(orders_in_progress) > 0


def void_totaled_service_order(
    pos_id,     # type: str
    table_id,   # type: str
    order_id,   # type: str
    is_mobile,  # type: bool
):
    # type: (...) -> bool

    success_clear_tenders = do_clear_service_tenders(
        pos_id=pos_id,
        table_id=table_id,
        is_mobile=is_mobile,
        clear_all_payments=True,
    )

    if success_clear_tenders:
        void_order(
            pos_id=pos_id,
            order_id=order_id,
            new_order_status=OrderStatus.SYSTEM_VOIDED.value,
            abandon=1
        )

    return success_clear_tenders


def _get_totaled_service_order(
    pos_id,              # type: str
    table_picture=None,  # type: Optional[eTree.Element]

):
    # type: (...) -> Optional[eTree.Element]

    model = get_model(pos_id)
    pos_ot = get_posot(model)
    if not table_picture:
        table_id = tableService().get_current_table_id(pos_id)
        table_picture = eTree.XML(tableService().get_table_info(pos_id, table_id))

    totaled_order = table_picture.findall(".//PaymentOrders/PaymentOrder")
    if not totaled_order:
        return None

    order_id_totaled_service = totaled_order[0].get('orderId')
    order_split_service = eTree.XML(pos_ot.orderPicture(pos_id, order_id_totaled_service))
    return order_split_service.find("Order")


@action
@user_must_be_logged
def close_table(
    pos_id,                     # type: str
    table_id,                   # type: str
    custom_properties=None,     # type: Optional[dict]
    danfe_type='',              # type: str
    user_id='',                 # type: str
):
    # type: (...) -> Optional[bool]

    logger.debug("Close table start. PosId: {}".format(pos_id))

    custom_properties = custom_properties or {}

    model = get_model(pos_id)
    wait_dlg_id = None
    table_picture = tableService().get_table_picture(pos_id, table_id)
    table_picture_xml = eTree.XML(table_picture)

    customer_doc = None
    customer_doc_property = table_picture_xml.find(".//CustomOrderProperties/OrderProperty[@key='CUSTOMER_DOC']")
    if customer_doc_property is not None:
        customer_doc = customer_doc_property.get("value")

    if customer_doc:
        custom_properties.update({"CUSTOMER_DOC": customer_doc})

    order_id = None
    try:
        table_orders = table_picture_xml.findall(".//Order")
        for order in table_orders:
            order_id = order.get("orderId")
            logger.debug("Order fiscalize start. PosId/OrderId: {}/{}".format(pos_id, order_id))
            wait_dlg_id = order_fiscalize(
                model=model,
                order=order,
                pos_id=pos_id,
                wait_dlg_id=wait_dlg_id,
                danfe_type=danfe_type,
                custom_properties=custom_properties,
            )
            logger.debug("Order fiscalize end. PosId/OrderId: {}/{}".format(pos_id, order_id))

        logger.debug("Table finalizer called. PosId/OrderId: {}/{}".format(pos_id, order_id))
        wait_dlg_id = table_finalizer(
            pos_id=pos_id,
            table_id=table_id,
            user_id=user_id,
            wait_dlg_id=wait_dlg_id,
        )

        from application.interactor import LoyaltyInteractor
        LoyaltyInteractor(pos_id).show_loyalty_customer_final_message(order_picture=table_picture_xml)

        _check_if_table_need_to_be_available_automatically(pos_id, table_id, table_picture)
        _verify_sangria(pos_id)
        table_picture = '0'

    except sysactions.StopAction:
        pass
    except Exception as ex:
        logger.exception("Close table error. PosId/OrderId: {}/{}".format(pos_id, order_id))
        sysactions.close_asynch_dialog(pos_id, wait_dlg_id)
        show_message_dialog(pos_id, "$ERROR", "$ERROR_SEND_NFE|%s" % ex.message)
    finally:
        create_table_modified_event(pos_id, table_picture)
        sysactions.close_asynch_dialog(pos_id, wait_dlg_id)

    return True


def _check_if_table_need_to_be_available_automatically(pos_id, table_id, table_picture=None, linked_tables=None):
    if not table_picture:
        table_picture = eTree.XML(tableService().get_table_info(pos_id, table_id))

    if type(table_picture) in [str, unicode]:
        table_picture = eTree.XML(table_picture)

    if set_table_available_automatically and not tableService().is_a_tab(pos_id, table_id, table_picture):
        if linked_tables is None:
            linked_tables = table_picture.get("linkedTables").split(",") if table_picture.get("linkedTables") else []

        for table in linked_tables:
            set_available(pos_id, table)

        set_available(pos_id, table_id)


def table_finalizer(
    pos_id,             # type: str
    table_id,           # type: str
    user_id,            # type: str
    wait_dlg_id,        # type: Optional[dict]
):
    close_table_interactor = CloseTableInteractor(tableService())
    close_table_interactor.execute(
        pos_id=pos_id,
        table_id=table_id,
        user_id=user_id
    )

    sysactions.close_asynch_dialog(pos_id, wait_dlg_id)
    return wait_dlg_id


def order_fiscalize(
    model,                   # type: eTree
    order,                   # type: eTree
    pos_id,                  # type: str
    wait_dlg_id,             # type: Optional[str]
    danfe_type,              # type: str
    custom_properties,       # type: Optional[Dict[str, str]]
):
    # type: (...) -> Optional[str]

    pos_ot = get_posot(model=model)
    order_id = order.get("orderId")

    try:
        sysactions.close_asynch_dialog(
            posid=pos_id,
            dlgid=wait_dlg_id,
        )
        logger.debug("Get customer document. PosId/OrderId: {}/{}".format(pos_id, order_id))
        document, set_order_custom_properties = _get_customer_document(
            pos_ot=pos_ot,
            pos_id=pos_id,
            custom_properties=custom_properties,
        )

        if document is None:
            logger.debug("Canceled dialog customer document. PosId/OrderId: {}/{}".format(pos_id, order_id))
            raise StopAction()

        logger.debug("Saving fiscal customer document. PosId/OrderId: {}/{}".format(pos_id, order_id))
        from posactions import get_fiscal_document
        get_fiscal_document(
            pos_id=pos_id,
            model=model,
            customer_document=document,
        )

        wait_dlg_id = _show_wait_dialog(pos_id=pos_id)

        if set_order_custom_properties:
            logger.debug("Set order custom properties. PosId/OrderId: {}/{}".format(pos_id, order_id))
            _set_order_custom_properties(
                pos_ot=pos_ot,
                order_id=order_id,
                document=document,
                custom_properties=custom_properties,
            )

        logger.debug("Get fiscal coupon printer. PosId/OrderId: {}/{}".format(pos_id, order_id))
        fiscal_coupon_printer = _get_printer(
            model=model,
            pos_id=pos_id,
            order=order,
        )

        logger.debug("Get danfe type. PosId/OrderId/DanfeType: {}/{}/{}".format(pos_id, order_id, danfe_type))
        danfe_type = _get_danfe_type(
            model=model,
            pos_id=pos_id,
            order_id=order_id,
            fiscal_coupon_printer=fiscal_coupon_printer,
            danfe_type=danfe_type,
        )

        logger.debug("Fiscalize Order. PosId/OrderId/DanfeType: {}/{}/{}".format(pos_id, order_id, danfe_type))
        response = _fiscalize(
            pos_id=pos_id,
            order_id=order_id,
            danfe_type=danfe_type,
        )
        if response.token == TK_SYS_ACK:
            logger.debug("Print fiscal coupon. PosId/OrderId: {}/{}".format(pos_id, order_id))
            _print_fiscal_coupon(
                order=order,
                pos_id=pos_id,
                current_printer=fiscal_coupon_printer,
                danfe_type=danfe_type,
            )
            return wait_dlg_id

        fiscal_ok, message = response.data.split('\0')
        if fiscal_ok == "OK":
            raise PrinterError(message)

        message = split_and_translate_message(
            model=model,
            message=message,
        )
        logger.debug("Error fiscalization. PosId/OrderId/Error: {}/{}/{}".format(pos_id, order_id, message))
        raise Exception(message)

    except sysactions.StopAction:
        if custom_properties:
            pos_ot.setOrderCustomProperties(
                properties=custom_properties,
                orderid=order_id,
            )
        raise
    except PrinterError as ex:
        sysactions.close_asynch_dialog(
            posid=pos_id,
            dlgid=wait_dlg_id,
        )

        logger.debug("Error printer fiscal coupon. PosId/OrderId/Error: {}/{}/{}".format(pos_id, order_id, ex))

        show_message_dialog(
            pos_id=pos_id,
            title="$INFORMATION",
            message="$ERROR_PRINT_NFE|{}".format(ex),
        )
    except Exception as ex:
        sysactions.close_asynch_dialog(
            posid=pos_id,
            dlgid=wait_dlg_id,
        )
        raise ex


def _get_customer_document(
    pos_ot,             # type: OrderTaker
    pos_id,             # type: str
    custom_properties,  # type: Optional[Dict[str, str]]
):
    # type: (...) -> Tuple[str, bool]

    customer_info_config = get_customer_info_config(pos_id=pos_id)
    ask_document_pinpad = get_ask_document_pinpad(pos_id=pos_id)
    document = custom_properties["CUSTOMER_DOC"] if "CUSTOMER_DOC" in custom_properties else ""
    set_order_custom_properties = True
    if "afterPaid" in customer_info_config['document']:
        loyalty_document = get_loyalty_document(
            pos_id=pos_id,
            pos_ot=pos_ot,
            ask_document_pinpad=ask_document_pinpad,
        )

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

    return document, set_order_custom_properties


def _set_order_custom_properties(
    pos_ot,                 # type: eTree.Element
    order_id,               # type: str
    document,               # type: Optional[str]
    custom_properties,      # type: Optional[Dict[str, str]]
):
    # type: (...) -> None

    order_properties_dict = update_custom_properties(customer_doc=document)
    if custom_properties:
        order_properties_dict.update(custom_properties)

    if len(order_properties_dict) > 0:
        pos_ot.setOrderCustomProperties(
            properties=order_properties_dict,
            orderid=order_id,
        )


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

    return show_message_dialog(
        pos_id=pos_id,
        title="$PLEASE_WAIT",
        message="$PROCESSING_PAYMENT_DATA",
        mask="NOCONFIRM",
        assync=True
    )


def _print_fiscal_coupon(
    order,              # type: eTree.Element
    pos_id,             # type: str
    current_printer,    # type: str
    danfe_type,         # type: str
):
    # type: (...) -> None

    try:
        order_id = order.get("orderId")
        print_data = get_nreports_service().end_of_sale(
            pos_id=pos_id,
            order_id=order_id,
            mobile=(danfe_type or "").startswith("APOS"),
        )
        msg = mbcontext.MB_EasySendMessage(
            dest_name=current_printer,
            token=TK_PRN_PRINT,
            format=DataType.json,
            data=print_data,
            timeout=30 * 1000000,
        )
        if msg.token != TK_SYS_ACK:
            raise PrinterError(msg.data)

    except (MBException, MBTimeout) as ex:
        raise PrinterError(ex)


def _fiscalize(
    pos_id,             # type: str
    order_id,           # type: str
    danfe_type,         # type: Optional[str]
):
    # type: (...) -> MBMessage

    data = [pos_id, order_id]
    if danfe_type:
        data.append(danfe_type)

    data = '\0'.join(data)

    return mbcontext.MB_EasySendMessage(
        dest_name="FiscalWrapper",
        token=TK_FISCALWRAPPER_PROCESS_REQUEST,
        format=FM_PARAM,
        data=data,
        timeout=180 * 1000000,
    )


def _get_printer(
    model,          # type: eTree.Element
    pos_id,         # type: str
    order=None,     # type: Optional[eTree.Element]
):
    # type: (...) ->  str

    try:
        sector_name = get_sector_name(order=order) if order else None
        printer = get_device_service(
            pos_id=pos_id,
            device_type="printer",
            sector=sector_name,
        )
        return printer
    except ErrorGettingDevice as ex:
        message = get_device_service_error_message(
            model=model,
            exception=ex,
        )
        raise PrinterError(message)
    except (MBException, MBTimeout) as ex:
        raise PrinterError(ex)


def _get_danfe_type(
    model,                      # type: eTree.Element
    pos_id,                     # type: str
    order_id,                   # type: str
    fiscal_coupon_printer,      # type: str
    danfe_type,                 # type: Optional[str]
):
    # type: (...) -> Optional[str]

    if not danfe_type:
        return None

    current_pos_printer = _get_printer(
        model=model,
        pos_id=pos_id,
    )
    if current_pos_printer == fiscal_coupon_printer:
        return danfe_type

    logger.debug("Change mobile printer to using POS printer. PosId/OrderId: {}/{}".format(pos_id, order_id))

    return None


@action
def set_available(pos_id, table_id):
    table_info = eTree.XML(tableService().get_table_info(
        pos_id=pos_id,
        table_id=table_id,
    ))
    if int(table_info.get('status')) not in [TableStatus.CLOSED.value, TableStatus.ABANDONED.value]:
        return

    if not tableService().is_a_tab(pos_id, table_id):
        set_table_available = SetTableAvailableInteractor(tableService())
        set_table_available.execute(pos_id, table_id)

def check_valid_line(model, line_number=None):
    if not line_number:
        return False

    if not can_void_line(model, line_number):
        return False

    return True


def set_custom_production_tag(
    pos_id,                     # type: str
    sale_line_json,             # type: Optional[str]
    hold='clear',               # type: Optional[str]
    fire='clear',               # type: Optional[str]
    dont_make='clear',          # type: Optional[str]
    production_course=None,     # type: Optional[Course]
    high_priority='clear',      # type: Optional[str]
    set_in_child=False,         # type: Optional[bool]
):
    # type: (...) -> bool

    model = get_model(pos_id)
    pos_ot = get_posot(model)
    sale_line = json.loads(sale_line_json)

    if "lineNumber" not in sale_line:
        show_messagebox(pos_id, message="$NEED_TO_HAVE_ITEM", icon="error")
        return False

    sale_line = get_sale_line_to_tag(
        model=model,
        sale_line=sale_line,
    )

    is_combo = sale_line["itemType"] == ProductType.COMBO

    current_properties = sale_line.get("customProperties", {})
    current_properties = json.loads(current_properties) if isinstance(current_properties, str) else current_properties
    is_high_priority_clean = high_priority == "clear" and current_properties.get("highPriority") == "true"

    if is_combo and (high_priority == "true" or is_high_priority_clean):
        set_in_child = True

    line_number = sale_line["lineNumber"]
    if not check_valid_line(model, line_number):
        show_messagebox(pos_id, message="$NEED_TO_HAVE_ITEM", icon="error")
        return False

    course = production_course.name if production_course else "clear"
    course_abbreviation = production_course.abbreviation if production_course else "clear"
    properties = {
        "hold": hold,
        "fire": fire,
        "dont_make": dont_make,
        "highPriority": high_priority,
        "course": course,
        "courseAbbreviation": course_abbreviation
    }
    child_properties = {}
    if is_high_priority_clean:
        child_properties = {
            "hold": "clear",
            "fire": "clear",
            "dont_make": "clear",
            "highPriority": "clear",
            "course": "clear",
            "courseAbbreviation": "clear",
        }

    try:
        pos_ot.blkopnotify = True
        item_id = sale_line["itemId"]
        part_code = sale_line["partCode"]
        set_sale_line_custom_property(
            model=model,
            pos_ot=pos_ot,
            custom_properties=properties,
            line_number=line_number,
            item_id=item_id,
            part_code=part_code,
            set_in_child=set_in_child,
            child_properties=child_properties,
        )
    finally:
        pos_ot.blkopnotify = False
        pos_ot.updateOrderProperties(pos_id)

    return True


def set_sale_line_custom_property(
    model,                  # type: eTree.Element
    pos_ot,                 # type: OrderTaker
    custom_properties,      # type: Dict[str, Any]
    line_number,            # type: str
    item_id,                # type: str
    part_code,              # type: str
    set_in_child=False,     # type: Optional[bool]
    child_properties=None,  # type: Optional[Dict[str, str]]
):
    # type: (...) -> None

    if not set_in_child:
        args = (line_number, item_id, '', part_code)
        pos_ot.setOrderCustomProperties(custom_properties, '', *args)
        return

    order = model.find("CurrentOrder/Order")
    sale_lines = order.findall("SaleLine")
    items_types = {}
    for current_sale_line in sale_lines:
        current_line_number = current_sale_line.get("lineNumber")
        if current_line_number != line_number:
            continue

        _set_line_properties(
            line_number=current_line_number,
            item_id=item_id,
            part_code=part_code,
            sale_line=current_sale_line,
            pos_ot=pos_ot,
            child_properties=child_properties,
            custom_properties=custom_properties,
            items_types=items_types,
        )


def _set_line_properties(
    line_number,        # type: str
    item_id,            # type: str
    part_code,          # type: str
    sale_line,          # type: eTree.Element
    pos_ot,             # type: OrderTaker
    child_properties,   # type: Dict[str, str]
    custom_properties,  # type: Dict[str, Any]
    items_types,        # type: Dict[str, str]
):
    # type: (...) -> None

    current_item_id = sale_line.get("itemId")
    current_part_code = sale_line.get("partCode")
    item_type = sale_line.get("itemType")
    items_types[current_item_id + "." + current_part_code] = item_type
    if item_type == ProductType.OPTION:
        return

    args = (line_number, current_item_id, '', current_part_code)
    parent_item_id = "{}.{}".format(item_id, part_code)
    is_child_line = parent_item_id in current_item_id
    is_parent_line = current_item_id == item_id and current_part_code == part_code
    if is_parent_line:
        custom_properties["isParent"] = is_parent_line
        pos_ot.setOrderCustomProperties(custom_properties, '', *args)
        return

    if is_child_line:
        if items_types.get(current_item_id) in [ProductType.PRODUCT, ProductType.CANADD]:
            return

        grand_parent_id = current_item_id.rsplit(".", 1)[0]
        if items_types.get(grand_parent_id) in [ProductType.PRODUCT, ProductType.CANADD]:
            return

        properties = child_properties if child_properties else custom_properties
        properties.pop("isParent", None)
        pos_ot.setOrderCustomProperties(properties, '', *args)


@action
@user_authorized_to_execute_action
def do_hold_item(
    pos_id,             # type: str
    sale_line_json,     # type: str
):
    # type: (...) -> bool

    selected_time = "-1"

    hold_times = ["$NOT_DEFINED"]
    hold_times.extend(get_hold_time_config())

    if len(hold_times) > 1:
        index = show_listbox(
            pos_id,
            hold_times,
            message="$TIME_TO_HOLD_QUESTION",
            buttons=OK_CANCEL_BUTTONS,
            icon="info",
            timeout=720000
        )

        if index is None:
            raise sysactions.StopAction()

        selected_time = "-1"
        if index != 0:
            selected_time = int(hold_times[index]) * 60

    return set_custom_production_tag(
        pos_id,
        sale_line_json,
        hold=selected_time,
    )


@action
@user_authorized_to_execute_action
def do_fire_item(pos_id, sale_line_json):
    return set_custom_production_tag(pos_id, sale_line_json, fire='true')


@action
@user_authorized_to_execute_action
def do_high_priority_item(pos_id, sale_line_json):
    return set_custom_production_tag(pos_id, sale_line_json, high_priority='true')


@action
@user_authorized_to_execute_action
def dont_make_item(pos_id, sale_line_json):
    return set_custom_production_tag(pos_id, sale_line_json, dont_make='true')


@action
@user_authorized_to_execute_action
def change_production_course_item(pos_id, sale_line_json):
    production_courses = get_courses()

    if len(production_courses) == 0:
        show_message_dialog(pos_id, "$INFORMATION", "$NO_COURSE_AVAILABLE")
        raise StopAction()

    model = get_model(pos_id)
    options = "|".join([translate_message(model, x.description.encode('UTF-8')) for x in production_courses])
    course = show_filtered_list_box_dialog(pos_id, options, "$SELECT_A_COURSE", "", "NOFILTER")
    if course is None:
        return

    course = production_courses[course]
    return set_custom_production_tag(pos_id, sale_line_json, production_course=course, set_in_child=True)


@action
@user_authorized_to_execute_action
def do_clean_item_tags(pos_id, sale_line_json, comment_id=None):
    model = get_model(pos_id)
    pos_ot = get_posot(model)
    if comment_id is not None and comment_id != '-1':
        pos_ot.delComment(comment_id)

    return set_custom_production_tag(pos_id, sale_line_json)


@action
@user_authorized_to_execute_action
def do_unlink_table(pos_id, source_table_id):
    model = get_model(pos_id)
    table_picture = eTree.XML(tableService().get_table_info(pos_id, source_table_id))

    linked_tables_id = table_picture.get("linkedTables")
    if not linked_tables_id:
        return

    linked_tables_id = linked_tables_id.split(",")
    if len(linked_tables_id) == 1:
        resp = show_message_options_dialog(pos_id, YES_NO_BUTTONS, "$INFORMATION", "$UNLINK_TABLE_CONFIRMATION")
        if resp is None or resp == 1:
            return False
        table_to_unlink = linked_tables_id[0]
    else:
        tables_list = tableService().list_tables(pos_id)
        dict_tables_list = dict([(str(x.id), (str(x.id), int(x.status), str(x.name))) for x in tables_list])
        linked_tables = [dict_tables_list[l] for l in linked_tables_id]
        selected_table = select_table_by_dialog(pos_id, model, table_list=linked_tables, split_tables=True)
        if selected_table is None:
            return False

        table_to_unlink = selected_table[0]

    pos_ts = get_pos_ts(model)
    user_id = model.find("Operator").get("id")
    pos_ts.unlinkTables(user_id, source_table_id, table_to_unlink)

    _check_if_table_need_to_be_available_automatically(pos_id, table_to_unlink)
    table_picture = tableService().get_table_picture(pos_id, source_table_id)

    create_table_modified_event(pos_id, table_picture)
    return True


@action
@user_authorized_to_execute_action
def choose_line_seat(pos_id, sale_line_json):
    choose_line_seat_interactor = ChooseLineSeatInteractor(pos_id, tableService())
    sale_line = json.loads(sale_line_json)
    choose_line_seat_interactor.execute(sale_line)


@action
@user_authorized_to_execute_action
def start_tab(pos_id, tab_number, seats):
    model = get_model(pos_id)
    pos_ts = get_pos_ts(model)

    user_id = model.find("Operator").get("id")

    business_period = model.find("PosState").get("period")
    business_period = business_period[0:4] + "-" + business_period[4:6] + "-" + business_period[6:8]

    custom_property = {"tabNumber": tab_number}
    response = pos_ts.startService(user_id, business_period, '', seats, custom_property)
    return response


@action
@user_authorized_to_execute_action
def get_operator_data(pos_id, user_id):
    model = get_model(pos_id)
    current_operator = get_current_operator(model).get("id")

    if current_operator == user_id:
        return json.dumps({"result": "SameUser"})

    user_xml = get_user_information(user_id)
    if user_xml is None:
        return json.dumps({"result": None})

    user_list = eTree.XML(user_xml).findall("user")
    user_filter = filter(lambda x: x.get("UserId") == str(user_id), user_list)
    if user_filter in [None, [], ""]:
        return json.dumps({"result": None})

    name = user_filter[0].get("LongName")
    return json.dumps({"result": {"name": name}})


def _is_any_manager_operator_logged(
    pos_id,     # type: str
):
    # type: (...) -> bool

    opened_users = list_all_opened_users(pos_id=pos_id)
    super_user_levels = [UserLevels.MANAGER.value, UserLevels.ADMINISTRATOR.value]
    for user in opened_users:
        if int(user.get("level")) in super_user_levels:
            return True
    return False


def _show_list_operators(
    pos_id,                     # type: str
    model=None,                 # type: Optional[eTree.XML]
    available_operators=None,   # type: Optional[List]
    ignore_user=None,           # type: Optional[int]
    sector=None,                # type: Optional[str]
    table_user=None,            # type: Optional[int]
):
    # type: (...) -> (int, list)

    ignore_user = get_current_operator(model=model).get("id") if model is not None else ignore_user
    filtered_operators = filter_operators(
        pos_id=pos_id,
        current_operator_id=str(ignore_user),
    )

    manager_opened_operators = []
    if table_operation_mode in [TableOperationMode.SHARED_SECTOR, TableOperationMode.INDIVIDUAL_SECTOR]:
        opened_users = list_all_opened_users(pos_id=pos_id)
        super_user_levels = [UserLevels.MANAGER.value, UserLevels.ADMINISTRATOR.value]
        manager_opened_operators = [
            (user.get("id"), "{} - {}".format(user.get("name"), user.get("id")))
            for user in opened_users if int(user.get("level")) in super_user_levels
        ]

        if sector:
            sector_operators = get_operator_sector_info(pos_id=pos_id)
            sector_operators = sector_operators.get(sector, [])

            filtered_operators = [user for user in filtered_operators if int(user[0]) in sector_operators]
            filtered_operators.extend(manager_opened_operators)

            table_user_sector = [x[0] for x in filtered_operators if int(x[0]) == table_user]
            if table_user_sector:
                return 0, table_user_sector

    if not filtered_operators:
        show_message_dialog(
            pos_id=pos_id,
            title="$INFORMATION",
            message="$ERROR_CHANGING_OPERATOR",
        )
        return -1, []

    operators_ids, operators_text = _split_operators_ids_and_formatted_text(filtered_operators=filtered_operators)
    if available_operators:
        if ignore_user in available_operators:
            available_operators.remove(ignore_user)
        operators_ids = [o[0] for o in filtered_operators if int(o[0]) in available_operators]
        operators_ids.extend([o[0] for o in manager_opened_operators])
        operators_text = [o[1] for o in filtered_operators if int(o[0]) in available_operators]
        operators_text.extend([o[1] for o in manager_opened_operators])

        if not operators_ids or not available_operators:
            show_message_dialog(
                pos_id=pos_id,
                title="$INFORMATION",
                message="$ERROR_CHANGING_OPERATOR",
            )
            return -1, []

    chosen_index = show_filtered_list_box_dialog(
        pos_id=pos_id,
        options=operators_text,
        title="$TRANSFER_TO_WHICH_OPERATOR",
    )
    return chosen_index, operators_ids


@action
@user_authorized_to_execute_action
def change_table_operator(
    pos_id,     # type: str
    table_id,   # type: str
):
    model = get_model(posid=pos_id)

    table_info = eTree.XML(tableService().get_table_info(
        pos_id=pos_id,
        table_id=table_id,
    ))
    table_sector = None
    if table_operation_mode in [TableOperationMode.SHARED_SECTOR, TableOperationMode.INDIVIDUAL_SECTOR]:
        table_sector = tableService().get_table_sector(
            table_picture=table_info,
            is_tab=table_info.get("id").startswith("TAB"),
        )
    chosen_index, operators_ids = _show_list_operators(
        pos_id=pos_id,
        model=model,
        sector=table_sector,
    )
    if chosen_index is None or chosen_index < 0:
        return False
    operator_id = str(operators_ids[chosen_index])

    try:
        operator_data = json.loads(get_operator_data(pos_id, operator_id))
        if operator_id not in [x.get("id") for x in list_all_opened_users(pos_id)]:
            show_message_dialog(pos_id, "$INFORMATION", "$OPERATOR_IS_NOT_OPENED")
            return False
        if not operator_data:
            show_message_dialog(pos_id, "$INFORMATION", "$ERROR_RETRIEVING_USER_DATA|{}".format(operator_id))
            return False
        elif operator_data['result'] is None:
            show_message_dialog(pos_id, "$INFORMATION", "$INVALID_USER_ENTERED|{}".format(operator_id))
            return False
        elif operator_data['result'] == "SameUser":
            show_message_dialog(pos_id, "$INFORMATION", "$SAME_USER_TRANSFER")
            return False
        else:
            resp = show_message_options_dialog(
                pos_id=pos_id,
                buttons=YES_NO_BUTTONS,
                title="$INFORMATION",
                message="$CONFIRM_TRANSFER_TABLE|{}".format(operator_data['result']['name']),
            )
            if resp is None or resp == 1:
                return False

        pos_ts = get_pos_ts(model)

        pos_ts.changeTableUser(operator_id, table_id)
        return deselect_table(pos_id)
    except (Exception,):
        logger.exception("Error changing table operator - pos_id: {}, table_id: {}".format(pos_id, table_id))
        show_message_dialog(pos_id, "$ERROR", "$ERROR_RETRIEVING_USER_DATA|{}".format(operator_id))


def filter_operators(pos_id, current_operator_id):
    opened_operators = list_all_opened_users(pos_id)
    operators = [_format_operator(o) for o in opened_operators if _is_valid_operator(o, current_operator_id)]
    return _sort_operators_by_name(operators)


def _sort_operators_by_name(formatted_operators):
    return sorted(formatted_operators, key=lambda t: t[1])


def _is_valid_operator(o, current_operator_id):
    return _isnt_current_operator(o, current_operator_id) and _operator_isnt_above_manager(o)


def _isnt_current_operator(operator, current_operator_id):
    return operator.get("id", "") != current_operator_id


def _operator_isnt_above_manager(o):
    return o.get("level") and int(o.get("level")) <= UserLevels.MANAGER.value


def _format_operator(o):
    operator_id = o.get("id", "")
    operator_name = o.get("name", "")
    return operator_id, "{} - {}".format(operator_name, operator_id)


def _split_operators_ids_and_formatted_text(filtered_operators):
    return map(list, zip(*filtered_operators))


@action
def do_change_table_seats(
    pos_id,           # type: str
    table_id=None,    # type: str
):
    # type: (...) -> bool

    try:
        service_seats = _get_service_seats_number(
            pos_id=pos_id,
            table_ids=[table_id],
        )
        if not service_seats:
            return False

        model = get_model(posid=pos_id)
        pos_ts = get_pos_ts(model=model)
        user_id = model.find("Operator").get("id")
        pos_ts.changeNumberOfSeats(
            user_id=user_id,
            number_of_seats=service_seats,
            table_id=table_id,
        )

        table_picture = tableService().get_table_picture(
            pos_id=pos_id,
            table_id=table_id,
        )
        create_table_modified_event(
            pos_id=pos_id,
            table_picture=table_picture,
        )
        return True

    except (Exception,):
        show_message_dialog(
            pos_id=pos_id,
            message="$ERROR_CHANGING_SEATS",
        )

    return False


def _get_service_tender_id(
    table_picture,      # type: eTree.XML
    order_tender_id     # type: str
):
    # type: (...) -> Optional[str]

    if order_tender_id is None:
        return None

    service_tenders = table_picture.findall(".//ServiceTenders/ServiceTender")
    for service_tender in service_tenders:
        if service_tender.get("orderTenderId") == order_tender_id:
            return service_tender.get("serviceTenderId")

    return None


def _clear_table_tender(
    table_picture,                  # type: eTree.XML
    tender,                         # type: eTree.XML
    is_mobile,                      # type: bool
    print_customer_receipt=None,    # type: Optional[bool]
    order_id=None,                  # type: Optional[str]
):
    # type: (...) -> Tuple[bool, Optional[bool]]

    pos_id = table_picture.get("currentPOSId")
    model = get_model(pos_id)
    print_customer_receipt_response = print_customer_receipt
    printer_interactor = PrinterInteractor(
        pos_id=pos_id,
        mb_context=mbcontext,
    )

    try:
        order_id = order_id or table_picture.get("currentOrderId")
        if tender is not None and is_electronic_payment(tender=tender):
            tender_id = tender.get("tenderId")
            log_message = "Canceling eft payment PosId: {} OrderId: {} TenderId: {}"
            logger.debug(log_message.format(pos_id, order_id, tender_id))

            if not _is_eft_tender_canceled(tender=tender):
                cancel_eft_data = cancel_eft_transaction(
                    pos_id=str(pos_id),
                    model=model,
                    order_id=order_id,
                    tender=tender,
                )

                if cancel_eft_data is None and not _can_cancel_payment_on_error(pos_id=pos_id):
                    logger.debug("Returning without clearing payments")
                    return False, print_customer_receipt_response

                if cancel_eft_data:
                    cancel_eft_receipt = json.loads(cancel_eft_data)
                    _save_eft_cancel_receipt(
                        pos_id=pos_id,
                        tender_id=tender_id,
                        order_id=order_id,
                        model=model,
                        cancel_eft_receipt=cancel_eft_receipt,
                    )

                    logger.debug("Printing cancel receipt")
                    print_customer_receipt_response = printer_interactor.print_tef_tender_receipt(
                        is_mobile=is_mobile,
                        order=table_picture,
                        tender_detail=cancel_eft_receipt,
                        print_customer_receipt=print_customer_receipt_response,
                    )

        _clear_payment_and_salve_receipt(
            pos_id=pos_id,
            order_id=order_id,
            table_picture=table_picture,
            tender=tender,
        )
    except (Exception, ):
        sys_log_exception("Error to clean table payments or to cancel eft transaction")
        return False, print_customer_receipt_response

    return True, print_customer_receipt_response


def _is_eft_tender_canceled(
    tender,     # type: eTree.Element
):
    # type: (...) -> bool

    return "receiptCancel" in json.loads(tender.get("tenderDetail"))


def _can_cancel_payment_on_error(
    pos_id,     # type: str
):
    # type: (...) -> Optional[bool]

    if not can_cancel_tef_payment_on_error():
        show_messagebox(
            posid=pos_id,
            message="$ERROR_CANCEL_EFT_TRANSACTION_STOP_PROGRESS",
            timeout=10000,
        )
        return False

    logger.debug("Can cancel tef payment on error")
    response = show_message_options_dialog(
        pos_id=pos_id,
        buttons="$YES|$NO",
        title="$INFORMATION",
        message="$ERROR_CANCEL_EFT_TRANSACTION",
    )

    return response is not None and response != 1


def _save_eft_cancel_receipt(
    pos_id,                 # type: str
    tender_id,              # type: str
    order_id,               # type: str
    model,                  # type: eTree.Element
    cancel_eft_receipt,     # type: Dict[str, str]
):
    # type: (...) -> None

    logger.debug("Save cancel eft receipt")

    cancel_eft_receipt["posId"] = pos_id
    cancel_eft_receipt["userId"] = model.find("Operator").get("id")
    cancel_eft_receipt["canceledAt"] = str(datetime.now())

    order_interactor = OrderInteractor(
        pos_id=pos_id,
    )
    order_xml = order_interactor.get_order_from_order_picture(
        order_id=order_id,
    )
    save_cancel_receipt(
        pos_id=pos_id,
        order=order_xml,
        tender_id=tender_id,
        data=json.dumps(cancel_eft_receipt),
    )


def _clear_payment_and_salve_receipt(
    pos_id,         # type: str
    order_id,       # type: str
    table_picture,  # type: eTree.XML
    tender,         # type: eTree.XML
):
    # type: (...) -> None

    logger.debug("Get service tender id")
    service_tender_id = _get_service_tender_id(
        table_picture=table_picture,
        order_tender_id=tender.get("tenderId") if tender is not None else None,
    )

    logger.debug("Clean table payments")
    table_picture = clean_table_payments(
        pos_id=pos_id,
        table_id=table_picture.get("id"),
        service_tender_id=service_tender_id,
    )

    if not is_electronic_payment(tender=tender):
        logger.debug("Save order cancel receipt")
        save_order_cancel_receipt(
            pos_id=pos_id,
            order_id=order_id,
            tender_id=tender.get("tenderId"),
        )

    logger.debug("Create table modified event")
    create_table_modified_event(
        pos_id=pos_id,
        table_picture=table_picture,
    )


@action
def do_clear_service_tenders(
    pos_id,                     # type: str
    table_id,                   # type: str
    is_mobile,                  # type: bool
    clear_all_payments=False,   # type: bool
):
    # type: (...) -> bool

    table_picture_xml = eTree.XML(
        tableService().get_table_picture(
            pos_id=pos_id,
            table_id=table_id
        )
    )

    order_picture = _get_totaled_service_order(
        pos_id=pos_id,
        table_picture=table_picture_xml
    )

    order_id = order_picture.get("orderId")
    tenders_to_clear = _get_tenders(order_picture=order_picture)
    is_mobile = str(is_mobile).lower() == "true"

    if not clear_all_payments:
        tenders_to_clear = _get_tenders_to_clear(
            pos_id=pos_id,
            order_id=order_id,
            is_mobile=is_mobile,
            tenders=tenders_to_clear,
            total_tender=float(table_picture_xml.get("totalTender")),
        )
        if tenders_to_clear is None:
            return False

    results = _clear_tenders(
        is_mobile=is_mobile,
        order_id=order_id,
        table_picture_xml=table_picture_xml,
        tenders=tenders_to_clear,
    )

    return all(results)


def _get_tenders_to_clear(
    pos_id,         # type: str
    order_id,       # type: str
    is_mobile,      # type: bool
    tenders,        # type: List[eTree.Element]
    total_tender,   # type: float
):
    # type: (...) -> Optional[List[eTree.Element]]

    if not _has_tenders(
        pos_id=pos_id,
        total_tender=total_tender,
        tenders=tenders,
    ):
        return None

    model = get_model(posid=pos_id)
    tenders_to_clear = _select_tender_action(
        model=model,
        pos_id=pos_id,
        tenders=tenders,
        is_mobile=is_mobile,
        order_id=order_id
    )
    return tenders_to_clear


def _get_tenders(
    order_picture,  # type: eTree.XML
):
    # type: (...) -> List[eTree.Element]

    tenders = filter(
        lambda t: float(t.get("tenderAmount")) > 0,
        order_picture.findall(".//TenderHistory/Tender")
    )
    return tenders


def _has_payment_to_cancel(
    tenders,      # type: List[eTree.Element]
):
    # type: (...) -> bool

    tender = next((tender for tender in tenders if float(tender.get("tenderAmount")) > 0), None)
    return tender is not None


def _has_tenders(
    pos_id,         # type: str
    total_tender,   # type: float
    tenders,        # type: List[eTree.Element]
):
    # type: (...) -> bool

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

    return True


def _clear_tenders(
    is_mobile,              # type: bool
    order_id,               # type: str
    table_picture_xml,      # type: eTree.XML
    tenders,                # type: List[eTree.Element]
):
    # type: (...) -> List[bool]

    last_response = None
    results = []
    for tender in tenders:
        result, last_response = _clear_table_tender(
            table_picture=table_picture_xml,
            tender=tender,
            is_mobile=is_mobile,
            print_customer_receipt=last_response,
            order_id=order_id
        )
        results.append(result)

    return results


def _select_tender_action(
    model,      # type: eTree.XML
    pos_id,     # type: str
    tenders,    # type: List[eTree.XML]
    is_mobile,  # type: bool
    order_id,   # type: str
):
    # type: (...) -> Optional[List[eTree.Element]]

    options_dict, disabled_items_index = get_tenders_options_dict(
        pos_id=pos_id,
        model=model,
        tenders=tenders,
        card_brands=get_card_brands(),
    )

    listbox_options = options_dict_to_str_list(
        model,
        options_dict
    )

    has_receipt_card = tender_has_receipt_card(
        tenders=tenders,
    )

    selected_option = show_order_tenders_options(
        pos_id=pos_id,
        listbox_options=listbox_options,
        has_receipt_card=has_receipt_card,
    )

    if selected_option is None:
        return None

    selected_button_option = selected_option.get("selected_button_index")
    selected_list_option = selected_option.get("selected_list_index")

    operator = model.find("Operator").get('id')

    selected_tender = tenders[options_dict[selected_list_option]["tender_index"]]
    if selected_button_option == 0:
        do_reprint_receipt(
            pos_id=pos_id,
            is_mobile=is_mobile,
            order_id=order_id,
            selected_tender=selected_tender,
            operator=operator,
        )
        return None
    elif selected_button_option == 1:
        return [selected_tender]

    return None


@user_authorized_to_execute_action
def do_reprint_receipt(
    pos_id,             # type: str
    is_mobile,          # type: bool
    order_id,           # type: str
    selected_tender,    # type: eTree.XML
    operator='',        # type: Optional[str]
    auth_data=None,     # type: Optional[str]
):
    # type (...) -> None

    fiscalization_type = "APOS" if is_mobile else ""

    receipts = [
        json.loads(selected_tender.get("tenderDetail")).get('ReceiptCustomer'),
        json.loads(selected_tender.get("tenderDetail")).get('ReceiptMerchant')
    ]
    reprinted = reprint_receipt(
        pos_id=pos_id,
        receipts=receipts,
        fiscalization_type=fiscalization_type,
        error_title="$REPRINT_TEF_ERROR",
    )
    tender_id = selected_tender.get("tenderId")

    if reprinted:
        _set_reprinted_tef_custom_property(
            pos_id=pos_id,
            order_id=str(order_id),
            tender_id=str(tender_id),
            operator=operator,
            auth_data=auth_data,
        )


def _get_tender_processor(
    tenders,    # type: List[eTree.XML]
):
    processors = []
    for tender in tenders:
        tender_detail = json.loads(tender.get("tenderDetail"))
        tender_processor = tender_detail.get("TransactionProcessor")
        if tender_processor:
            processors.append(tender_processor)

    return processors[-1] if processors else ""


def _set_reprinted_tef_custom_property(
    pos_id,     # type: str
    order_id,   # type: str
    tender_id,  # type: str
    operator,   # type: str
    auth_data,  # type: Optional[str]
):
    # type: (...) -> None

    reprinted_tef_custom_property = get_custom_property_value(
        pos_id=pos_id,
        custom_property="REPRINTED_TEF",
        order_id=order_id,
    )

    reprinted_tef_custom_property = json.loads(reprinted_tef_custom_property) if reprinted_tef_custom_property else []

    custom_property_json = {
        "orderId": order_id,
        "tenderId": tender_id,
        "requestUserId": operator,
        "authUserId": str(auth_data if auth_data else operator),
    }

    if custom_property_json not in reprinted_tef_custom_property:
        reprinted_tef_custom_property.append(custom_property_json)

    tableService.set_order_custom_property(
        pos_id=pos_id,
        key="REPRINTED_TEF",
        value=json.dumps(reprinted_tef_custom_property),
        order_id=order_id,
    )


def clean_table_payments(
    pos_id,                     # type: str
    table_id,                   # type: str
    service_tender_id=None,     # type: Optional[str]
    delete_order_tender=True,   # type: Optional[bool]
):
    # type: (...) -> str

    model = get_model(posid=pos_id)
    user_id = tableService.get_user_id(
        model=model,
        mandatory=False,
    )
    tender_id = service_tender_id or ""

    log_msg = "Cleaning payments of posId/tableId: {}#{}. TenderId: {}"
    logger.info(log_msg.format(pos_id, table_id, tender_id))

    pos_ts = get_pos_ts(model=model)
    table_picture = eTree.XML(
        pos_ts.clearServiceTenders(
            user_id=user_id,
            table_id=table_id,
            service_tender_id=tender_id,
            delete_order_tender=delete_order_tender
        )
    )

    return eTree.tostring(table_picture)


@action
@user_authorized_to_execute_action
def do_change_tip(pos_id, table_id):
    model = get_model(pos_id)

    options = ['PERCENTAGE', 'VALUE']
    options = map(lambda x: translate_message(model, x), options)

    tip_type = show_filtered_list_box_dialog(pos_id, "|".join(options), "$SELECT_TYPE", "", "NOFILTER")
    if tip_type is None:
        return False

    title = "$SELECT_TIP_PERCENTAGE" if tip_type == 0 else "$SELECT_TIP_VALUE"
    mask = "CURRENCY" if tip_type == 1 else "NUMBER"

    tip_value = show_numpad_dialog(pos_id, title, "", mask)
    if tip_value is None:
        return False

    percentage = ""
    amount = ""
    if tip_type == 0:
        percentage = tip_value or 0
    else:
        amount = tip_value or 0

    error_message = None
    try:
        do_change_tip_interactor = DoChangeTipInteractor(tableService())
        table_picture = do_change_tip_interactor.execute(pos_id, table_id, str(percentage), str(amount))
        create_table_modified_event(pos_id, table_picture)
    except InvalidTipError as ex:
        if ex.code == InvalidTipError.ErrorCode.CANNOT_USE_TIP_BY_AMOUNT_AND_PERCENTAGE:
            error_message = "$CANNOT_USE_TIP_BY_AMOUNT_AND_PERCENTAGE"
        elif ex.code == InvalidTipError.ErrorCode.TIP_AMOUNT_MUST_BE_VALID:
            error_message = "$TIP_AMOUNT_MUST_BE_VALID"
    except (Exception,) as ex:
        error_message = ex.message
    finally:
        if error_message:
            sys_log_exception(error_message)
            translated_error_message = get_translated_exception_message(
                model=model,
                exception_message=error_message,
            )
            show_messagebox(pos_id, message=translated_error_message, icon="error")
            raise StopAction()

    return True

def process_discount_in_table(pos_id, order, table_id):
    if table_id and order and order.get('state') == "TOTALED":
        model = get_model(pos_id)
        pos_ts = get_pos_ts(model)
        user_id = model.find("Operator").get("id")
        pos_ts.updateTableTenders(user_id, table_id)

        table_picture = _change_tip_after_discount(pos_id, table_id)
        if table_picture:
            create_table_modified_event(pos_id, table_picture)


def _change_tip_after_discount(pos_id, table_id):
    table_info = tableService().get_table_info(pos_id, table_id)
    table_info_xml = eTree.XML(table_info)

    properties = table_info_xml.find("Properties")
    tip_type = (properties.get("TIP_TYPE") or '') if properties is not None else ''

    table_tip_rate = table_info_xml.get("tipRate") if tip_type == 'Percentage' else ""
    table_tip_amount = "" if table_tip_rate != "" else table_info_xml.get("tipAmount")

    do_change_tip_interactor = DoChangeTipInteractor(tableService())
    return do_change_tip_interactor.execute(pos_id, table_id, table_tip_rate, table_tip_amount)


@action
@user_authorized_to_execute_action
def totaled_total_report(
    pos_id,                         # type: str
    table_id,                       # type: str
    force_print_by_table=False,     # type: Optional[bool]
    is_mobile=False,                # type: Optional[bool]
):
    order_interactor = OrderInteractor(pos_id=pos_id)
    order = order_interactor.get_order_from_order_picture(pos_id=pos_id,)

    order_is_totaled = order is not None and order.get('state') == "TOTALED"
    force_print_by_table = _get_bool_value(value=force_print_by_table)
    is_mobile = _get_bool_value(value=is_mobile)

    show_pre_account_dialog(
        pos_id=pos_id,
        table_id=table_id,
        force_print_by_table=force_print_by_table,
        order_is_totaled=order_is_totaled,
        is_mobile=is_mobile,
        order=order,
    )


@action
@user_authorized_to_execute_action
def total_report(
    pos_id,                                 # type: str
    table_id,                               # type: str
    force_print_by_table=False,             # type: bool
    force_print_items_conference=False,     # type: bool
    is_mobile=False,                        # type: bool
):
    generate_total_report(
        pos_id=pos_id,
        table_id=table_id,
        force_print_by_table=force_print_by_table,
        force_print_items_conference=force_print_items_conference,
        is_mobile=is_mobile,
    )


def generate_total_report(
    pos_id,                                 # type: str
    table_id,                               # type: str
    force_print_by_table=False,             # type: bool
    force_print_items_conference=False,     # type: bool
    is_mobile=False,                        # type: bool
):
    model = get_model(pos_id)
    order = get_current_order(model)

    order_is_totaled = order is not None and order.get('state') == "TOTALED"
    force_print_by_table = _get_bool_value(value=force_print_by_table)
    force_print_items_conference = _get_bool_value(value=force_print_items_conference)
    is_mobile = _get_bool_value(value=is_mobile)

    if not get_pre_account_with_discount() or force_print_items_conference:
        show_pre_account_dialog(
            pos_id=pos_id,
            table_id=table_id,
            force_print_by_table=force_print_by_table,
            order_is_totaled=order_is_totaled,
            order=order,
            is_mobile=is_mobile,
        )
        return

    do_total_table(
        pos_id=pos_id,
        table_id=table_id,
        show_pre_account=True,
        order_is_totaled=order_is_totaled,
        is_mobile=is_mobile,
    )


def _get_bool_value(
    value,  # type: Any[Union[bool, str]]
):
    # type: (...) -> bool

    return value.lower() == "true" if isinstance(value, basestring) else value


def _get_filtered_seat(
    pos_id,     # type: str
    seats       # type: List[str]
):
    # type: (...) -> str

    if len(seats) > 1:
        model = get_model(pos_id)
        listbox_options = ["$ALL_SEATS"]
        listbox_options.extend([translate_message(model, "SEAT").format(seat) for seat in seats])
        title = "$SEAT_SELECTION_TITLE"
        index = show_listbox(
            posid=pos_id,
            options=listbox_options,
            message=title
        )
        if index is None:
            raise StopAction()
        else:
            seat = seats[index - 1] if index != 0 else "0"
            return seat
    else:
        return "0"


def show_pre_account_dialog(
    pos_id,                         # type: str
    table_id,                       # type: str
    force_print_by_table=False,     # type: bool
    order_is_totaled=False,         # type: bool
    preview=True,                   # type: bool
    filter_seat="0",                # type: str
    order=None,                     # type: Optional[eTree.XML]
    is_mobile=False,                # type: bool
):
    # type: (...) -> None

    print_pre_account_by = "TABLE"
    message = ""
    print_by = ""
    select_report_is_seat_or_table = not force_print_by_table
    table_picture = eTree.XML(tableService().get_table_picture(
            pos_id=pos_id,
            table_id=table_id
    ))

    seats_with_order = _get_seats_with_order(
        pos_id=pos_id,
        table_id=table_id
    )

    table_properties = table_picture.find("Properties")
    if table_properties.attrib is not None and table_properties.attrib.get("MODE") == "tabs":
        message = "$PRE_ACCOUNT_BY_TABS_OR_BY_TABLE"
        print_by = "TAB"
    elif select_report_is_seat_or_table and seats_with_order:
        message = "$PRE_ACCOUNT_BY_SEAT_OR_BY_TABLE"
        print_by = "SEATS"

    if print_by and message:
        option_selected = show_message_options_dialog(
                pos_id=pos_id,
                buttons="${}|$TABLE".format(print_by),
                message=message
        )
        if option_selected is None:
            return

        if option_selected == 0:
            print_pre_account_by = print_by
            if print_by == "SEATS" and seats_with_order and filter_seat == "0":
                filter_seat = _get_filtered_seat(
                    pos_id=pos_id,
                    seats=seats_with_order
                )
    preview = preview if not preview else not force_print_by_table
    payments = [float(item.get('tenderAmount')) > 0 for item in table_picture.findall('ServiceTenders/ServiceTender')]
    if print_pre_account_by == 'SEATS' and sum(payments) > 0:
        option = show_message_options_dialog(
            pos_id=pos_id,
            buttons="$OK|$PREVIOUS",
            title=CONFIRMATION_TITLE,
            message="$PAYMENTS_NOT_DISTRIBUTED",
        )
        if option == 1:
            return

    if print_pre_account_by in ["SEATS", "TAB"]:
        print_data = _get_report_print_by_seats(
            pos_id=pos_id,
            table_id=table_id,
            is_mobile=is_mobile,
            order_is_totaled=order_is_totaled,
            filter_seat=filter_seat,
            print_pre_account_by=print_pre_account_by,
        )
    else:
        print_data = _get_report_print_data(
            pos_id=pos_id,
            table_id=table_id,
            is_mobile=is_mobile,
            order_is_totaled=order_is_totaled,
        )

    nreports_preview_and_print.print_preview_report(
        pos_id=pos_id,
        report_data=print_data,
        preview=preview,
        order=order,
        allow_printing=get_pre_account_allow_printing(),
    )


@action
def get_special_catalog(pos_id):
    model_service = ModelService(pos_id=pos_id)
    return model_service.get_special_catalog()


@action
def get_floor_plan(pos_id):
    try:
        config = sysactions.get_cfg(pos_id)
        floor_plan_path = config.find_values("FloorPlan.FloorPlanLayoutFile")
        if not floor_plan_path:
            logger.info("Getting Floor Plan - No FloorPlan group found in configuration")
            return _get_single_floor_plan_file()

        floor_plan_path = floor_plan_path[0]
        if not floor_plan_path or not os.path.isfile(floor_plan_path):
            logger.info("Getting Floor Plan - No floor plan found")
            return '{}'

        try:
            with open(floor_plan_path, mode="rb") as floor_plan_file:
                data = floor_plan_file.read()
                return data
        except (Exception,):
            logger.exception("Getting Floor Plan - Error reading floor plan")
    except (Exception,):
        logger.exception("Getting Floor Plan - Error loading floor plan")
        return '{}'


def _get_single_floor_plan_file():
    try:
        config = cfgtools.read(os.environ["LOADERCFG"])
        floor_plan_paths = config.find_values("Customizations/FloorPlanJsonPath") or []
        for path in floor_plan_paths:
            if path != "" and os.path.isfile(path):
                try:
                    with open(path, mode="rb") as floor_plan_file:
                        data = floor_plan_file.read()
                        return data
                except (Exception,):
                    logger.exception("Getting Floor Plan - Error reading floor plan")
                    continue

        return "{}"

    except (Exception,):
        logger.exception("Getting Floor Plan - Error loading floor plan")
        return '{}'


@action
def getMyTable(pos_id):
    try:
        if has_table_service():
            selected_table = tableService().get_selected_table(pos_id)
            if selected_table:
                return tableService().get_table_picture(pos_id, selected_table.id)
            order_table_id = get_custom_property_value(
                pos_id=pos_id,
                custom_property='TABLE_ID',
            )
            if order_table_id:
                order_interactor = OrderInteractor(
                    pos_id=pos_id,
                )
                if order_interactor.is_order_in_progress():
                    return tableService().get_table_picture(
                        pos_id=pos_id,
                        table_id=order_table_id,
                    )
    except TableServiceException:
        sys_log_exception("Error getting Current Table")

    return '{}'


@action
def get_table_picture(pos_id, table_id):
    try:
        model = get_model(pos_id)
        current_operator = get_current_operator(model)
        table_picture = tableService().get_table_picture(pos_id, table_id)

        if current_operator is None:
            create_table_modified_event(pos_id, table_picture)
            return True

        current_operator_id = current_operator.get("id")
        current_operator_level = current_operator.get("level")
        if current_operator_level is None:
            current_operator_level = eTree.XML(get_user_information(current_operator_id)).find("user").get("Level")

        current_operator_level = int(current_operator_level)
        table_picture_xml = eTree.XML(table_picture)
        table_user = table_picture_xml.get("userId")
        table_user_level = get_user_level(table_user)
        pos_function = model.find("WorkingMode").get("posFunction")
        is_table_available = int(table_picture_xml.get('status')) == TableStatus.AVAILABLE.value
        if table_user_level is None and not is_table_available:
            return

        operator_has_level = current_operator_level >= table_user_level
        is_same_operator = current_operator_id == table_user
        cant_see_table = not operator_has_level and not is_same_operator and pos_function != 'CS'
        if not is_table_available and cant_see_table and not can_see_table_from_another_operator:
            return False

        create_table_modified_event(pos_id, table_picture)
        return True

    except TableServiceException:
        sysactions.show_messagebox(pos_id, "$GETTING_TOTALED_TABLE")
        return False


@action
@user_authorized_to_execute_action
def get_manager_authorization(_):
    return True


@action
@user_authorized_to_execute_action
def get_reorganize_seats_authorization(_):
    return True


def _number_of_seats_is_valid(seats):
    return int(seats) <= max_number_of_seats


def _fill_pos_list_with_can_open_day_pos(
    pos_list,       # type: List[str]
    error_dict,     # type: Dict[str, str]
):
    # type: (...) -> None

    for pos_id in list(pos_list):
        model = get_model(pos_id)
        if model.find("PosState").get("state") not in ["CLOSED", "UNDEFINED"]:
            if model.find("PosState").get("period") != datetime.today().strftime('%Y%m%d'):
                can_open_day = translate_message(model, "DAY_IS_NOT_CLOSED")
            else:
                can_open_day = translate_message(model, "DAY_IS_ALREADY_OPENED")

            _fill_error_dict(error_dict, pos_id, can_open_day)
            pos_list.remove(pos_id)


def select_table_by_dialog(pos_id, model, table_list=None, tab_list=None, split_tables=False, title="$SELECT_TABLE"):
    tables = []
    options = []

    if tab_list is not None:
        sorted_tabs = sorted(tab_list, key=lambda tup: tup[0].items()[0][1])
        tables.extend(sorted_tabs)
        for table in sorted_tabs:
            if isinstance(table, tuple):
                table_status = translate_message(model, TableStatus.get_status_label(table[1]))
                table_message = translate_message(model,
                                                  'TAB_INFORMATION2',
                                                  str(table[0].values()[0].zfill(3)), table_status)
            else:
                table_message = translate_message(model, 'TAB_INFORMATION', table.zfill(3))

            options.append(table_message)

    if table_list is not None:
        single_message = translate_message(model, 'TABLE_INFORMATION')
        status_message = translate_message(model, 'TABLE_INFORMATION2')

        sorted_tables = sorted(table_list, key=lambda tup: tup[2])
        if not split_tables:
            sorted_tables = sorted(sorted_tables, key=lambda tup: int(tup[1]), reverse=True)

        tables.extend(sorted_tables)
        table_status = {}
        for table in sorted_tables:
            if isinstance(table, tuple):
                status = TableStatus.get_status_label(table[1]) if not split_tables else None
                if status and status not in table_status:
                    table_status[status] = translate_message(model, status)
                if split_tables:
                    table_message = single_message.format(table[2][:20])
                else:
                    table_message = status_message.format(table[2][:20], table_status[status])
            else:
                table_message = single_message.format(table.zfill(3))

            options.append(table_message)

    if len(options) < 1:
        show_message_dialog(pos_id, "$INFORMATION", "$NO_OPTIONS_TO_SHOW")
        return None

    info_text = "|".join(options) if hasattr(options, '__iter__') else str(options)
    table_index = show_filtered_list_box_dialog(pos_id, info_text, title, "", "")
    if table_index is None:
        return None

    if isinstance(tables[int(table_index)][0], dict):
        return tables[int(table_index)][0].items()[0]

    return tables[int(table_index)]


def _get_table_id(
    pos_id,     # type: str
    model,      # type: eTree.XML
):
    # type: (...) -> str

    table_list = []
    tab_list = []
    for table in tableService().list_tables(pos_id):
        is_tab = False
        table_tuple = []
        is_pos_cashier = get_posfunction(model) == "CS"
        if table.tab_id is None and "TAB" not in table.id:
            available_in_cashier = is_pos_cashier and table.status == TableStatus.AVAILABLE
            totaled_in_order_taker = get_posfunction(model) == "OT" and table.status == TableStatus.TOTALED
            if not available_in_cashier and not totaled_in_order_taker:
                table_tuple = [(str(table.id), int(table.status), str(table.name))]
        elif table.tab_id is not None and int(table.status) not in [TableStatus.CLOSED, TableStatus.ABANDONED]:
            is_tab = True
            table_tuple = [({str(table.id): table.tab_id}, int(table.status), str(table.name))]

        if table_tuple:
            table_obj = Table(
                id=table.id,
                status=table.status,
                slice_parent_table_id=table.slice_parent_table_id,
                user_id=table.user_id,
                sub_sector_name=table.sub_sector_name,
            )
            if is_pos_cashier or allowed_to_open_table(
                pos_id=pos_id,
                model=model,
                table=table_obj,
                is_tab=is_tab,
            ):
                tab_list.extend(table_tuple) if is_tab else table_list.extend(table_tuple)

    selected_table = select_table_by_dialog(pos_id, model, table_list=table_list, tab_list=tab_list)
    if selected_table is None:
        return ''

    table_id = selected_table[0]

    return table_id


def _get_service_seats_number(
    pos_id,     # type: str
    table_ids,  # type: List[str]
):
    # type: (...) -> Optional[str]

    service_seats = sysactions.show_keyboard(
        posid=pos_id, message="",
        title="$SELECT_QUANTITY_OF_SEATS",
        mask="INTEGER",
        numpad=True,
        buttons=OK_CANCEL_BUTTONS,
    )
    if service_seats is None:
        return None

    if service_seats == "" or service_seats == "NaN" or int(service_seats) == 0:
        show_message_dialog(pos_id, "$INFORMATION", "$INVALID_NUMBER_OF_SEATS")
        return None

    if not _number_of_seats_is_valid(seats=service_seats):
        show_message_dialog(pos_id, "$INFORMATION", "$MAX_NUMBER_OF_SEATS")
        return None

    if max_seats_reached not in ["error", "alert"] or not table_ids:
        return service_seats

    table_seats = 0
    for table_id in table_ids:
        table_seats += _get_table_seats(pos_id, table_id)

    if int(service_seats) <= int(table_seats):
        return service_seats

    if max_seats_reached == "error":
        show_message_dialog(pos_id, "$INFORMATION", "$MAX_NUMBER_OF_SEATS")
        return None

    if max_seats_reached == "alert":
        message = "$MAX_NUMBER_OF_SEATS_QUESTION"
        resp = show_message_options_dialog(
            pos_id=pos_id,
            buttons=OK_CANCEL_BUTTONS,
            message=message,
        )
        if resp is None or resp == 1:
            return None

    return service_seats


def _get_table_seats(pos_id, table_id):
    table_seats = 0
    table_info = tableService().get_table_info(pos_id, table_id)
    table_info_xml = eTree.XML(table_info)
    table_seats += int(table_info_xml.get("seats") or 0)

    linked_tables = table_info_xml.get("linkedTables")
    if linked_tables:
        for linked_table in linked_tables.split(","):
            table_seats += _get_table_seats(pos_id, linked_table)

    return table_seats


def _process_closed_table(pos_id, table_id):
    resp = show_message_options_dialog(
        pos_id=pos_id,
        buttons="$OK|$PREVIOUS",
        title="$INFORMATION",
        message="$CONFIRM_SET_AVAILABLE",
    )
    if resp is None or resp == 1:
        return False

    set_available(pos_id, table_id)
    return True


def _get_seats_with_order(
    pos_id,     # type: str
    table_id    # type: str
):
    # type: (...) -> List[str]

    seat_with_orders = []
    table_picture = eTree.XML(tableService().get_table_picture(pos_id, table_id))
    table_orders = table_picture.findall(".//Order")
    service_seats = int(table_picture.get("serviceSeats"))
    if service_seats <= 1:
        return seat_with_orders

    for order in table_orders:
        sale_lines = order.findall(".//SaleLine")
        _update_seat_with_orders(
            sale_lines=sale_lines,
            seat_with_orders=seat_with_orders,
        )

    seat_with_orders = sorted(seat_with_orders, key=lambda g: int(g))
    return seat_with_orders


def _update_seat_with_orders(
    sale_lines,         # type: List[eTree.Element]
    seat_with_orders,   # type: List[str]
):
    # type: (...) -> None

    for sale_line in sale_lines:
        if int(sale_line.get("level") or 0) != 0:
            continue

        sale_line_qty = float(sale_line.get("qty"))
        properties = json.loads(sale_line.get("customProperties") or "{}")
        if "seat" not in properties:
            continue

        if properties["seat"] != "0" and properties["seat"] not in seat_with_orders and sale_line_qty > 0:
            seat_with_orders.append(properties["seat"])


def _set_orders_hold_items(pos_id, table_id, hold_items=None):
    hold_items = "" if hold_items is None else hold_items
    tableService().set_table_custom_property(pos_id, "ORDERS_HOLD_ITEMS", prop_value=hold_items, table_id=table_id)


def has_table_service(ignore_cache=False):
    global table_service
    global last_cache_time

    seconds_to_renew = get_seconds_to_renew_cache(60 * 5)
    if ignore_cache or reached_seconds_to_renew_cache(last_cache_time, seconds_to_renew):
        with lock:
            last_cache_time = datetime.now()
            table_service = mbcontext.MB_LocateService(mbcontext.hv_service, 'TABLEMGR', maxretries=1)

    return table_service is not None


@action
def check_has_table_service(_):
    return has_table_service(ignore_cache=True)


def _is_seat(
    table_el  # type: eTree.Element
):
    # type: (...) -> bool

    table_id = table_el.get("id") or ""
    return table_id.isdigit() and table_el.get("typeDescr") == "Seat"


def _has_tabs(
    table_el  # type: eTree.Element
):
    # type: (...) -> bool

    table_mode = "seats"
    if table_el.find("Properties") is not None:
        table_mode = table_el.find("Properties").get("MODE")

    return _is_seat(table_el) and table_mode == "tabs"


def _check_tab_user_sector(
    table_picture,  # type: Any[Union[str, eTree.XML]]
    pos_id,         # type: str
    reopen=False,   # type: Optional[bool]
):
    # type: (...) -> (Any[Union[str, eTree.XML]], bool)

    sector_mode = table_operation_mode in [TableOperationMode.SHARED_SECTOR, TableOperationMode.INDIVIDUAL_SECTOR]
    if not sector_mode:
        return table_picture, False

    model = get_model(posid=pos_id)
    pos_ts = get_pos_ts(model=model)
    table_picture_xml = eTree.XML(table_picture) if isinstance(table_picture, str) else table_picture
    table_user_id = int(table_picture_xml.get("userId"))
    table_id = table_picture_xml.get("id")

    if reopen:
        changed_user = _check_if_tab_id_is_required(
            pos_id=pos_id,
            table_id=table_id,
        )
    else:
        new_operator = _show_list_operators(
            pos_id=pos_id,
            sector="$TABS",
            table_user=table_user_id,
        )
        if new_operator[0] < 0:
            return None, False

        new_operator = new_operator[1][new_operator[0]]

        pos_ts.changeTableUser(
            user_id=new_operator,
            table_id=table_id,
        )

        changed_user = True if new_operator != table_user_id and sector_mode else False

    table_picture = tableService().get_table_picture(
        pos_id=pos_id,
        table_id=table_id,
    )

    return table_picture, changed_user


def _get_recovery_tabs_available(
    pos_id,     # type: int
):
    # type: (...) -> Optional[List[str]]

    tabs_result = sysactions.get_cfg(
       posid=pos_id,
    ).find_values(
       path="RecoverMenuOptions",
    )

    return tabs_result


def save_order_cancel_receipt(
    pos_id,         # type: str
    order_id,       # type: str
    tender_id       # type: str
):
    # type: (...) -> bool

    model = get_model(pos_id)
    order_taker = get_posot(model=model)
    order_picture = eTree.XML(order_taker.orderPicture(
        posid=pos_id,
        orderid=order_id,
    )).find(".//Order")
    tender = get_order_tender(
        order_picture,
        tender_id
    )
    tender_details = json.loads(tender.get("tenderDetail"))
    receipt_cancel = {
        "posId": pos_id,
        "userId": model.find("Operator").get("id"),
        "canceledAt": str(datetime.now())
    }

    if "receiptCancel" in tender_details:
        tender_details["receiptCancel"].update(receipt_cancel)
    else:
        tender_details["receiptCancel"] = receipt_cancel

    tender = {'tenderdetail': json.dumps(tender_details), 'tenderid': int(tender_id)}

    return update_tenders(
        pos_id=pos_id,
        order_id=order_picture.get("orderId"),
        tender_id=tender_id,
        new_tender_detail=json.dumps([tender]),
    )


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

    try:
        tableService().reset_store_layout(pos_id=pos_id)
    except MBException:
        message = "Error resetting store layout"
        logger.exception(message)


def _get_report_print_data(
    pos_id,             # type: str
    table_id,           # type: str
    is_mobile,          # type: bool
    order_is_totaled,   # type: bool
):
    # type: (...) -> str

    if get_pre_account_with_discount() or order_is_totaled:
        return get_nreports_service().pre_account(
            pos_id=pos_id,
            table_id=table_id,
            is_mobile=is_mobile,
        )

    return get_nreports_service().conference_items(
        pos_id=pos_id,
        table_id=table_id,
        is_mobile=is_mobile
    )


def _get_report_print_by_seats(
    pos_id,                 # type: str
    table_id,               # type: str
    is_mobile,              # type: bool
    order_is_totaled,       # type: bool
    filter_seat,            # type: str
    print_pre_account_by,   # type: str
):
    # type: (...) -> str

    if get_pre_account_with_discount() or order_is_totaled:
        return get_nreports_service().pre_account_by_seats(
            pos_id=pos_id,
            table_id=table_id,
            filter_table_seat=filter_seat,
            print_by=print_pre_account_by,
            is_mobile=is_mobile,
       )

    return get_nreports_service().conference_items_by_seats(
        pos_id=pos_id,
        table_id=table_id,
        filter_table_seat=filter_seat,
        print_by=print_pre_account_by,
        is_mobile=is_mobile,
    )
