# -*- coding: utf-8 -*-
import datetime
import locale
import time
import json
import ast
import re
import cfgtools
import os

from xml.etree import cElementTree as eTree
from cStringIO import StringIO
from collections import defaultdict
from decimal import Decimal as D

import sysactions
from systools import sys_log_error, sys_log_exception, sys_parsefilepath
from reports import dbd, mbcontext, Report
from msgbus import TK_POS_GETPOSLIST, TK_SYS_NAK, TK_SYS_ACK
from operator import itemgetter
from brandreport import BrandReport
from brandreport.repository import OrderRepository as BrandOrderRepository,\
                                   FiscalRepository as BrandFiscalRepository,\
                                   TenderRepository as BrandTenderRepository
from salessummaryreport import SalesSummary
from cashreport import CashReport
from cashreport.repository import \
    OrderRepository,\
    AccountRepository,\
    TenderRepository,\
    FiscalRepository,\
    PosCtrlRepository,\
    TableRepository, \
    ProductRepository as CashProductRepository
from typing import Optional, Union, List
from utils import get_store_id, get_pos_list
from voidedlinesreport import VoidedLinesReport
from voidedlinesreport.repository import \
    OrderRepository as VoidedLinesOrderRepository, \
    UsersRepository as VoidedLinesUsersRepository
from pmixreport import PMixReport
from pmixreport.repository import ProductRepository, OrderRepository as PmixOrderRepository
from voidedreport import VoidedReport
from voidedreport.repository import OrderRepository as VoidedOrderRepository
from averageticketreport import AverageTicketReport
from averageticketreport.repository import TableServiceRepository as AverageTicketTableServiceRepository,\
                                           OrderRepository as AverageTicketOrderRepository
from closeoperatorreport import CloseOperatorReport
from closeoperatorreport.repository import OrderRepository as CloseOperatorOrderRepository
from closeoperatorreport.repository import ProductRepository as CloseOperatorProductRepository
from closeoperatorreport.repository import AccountRepository as CloseOperatorAccountRepository
from transfersreport import TransfersReport
from transfersreport.repository import AccountRepository as TransferAccountRepository
from discountreport import DiscountReport
from discountreport.repository import OrderRepository as DiscountOrderRepository
from helper import remove_accents



sysactions.mbcontext = mbcontext

COLS = 38
SEPARATOR = (b'\x3d' * COLS)
SINGLE_SEPARATOR = (b'\x2d' * COLS)
DATE_TIME_FMT = "%d/%m/%Y %H:%M:%S"
DATE_FMT = "%d/%m/%Y"
CONSOLIDATED = True
ZERO = D("0.00")


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


def _manager_report_header(title, posid, oper_id, bus_period):
    title = _center(title)
    current_datetime = time.strftime(DATE_TIME_FMT)
    formatted_store_id = get_store_id()

    if len(bus_period) == 8:
        business_period = "%02d/%02d/%04d" % (int(bus_period[6:8]), int(bus_period[4:6]), int(bus_period[:4]))
    else:
        business_period = "%s (Dia Fechado)" % bus_period

    if int(oper_id) == 0:
        oper_id = "Todos"
    else:
        if len(oper_id) == 1:
            oper_id = "0%s" % (oper_id)

    return (
        """%(SEPARATOR)s
%(title)s
  Loja..........: %(formatted_store_id)s
  Data/hora.....: %(current_datetime)s
  Data Negocio..: %(business_period)s
  ID Operador...: %(oper_id)02s (POS # %(posid)02d)
""" % _join(globals(), locals()))


def _cut(s):
    return s[:COLS] if (len(s) > COLS) else s


def _center(s):
    s = _cut(s)
    miss = COLS - len(s)
    if miss == 0:
        return s
    left = miss / 2
    return (" " * left) + s


def _fmt_number(number, decimalPlaces=2, decimalSep='.', thousandsSep=','):
    # Gets sign
    sign = '-' if number < 0 else ''
    number = abs(number)

    # Splits on decimals
    number, dec = ("%.*f" % (decimalPlaces, number)).split('.')

    # Adds thousands separators
    if thousandsSep:
        sepPos = len(number) - 3
        while sepPos >= 1:
            number = number[:sepPos] + thousandsSep + number[sepPos:]
            sepPos -= 3

    # Adds decimals separator
    if decimalSep:
        number += (decimalSep + dec)

    return sign + number


def cash(posid, period, operatorid, store_wide, posnumbers, report_type="0", session_id="", manager_user="", *args):
    report = StringIO()
    posnumbers = posnumbers.split('|') if posnumbers else []
    store_wide = (store_wide.lower() == "true")

    if report_type == "sales_report":
        report_type = "Relatorio Vendas"

    elif report_type == "logoffuser":
        report_type = "Logout Operador"

    elif report_type == "cashier_flash":
        report_type = "Relatorio Surpresa"

    elif report_type == "end_of_day":
        report_type = "Fechamento do Dia"

    if store_wide:
        report_type = "%s (Loja)" % (report_type)
    else:
        posnumbers = [posid]

    manager_user = None if manager_user in ["None", ""] else manager_user
    formatted_date = datetime.datetime.strptime(period, '%Y%m%d').strftime('%d/%m/%Y')
    report.write(_manager_report_header(report_type, int(posid), manager_user or operatorid, formatted_date))

    operators = []
    voided_item_qty, voided_item_amount = (0, 0)
    transfer_in_qty, transfer_in_amount, transfer_out_qty, transfer_out_amount, skim_qty, skim_amount, donated_qty, donated_amount = (0, 0, 0, 0, 0, 0, 0, 0)
    paid_qty, net_sales, voided_net_coupon, initialfloat, initial_coupon, total_coupon, total_declared = (0, 0, 0, 0, 0, 0, 0)
    voided_coupon_qtd, gross_sales, voided_gross_coupon, refund_gross_amount, refund_net_amount, refund_qty_count = (0, 0, 0, 0, 0, 0)
    waste_gross_amount, waste_net_amount, waste_qty_count, tax_sales = (0, 0, 0, 0)
    cash_refund_amount, cash_gross_amount = (0, 0)
    donations_amount, donations_refunds_amount, donations_qty = (ZERO, ZERO, 0)
    total_discount_amount = ZERO
    total_tip, cash_tips, refunded_tips, voided_tips = (0, 0, 0, 0)
    tip_amount = 0

    pos_included = []
    any_data = False
    discountInfos = defaultdict(lambda: {"Descr": "", "Qty": 0, "Amt": ZERO})
    tenderInfos = defaultdict(lambda: {"TenderId": "", "TenderDescr": "", "TenderTotal": ZERO})
    donationInfos = defaultdict(lambda: {"Descr": "", "Qty": 0, "Amt": ZERO})
    list_orderid_fiscal = []
    is_print_flag_card = False

    for posno in posnumbers:
        pos_included.append(posno)

        # create database connection
        conn = None
        try:
            try:
                conn = dbd.open(mbcontext, dbname=str(posno))
            except:
                sys_log_exception("Excecao abrindo conexao com pos: " + str(posno))
                continue

            # reserve a database connection
            conn.transaction_start()

            # set the period
            conn.query("DELETE FROM temp.ReportsPeriod")
            conn.query("INSERT INTO temp.ReportsPeriod(StartPeriod,EndPeriod) VALUES(%s,%s)" % (period, period))

            if (report_type == "Relatorio Surpresa") and int(operatorid):
                # Retrieve only the latest session id (eLanes ticket #15)
                cursor = conn.select("SELECT SessionId FROM posctrl.UserSession WHERE BusinessPeriod='%s' AND PosId=%s AND OperatorId=%s ORDER BY OpenTime DESC LIMIT 1" % (period, posid, operatorid))
                if cursor.rows():
                    session_id = cursor.get_row(0).get_entry(0)

            # select temporary table
            if session_id:
                cursor = conn.select("SELECT * from temp.CASHView WHERE BusinessPeriod='%s' AND SessionId='%s';" % (period, session_id))
            elif int(operatorid):
                cursor = conn.select("SELECT * from temp.CASHView WHERE PosId='%s' AND BusinessPeriod='%s' AND OperatorId='%s';" % (posno, period, operatorid))
            else:
                cursor = conn.select("SELECT * from temp.CASHView WHERE PosId='%s' AND BusinessPeriod='%s';" % (posno, period))

            for row in cursor:
                any_data = True
                operator_id = row.get_entry("OperatorId")  # ID Operador
                operator_name = row.get_entry("OperatorName")  # Operator Name

                # Get the Initial Coupon for the Day
                if row.get_entry("InitialOrderId"):
                    initial_coupon = min((initial_coupon, int(row.get_entry("InitialOrderId"))))

                # List all operators # for the Day
                operators.append(row.get_entry("OperatorId"))

                # Sum all initial float for the Day
                initialfloat = initialfloat + D(row.get_entry("InitialFloat"))

                # Sum all gross sales for the Day
                gross_sales = gross_sales + D(row.get_entry("PaidGrossAmt"))

                # Sum all net sales for the Day
                net_sales = net_sales + D(row.get_entry("PaidNetAmt"))

                # Sum all paid quantities for the Day
                paid_qty = paid_qty + int(row.get_entry("PaidCount"))

                # Sum all gross voided sales for the day
                voided_gross_coupon = voided_gross_coupon + D(row.get_entry("VoidGrossAmt"))

                # Sum all net voided sales for the day
                voided_net_coupon = voided_net_coupon + D(row.get_entry("VoidNetAmt"))

                # Sum all voided quantities for the day
                voided_coupon_qtd = voided_coupon_qtd + int(row.get_entry("VoidCount"))

                # Sum all coupon quantities (transactions) for the day
                total_coupon = total_coupon + int(row.get_entry("TransactCount"))

                # Get the last order ID (coupon number) for the Day
                # if row.get_entry("FinalOrderId"):
                #     final_coupon = int(row.get_entry("FinalOrderId"))

                # Sum all refund gross amount for the Day
                refund_gross_amount = refund_gross_amount + D(row.get_entry("RefundGrossAmt"))

                # Sum all refund net amount for the Day
                refund_net_amount = refund_net_amount + D(row.get_entry("RefundNetAmt"))

                # Sum all refund quantities for the Day
                refund_qty_count = refund_qty_count + int(row.get_entry("RefundCount"))

                # Sum all waste gross amount for the Day
                waste_gross_amount = waste_gross_amount + D(row.get_entry("WasteGrossAmt"))

                # Sum all waste net amount for the Day
                waste_net_amount = waste_net_amount + D(row.get_entry("WasteNetAmt"))

                # Sum all waste quantities for the Day
                waste_qty_count = waste_qty_count + int(row.get_entry("WasteCount"))

                # Sum all CASH refund amounts
                cash_refund_amount += D(row.get_entry("CashRefundAmount") or 0)

                # Sum all CASH gross sales amounts
                cash_gross_amount += D(row.get_entry("CashGrossAmount") or 0)

                # Get the Initial GT(Gran Total) Gross
                # if row.get_entry("InitialPOSForeverTotalGross"):
                #     initial_GT_gross = D(row.get_entry("InitialPOSForeverTotalGross"))

                # Sum all voided items quantities for the Day
                if row.get_entry("ReducedItemsQty"):
                    voided_item_qty = voided_item_qty + int(row.get_entry("ReducedItemsQty"))

                # Sum all voided items amount for the Day
                if row.get_entry("ReducedAmount"):
                    voided_item_amount = voided_item_amount + D(row.get_entry("ReducedAmount"))

                # Sum all Transfer IN quantities for the Day
                if row.get_entry("TransferInQty"):
                    transfer_in_qty = transfer_in_qty + int(row.get_entry("TransferInQty"))

                # Sum all Transfer IN Amount for the Day
                if row.get_entry("TransferInAmount"):
                    transfer_in_amount = transfer_in_amount + D(row.get_entry("TransferInAmount"))

                # Sum all Transfer OUT quantities for the Day
                if row.get_entry("TransferOutQty"):
                    transfer_out_qty = transfer_out_qty + int(row.get_entry("TransferOutQty"))

                # Sum all Transfer OUT Amount for the Day
                if row.get_entry("TransferOutAmount"):
                    transfer_out_amount = transfer_out_amount + D(row.get_entry("TransferOutAmount"))

                # Sum all Skim quantities for the Day
                if row.get_entry("SkimQty"):
                    skim_qty = skim_qty + int(row.get_entry("SkimQty"))

                # Sum all Skim Amount for the Day
                if row.get_entry("SkimAmount"):
                    skim_amount = skim_amount + D(row.get_entry("SkimAmount"))

                if row.get_entry("DiscountSessionInfo"):
                    discountInfo = eval(row.get_entry("DiscountSessionInfo"))
                    for discount in discountInfo:
                        consolidated = discountInfos[discount["Code"]]
                        amt = D(discount["Amt"])
                        consolidated["Descr"] = discount["Descr"]
                        consolidated["Qty"] += int(discount["Qty"])
                        consolidated["Amt"] += amt
                        total_discount_amount += amt

                if row.get_entry("TenderSessionInfo"):
                    tenderInfo = eval(row.get_entry("TenderSessionInfo"))
                    for tender in tenderInfo:
                        consolidated = tenderInfos[tender["TenderId"]]
                        consolidated["TenderId"] = tender["TenderId"]
                        consolidated["TenderDescr"] = tender["TenderDescr"]
                        consolidated["TenderTotal"] += D(tender["TenderTotal"])
                        tip_amount += D(tender["TipAmount"]) if "TipAmount" in tender else 0

            # Select declared cash information
            cursor = conn.select("""SELECT
                        COALESCE(sum(tr.amount), 0) AS "Declared"
                     FROM posctrl.UserSession us
                      LEFT JOIN account.Transfer tr ON us.SessionId = tr.SessionId AND tr.Description = 'DECLARED_AMOUNT'
                     WHERE tr.Period = %s AND tr.PosId = %s %s;""" % (period, posno, '' if int(operatorid) == 0 else ('AND us.OperatorId = %s' % operatorid)))

            for row in cursor:
                total_declared += D(row.get_entry('Declared'))

            # Select the donations information
            if session_id:
                where_clause = "WHERE O.BusinessPeriod='%s' AND U.SessionId='%s'" % (period, session_id)
            elif int(operatorid):
                where_clause = "WHERE U.PosId='%s' AND O.BusinessPeriod='%s' AND U.OperatorId='%s'" % (posno, period, operatorid)
            else:
                where_clause = "WHERE U.PosId='%s' AND O.BusinessPeriod='%s'" % (posno, period)

            sql = """
            SELECT
            ProductCode AS DonationCode,
            ProductName AS DonationName,
            tdsum(CASE WHEN X.IsRefund THEN '0.00' ELSE TotalPrice END) AS DonatedAmount,
            tdsum(CASE WHEN X.IsRefund THEN TotalPrice ELSE '0.00' END) AS RefundedAmount,
            sum(DonatedQty) AS DonatedQty
            FROM (
                SELECT
                    OI.PartCode AS ProductCode,
                    tdsum(tdmul(COALESCE(OI.OverwrittenUnitPrice, PR.DefaultUnitPrice, '0.00'), OI.OrderedQty)) AS TotalPrice,
                    P.ProductName AS ProductName,
                    (CASE WHEN O.OrderType=0 THEN 1 ELSE 0 END) AS DonatedQty,
                    (CASE WHEN O.OrderType=1 THEN 1 ELSE 0 END) AS IsRefund
                FROM orderdb.Orders O
                JOIN orderdb.OrderItem OI
                    ON OI.OrderId=O.OrderId AND O.StateId=5 AND OI.OrderedQty>0
                JOIN productdb.ProductCustomParams PCP
                    ON OI.PartCode=PCP.ProductCode AND LOWER(PCP.CustomParamId)='familygroup' AND LOWER(PCP.CustomParamValue)='donation'
                JOIN productdb.Product P
                    ON P.ProductCode=OI.PartCode
                JOIN posctrl.UserSession U
                    ON O.BusinessPeriod=U.BusinessPeriod AND O.SessionId=U.SessionId
                LEFT JOIN productdb.Price PR
                    ON PR.PriceKey=OI.PriceKey
                %(where_clause)s AND O.OrderType IN (0,1)
                GROUP BY OI.OrderId,OI.PartCode
            ) X
            GROUP BY X.ProductCode
            """ % ({"where_clause": where_clause})

            cursor = conn.select(sql)
            for row in cursor:
                consolidated = donationInfos[str(row.get_entry("DonationCode"))]
                consolidated["Descr"] = str(row.get_entry("DonationName"))
                consolidated["Qty"] += int(row.get_entry("DonatedQty"))
                consolidated["Amt"] += D(row.get_entry("DonatedAmount"))
                donations_qty += int(row.get_entry("DonatedQty"))
                donations_amount += D(row.get_entry("DonatedAmount"))
                donations_refunds_amount += D(row.get_entry("RefundedAmount"))

            if session_id:
                cursor = conn.select("""SELECT COALESCE(COUNT(ocp.value), 0) AS DonatedQty, COALESCE(SUM(ocp.Value), 0) AS DonatedAmount FROM OrderCustomProperties ocp 
                                        INNER JOIN Orders o 
                                        ON o.OrderId = ocp.OrderId 
                                        WHERE ocp.Key = 'DONATION_VALUE' 
                                        AND o.BusinessPeriod = '%s' 
                                        AND o.SessionId = '%s'""" % (period, session_id))
            else:
                cursor = conn.select("""SELECT COALESCE(COUNT(ocp.value), 0) AS DonatedQty, COALESCE(SUM(ocp.Value), 0) AS DonatedAmount FROM OrderCustomProperties ocp 
                                        INNER JOIN Orders o 
                                        ON o.OrderId = ocp.OrderId 
                                        WHERE ocp.Key = 'DONATION_VALUE' 
                                        AND o.BusinessPeriod = '%s'""" % period)
            row = cursor.get_row(0)

            donated_amount = D(row.get_entry("DonatedAmount") or 0)
            donated_qty = D(row.get_entry("DonatedQty") or 0)

        finally:
            if conn:
                conn.close()

        # create database connection
        conn = None
        try:
            conn = dbd.open(mbcontext, dbname=str(posno))

            # reserve a database connection
            conn.transaction_start()

            originatorid = "POS%04d" % int(posno)

            if int(operatorid):
                sql = """SELECT OT.OrderId, O.OrderType, O.StateId, OT.OrderTenderId, COALESCE(OT.TipAmount,'0.00') AS TipAmount, TT.TenderId, TT.TenderDescr
                     FROM orderdb.OrderTender OT
                     JOIN productdb.TenderType TT ON TT.TenderId=OT.TenderId
                     JOIN orderdb.Orders O ON O.OrderId=OT.OrderId
                     WHERE O.BusinessPeriod='%s'
                     AND O.OriginatorId = '%s'
                     AND O.StateId IN (5,4)
                     AND O.SessionId LIKE '%%user=%s,%%'
                    """ % (period, originatorid, operatorid)
            else:
                sql = """SELECT OT.OrderId, O.OrderType, O.StateId, OT.OrderTenderId, COALESCE(OT.TipAmount,'0.00') AS TipAmount, TT.TenderId, TT.TenderDescr
                     FROM orderdb.OrderTender OT
                     JOIN productdb.TenderType TT ON TT.TenderId=OT.TenderId
                     JOIN orderdb.Orders O ON O.OrderId=OT.OrderId
                     WHERE O.BusinessPeriod='%s'
                     AND O.OriginatorId = '%s'
                     AND O.StateId IN (5,4)
                    """ % (period, originatorid)

            if session_id:
                sql += "AND O.SessionId = '%s'" % session_id

            cursor = conn.select(sql)

            for row in cursor:
                list_orderid_fiscal.append(row.get_entry("OrderId"))

                if int(row.get_entry("OrderType")) == 0 and int(row.get_entry("StateId")) == 5:
                    total_tip += D(row.get_entry("TipAmount"))
                    if row.get_entry("TenderId") == "0":
                        cash_tips += D(row.get_entry("TipAmount"))
                elif int(row.get_entry("OrderType")) == 0 and int(row.get_entry("StateId")) == 4:
                    voided_tips += D(row.get_entry("TipAmount"))
                elif int(row.get_entry("OrderType")) == 1 and int(row.get_entry("StateId")) == 5:
                    refunded_tips += D(row.get_entry("TipAmount"))

        finally:
            if conn:
                conn.close()

    # Tax calculation
    tax_sales = (gross_sales - net_sales)

    if is_print_flag_card:
        fiscal_conn, cursor_card, card_flags = None, None, None
        try:
            sql = """SELECT
                CASE WHEN bc.Descricao IS NOT NULL THEN bc.Descricao ELSE 'OUTROS' END as desc,
                pd.Type as type,
                sum(pd.Amount) as amout
            FROM fiscal.PaymentData pd
            LEFT JOIN fiscal.BandeiraCartao bc on pd.Bandeira = bc.Bandeira
            WHERE pd.Type IN (1,2) AND pd.OrderId IN {0}
            GROUP BY bc.Descricao, pd.Type""".format('(' + ', '.join((str(n) for n in list_orderid_fiscal)) + ')')

            fiscal_conn = dbd.open(mbcontext, service_name="FiscalPersistence")
            cursor_card = fiscal_conn.select(sql)
            card_flags = [dict([(cursor_card.get_name(col_id), row.get_entry(col_id)) for col_id in range(cursor_card.cols())]) for row in cursor_card]
        except:
            sys_log_exception("Excecao get flags")
        finally:
            if fiscal_conn:
                fiscal_conn.close()

    gross_sales += voided_gross_coupon
    refund_tax_amount = (refund_gross_amount - refund_net_amount)
    net_sales -= refund_net_amount
    tax_sales -= refund_tax_amount

    temp_pos_list = "POS incluidos...: "+" ".join(posnumbers)
    report.write("%s\n" % (SEPARATOR))
    report.write(temp_pos_list[:COLS])
    if len(temp_pos_list) > COLS:
        report.write("\n             %s" % temp_pos_list[COLS:2*COLS-18])
    if len(temp_pos_list) > 2*COLS-18:
        report.write("\n             %s" % temp_pos_list[2 * COLS - 18:3 * COLS - 36])
    report.write("\n%s\n" % (SEPARATOR))

    # Check if there is any data to print
    if not any_data:
        # report.write(_center("Dados nao encontrados para o operador # %s\n" % (operatorid)))
        report.write(_center("Sem dados para o movimento\n"))
        report.write("%s\n" % (SEPARATOR))
        return report.getvalue()

    def add_line(descr, qty=None, amount=None, info=None):
        DESCR = 17
        if amount is not None:
            amount = _fmt_number(float(amount))

        descr = descr[0:DESCR]

        if len(descr) < DESCR:
            descr += '.' * (DESCR - len(descr))

        if qty is not None:
            line = "%s: [%4s] " % (descr, qty)
        else:
            line = "%s: " % (descr)

        if amount is not None:
            if qty is None:
                line += "       "
            remain = COLS - len(line) - 3
            line += "R$ %*s\n" % (remain, amount)
        elif info is not None:
            remain = COLS - len(line)
            line += "%*.*s\n" % (remain, remain, info)
        else:
            line += "\n"

        report.write(line)

    # Starting Printing
    report.write("Descricao         Qtd         Total\n")

    if int(operatorid) == 0:
        # Operators (Quantity)
        add_line("Operadores", len(operators), None)
    else:
        # ID Operador
        add_line("ID Operador", info=operator_id)
        # Operator Name
        add_line("Nome Operador", info=operator_name)

    # Total Transactions Count (Quantity)
    add_line("Total Pedidos", info=total_coupon)

    # Gross Sales
    add_line("Vendas", None, gross_sales + tip_amount)

    # Voided (Coupons) Gross Amount
    add_line("Cancelamentos", voided_coupon_qtd, voided_gross_coupon)

    # Net Sales Amount
    add_line("Venda Liquida", None, net_sales + tax_sales + tip_amount)

    # Paid Quantity
    add_line("Pedidos Pagos", info=paid_qty)

    # TenderSessionInfo - get the information of specific tenders
    add_line("Total Recebido")
    for tenderid, tender in sorted(tenderInfos.items()):
        amt, descr = map(tender.get, ("TenderTotal", "TenderDescr"))
        if descr == 'CARTAO CREDITO' and is_print_flag_card:
            descr = "  " + descr
            add_line(descr, None, amt)
            for flag in filter(lambda x : x.get('type') == '1', card_flags):
                add_line("    " + flag.get('desc'), None, flag.get('amout'))
        elif descr == 'CARTAO DEBITO' and is_print_flag_card:
            descr = "  " + descr
            add_line(descr, None, amt)
            for flag in filter(lambda x : x.get('type') == '2', card_flags):
                add_line("    " + flag.get('desc'), None, flag.get('amout'))
        else:
            descr = "  " + descr
            add_line(descr, None, amt)

    # Skims
    add_line("Sangria", transfer_out_qty, transfer_out_amount)
    add_line("Suprimentos", transfer_in_qty, transfer_in_amount)

    report.write("%s\n" % (SEPARATOR))

    return report.getvalue()


new_pos_list = None
fiscal_sent_dir = None
tip_operator_who_closed = None


def new_cash_report(date_type, pos_id, report_pos, initial_date, end_date, operator_id, session_id, store_id, business_period=None, codigo_centro=None, close_time=None, is_table_service='False', only_body='False', only_payments='False', is_close_operator=False, is_integrated_delivery='False'):
    global new_pos_list, fiscal_sent_dir

    if session_id == 'None':
        session_id = None

    if date_type != "JsonBusinessPeriod" and new_pos_list is None:
        msg = mbcontext.MB_EasySendMessage("PosController", TK_POS_GETPOSLIST)
        if msg.token != TK_SYS_ACK:
            sys_log_error("Could not retrieve PosList")
            raise Exception("Could not retrieve PosList")

        new_pos_list = sorted(map(int, msg.data.split("\0")))

    if fiscal_sent_dir is None:
        config = cfgtools.read(os.environ["LOADERCFG"])
        fiscal_sent_dir = sys_parsefilepath(config.find_value("CashReport.FiscalSentDir").strip())

    order_repository = OrderRepository(mbcontext, new_pos_list, fiscal_sent_dir)
    account_repository = AccountRepository(mbcontext)
    tender_repository = TenderRepository(mbcontext)
    fiscal_repository = FiscalRepository(mbcontext)
    pos_ctrl_repository = PosCtrlRepository(mbcontext)
    table_repository = TableRepository(mbcontext)
    product_repository = CashProductRepository(mbcontext)

    operator_id = str(int(0 if operator_id in (None, 'None') else operator_id))
    if operator_id == '0':
        operator_id = None

    report_pos = str(int(0 if report_pos in (None, 'None') else report_pos))
    is_integrated_delivery = is_integrated_delivery.lower() != 'false'
    if report_pos == '0' and not is_integrated_delivery:
        report_pos = None


    is_table_service = is_table_service.lower() == 'true' if type(is_table_service) == str else is_table_service
    only_body = only_body.lower() == 'true' if type(only_body) == str else only_body
    only_payments = only_payments.lower() == 'true' if type(only_payments) == str else only_payments

    cash_report = CashReport(order_repository, account_repository, tender_repository, fiscal_repository, pos_ctrl_repository, table_repository, product_repository, store_id)
    initial_date = datetime.datetime.strptime(initial_date, "%Y%m%d") if date_type != "SessionId" else None
    end_date = datetime.datetime.strptime(end_date, "%Y%m%d") if date_type != "SessionId" else None
    pos_id = int(pos_id)
    voided_lines = get_voided_lines(report_pos, initial_date, end_date, operator_id, session_id) if date_type != "SessionId" or is_close_operator else None

    if date_type == "RealDate":
        report = cash_report.generate_cash_report_by_real_date(pos_id, report_pos, initial_date, end_date, operator_id, voided_lines, is_table_service=is_table_service, only_body=only_body, session_id=session_id, only_payments=only_payments, is_integrated_delivery=is_integrated_delivery)
    elif date_type == "BusinessPeriod":
        report = cash_report.generate_cash_report_by_business_period(pos_id, report_pos, initial_date, end_date, operator_id, voided_lines, is_table_service=is_table_service, only_body=only_body, only_payments=only_payments)
    elif date_type == "SessionId":
        report = cash_report.generate_cash_report_by_session_id(pos_id, session_id, voided_lines, is_table_service=is_table_service, only_body=only_body, only_payments=only_payments)
    elif date_type == "JsonBusinessPeriod":
        report = cash_report.generate_cash_report_by_real_date(pos_id, report_pos, initial_date, end_date, operator_id, voided_lines, business_period, codigo_centro, close_time, only_body=only_body, only_payments=only_payments)
    else:
        report = cash_report.generate_cash_report_by_xml(pos_id, initial_date, end_date, is_table_service=is_table_service, only_body=only_body, only_payments=only_payments)

    return report


def sales_summary_report(
    pos_id,           # type: str
    initial_date,     # type: str
    end_date,         # type: str
    store_id          # type: str
 ):
    # type: (...)  -> str

    summary_report = SalesSummary(mbcontext, store_id)
    initial_date = datetime.datetime.strptime(initial_date, "%Y%m%d")
    end_date = datetime.datetime.strptime(end_date, "%Y%m%d")

    return summary_report.generate_summary_report(int(pos_id), initial_date, end_date)


def generate_voided_lines_report(report_pos, initial_date, end_date, only_body=False, operator_id=None):
    # type: (str, str, str, Optional[bool], Optional[Union[str, None]]) -> unicode
    try:
        report_pos = None if report_pos in ["", "0", "None", None] else str(report_pos)
        operator_id = None if operator_id in ["", "0", "None", None] else int(operator_id)
        only_body = only_body.lower() == "true" if only_body else False
        initial_date = datetime.datetime.strptime(initial_date, "%Y%m%d")
        end_date = datetime.datetime.strptime(end_date, "%Y%m%d")

        order_repository = VoidedLinesOrderRepository(mbcontext, get_pos_list(mbcontext))
        users_repository = VoidedLinesUsersRepository(mbcontext)
        voided_lines_report = VoidedLinesReport(order_repository, users_repository, get_store_id())
        report = voided_lines_report.generate_voided_lines_report(report_pos,
                                                                  initial_date,
                                                                  end_date,
                                                                  only_body,
                                                                  operator_id)

        return report
    except Exception as _:
        sys_log_exception("Error on [generate_voided_lines_report]")
        raise


def get_voided_lines(report_pos, initial_date, end_date, operator_id=None, session_id=None):
    try:
        operator_id = int(operator_id) if operator_id else None
        order_repository = VoidedLinesOrderRepository(mbcontext, get_pos_list(mbcontext))
        users_repository = VoidedLinesUsersRepository(mbcontext)
        voided_lines_report = VoidedLinesReport(order_repository, users_repository, get_store_id())
        voided_lines = voided_lines_report.get_voided_lines(report_pos, initial_date, end_date, operator_id, session_id)
        return voided_lines
    except Exception as _:
        sys_log_exception("Error on [generate_voided_lines_report]")
        raise


def generate_paid_order_cash_report_by_date(initial_date, end_date):
    global new_pos_list, fiscal_sent_dir

    if new_pos_list is None:
        msg = mbcontext.MB_EasySendMessage("PosController", TK_POS_GETPOSLIST)
        if msg.token != TK_SYS_ACK:
            sys_log_error("Could not retrieve PosList")
            raise Exception("Could not retrieve PosList")

        new_pos_list = sorted(map(int, msg.data.split("\0")))

    if fiscal_sent_dir is None:
        config = cfgtools.read(os.environ["LOADERCFG"])
        fiscal_sent_dir = sys_parsefilepath(config.find_value("CashReport.FiscalSentDir").strip())

    order_repository = OrderRepository(mbcontext, new_pos_list, fiscal_sent_dir)
    tender_repository = TenderRepository(mbcontext)
    cash_report = CashReport(order_repository, None, tender_repository, None, None, None, None, None)
    formatted_initial_date = datetime.datetime.strptime(initial_date, "%Y%m%d")
    formatted_end_date = datetime.datetime.strptime(end_date, "%Y%m%d")
    orders = cash_report.generate_paid_order_cash_report_by_date(formatted_initial_date, formatted_end_date)

    order_id_and_totals = [{"orderId": str(order.order_id), "total": order.total} for order in orders]

    return json.dumps(order_id_and_totals)


def sales_by_brand(date_type, pos_id, report_pos, initial_date, end_date, operator_id, session_id, store_id):
    # type: (unicode, int, unicode, unicode, unicode, unicode, unicode, unicode) -> str
    global new_pos_list, fiscal_sent_dir

    if new_pos_list is None:
        msg = mbcontext.MB_EasySendMessage("PosController", TK_POS_GETPOSLIST)
        if msg.token != TK_SYS_ACK:
            sys_log_error("Could not retrieve PosList")
            raise Exception("Could not retrieve PosList")

        new_pos_list = sorted(map(int, msg.data.split("\0")))

    order_repository = BrandOrderRepository(mbcontext, new_pos_list)
    fiscal_repository = BrandFiscalRepository(mbcontext)
    tender_repository = BrandTenderRepository(mbcontext)

    operator_id = str(int(0 if operator_id in (None, 'None') else operator_id))
    if operator_id == '0':
        operator_id = None

    report_pos = str(int(0 if report_pos in (None, 'None') else report_pos))
    if report_pos == '0':
        report_pos = None

    flag_report = BrandReport(order_repository, fiscal_repository, tender_repository, get_store_id())
    if date_type == "RealDate":
        initial_date = datetime.datetime.strptime(initial_date, "%Y%m%d")
        end_date = datetime.datetime.strptime(end_date, "%Y%m%d")
        report = flag_report.generate_brand_report_by_real_date(pos_id, report_pos, initial_date, end_date, operator_id)
    elif date_type == "BusinessPeriod":
        initial_date = datetime.datetime.strptime(initial_date, "%Y%m%d")
        end_date = datetime.datetime.strptime(end_date, "%Y%m%d")
        report = flag_report.generate_brand_report_by_business_period(pos_id, report_pos, initial_date, end_date, operator_id)
    else:
        report = flag_report.generate_brand_report_by_session_id(pos_id, session_id)

    return report


def voided_orders_report(date_type, pos_id, report_pos, initial_date, end_date, operator_id, session_id, store_id):
    # type: (unicode, int, unicode, unicode, unicode, unicode, unicode, unicode) -> str
    global new_pos_list, fiscal_sent_dir

    if new_pos_list is None:
        msg = mbcontext.MB_EasySendMessage("PosController", TK_POS_GETPOSLIST)
        if msg.token != TK_SYS_ACK:
            sys_log_error("Could not retrieve PosList")
            raise Exception("Could not retrieve PosList")

        new_pos_list = sorted(map(int, msg.data.split("\0")))

    order_repository = VoidedOrderRepository(mbcontext, new_pos_list)

    operator_id = str(int(0 if operator_id in (None, 'None') else operator_id))
    if operator_id == '0':
        operator_id = None

    report_pos = str(int(0 if report_pos in (None, 'None') else report_pos))
    if report_pos == '0':
        report_pos = None

    voided_report = VoidedReport(order_repository, get_store_id())
    if date_type == "RealDate":
        initial_date = datetime.datetime.strptime(initial_date, "%Y%m%d")
        end_date = datetime.datetime.strptime(end_date, "%Y%m%d")
        report = voided_report.generate_voided_report_by_real_date(pos_id, report_pos, initial_date, end_date, operator_id)
    elif date_type == "BusinessPeriod":
        initial_date = datetime.datetime.strptime(initial_date, "%Y%m%d")
        end_date = datetime.datetime.strptime(end_date, "%Y%m%d")
        report = voided_report.generate_voided_report_by_business_period(pos_id, report_pos, initial_date, end_date, operator_id)
    else:
        report = voided_report.generate_voided_report_by_session_id(pos_id, session_id)

    return report


def discount_report(date_type, pos_id, report_pos, initial_date, end_date, operator_id, session_id, store_id, business_period=None, codigo_centro=None, close_time=None, is_table_service='False'):
    global new_pos_list, fiscal_sent_dir

    order_repository = DiscountOrderRepository(mbcontext, get_pos_list(mbcontext))
    report_generator = DiscountReport(order_repository, get_store_id())

    report = ""
    if date_type == "SessionId":
        report = report_generator.generate_discount_report_by_session_id(pos_id, session_id)
    else:
        initial_date = datetime.datetime.strptime(initial_date, "%Y%m%d")
        end_date = datetime.datetime.strptime(end_date, "%Y%m%d")
        report_pos = _convert_report_pos(report_pos)
        operator_id = _convert_operator_id(operator_id)

        if date_type == "RealDate":
            report = report_generator.generate_discount_report_by_real_date(pos_id,
                                                                            report_pos,
                                                                            initial_date,
                                                                            end_date,
                                                                            operator_id)
        elif date_type == "BusinessPeriod":
            report = report_generator.generate_discount_report_by_business_period(pos_id,
                                                                                  report_pos,
                                                                                  initial_date,
                                                                                  end_date,
                                                                                  operator_id)

    return report


def _convert_report_pos(report_pos):
    report_pos = str(int(0 if report_pos in (None, 'None') else report_pos))
    if report_pos == '0':
        report_pos = None
    return report_pos


def _convert_operator_id(operator_id):
    operator_id = str(int(0 if operator_id in (None, 'None') else operator_id))
    if operator_id == '0':
        operator_id = None
    return operator_id


def new_pmix_report(date_type, pos_id, report_pos, initial_date, end_date, operator_id, session_id, store_id, report_model):
    # type: (unicode, int, str, unicode, unicode, unicode, unicode, unicode, str) -> str
    global new_pos_list

    if new_pos_list is None:
        msg = mbcontext.MB_EasySendMessage("PosController", TK_POS_GETPOSLIST)
        if msg.token != TK_SYS_ACK:
            sys_log_error("Could not retrieve PosList")
            raise Exception("Could not retrieve PosList")

        new_pos_list = sorted(map(int, msg.data.split("\0")))

    if report_pos == '0':
        order_repository = PmixOrderRepository(mbcontext, new_pos_list)
    else:
        order_repository = PmixOrderRepository(mbcontext, [int(report_pos)])
    product_repository = ProductRepository(mbcontext)

    pmix_report = PMixReport(order_repository, product_repository, get_store_id(), operator_id)
    initial_date = datetime.datetime.strptime(initial_date, "%Y%m%d")
    end_date = datetime.datetime.strptime(end_date, "%Y%m%d")
    if date_type == "RealDate":
        report = pmix_report.generate_pmix_report_by_real_date(pos_id, initial_date, end_date, report_model, operatorid=operator_id)
    else:
        report = pmix_report.generate_pmix_report_by_business_period(pos_id, initial_date, end_date, report_model, operatorid=operator_id)

    return report


def pmix_order_items(initial_date, end_date, store_id, operator_id):
    # type: (unicode, unicode, unicode, int) -> List[OrderItem]
    global new_pos_list

    if new_pos_list is None:
        msg = mbcontext.MB_EasySendMessage("PosController", TK_POS_GETPOSLIST)
        if msg.token != TK_SYS_ACK:
            sys_log_error("Could not retrieve PosList")
            raise Exception("Could not retrieve PosList")

        new_pos_list = sorted(map(int, msg.data.split("\0")))

    order_repository = PmixOrderRepository(mbcontext, new_pos_list)
    product_repository = ProductRepository(mbcontext)

    pmix_report = PMixReport(order_repository, product_repository, get_store_id(), operator_id)

    initial_date = datetime.datetime.strptime(initial_date, "%Y-%m-%d %H:%M:%S.%f")
    end_date = datetime.datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S.%f")

    report = pmix_report.generate_pmix_order_items(initial_date, end_date)

    return json.dumps(map(lambda x: x.__dict__, report))


def close_operator_report(pos_id, period, operator_id, session_id, authorizer=None):
    global new_pos_list

    if new_pos_list is None:
        msg = mbcontext.MB_EasySendMessage("PosController", TK_POS_GETPOSLIST)
        if msg.token != TK_SYS_ACK:
            sys_log_error("Could not retrieve PosList")
            raise Exception("Could not retrieve PosList")

        new_pos_list = sorted(map(int, msg.data.split("\0")))

    close_operator_report_generator = CloseOperatorReport(
        CloseOperatorOrderRepository(mbcontext, new_pos_list),
        CloseOperatorProductRepository(mbcontext),
        CloseOperatorAccountRepository(mbcontext),
        get_store_id()
    )

    initial_date = end_date = period
    cash_report = new_cash_report("SessionId", pos_id, None, initial_date, end_date, operator_id, session_id,
                                  get_store_id(), only_body=True, is_close_operator=True)
    return close_operator_report_generator.generate_close_operator_report(pos_id, operator_id, period, session_id,
                                                                          cash_report, authorizer)


def transfer_report(pos_id, operator_id, transfer_type, amount, period, envelope_number, print_operator_copy):
    transfer = TransfersReport(TransferAccountRepository(mbcontext), get_store_id())
    return transfer.generate_transfers_report(pos_id, operator_id, transfer_type, amount, period, envelope_number,
                                              print_operator_copy)


def transfer_report_by_period(pos_id, operator_id, transfer_type, period, *args):
    transfer = TransfersReport(TransferAccountRepository(mbcontext), get_store_id())
    return transfer.generate_transfers_report_by_period(pos_id, operator_id, transfer_type, period)


def productListReport(posid, period, show_inactive="true"):
    show_inactive = True if (show_inactive.lower() == "true") else False
    conn = None
    try:
        conn = dbd.open(mbcontext)
        sql = """
        SELECT P.ProductCode as ProductCode,
               P.ProductName as ProductName,
               COALESCE(PR.DefaultUnitPrice, '0.00') as Price
        FROM
            productdb.Product P
        JOIN productdb.ProductKernelParams PKP ON PKP.ProductCode=P.ProductCode
        LEFT JOIN
            productdb.Price PR
            ON  PR.ProductCode=P.ProductCode AND
                PR.Context IN (
                    SELECT DISTINCT ProductCode
                    FROM productdb.ProductKernelParams
                    WHERE ProductType=3
                ) AND
                DATE('%s-%s-%s') BETWEEN PR.ValidFrom AND PR.ValidThru
        WHERE
            P.ProductCode NOT IN (SELECT DISTINCT ClassCode FROM productdb.ProductClassification)
            AND PKP.Enabled IN (1, %s)
        GROUP BY ProductCode, Price
        ORDER BY P.ProductCode
        """ % (period[:4], period[4:6], period[6:8], ('0' if show_inactive else "NULL"))

        cursor = conn.select(sql)
        report = StringIO()

        if show_inactive:
            title = "Lista de Produtos (Todos os itens)"
        else:
            title = "Lista de Produtos (Apenas ativos)"

        report.write(_manager_report_header(title, int(posid), 0, period))
        report.write("%s\n" % (SEPARATOR))
        report.write("\n")
        #             12345678901234567890123456789012345678
        report.write("  Codigo Descricao            Preco\n")

        for row in cursor:
            code, descr, price = map(row.get_entry, ("ProductCode", "ProductName", "Price"))
            report.write("%8s %-20.20s  R$ %-7.7s\n" % (code, descr, _fmt_number(float(price))))

        report.write("%s\n" % (SEPARATOR))
        report.write("\n")
    finally:
        if conn:
            conn.close()

    return report.getvalue()


def dayOpen_report(pos_id, period, pos_numbers, store_wide="false", operator=""):
    report = StringIO()
    operator = 0 if operator in ["None", ""] else operator

    title = "Abertura do Dia (Loja)" if store_wide.lower() == "true" else "Abertura do Dia"
    report.write(_manager_report_header(title, int(pos_id), operator, period))

    report.write("\n")

    temp_pos_list = "POS incluidos...: "+" ".join(pos_numbers.split('|'))
    report.write("%s\n" % (SEPARATOR))
    report.write(temp_pos_list[:COLS])
    if len(temp_pos_list) > COLS:
        report.write("\n                  %s" % temp_pos_list[COLS:2*COLS-18])
    if len(temp_pos_list) > 2*COLS-18:
        report.write("\n                  %s" % temp_pos_list[2 * COLS - 18:2 * COLS - 36])
    report.write("\n%s\n" % (SEPARATOR))
    return report.getvalue()


def login_operator_report(pos_id, operator_id, operator_name, initial_float, period, authorized_by):
    report = StringIO()
    report.write(_manager_report_header("Login Operador", int(pos_id), operator_id, period))
    report.write("%s\n" % SEPARATOR)
    report.write("\n")
    report.write("Nome do Operador...: %16s\n" % (operator_name[:16]))
    report.write("ID Operador........: %16s\n" % operator_id)
    report.write("Autorizado por.....: %16s\n" % authorized_by)
    report.write("Balanco Inicial....: R$ %13.2f\n" % (float(initial_float or 0)))

    report.write("\n")
    report.write("%s\n" % SEPARATOR)

    return report.getvalue()


def sats_report(pos_id, sat_status_list, *args):
    title = _center("SATs Status")
    current_datetime = time.strftime(DATE_TIME_FMT)

    # transform the string sat_status_list back into a dictionary
    d = ast.literal_eval(sat_status_list)

    # extract the POS id´s from the string sat_status_list
    pos_list = sorted(map(int, re.findall(r'[1-9]+', sat_status_list)))

    # create string I/O to append the report info
    report = StringIO()
    report.write(
        """%(SEPARATOR)s
%(title)s
  Data/hora.....: %(current_datetime)s
  POS...........: %(pos_list)s
""" % _join(globals(), locals()))
    width = 38
    report.write("%s\n" % SEPARATOR)
    report.write("  POS ID")
    report.write("SAT Status\n".rjust(width-len("  POS ID")))
    report.write("%s\n" % SINGLE_SEPARATOR)

    for pos in pos_list:
        report.write("  %s " % str(pos).zfill(2))
        status = str(d["SAT%s" % str(pos).zfill(2)])
        report.write("%s\n" % status.rjust(width - len("   %s" % str(pos)), "."))

    report.write("%s\n" % SEPARATOR)
    report.write("\n")
    report.write("%s\n" % SEPARATOR)

    return report.getvalue()


def kds_report(pos_id, kds_status_list, *args):
    # create string I/O to append the report info
    report = StringIO()
    title = _center("KDSs Status")
    current_datetime = time.strftime(DATE_TIME_FMT)

    # transform the string kds_status_list back into a list of lists
    kds_list = ast.literal_eval(kds_status_list)
    kds_list = sorted(kds_list, key=itemgetter(1))

    # deleting indexes
    for i in kds_list:
        del i[0]

    report.write(
        """%(SEPARATOR)s
%(title)s
  Data/hora.....: %(current_datetime)s
""" % _join(globals(), locals()))
    width = 38
    report.write("%s\n" % SEPARATOR)
    report.write("  KDS")
    report.write("Status\n".rjust(width-len("  KDS")))
    report.write("%s\n" % SINGLE_SEPARATOR)

    for kds_info in kds_list:
        kds = str(kds_info[0])
        report.write("  %s" % kds)
        report.write("%s\n" % kds_info[1].rjust(width-len("   %s" % kds), "."))

    report.write("%s\n" % SEPARATOR)
    report.write("\n")
    report.write("%s\n" % (SEPARATOR))

    return report.getvalue()


def printer_report(posid, printer_status_list, *args):
    # create string I/O to append the report info
    report = StringIO()
    title = _center("Printer Status")
    current_datetime = time.strftime(DATE_TIME_FMT)

    # transform the string kds_status_list back into a list of lists
    printer_list = ast.literal_eval(printer_status_list)

    report.write(
        """%(SEPARATOR)s
%(title)s
  Data/hora.....: %(current_datetime)s
""" % _join(globals(), locals()))
    width = 38
    report.write("%s\n" % SEPARATOR)
    report.write("  Impressora")
    report.write("Status\n".rjust(width-len("  Impressora")))
    report.write("%s\n" % SINGLE_SEPARATOR)

    for printer_info in printer_list:
        printer_name = str(printer_info[0])
        printer_info = printer_info[1]
        report.write("  %s" % printer_name)
        report.write("%s\n" % printer_info.rjust(width - len("   %s" % printer_name), "."))

    report.write("%s\n" % SEPARATOR)

    return report.getvalue()


def versions_report(posid, versions_json, *args):
    report = StringIO()
    title = _center("Versões POS")
    current_datetime = time.strftime(DATE_TIME_FMT)

    pos_versions = json.loads(versions_json)
    core_version = pos_versions["versions"]["core"]
    src_version = pos_versions["versions"]["src"]
    catalog_version = pos_versions["versions"]["catalog"]

    core_version_date = pos_versions["dates"]["core"]
    src_version_date = pos_versions["dates"]["src"]
    catalog_version_date = pos_versions["dates"]["catalog"]
    loader_version_date = pos_versions["dates"]["loader"]
    user_version_date = pos_versions["dates"]["user"]

    core_text = "Core...: {}".format(core_version)
    src_text = "Source.: {}".format(src_version)
    catalog_text = "Menu...: {}".format(catalog_version)
    loader_text = "Loaders:"
    users_text = "Users..:"


    width = 38
    report.write(
        """%(SEPARATOR)s
%(title)s
Data/hora........: %(current_datetime)s
""" % _join(globals(), locals()))
    report.write("%s\n" % SEPARATOR)
    line_format = "{}{}\n"
    report.write(line_format.format(core_text, core_version_date.rjust(width - len(core_text), " ")))
    report.write(line_format.format(src_text, src_version_date.rjust(width - len(src_text), " ")))
    report.write(line_format.format(catalog_text, catalog_version_date.rjust(width - len(catalog_text), " ")))
    report.write(line_format.format(loader_text, loader_version_date.rjust(width - len(loader_text), " ")))
    report.write(line_format.format(users_text, user_version_date.rjust(width - len(users_text), " ")))
    report.write("%s\n" % SEPARATOR)

    return report.getvalue()


def sats_op_report(posid, sats_op_list, *args):
    # create string I/O to append the report info
    report = StringIO()
    title = _center("SAT Status Operacional")
    current_datetime = time.strftime(DATE_TIME_FMT)

    splitter = sats_op_list.split("|")

    report.write(
        """%(SEPARATOR)s
%(title)s
  Data/hora.....: %(current_datetime)s
""" % _join(globals(), locals()))
    width = 38
    report.write("%s\n" % SEPARATOR)
    report.write("  SAT %s\n" % posid)
    report.write("%s\n" % SINGLE_SEPARATOR)

    if "ERROR -" in splitter[0]:
        report.write(splitter[0] + '\n')
    else:
        report.write("  Numero de série:")
        report.write("%s\n" % splitter[5].rjust(width - len("  Numero de série:"), "."))
        report.write("  Tipo de Lan:")
        report.write("%s\n" % splitter[6].rjust(width - len("   Tipo de Lan:"), "."))
        report.write("  IP da LAN:")
        report.write("%s\n" % splitter[7].rjust(width - len("   IP da LAN:"), "."))
        report.write("  MAC:")
        report.write("%s\n" % splitter[8].rjust(width - len("   MAC:"), "."))
        report.write("  Máscara:")
        report.write("%s\n" % splitter[9].rjust(width - len("  Máscara:"), "."))
        report.write("  Gateway:")
        report.write("%s\n" % splitter[10].rjust(width - len("   Gateway:"), "."))
        report.write("  DNS1:")
        report.write("%s\n" % splitter[11].rjust(width - len("   DNS1:"), "."))
        report.write("  DNS2:")
        report.write("%s\n" % splitter[12].rjust(width - len("   DNS2:"), "."))
        report.write("  Status da rede:")
        report.write("%s\n" % splitter[13].rjust(width - len("   Status da rede:"), "."))
        report.write("  Nível da Bateria:")
        report.write("%s\n" % splitter[14].rjust(width - len("  Nível da Bateria:"), "."))
        report.write("  Memória Total:")
        report.write("%s\n" % splitter[15].rjust(width - len("  Memória Total:"), "."))
        report.write("  Memória Usada:")
        report.write("%s\n" % splitter[16].rjust(width - len("  Memória Usada:"), "."))
        report.write("  Data e hora:")
        date_and_time = splitter[17][6:8] + "/" + splitter[17][4:6] + "/" + splitter[17][:4] + " " + splitter[17][8:10] + ":" + splitter[17][10:12] + ":" + splitter[17][12:14]
        report.write("%s\n" % date_and_time.rjust(width - len("   Data e hora:"), "."))
        report.write("  Versão do Software:")
        report.write("%s\n" % splitter[18].rjust(width - len("  Versão do Software:"), "."))
        report.write("  Última comunicação com a SEFAZ:\n")
        report.write("      Data:")
        last_com_date = splitter[24][6:8] + "/" + splitter[24][4:6] + "/" + splitter[24][:4]
        report.write("%s\n" % last_com_date.rjust(width - len("       Data:"), "."))
        report.write("      Hora:")
        last_com_time = splitter[24][8:10] + ":" + splitter[24][10:12] + ":" + splitter[24][12:14]
        report.write("%s\n" % last_com_time.rjust(width - len("       Hora:"), "."))
        report.write("  Data certificado:")
        date_and_time = splitter[26][6:8] + "/" + splitter[26][4:6] + "/" + splitter[26][:4]
        report.write("%s\n" % date_and_time.rjust(width - len("   Data certificado:"), "."))
        report.write("  Estado SAT:")
        report.write("%s\n" % splitter[27][:1].rjust(width - len("   Estado SAT:"), "."))

    report.write("%s\n" % SEPARATOR)
    report.write("\n")
    report.write("%s\n" % SEPARATOR)

    return report.getvalue()


def delivery_json_report(pos_id, order_number, json_order):
    # type: (str, str, unicode) -> unicode
    """
        nodes_dict:
        { ( tag, father ) : { text: '', level: '', sort_Key: '' }
    """
    json_order = json_order.encode('utf-8', errors='ignore')
    nodes_dict = json.loads(json_order)
    model = sysactions.get_model(pos_id)
    title = sysactions.translate_message(model, "DELIVERY_PED", str(order_number))

    report = StringIO()
    title = _center(title)
    current_datetime = _center(time.strftime(DATE_TIME_FMT))

    report.write("{}\n".format(SEPARATOR))
    report.write("{}\n".format(title))
    report.write("{}\n".format(current_datetime))
    report.write("{}\n".format(SEPARATOR))
    report.write("\n\n")
    level = 0

    def recursive_dict_print(dict_data, lvl):
        for tag in dict_data:
            tag_descr = remove_accents(tag)
            value_descr = dict_data[tag]
            if isinstance(value_descr, dict):
                for count in range(lvl):
                    report.write('  ')
                report.write("{}\n".format(tag_descr))
                lvl += 1
                recursive_dict_print(value_descr, lvl)
            elif isinstance(value_descr, list):
                for x in value_descr:
                    recursive_dict_print(x, lvl)
                    report.write("\n")
            else:
                value_descr = remove_accents(value_descr)
                for count in range(lvl):
                    report.write('  ')
                report.write("{}: {}\n".format(tag_descr, value_descr))

    recursive_dict_print(nodes_dict, level)

    return report.getvalue()


def delivery_ped_report(pos_id, order_number, ped_xml):
    # type: (str, str, str) -> unicode
    """
        nodes_dict:
        { ( tag, father ) : { text: '', level: '', sort_Key: '' }
    """
    nodes_dict = {}

    def _get_xml_node_values(xml, lvl=1, sort_key=0):
        father = xml.tag
        for node in xml:
            value = remove_accents(node.text) if node.text else ''
            sort_key += 1
            nodes_dict[(node.tag, father)] = {'value': value, 'level': lvl, 'sort_key': sort_key}
            if len(node) > 0:
                sort_key += _get_xml_node_values(node, lvl + 1, sort_key)

        return sort_key

    _get_xml_node_values(eTree.fromstring(ped_xml))

    model = sysactions.get_model(pos_id)
    title = sysactions.translate_message(model, "DELIVERY_PED", str(order_number))

    nodes_dict = sorted(nodes_dict.items(), key=lambda x: x[1]['sort_key'])

    report = StringIO()

    title = _center(title)
    current_datetime = _center(time.strftime(DATE_TIME_FMT))

    report.write("{}\n".format(SEPARATOR))
    report.write("{}\n".format(title))
    report.write("{}\n".format(current_datetime))
    report.write("{}\n".format(SEPARATOR))
    report.write("\n\n")

    fathers_list = list(dict.fromkeys([x[1] for x in [x[0] for x in nodes_dict]]))
    for tag, values in nodes_dict:
        tag_descr = tag[0].rjust(len(tag[0]) + values['level'])
        value_descr = values['value']
        if tag[0] in fathers_list:
            report.write("{}\n".format(tag_descr))
        else:
            report.write("{}: {}\n".format(tag_descr, value_descr))

    return report.getvalue()


def tip_report(date_type, pos_id, report_pos, initial_date, end_date, operator_id, session_id, *args):
    global new_pos_list, tip_operator_who_closed

    if tip_operator_who_closed is None:
        config = cfgtools.read(os.environ["LOADERCFG"])
        tip_operator_who_closed = (config.find_value("TipReport.OperatorWhoClosed") or "false").strip() == "true"

    report = StringIO()
    title = _center("Relatorio de Gorjeta")
    current_datetime = time.strftime(DATE_TIME_FMT)
    formatted_store_id = get_store_id()

    if operator_id != "0" and operator_id is not None:
        operator_name = eTree.XML(sysactions.get_user_information(operator_id)).find(".//user").get("LongName")[:20]
    else:
        operator_name = "Todos"

    formatted_start_date = datetime.datetime.strptime(initial_date, "%Y%m%d").strftime("%d/%m/%y")
    formatted_end_date = datetime.datetime.strptime(end_date, "%Y%m%d").strftime("%d/%m/%y")

    report.write(
"""%(SEPARATOR)s
%(title)s
Loja.........: %(formatted_store_id)s
Data/hora....: %(current_datetime)s
Periodo......: %(formatted_start_date)s - %(formatted_end_date)s
Operador.....: %(operator_name)s
%(SEPARATOR)s
Operador               Venda   Gorjeta  
""" % _join(globals(), locals()))
    width = 38
    report.write("%s\n" % SEPARATOR)
    column = "'PAID'" if tip_operator_who_closed else "'PAID', 'ABANDONED' , 'STORED'"
    conn = None
    try:
        conn = dbd.open(mbcontext, dbname=str(pos_id))
        conn.transaction_start()

        query = """SELECT UserId, 
                          SUM(TotalGross) as Sales, 
                          SUM(OrderTip) as Tip, 
                          Count(*) as Tables, 
                          SUM(NumberOfSeats) as Seats
                        FROM (
                              SELECT sop.userid, 
                                     stip.serviceid, 
                                     soa.TotalGross / soap.users as TotalGross, 
                                     stip.calculatedtip / soap.users AS OrderTip, 
                                     tsvc.NumberOfSeats
                                FROM servicetip stip
                                JOIN serviceorders sords on sords.serviceid = stip.serviceid
                                JOIN tableservice tsvc on tsvc.serviceid = stip.serviceid
                                JOIN (SELECT userid, serviceid, TotalGross 
                                        FROM serviceorders 
                                       WHERE state IN ({0})) sop 
                                  ON sop.serviceid = stip.serviceid
                                JOIN (SELECT serviceid, count(DISTINCT UserId) as users 
                                        FROM serviceorders 
                                       WHERE state IN ({0}) 
                                       GROUP BY serviceid) soap on soap.serviceid = sop.serviceid
                                JOIN (SELECT serviceid, sum(TotalGross) as TotalGross 
                                        FROM serviceorders 
                                       WHERE state IN ('PAID')
                                       GROUP BY serviceid) soa on soa.serviceid = sop.serviceid
                               WHERE strftime('%Y%m%d', tsvc.BusinessPeriod, 'utc') >= '{1}' 
                                 AND strftime('%Y%m%d', tsvc.BusinessPeriod, 'utc') <= '{2}'
                                 AND sords.orderId in (SELECT DISTINCT Orderid 
                                                         FROM ServiceSplit 
                                                        WHERE serviceid = stip.serviceid)
                            GROUP BY sop.userid, stip.serviceid, stip.calculatedtip, tsvc.NumberOfSeats
                            ORDER BY stip.ServiceId
                        ) GROUP BY userId ORDER BY userId""".format(column, initial_date, end_date)

        cursor = conn.select(query)

        try:
            locale.setlocale(locale.LC_ALL, "portuguese_brazil")
        except:
            locale.setlocale(locale.LC_ALL, "pt_BR.UTF-8")

        report_lines_dict = {}
        for row in cursor:
            user = row.get_entry(0)
            operator_name = eTree.XML(sysactions.get_user_information(user)).find(".//user").get("LongName")
            if operator_id != "0" and operator_id != user:
                continue
            report_lines_dict[operator_name] = TipInfo()
            report_lines_dict[operator_name].total_amount = float(row.get_entry(1) or 0)
            report_lines_dict[operator_name].tip_amount = float(row.get_entry(2) or 0)

        total_amount = 0.0
        total_tip = 0.0

        from collections import OrderedDict
        report_lines_dict = OrderedDict(sorted(report_lines_dict.items()))
        for operator_name, value in report_lines_dict.iteritems():
            _format_line(report, 19, operator_name.upper()[:17])
            _format_line(report, 10, '{0:>9}'.format(locale.format("%.2f", value.total_amount, True)))
            _format_line(report, 10, '{0:>9}'.format(locale.format("%.2f", value.tip_amount, True)))
            report.write("\n")

            total_amount = total_amount + value.total_amount
            total_tip = total_tip + value.tip_amount

        report.write("%s\n" % SEPARATOR)
        report.write("Total Vendas........: R$ {:>13}\n".format(locale.format("%.2f", total_amount, True)))
        report.write("Total Valor Gorjetas: R$ {:>13}\n".format(locale.format("%.2f", total_tip, True)))

    except Exception as _:
        pass
    finally:
        if conn:
            conn.close()

    return report.getvalue()


def generate_tickets_report(business_period):
    business_period = business_period[:10]
    pos_list = _get_quick_service_pos_list()
    average_ticket_report = AverageTicketReport(AverageTicketTableServiceRepository(mbcontext),
                                                AverageTicketOrderRepository(mbcontext, pos_list))
    report = average_ticket_report.generate_tickets_report(business_period)
    return report


def _get_quick_service_pos_list():
    msg = mbcontext.MB_EasySendMessage("PosController", TK_POS_GETPOSLIST)
    if msg.token == TK_SYS_NAK:
        sys_log_error("Could not retrieve PosList")
        raise Exception("Could not retrieve PosList")

    pos_list = []
    for pos in msg.data.split("\0"):
        if sysactions.get_model(pos).find("WorkingMode").get("usrCtrlType") == "QSR":
            pos_list.append(pos)

    return sorted(map(int, pos_list))


def _format_line(report, field_qty, text):
    report_line = text[:field_qty]
    for _ in range(len(report_line), field_qty):
        report_line = report_line + " "

    report.write(report_line)


class TipInfo(object):
    def __init__(self):
        self.table_list = []
        self.number_of_seats = 0
        self.total_amount = 0.0
        self.tip_amount = 0.0
