import com.gridnine.xtrip.common.l10n.model.L10nStringHelper
import com.gridnine.xtrip.common.l10n.model.LocaleHelper
import com.gridnine.xtrip.common.l10n.model.PersonalLocalizableNameUtil
import com.gridnine.xtrip.common.model.EntityContainer
import com.gridnine.xtrip.common.model.EntityReference
import com.gridnine.xtrip.common.model.XCloneModelHelper
import com.gridnine.xtrip.common.model.booking.*
import com.gridnine.xtrip.common.model.booking.air.Product
import com.gridnine.xtrip.common.model.booking.air.ProductFop
import com.gridnine.xtrip.common.model.booking.commission.DiscountProperties
import com.gridnine.xtrip.common.model.booking.commission.FeeProperties
import com.gridnine.xtrip.common.model.booking.commission.Operation
import com.gridnine.xtrip.common.model.booking.commission.PaymentFeeProperties
import com.gridnine.xtrip.common.model.booking.railway.RailwayProduct
import com.gridnine.xtrip.common.model.dict.*
import com.gridnine.xtrip.common.model.entity.EntityStorage
import com.gridnine.xtrip.common.model.handlers.ProductHandler
import com.gridnine.xtrip.common.model.helpers.*
import com.gridnine.xtrip.common.model.profile.*
import com.gridnine.xtrip.common.model.system.PaymentType
import com.gridnine.xtrip.common.util.BooleanUtil
import com.gridnine.xtrip.common.util.FormattedNumberUtil
import com.gridnine.xtrip.common.util.MiscUtil
import com.gridnine.xtrip.common.util.TextUtil

import java.text.SimpleDateFormat

createStyle(name: "left", h_alignment: "LEFT")
createStyle(name: "right", h_alignment: "RIGHT")
createStyle(name: "center", h_alignment: "CENTER")
createStyle(name: "left-bold", h_alignment: "LEFT", fontBold : true)
createStyle(name: "right-bold", h_alignment: "RIGHT", fontBold : true)
createStyle(name: "center-bold-italic", h_alignment: "CENTER", fontBold : true, fontItalic : true)
createStyle(name: "left-bold-italic-underline", h_alignment: "LEFT", fontBold : true, fontUnderline : "SINGLE", fontItalic : true)
createStyle(name: "right-bold-italic-underline", h_alignment: "RIGHT", fontBold : true, fontUnderline : "SINGLE", fontItalic : true)
createStyle(name: "center-bold", h_alignment: "CENTER", fontBold : true)
createStyle(name: "center-center-bold", h_alignment: "CENTER", v_alignment: "CENTER", fontBold : true)
createStyle(name: "left-bold-border", h_alignment: "LEFT", fontBold : true, leftBorder:'THIN', rightBorder:'THIN', topBorder:'THIN', bottomBorder:'THIN', wrapText: true)
createStyle(name: "left-center-bold-border", h_alignment: "LEFT", v_alignment: "CENTER", fontBold : true, leftBorder:'THIN', rightBorder:'THIN', topBorder:'THIN', bottomBorder:'THIN', wrapText: true)
createStyle(name: "right-bold-border", h_alignment: "RIGHT", fontBold : true, leftBorder:'THIN', rightBorder:'THIN', topBorder:'THIN', bottomBorder:'THIN', wrapText: true)
createStyle(name: "center-bold-border", h_alignment: "CENTER", fontBold : true, leftBorder:'THIN', rightBorder:'THIN', topBorder:'THIN', bottomBorder:'THIN', wrapText: true)
createStyle(name: "left-border", h_alignment: "LEFT", leftBorder:'THIN', rightBorder:'THIN', topBorder:'THIN', bottomBorder:'THIN', wrapText: true, fontUnderline : "NONE", fontItalic : false)
createStyle(name: "right-border", h_alignment: "RIGHT", leftBorder:'THIN', rightBorder:'THIN', topBorder:'THIN', bottomBorder:'THIN', wrapText: true)
createStyle(name: "center-border", h_alignment: "CENTER", leftBorder:'THIN', rightBorder:'THIN', topBorder:'THIN', bottomBorder:'THIN', wrapText: true)
createStyle(name: "center-center-border", h_alignment: "CENTER", v_alignment: "CENTER", leftBorder:'THIN', rightBorder:'THIN', topBorder:'THIN', bottomBorder:'THIN', wrapText: true)
createStyle(name: "center-border-number", format: '0', h_alignment: "CENTER", leftBorder:'THIN', rightBorder:'THIN', topBorder:'THIN', bottomBorder:'THIN', wrapText: true)
createStyle(name: "left-border-number", format: '0', h_alignment: "LEFT", leftBorder:'THIN', rightBorder:'THIN', topBorder:'THIN', bottomBorder:'THIN', wrapText: true)
createStyle(name: "right-border-number", format: '0', h_alignment: "RIGHT", leftBorder:'THIN', rightBorder:'THIN', topBorder:'THIN', bottomBorder:'THIN', wrapText: true)
createStyle(name: "right-bold-border-number", format: '0', fontBold : true, h_alignment: "RIGHT", leftBorder:'THIN', rightBorder:'THIN', topBorder:'THIN', bottomBorder:'THIN', wrapText: true)
createStyle(name: "center-bold-border-number", format: '0', h_alignment: "CENTER", fontBold : true, leftBorder:'THIN', rightBorder:'THIN', topBorder:'THIN', bottomBorder:'THIN', wrapText: true)
createStyle(name: "center-center-border", format: '0', h_alignment: "CENTER", v_alignment: "CENTER", leftBorder:'THIN', rightBorder:'THIN', topBorder:'THIN', bottomBorder:'THIN', wrapText: true)
createStyle(name: "center-center-border-italic", h_alignment: "CENTER", v_alignment: "CENTER", leftBorder:'THIN', rightBorder:'THIN', topBorder:'THIN', bottomBorder:'THIN', wrapText: true, fontItalic : true)
createStyle(name: "center-center-border-italic-number", format: '0', h_alignment: "CENTER", v_alignment: "CENTER", leftBorder:'THIN', rightBorder:'THIN', topBorder:'THIN', bottomBorder:'THIN', wrapText: true, fontItalic : true)
createStyle(name: "center-center-border-italic-header", h_alignment: "CENTER", v_alignment: "CENTER", leftBorder:'THIN', rightBorder:'THIN', topBorder:'THIN', bottomBorder:'MEDIUM', wrapText: true, fontItalic : true)
createStyle(name: "wrap", wrapText: true)

def printHead() {
    nextRow(); rowHeight(15); 3.times { nextColumn() }
    text("Ежедневный отчет оператора по продаже авиабилетов", "center-bold-italic", 13, 1)

    nextRow(); 3.times { nextColumn() }
    text("ВНУТРЕННИЕ, МЕЖДУНАРОДНЫЕ", "center-bold-italic", 13, 1)

    nextRow(); 4.times { nextColumn() }
    text(salesPointAddress(), "center-bold-italic", 13, 1)

    nextRow(); 6.times { nextColumn() }
    text(reportPeriod(), "center-bold-italic", 8, 1)
}

def salesPointAddress() {
    EntityReference<Person> agentRef = (EntityReference<Person>) parameters['agent']
    EntityContainer<Person> agentCont = EntityStorage.get().resolve(agentRef)

    if (agentCont) {
        for (PlaceOfSales sp : agentCont.entity.salesPoints) {
            EntityContainer<SalesPoint> ect =
                    (EntityContainer<SalesPoint>) EntityStorage.get().resolve(sp.salesPoint)

            if (ect) {
                for (Address address : ect.entity.addresses) {
                    if (address.addressType == AddressType.CONTACT) {
                        String result =
                                ProfileHelper.buildFullAddress(
                                        address, LocaleHelper.RU_LOCALE, false)
                        if (TextUtil.nonBlank(result)) {
                            return result
                        }
                    }
                }

                if (ect.entity.addresses.size() == 1) {
                    String result =
                            ProfileHelper.buildFullAddress(
                                    ect.entity.addresses.get(0),
                                    LocaleHelper.RU_LOCALE, false)
                    if (TextUtil.nonBlank(result)) {
                        return result
                    }
                }
            } else {
                error("не удалось загрузить профиль точки продаж " +
                        sp.salesPoint ? sp.salesPoint.caption : sp.toString())
            }
        }
    }

    return ""
}

def reportPeriod() {
    SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yy")
    Date periodBeginParameter = parameters['key-report-params']?.periodBegin
    Date periodEndParameter = parameters['key-report-params']?.periodEnd

    if (periodBeginParameter && periodEndParameter) {
        if (MiscUtil.isSameDay(periodBeginParameter, periodEndParameter)) {
            return sdf.format(periodBeginParameter)
        }
        if (MiscUtil.isSameMonth(periodBeginParameter, periodEndParameter)) {
            return new SimpleDateFormat("dd").format(periodBeginParameter) +
                    "-" + sdf.format(periodEndParameter)
        }
    }

    return (periodBeginParameter ? sdf.format(periodBeginParameter) : "") +
            "-" +
            (periodEndParameter ? sdf.format(periodEndParameter) : "")
}

def printRegistry(List<BaseProduct> tickets) {
    printHeader()
    printTickets(tickets)
}

def printHeader() {
    nextRow(); rowHeight(90)

    text("Контрагент", "center-center-border-italic-header")                            // 1
    nextColumn();text("ли" + "\n" + "ни" + "\n" + "и")                                  // 2
    nextColumn();text("покупатель")                                                     // 3
    nextColumn();text("фамилия")                                                        // 4
    nextColumn();text("маршрут")                                                        // 5
    nextColumn();text("плечо")                                                          // 6
    nextColumn();text("номер бланка")                                                   // 7
    nextColumn();text("перевозчик")                                                     // 8
    nextColumn();text("Тариф")                                                          // 9
    nextColumn();text("тариф за" + "\n" + "страховки")                                  // 10
    nextColumn();text("сбор" + "\n" + "за" + "\n" + "бланк")                            // 11
    nextColumn();text("аэроп." + "\n" + "Сбор")                                         // 12
    nextColumn();text("такса" + "\n" + "ткп")                                           // 13
    nextColumn();text("Сбор за" + "\n" + "пере" + "\n" + "брониро" + "\n" + "вание")    // 14
    nextColumn();text("Сбор" + "\n" + "за" + "\n" + "бронь")                            // 15
    nextColumn();text("Сбор за" + "\n" + "возврат")                                     // 16
    nextColumn();text("штраф")                                                          // 17
    nextColumn();text("скидка," + "\n" + "руб")                                         // 18
    nextColumn();text("%" + "\n" + "ком")                                               // 19
    nextColumn();text("сумма")                                                          // 20
    nextColumn();text("К\\К")                                                           // 21
    nextColumn();text("Расчеты")                                                        // 22
    nextColumn();text("НАЛ")                                                            // 23
    nextColumn();text("рекламация")                                                     // 24
    nextColumn();text("Б/н")                                                            // 25
    nextColumn();text("№ счёта")                                                        // 26
    nextColumn();text("дата" + "\n" + "вылета")                                         // 27
    nextColumn();text("дата" + "\n" + "прибытия")                                       // 28
    nextColumn();text("Сумма по кк")                                                    // 29
    nextColumn();columnWidth(1)                                                         // 30
    nextColumn();text("НДС 20%")                                                        // 31
    nextColumn();columnWidth(1)                                                         // 32
    nextColumn();columnWidth(1)                                                         // 33
    nextColumn();text("НДС 10%")                                                        // 34
}

def printTickets(List<BaseProduct> tickets) {
    int count = 0
    tickets.each { BaseProduct product ->
        int conjCount = GeneralProductHelper.getHandler(product).getConjunction(product)

        if (!issuedBefore(product)) {
            for (int n = 0; n <= conjCount; n++) {
                printTicket(product, n)
                count++
            }
        }
    }

    printTableTotal(count)
}

def collectSortedTickets() {
    def fitByBlankOwner = { BaseProduct product ->
        def blankOwnerRef = GeneralProductHelper.getHandler(product).getBlankOwner(product)
        def blankOwners = parameters["blank_owners"]

        return blankOwners.isEmpty() ||
                (blankOwnerRef && blankOwners.contains(blankOwnerRef))
    }

    List<BaseProduct> result = new ArrayList<>()

    tickets { CommonProductIndex idx ->
        BaseProduct product =
                BookingHelper.findProductByUid(
                        idx.navigationKey,
                        EntityStorage.get().resolve(idx.source).entity)

        if (!product) {
            error(String.format("не удалось загрузить продукт %s (uid %s) в заказе %s",
                    TextUtil.join(", ", idx.systemNumbers), idx.navigationKey, idx.source))
            return
        }
        if (GeneralProductHelper.getStatus(product) == ProductStatus.EXCHANGE) {
            return
        }
        if (!fitByBlankOwner(product)) {
            return
        }

        result.add(product)
    }

    result.sort(ticketComparator())

    return result
}

def ticketComparator() {
    def compareBlankOwners = { BaseProduct p1, BaseProduct p2 ->
        EntityReference<Organization> blankOwner1 = GeneralProductHelper.getBlankOwner(p1)
        EntityReference<Organization> blankOwner2 = GeneralProductHelper.getBlankOwner(p2)

        return MiscUtil.compare(blankOwner1?.uid, blankOwner2?.uid)
    }
    def sortingTicketNumber = { BaseProduct p ->
        String result = carrierNumber(p) + ticketNumber(p, 0)

        if (isAirMco(p)) {
            BaseProduct sortProduct = relatedProductForMco(p)
            if (sortProduct) {
                if (GeneralProductHelper.getStatus(p) == ProductStatus.EXCHANGE
                        && GeneralProductHelper.getHandler(p).getNextProduct(p)) {
                    sortProduct = GeneralProductHelper.getHandler(p).getNextProduct(p)
                }
                result = carrierNumber(sortProduct) + ticketNumber(sortProduct, 0)
            }
        }

        return result
    }
    def isRefundPenaltyMCO = { BaseProduct p ->
        return isAirMco(p) &&
                (mcoCategory(p) == MCOCategory.PENALTY ||
                        mcoCategory(p) == MCOCategory.REBOOKING) &&
                relatedProductForMco(p)?.status == ProductStatus.REFUND
    }
    def isExchangePenaltyMCO = { BaseProduct p ->
        return isAirMco(p) &&
                (mcoCategory(p) == MCOCategory.PENALTY ||
                        mcoCategory(p) == MCOCategory.REBOOKING) &&
                relatedProductForMco(p)?.status == ProductStatus.EXCHANGE
    }
    def isRefundReceiptMCO = { BaseProduct p ->
        return isAirMco(p) &&
                mcoCategory(p) == MCOCategory.INFORMATION &&
                relatedProductForMco(p)?.status == ProductStatus.REFUND
    }
    def isExchangeReceiptMCO = { BaseProduct p ->
        return isAirMco(p) &&
                mcoCategory(p) == MCOCategory.INFORMATION &&
                relatedProductForMco(p)?.status == ProductStatus.EXCHANGE
    }

    return new Comparator<BaseProduct>() {
        @Override
        int compare(final BaseProduct p1, final BaseProduct p2) {
            int res = compareBlankOwners(p1, p2)
            if (res != 0) {
                return res
            }

            res = TextUtil.compare(
                    sortingTicketNumber(p1), sortingTicketNumber(p2),
                    false, true)
            if (res != 0) {
                return res
            }

            if (isRefundPenaltyMCO(p1) != isRefundPenaltyMCO(p2)) {
                return isRefundPenaltyMCO(p1) ? 1 : 0
            }
            if (isExchangePenaltyMCO(p1) != isExchangePenaltyMCO(p2)) {
                return isExchangePenaltyMCO(p1) ? 1 : 0
            }
            if (isRefundReceiptMCO(p1) != isRefundReceiptMCO(p2)) {
                return isRefundReceiptMCO(p1) ? 1 : 0
            }
            if (isExchangeReceiptMCO(p1) != isExchangeReceiptMCO(p2)) {
                return isExchangeReceiptMCO(p1) ? 1 : 0
            }

            res = TextUtil.compare(
                    ticketNumber(p1, 0), ticketNumber(p2, 0),
                    false, true)
            if (res != 0) {
                return res
            }

            // code below may possible violate general contract of sorting
            if ((GeneralProductHelper.getStatus(p1) == ProductStatus.REFUND ||
                    GeneralProductHelper.getStatus(p1) == ProductStatus.EXCHANGE)
                    && (GeneralProductHelper.getStatus(p2) == ProductStatus.SELL)) {
                return 1
            }

            return -1
        }
    }
}

def printTicket(BaseProduct product, int conj) {
    nextRow(); rowHeight(30)

    text(contractor(product), "center-center-border-italic")
    nextColumn(); text(lines(product))
    nextColumn(); text(client(product))
    nextColumn(); text(lastName(product))
    nextColumn(); text(routeOrVoid(product))
    nextColumn(); text(arm(product))
    nextColumn(); text(ticketNumber(product, conj))
    nextColumn(); text(carrierNumber(product))

    if (conj == 0) {
        if (creditCardType(product) != PaymentType.CREDIT_CARD_AGENCY) {
            nextColumn(); number(equivalentFare(product), "center-center-border-italic-number")
            nextColumn(); number(railwayFeeValue(product, GdsName.SPGD), "center-center-border-italic-number")
            nextColumn(); number(MiscUtil.sum(blankTax(product), railwayFeeValue(product, GdsName.TELETRAIN)), "center-center-border-italic-number")
            nextColumn(); number(otherTaxes(product), "center-center-border-italic-number")
            nextColumn(); number(tchTax(product), "center-center-border-italic-number")
        } else {
            5.times {
                nextColumn(); text("", "center-center-border-italic")
            }
        }
        nextColumn(); number(railwayClientEquivalentFee(product), "center-center-border-italic-number")
        nextColumn(); number(defaultClientEquivalentFee(product), "center-center-border-italic-number")
        nextColumn(); number(refundFee(product))
        nextColumn(); number(penalty(product))
        nextColumn(); number(discount(product))
        nextColumn(); number(commissionRate(product))
        nextColumn(); number(commissionValue(product))
        nextColumn(); number(creditCardAmount(product))
        nextColumn(); number(fees(product))
        nextColumn(); number(cashAmount(product))
        nextColumn(); number(reclamation(product))
        nextColumn(); number(invoiceAmount(product))
        nextColumn(); text(invoiceNumber(product), "center-center-border-italic")
    } else {
        18.times {
            nextColumn(); text("", "center-center-border-italic")
        }
    }
    nextColumn(); text(firstTravelDate(product), "center-center-border-italic")
    nextColumn(); text(lastTravelDate(product), "center-center-border-italic")
    nextColumn(); number(creditCardAgencyAmount(product), "center-center-border-italic-number")
    nextColumn()
    nextColumn(); number(getVat(product, false))
    nextColumn()
    nextColumn()
    nextColumn(); number(getVat(product, true))
}

def printTableTotal(int ticketsCount) {
    def printTotalFormula = {
        formula("SUM(${cellIndex(-ticketsCount - 1, 0)}:${cellIndex(-1, 0)})", "center-center-border-italic-number")
        nextColumn()
    }
    def printEmpty = {
        text("", "center-center-border-italic")
        nextColumn()
    }

    // there is empty row between tickets and total. it's bug maybe
    nextRow(); rowHeight(15)
    31.times {
        printEmpty()
    }

    nextRow(); rowHeight(30)
    8.times{
        printEmpty()
    }
    10.times{
        printTotalFormula()
    }
    1.times{
        printEmpty()
    }
    6.times{
        printTotalFormula()
    }
    3.times{
        printEmpty()
    }
    1.times{
        printTotalFormula()
    }
    2.times{
        printTotalFormula()
    }
}

boolean issuedBefore(BaseProduct product) {
    Date reportBeginDate = parameters['key-report-params']?.periodBegin
    Date issueDate = issueDate(product)

    return reportBeginDate && (!issueDate || issueDate.before(reportBeginDate))
}

def contractor(BaseProduct product) {
    ProductHandler<BaseProduct> handler = GeneralProductHelper.getHandler(product)

    if (TextUtil.nonBlank(handler.getValidatorNumber(product))) {
        ValidatorIndex index =
                ValidatorHelper.findValidator(
                        handler.getValidatorNumber(product),
                        handler.getBlankOwner(product),
                        handler.findIssueDate(product))

        if (index?.supplier) {
            return index.supplier.toString()
        }
    }

    return ""
}

def lines(BaseProduct product) {
    if (isVoid(product)) {
        return ""
    }

    if (isAirMco(product)) {
        BaseProduct relatedProduct = relatedProductForMco(product)
        if (relatedProduct) {
            return lines(relatedProduct)
        }
    }

    ProductHandler<BaseProduct> handler = GeneralProductHelper.getHandler(product)

    TransportationType type = handler.getTransportationType(product)
    if (type) {
        switch (type) {
            case TransportationType.DOMESTIC:
                return "в"
            case TransportationType.INTERNATIONAL:
                return "м"
            case TransportationType.COMBINED:
                return "в+м"
        }
    }

    return "?"
}

def client(BaseProduct product) {
    def clientCont =
            EntityStorage.get().resolve(
                    product?.reservation?.bookingFile?.customerProfile)

    if (isPrivatePerson(clientCont?.entity)) {
        return "ФЛ"
    } else {
        return TextUtil.nonNullStr(
                L10nStringHelper.getValue(
                        clientCont?.entity?.shortName,
                        LocaleHelper.RU_LOCALE, false))
    }
}

def isPrivatePerson(Organization org) {
    String privatePersonCode =
            DictHelper.getPreferenceValue(
                    PreferenceKey.RETAIL_PROFILE_CODE, null)

    return TextUtil.nonNullStr(privatePersonCode) &&
            privatePersonCode.equals(org?.code)
}

def lastName(BaseProduct product) {
    ProductHandler<BaseProduct> handler = GeneralProductHelper.getHandler(product)

    for (Traveller traveller : handler.getTravellers(product)) {
        if (TextUtil.nonBlank(traveller?.passport?.lastName)) {
            return TextUtil.capitalize(traveller.passport.lastName.toLowerCase())
        }

        if (traveller?.name) {
            String firstWord = new StringTokenizer(traveller.name, " /").nextToken()

            if (TextUtil.nonBlank(firstWord)) {
                return TextUtil.capitalize(firstWord.toLowerCase())
            }
        }
    }

    return ""
}

def routeOrVoid(BaseProduct product) {
    if (isVoid(product)) {
        return "аннулирован"
    }

    if (isAirMco(product)) {
        BaseProduct relatedProduct = relatedProductForMco(product)
        if (relatedProduct) {
            return route(relatedProduct)
        } else {
            return ""
        }
    }

    return route(product)
}

def route(BaseProduct product) {
    if (product instanceof Product &&
            AirProductHelper.getTransportationType(product) == TransportationType.DOMESTIC) {
        return TextUtil.nonNullStr(AirProductHelper.getRouteLine(
                product, null, true, CodeSystem.CRT)?.toLowerCase())
    }

    return TextUtil.nonNullStr(
            GeneralProductHelper.getHandler(product)
                    .getRouteLine(product)?.toLowerCase())
}


def arm(BaseProduct product) {
    int count = 0

    if (!isVoid(product)) {
        if (isAirMco(product)) {
            BaseProduct relatedProduct = relatedProductForMco(product)
            if (relatedProduct) {
                return arm(relatedProduct)
            }
        } else if (product instanceof Product) {
            int temp = 0
            ((Product) product).segmentTariffs.each {
                temp += it.segments.size()
            }
            if (temp > 0) {
                count = temp
            }
        } else if (product instanceof RailwayProduct) {
            int temp = ((RailwayProduct) product).segments.size()
            if (temp > 0) {
                count = temp
            }
        }
    }

    return count > 0 ? Integer.toString(count) : ""
}

def ticketNumber(BaseProduct product, int conj) {
    String result = TextUtil.nonNullStr(GeneralProductHelper.getSystemNumber(product))

    if (TextUtil.nonBlank(result) && conj > 0) {
        try {
            result = FormattedNumberUtil.add(result, conj)
        } catch (Throwable t) {
            error(String.format(
                    "Билет %s: не удается генерировать системный номер присоединенного билета.",
                    result), t)
        }
    }

    if (isRefund(product)) {
        result += "в"
    } else if (isExchange(product)) {
        result += "о"
    }

    return result
}

def carrierNumber(BaseProduct product) {
    return TextUtil.nonNullStr(
            GeneralProductHelper.getHandler(product)
                    .getCarrierNumber(product))
}

def equivalentFare(BaseProduct product) {
    if (isVoid(product)) {
        return BigDecimal.ZERO
    }

    BigDecimal result =
            creditCardType(product) ? BigDecimal.ZERO :
                    GeneralProductHelper.getEquivalentFare(product)

    if (isAirMco(product)) {
        if (mcoCategory(product) && isSell(product)) {
            switch (mcoCategory(product)) {
                case MCOCategory.PENALTY:
                case MCOCategory.REBOOKING:
                    result = null
                    break
                case MCOCategory.INFORMATION:
                    result = BigDecimal.ZERO
            }
        }
    } else {
        if (isRefund(product)) {
            result = MiscUtil.negate(result)
        } else if (isExchange(product)) {
            result = MiscUtil.sub(
                    result, equivalentFare(previousProduct(product)))
        }
    }

    return result
}

// it was ruTax in code of old report
def blankTax(BaseProduct product) {
    BigDecimal result = BigDecimal.ZERO

    if (isVoid(product)) {
        return result
    }

    if (isAirMco(product)) {
        if (mcoCategory(product) && isSell(product)) {
            switch (mcoCategory(product)) {
                case MCOCategory.PENALTY:
                case MCOCategory.REBOOKING:
                    if (!creditCardType(product)) {
                        if (isTCHTicket(product)) {
                            result = null
                        } else {
                            result = AirProductTaxHelper.getTaxesForBlankEquivalentAmount((Product) product)
                        }
                    }
            }
        }
    } else {
        if (!creditCardType(product)) {
            if (isTCHTicket(product)) {
                result = null
            } else {
                if (product instanceof Product) {
                    result = AirProductTaxHelper.getTaxesForBlankEquivalentAmount(product)
                } else {
                    result = GeneralProductHelper.getTaxesForBlankEquivalentAmount(product)
                }
            }
        }

        if (isRefund(product)) {
            result = MiscUtil.negate(result)
        } else if (isExchange(product)) {
            result = MiscUtil.sub(
                    result, blankTax(previousProduct(product)))
        }
    }

    return result
}

def otherTaxes(BaseProduct product) {
    BigDecimal result = BigDecimal.ZERO

    if (isVoid(product)) {
        return result
    }

    if (isAirMco(product)) {
        if (mcoCategory(product) && isSell(product)) {
            switch (mcoCategory(product)) {
                case MCOCategory.PENALTY:
                case MCOCategory.REBOOKING:
                    if (!creditCardType(product)) {
                        result = MiscUtil.sub(
                                GeneralProductHelper.getEquivalentTaxAmount(
                                        GeneralProductHelper.getTaxes(product)),
                                blankTax(product), tchTax(product))
                    }
            }
        }
    } else {
        if (!creditCardType(product)) {
            result = MiscUtil.sub(
                    GeneralProductHelper.getEquivalentTaxAmount(
                            GeneralProductHelper.getTaxes(product)),
                    (isRefund(product) ? MiscUtil.negate(blankTax(product)) : blankTax(product)),
                    tchTax(product))
        }

        if (isRefund(product)) {
            result = MiscUtil.negate(result)
        } else if (isExchange(product)) {
            result = MiscUtil.sub(
                    result, otherTaxes(previousProduct(product)))
        }
    }

    return result
}

def tchTax(BaseProduct product) {
    BigDecimal result = BigDecimal.ZERO

    if (isVoid(product)) {
        return result
    }

    if (isAirMco(product)) {
        if (mcoCategory(product) && isSell(product)) {
            switch (mcoCategory(product)) {
                case MCOCategory.PENALTY:
                case MCOCategory.REBOOKING:
                    if (!creditCardType(product)) {
                        if (isTCHTicket(product)) {
                            result = AirProductTaxHelper.getTaxesForBlankEquivalentAmount((Product) product)
                        }
                    }
            }
        }
    } else {
        if (!creditCardType(product)) {
            if (isTCHTicket(product)) {
                if (product instanceof Product) {
                    result = AirProductTaxHelper.getTaxesForBlankEquivalentAmount(product)
                } else {
                    result = GeneralProductHelper.getTaxesForBlankEquivalentAmount(product)
                }
            }
        }

        // do not negate if refund because ZZ tax is non-refundable
        if (isExchange(product)) {
            result = MiscUtil.sub(
                    result, tchTax(previousProduct(product)))
        }
    }

    return result
}

def railwayClientEquivalentFee(BaseProduct product) {
    BigDecimal result = null

    if (product instanceof RailwayProduct) {
        if (!creditCardType(product)) {
            result = railwayCalculateClientEquivalentFee(product, false, true)
        }

        if (isRefund(product)) {
            result = MiscUtil.negate(result)
        }
    }


    return result
}

def defaultClientEquivalentFee(BaseProduct product) {
    BigDecimal result = null

    if (!creditCardType(product)) {
        if (product instanceof RailwayProduct) {
            result = railwayCalculateClientEquivalentFee(product, true, false)
        } else {
            result = defaultCalculateClientEquivalentFee(product)
        }
    }

    if (!isAirMco(product) && isRefund(product)) {
        result = MiscUtil.negate(result)
    }

    return result
}

def defaultCalculateClientEquivalentFee(BaseProduct product) {
    BigDecimal result = null

    GeneralProductHelper.getUnmodifiableCommissions(product, ContractType.CLIENT).each {
        if (BookingHelper.isBSPCommission(it?.commissionProperties)) {
            return
        }
        if (!it.equivalentAmount) {
            return
        }

        def commissionProperties =
                EntityStorage.get().resolve(it.commissionProperties)

        if (commissionProperties?.entity instanceof FeeProperties) {
            if (((FeeProperties) commissionProperties.entity).operation == Operation.REFUND
                || ((FeeProperties) commissionProperties.entity).chargeToClientForAllContractTypes) {
                return
            }
            result = MiscUtil.sum(result, it.equivalentAmount.abs())
        }

        if (commissionProperties?.entity instanceof PaymentFeeProperties) {
            if (isRefund(product)
                    && ((PaymentFeeProperties) commissionProperties.entity).chargeOnRefund
                    && (it.equivalentAmount.signum() < 0)) {
                return
            }
            result = MiscUtil.sum(result, it.equivalentAmount.abs())
        }
    }

    return result
}

def railwayCalculateClientEquivalentFee(RailwayProduct product,
                                        boolean translatedVendorFee,
                                        boolean includeChargeToClientForAllContractTypes) {
    def hasTranslatedVendorFee = { GeneralProductContractRelationData relationData ->
        return BooleanUtil.nullAsFalse(relationData?.generalData?.translateSupplierFees)
    }
    def isTranslatedCommission = { GeneralProductCommission commission,
                                   List<GeneralProductCommission> vendorCommissions ->
        return vendorCommissions.any {
            return MiscUtil.equals(it.equivalentAmount, commission.equivalentAmount) &&
                    MiscUtil.equals(it.commissionProperties, commission.commissionProperties)
        }
    }

    BigDecimal result = null

    def clientContractRelation = GeneralProductHelper.getClientContractRelation(product)
    def vendorCommissions = GeneralProductHelper.getVendorCommissions(product)

    clientContractRelation.commissions.each {
        if (BookingHelper.isBSPCommission(it?.commissionProperties)) {
            return
        }
        if (!it.equivalentAmount) {
            return
        }
        if (translatedVendorFee !=
                (hasTranslatedVendorFee(clientContractRelation) &&
                        isTranslatedCommission(it, vendorCommissions))) {
            return
        }

        def commissionProperties =
                EntityStorage.get().resolve(it.commissionProperties)

        if (commissionProperties?.entity instanceof FeeProperties) {
            if (((FeeProperties) commissionProperties.entity).operation == Operation.REFUND) {
                return
            }
            if (!includeChargeToClientForAllContractTypes
                    && ((FeeProperties) commissionProperties.entity).chargeToClientForAllContractTypes) {
                return
            }
            result = MiscUtil.sum(result, it.equivalentAmount.abs())
        }

        if (commissionProperties?.entity instanceof PaymentFeeProperties) {
            if (isRefund(product)
                    && ((PaymentFeeProperties) commissionProperties.entity).chargeOnRefund
                    && (it.equivalentAmount.signum() < 0)) {
                return
            }
            result = MiscUtil.sum(result, it.equivalentAmount.abs())
        }
    }

    return result
}

def refundFee(BaseProduct product) {
    BigDecimal result = null

    if (!creditCardType(product)) {
        GeneralProductHelper.getUnmodifiableCommissions(product, ContractType.CLIENT).each {
            if (BookingHelper.isBSPCommission(it?.commissionProperties)) {
                return
            }
            if (!it.equivalentAmount) {
                return
            }

            def commissionProperties =
                    EntityStorage.get().resolve(it.commissionProperties)

            if (commissionProperties?.entity instanceof FeeProperties) {
                if (((FeeProperties) commissionProperties.entity).operation == Operation.REFUND) {
                    result = MiscUtil.sum(result, it.equivalentAmount.abs())
                }
            }

            if (commissionProperties?.entity instanceof PaymentFeeProperties) {
                if (isRefund(product)
                        && ((PaymentFeeProperties) commissionProperties.entity).chargeOnRefund
                        && (it.equivalentAmount.signum() < 0)) {
                    result = MiscUtil.sum(result, it.equivalentAmount.abs())
                }
            }
        }
    }

    return result
}

def penalty(BaseProduct product) {
    BigDecimal result = BigDecimal.ZERO

    if (isVoid(product)) {
        return result
    }

    if (isAirMco(product)) {
        if (mcoCategory(product) && isSell(product)) {
            switch (mcoCategory(product)) {
                case MCOCategory.PENALTY:
                case MCOCategory.REBOOKING:
                    result = GeneralProductHelper.getEquivalentFare(product)
            }
        }
    } else {
        if ((isRefund(product) || isExchange(product)) && !hasRelatedPenaltyMCO(product)) {
            BigDecimal penalty = GeneralProductHelper.getHandler(product).getPenalty(product)

            if (!MiscUtil.isZero(penalty, true)) {
                result = penalty
            } else {
                result = null
            }
        }
    }

    return result
}

def discount(BaseProduct product) {
    BigDecimal result = null

    if (!creditCardType(product)) {
        GeneralProductHelper.getUnmodifiableCommissions(product, ContractType.CLIENT).each {
            if (BookingHelper.isBSPCommission(it?.commissionProperties)) {
                return
            }
            if (!it.equivalentAmount) {
                return
            }

            if (DiscountProperties.class.equals(it?.commissionProperties?.type)) {
                result = MiscUtil.sum(result, it.equivalentAmount)
            }
        }
    }

    result = MiscUtil.negate(result)

    return result
}

def commissionRate(BaseProduct product) {
    Double result = null

    if (!creditCardType(product)) {
        GeneralProductHelper.getUnmodifiableCommissions(product, ContractType.VENDOR).each {
            if (BookingHelper.isBSPCommission(it?.commissionProperties)) {
                return
            }
            if (!it.equivalentAmount) {
                return
            }
            if (FeeProperties.class.equals(it?.commissionProperties?.type) ||
                    PaymentFeeProperties.class.equals(it?.commissionProperties?.type) ||
                    DiscountProperties.class.equals(it?.commissionProperties?.type)) {
                return
            }

            result = updateCommissionRate(result, it.rate)
        }

        // Take BSP commission value into account only in case of ticket is a
        // BSP ticket and blank owner is the same as supplier (XTR-4039)
        if (product instanceof Product) {
            if (AirProductHelper.isBSPTicket(product)
                    && MiscUtil.equals(product.getBlankOwnerRef(),
                    AirProductHelper.getSupplier(product))) {
                result = updateCommissionRate(
                        result, product.bspCommissionRate)
            }
        }
    }

    return result
}

def updateCommissionRate(final Double currentRate,
                         final Double commissionRate) {
    if (!commissionRate || (Math.abs(commissionRate) < 0.001)) {
        return currentRate
    }
    if (!currentRate) {
        return commissionRate
    }

    return Math.abs(currentRate) > Math.abs(commissionRate) ? currentRate : commissionRate
}

def railwayFeeValue(BaseProduct product, GdsName gdsName) {
    BigDecimal result = null

    if (product instanceof RailwayProduct && product.reservation != null &&
            CommonReservationGdsNameInfoHelper.containsGds(product.reservation, gdsName)) {
        result = feeValue(product)
    }

    return result
}

def feeValue(BaseProduct product) {
    BigDecimal result = BigDecimal.ZERO

    if (isVoid(product)) {
        return result
    }

    if (!creditCardType(product)) {
        GeneralProductHelper.getUnmodifiableCommissions(product, ContractType.VENDOR).each {
            if (BookingHelper.isBSPCommission(it?.commissionProperties)) {
                return
            }
            if (!it.equivalentAmount) {
                return
            }
            if (FeeProperties.class.equals(it?.commissionProperties?.type) ||
                    PaymentFeeProperties.class.equals(it?.commissionProperties?.type)) {
                result = MiscUtil.sum(result, it.equivalentAmount)
            }
        }

        // Take BSP commission value into account only in case of ticket is a
        // BSP ticket and blank owner is the same as supplier (XTR-4039)
        if (product instanceof Product) {
            if (AirProductHelper.isBSPTicket(product)
                    && MiscUtil.equals(product.getBlankOwnerRef(),
                    AirProductHelper.getSupplier(product))) {
                result = MiscUtil.sum(result, product.bspCommissionValue?.value)
            }
        }
    }

    if (!isAirMco(product)) {
        if (isRefund(product)) {
            result = MiscUtil.negate(result)
        } else if (isExchange(product)) {
            result = MiscUtil.sub(
                    result, feeValue(previousProduct(product)))
        }
    }

    return result
}

def commissionValue(BaseProduct product) {
    BigDecimal result = BigDecimal.ZERO

    if (isVoid(product)) {
        return result
    }

    if (!creditCardType(product)) {
        GeneralProductHelper.getUnmodifiableCommissions(product, ContractType.VENDOR).each {
            if (BookingHelper.isBSPCommission(it?.commissionProperties)) {
                return
            }
            if (!it.equivalentAmount) {
                return
            }
            if (FeeProperties.class.equals(it?.commissionProperties?.type) ||
                    PaymentFeeProperties.class.equals(it?.commissionProperties?.type) ||
                    DiscountProperties.class.equals(it?.commissionProperties?.type)) {
                return
            }

            result = MiscUtil.sum(result, it.equivalentAmount)
        }

        // Take BSP commission value into account only in case of ticket is a
        // BSP ticket and blank owner is the same as supplier (XTR-4039)
        if (product instanceof Product) {
            if (AirProductHelper.isBSPTicket(product)
                    && MiscUtil.equals(product.getBlankOwnerRef(),
                    AirProductHelper.getSupplier(product))) {
                result = MiscUtil.sum(result, product.bspCommissionValue?.value)
            }
        }
    }

    if (!isAirMco(product)) {
        if (isRefund(product)) {
            result = MiscUtil.negate(result)
        } else if (isExchange(product)) {
            result = MiscUtil.sub(
                    result, commissionValue(previousProduct(product)))
        }
    }

    return result
}

def creditCardAmount(BaseProduct product) {
    BigDecimal result = null

    if (!isVoid(product)
            && !isAirMco(product)
            && creditCardType(product) == PaymentType.CREDIT_CARD) {
        result = paymentTypes(product).get(PaymentType.CREDIT_CARD)
    }

    return result
}

def creditCardAgencyAmount(BaseProduct product) {
    BigDecimal result = null

    if (!isVoid(product)
            && !isAirMco(product)
            && creditCardType(product) == PaymentType.CREDIT_CARD_AGENCY) {
        result = paymentTypes(product).get(PaymentType.CREDIT_CARD_AGENCY)
    }

    return result
}

def fees(BaseProduct product) {
    BigDecimal result = BigDecimal.ZERO

    if (isVoid(product)) {
        return result
    }

    Date startDate = parameters['key-report-params']?.periodBegin
    Date endDate = parameters['key-report-params']?.periodEnd

    if (startDate && endDate) {
        collectFops(product).each {
            if (it.type != PaymentType.CASH
                    && it.type != PaymentType.CREDIT_CARD
                    && it.type != PaymentType.CREDIT_CARD_AGENCY) {
                return
            }
            if (!MiscUtil.isBetween(startDate, endDate, it.operationDate)) {
                return
            }
            result = MiscUtil.sum(result, it.equivalentAmount)
        }
    }

    if (!isAirMco(product)) {
        if (isRefund(product)) {
            result = MiscUtil.negate(result)
        } else if (isExchange(product)) {
            result = MiscUtil.sub(result, fees(previousProduct(product)))
        }
    }

    return result
}

def cashAmount(BaseProduct product) {
    BigDecimal result = null

    if (!isAirMco(product) && (isSell(product) || isExchange(product))) {
        result = paymentTypes(product).get(PaymentType.CASH)

        // There was check like (result != null && result != BigDecimal.ZERO) in old report. It works like that in Groovy
        // Check by equals/compareTo will change result in some cases. Not sure if this should work like this
        if (result && !result.is(BigDecimal.ZERO)) {
            result = MiscUtil.sum(result, discount(product))
        }
    }

    return result
}

def reclamation(BaseProduct product) {
    BigDecimal result = null

    if (!isAirMco(product) && isRefund(product)) {
        BigDecimal value = paymentTypes(product).get(PaymentType.CASH)
        if (!MiscUtil.isZero(value, true)) {
            result = value
        }
    }

    return result
}

def invoiceAmount(BaseProduct product) {
    BigDecimal result = null

    if (!isAirMco(product)) {
        if (isSell(product) || isExchange(product)) {
            result = paymentTypes(product).get(PaymentType.INVOICE)

            // There was check like (result != null && result != BigDecimal.ZERO) in old report. It works like that in Groovy
            // Check by equals/compareTo will change result in some cases. Not sure if this should work like this
            if (result && !result.is(BigDecimal.ZERO)) {
                result = MiscUtil.sum(result, discount(product))
            }
        } else if (isRefund(product)) {
            result = paymentTypes(product).get(PaymentType.INVOICE)
        }
    }

    return result
}

def invoiceNumber(BaseProduct product) {
    String result = null

    if (!isAirMco(product) && (isSell(product) || isExchange(product))) {
        OUTER : for (Payment payment : product?.reservation?.bookingFile?.payments) {
            for (BillingItem billingItem : payment?.billingItems) {
                if (billingItem?.productUids?.contains(product.uid)) {
                    for (FinanceDocument financeDocument : payment.financeDocuments) {
                        if (financeDocument.type == FinanceDocumentType.INVOICE) {
                            result = financeDocument.getNumber()
                            break OUTER
                        }
                    }
                }
            }
        }
    }

    return TextUtil.nonNullStr(result)
}

def firstTravelDate(BaseProduct product) {
    String result = null

    if (!isAirMco(product) && (isSell(product) || isExchange(product))) {
        Date firstTravelDate =
                GeneralProductHelper.getHandler(product)
                        .findFirstTravelDate(product)

        if (firstTravelDate) {
            result = new SimpleDateFormat("dd.MM.yyyy").format(firstTravelDate)
        }
    }

    return TextUtil.nonNullStr(result)
}

def lastTravelDate(BaseProduct product) {
    String result = null

    if (!isAirMco(product) && (isSell(product) || isExchange(product))) {
        Date lastTravelDate =
                GeneralProductHelper.getHandler(product)
                        .findLastTravelDate(product)

        if (lastTravelDate) {
            result = new SimpleDateFormat("dd.MM.yyyy").format(lastTravelDate)
        }
    }

    return TextUtil.nonNullStr(result)
}

def getVat(BaseProduct product, boolean is10) {
    BigDecimal vat = BigDecimal.ZERO
    VatDetalization vatDetalization = GeneralProductHelper.getHandler(product)?.getVendorVatDetalization(product)

    if (vatDetalization != null) {
        List<VatComponent> vats = vatDetalization.components
        if(vats != null && !vats.isEmpty()){
           for (VatComponent component : vats) {
               if (component.rate != null && component.rate != 0d){
                   if(is10){
                       if (10 == (int)component.rate) {
                           vat = MiscUtil.sum(vat, component.sum)
                       }
                   } else {
                       if (10 != (int)component.rate) {
                           vat = MiscUtil.sum(vat, component.sum)
                       }
                   }
               }
           }
        }
    }

    return vat
}

def paymentTypes(BaseProduct product) {
    def supportedPaymentTypes =
            EnumSet.of(
                    PaymentType.CASH, PaymentType.INVOICE,
                    PaymentType.PTA, PaymentType.CREDIT_CARD,
                    PaymentType.CREDIT_CARD_AGENCY)

    Map<PaymentType, BigDecimal> result = new EnumMap<>(PaymentType.class)
    supportedPaymentTypes.each {
        result.put(it, BigDecimal.ZERO)
    }

    List<GeneralProductFop> fops = collectFops(product)

    fops.each {
        if (!supportedPaymentTypes.contains(it?.type)) {
            return
        }
        if ((it.type == PaymentType.CREDIT_CARD
                || it.type == PaymentType.CREDIT_CARD_AGENCY)
                && GeneralProductHelper.isServiceFop(it)) {
            return
        }

        result.put(it.type, MiscUtil.sum(result.get(it.type), it.equivalentAmount))
    }

    if (!isAirMco(product)) {
        if (isRefund(product)) {
            supportedPaymentTypes.each {
                if (result.get(it)) {
                    result.put(it, MiscUtil.negate(result.get(it)))
                }
            }
        } else if (isExchange(product)) {
            def previousPaymentTypes = paymentTypes(previousProduct(product))

            supportedPaymentTypes.each {
                if (result.get(it) && previousPaymentTypes.get(it)) {
                    result.put(it,
                            MiscUtil.sub(
                                    result.get(it),
                                    previousPaymentTypes.get(it)))
                }
            }
        }
    }

    return result
}

def collectFops(BaseProduct product) {
    List<GeneralProductFop> result
    if (product instanceof Product) {
        result = collectAirFops((Product) product)
    } else {
        result = collectGeneralFops(product)
    }

    if (result.empty &&
            (!isVoid(product) &&
                    !(isAirMco(product) && mcoCategory(product) == MCOCategory.INFORMATION))) {
        error(String.format("Билет %s: отсутствует информация о ФОП",
                ticketNumber(product, 0)))
    }

    return result
}

// logic from com.gridnine.xtrip.common.reports.model.BaseReportTicket.collectFops
def collectAirFops(Product product) {
    def findRelatedPaymentType = { ProductFop fop, Collection<ProductFop> relFops ->
        if (relFops.size() == 1) {
            return relFops.iterator().next().getType()
        }
        if (!fop?.amount?.value) {
            return null
        }
        for (ProductFop relFop : relFops) {
            if (!relFop?.amount?.value) {
                continue
            }
            if (Math.abs(relFop.amount.value.subtract(fop.amount.value).doubleValue()) < 0.0001) {
                return relFop.type
            }
        }
        return null
    }

    List<GeneralProductFop> result = new ArrayList<>()

    for (ProductFop fop : AirProductHelper.getClientFops(product)) {
        if (fop.type != PaymentType.TICKET) {
            result.add(AirProductHelper.toGeneralProductFop(fop))
        } else {
            if (fop.exchangePaymentType) {
                    GeneralProductFop fop2 = AirProductHelper.toGeneralProductFop(fop)
                    fop2.setType(fop.getExchangePaymentType())
                    result.add(fop2)
            } else if (isVoid(product)) {
                GeneralProductFop fop2 = AirProductHelper.toGeneralProductFop(fop)
                result.add(fop2)
            } else {
                if (!product.previousProduct) {
                    error(String.format(
                            "Билет %s: отсутствует билет, принятый к обмену", product.systemNumber))
                    continue
                }

                PaymentType relatedPaymentType = findRelatedPaymentType(
                        fop, AirProductHelper.getClientFops(product.previousProduct))
                if (!relatedPaymentType) {
                    error(String.format(
                            "Билет %s: не удается найти ФОП в билете, принятом к обмену, связанный с ФОП типа %s на сумму %s",
                            product.systemNumber, PaymentType.TICKET, fop?.amount?.value))
                    continue
                }

                GeneralProductFop fop2 = AirProductHelper.toGeneralProductFop(fop)
                fop2.setType(relatedPaymentType)
                result.add(fop2)
            }
        }
    }

    return result
}

// logic from com.gridnine.xtrip.server.reports.prepare.agentreports.GeneralProductAgentReportPrepareHelper.collectFops
def collectGeneralFops(BaseProduct product) {
    def findRelatedPaymentType = { GeneralProductFop fop, Collection<GeneralProductFop> relFops ->
        if (relFops.size() == 1) {
            return relFops.iterator().next().getType()
        }
        if (!fop?.equivalentAmount) {
            return null
        }
        for (GeneralProductFop relFop : relFops) {
            if (!relFop?.equivalentAmount) {
                continue
            }
            if (Math.abs(relFop.equivalentAmount.subtract(fop.equivalentAmount).doubleValue()) < 0.0001) {
                return relFop.type
            }
        }
        return null
    }

    List<GeneralProductFop> result = new ArrayList<>()

    for (GeneralProductFop fop : GeneralProductHelper.getClientFops(product)) {
        if (fop.type != PaymentType.TICKET) {
            result.add(fop)
        } else {
            if (!previousProduct(product)) {
                error(String.format("Билет %s: отсутствует билет, принятый к обмену",
                        ticketNumber(product, 0)))
                continue
            }

            PaymentType relatedPaymentType = findRelatedPaymentType(
                    fop, GeneralProductHelper.getClientFops(previousProduct(product)))
            if (!relatedPaymentType) {
                error(String.format("Билет %s: не удается найти ФОП в билете, принятом к обмену, связанный с ФОП типа %s на сумму %s",
                        ticketNumber(product, 0), PaymentType.TICKET, fop?.equivalentAmount))
                continue
            }


            GeneralProductFop fop2 = new GeneralProductFop()
            XCloneModelHelper.copy(fop, fop2, true)
            fop2.setType(relatedPaymentType)
            result.add(fop2)
        }
    }

    return result
}

def relatedProductForMco(BaseProduct product) {
    if (!isAirMco(product)) {
        throw IllegalStateException("Attempt to get related product for non-MCO")
    }

    if (product instanceof Product) {
        Product relatedProduct =
                AirProductHelper.findRelatedAirProduct((Product) product, null, false)

        if (!relatedProduct
                && (mcoCategory(product) != MCOCategory.GROUP_PREPAYMENT)
                && (mcoCategory(product) != MCOCategory.PTA)) {

            warn(String.format("Билет %s: отсутствует связанный билет",
                    GeneralProductHelper.getSystemNumber(product)))
        }

        return relatedProduct
    }

    return null
}

def hasRelatedPenaltyMCO(final BaseProduct product) {
    if (product instanceof Product) {
        for (Product relatedProduct : AirProductHelper.getMcoByRelatedProduct(product)) {
            if ((relatedProduct.mcoCategory == MCOCategory.PENALTY)
                    || (relatedProduct.mcoCategory == MCOCategory.REBOOKING)) {
                return true
            }
        }
    }

    return false
}

def mcoCategory(BaseProduct product) {
    if (!isAirMco(product)) {
        throw IllegalStateException("Attempt to get mco category for non-MCO")
    }

    if (product instanceof Product) {
        return ((Product) product).mcoCategory
    }

    return null
}

def creditCardType(BaseProduct product) {
    for (GeneralProductFop fop : GeneralProductHelper.getClientFops(product)) {
        if (fop.type == PaymentType.CREDIT_CARD
                || fop.type == PaymentType.CREDIT_CARD_AGENCY) {
            return fop.type
        }
    }

    return null
}

def isTCHTicket(BaseProduct product) {
    return "Ш1".equalsIgnoreCase(
            EntityStorage.get().resolve(
                    GeneralProductHelper.getBlankOwner(product))?.entity?.code)
}

def isAirMco(BaseProduct product) {
    if (product instanceof Product) {
        return ((Product) product).productCategory == ProductCategory.MCO
    }

    return false
}

def isSell(BaseProduct product) {
    return GeneralProductHelper.getStatus(product) == ProductStatus.SELL && !isExchange(product)
}

def isRefund(BaseProduct product) {
    return GeneralProductHelper.getStatus(product) == ProductStatus.REFUND
}

def isExchange(BaseProduct product) {
    return GeneralProductHelper.getStatus(product) == ProductStatus.SELL &&
            previousProduct(product) &&
            GeneralProductHelper.getStatus(previousProduct(product)) == ProductStatus.EXCHANGE
}

def isVoid(BaseProduct product) {
    return GeneralProductHelper.getStatus(product) == ProductStatus.VOID
}

def previousProduct(BaseProduct product) {
    return GeneralProductHelper.getHandler(product).getPreviousProduct(product)
}

def printDebt(List<BaseProduct> tickets) {
    nextRow(); rowHeight(15)
    int startRowIndex = getRowIndex()

    printDebtHeader()
    int linesCount = printDebtTable(tickets)
    printDebtTotal(linesCount)

    moveTo(startRowIndex, 0)
}

def printDebtHeader() {
    createStyle(name: "center-border-1", leftBorder:'MEDIUM', rightBorder:'MEDIUM', topBorder:'MEDIUM', bottomBorder:'MEDIUM', fontBold : true, parent: "center-border")
    createStyle(name: "center-border-2", leftBorder:'MEDIUM', rightBorder:'MEDIUM', topBorder:'MEDIUM', bottomBorder:'MEDIUM', fontHeight : 9, fontItalic : true, parent: "center-border")
    createStyle(name: "center-border-3", leftBorder:'MEDIUM', rightBorder:'MEDIUM', topBorder:'MEDIUM', bottomBorder:'MEDIUM', parent: "center-border")

    def nextCellRow = {
        nextRow()
        9.times {
            nextColumn()
        }
    }

    nextCellRow(); text("Состояние долга на конец дня", "center-border-1", 7, 1)
    7.times {
        nextColumn()
    }
    text("Дата возн. долга", "center-border-2")

    nextCellRow(); text("Организация", "center-border-3", 6, 1)
    6.times {
        nextColumn()
    }
    text("сумма", "center-border-3")
    nextColumn(); text("")
}

def printDebtTable(List<BaseProduct> tickets) {
    createStyle(name: "center-border-1", leftBorder:'MEDIUM', parent: "center-border")
    createStyle(name: "center-border-2", rightBorder:'MEDIUM', parent: "center-border")

    def printDebtLine = { Object[] debtData ->
        nextRow()
        9.times {
            nextColumn()
        }

        String title = "FEE" != debtData[3] ?
                TextUtil.nonNullStr((String) debtData[0]) :
                "Сбор " + debtData[0]

        text(title, "center-border-1", 6, 1)
        6.times {
            nextColumn()
        }

        number(debtData[2], "center-border")
        nextColumn()

        text((debtData[1] ? new SimpleDateFormat("dd.MM.yy").format(debtData[1]) : ""), "center-border-2")
    }
    def printEmptyRow = {
        printDebtLine(new Object[4])
    }

    int linesCount = 0

    def debtData = collectDebtData(tickets)
    debtData.each {
        printDebtLine(it)
        linesCount++
    }

    int minDebtLines = 14
    if (debtData.size() < minDebtLines) {
        (minDebtLines - debtData.size()).times {
            printEmptyRow()
            linesCount++
        }
    }

    return linesCount
}

def printDebtTotal(int linesCount) {
    createStyle(name: "center-1", topBorder:'MEDIUM', parent: "center")
    createStyle(name: "center-2", topBorder:'MEDIUM', bottomBorder:'MEDIUM', parent: "center")

    nextRow()
    9.times {
        nextColumn()
    }

    text("", "center-1")
    nextColumn()
    5.times {
        text("", "center-2")
        nextColumn()
    }
    formula("SUM(${cellIndex(-linesCount, 0)}:${cellIndex(-1, 0)})", "center-1")
    nextColumn()
    text("", "center-2")
}

def collectDebtData(List<BaseProduct> tickets) {
    Map<String, Object[]> debtData = new TreeMap<>()

    tickets.each {
        fillDebtData(it, debtData, "FEE")
        fillDebtData(it, debtData, "TICKET")
    }

    List<Object[]> result = new ArrayList<>(debtData.values())
    result.sort(debtPaymentComparator())

    return result
}

def fillDebtData(BaseProduct product, Map<String, Object[]> debtData, String type) {
    String productKey = productKey(product)
    if (!productKey) {
        return
    }

    BigDecimal debtAmount = debtAmount(product, type)

    if (type == "TICKET") {
        debtAmount = MiscUtil.sum(debtAmount, discount(product))
    }

    if (!MiscUtil.isZero(debtAmount, true)) {
        String key = type + productKey
        if (debtData.containsKey(key)) {
            def debt = debtData.get(key)
            if (!debt[1] || debt[1].after(MiscUtil.setDayEndTime(issueDate(product)))) {
                debt[1] = issueDate(product)
            }
            debt[2] = MiscUtil.sum((BigDecimal) debt[2], debtAmount)
        } else {
            debtData.put(key, [createClientTitle(product), issueDate(product), debtAmount, type] as Object[])
        }
    }
}

def debtAmount(BaseProduct product, String type) {
    BigDecimal result = BigDecimal.ZERO

    Date endDate = parameters['key-report-params']?.periodEnd
    def fops = collectFops(product)
    if (fops.empty || !endDate) {
        return result
    }

    fops.each {
        if (!it.operationDate || it.operationDate.after(endDate)) {
            if (type == "FEE" && GeneralProductHelper.isServiceFop(it)) {
                return
            }
            if (type == "TICKET" && !GeneralProductHelper.isServiceFop(it)) {
                return
            }

            result = MiscUtil.sum(result, it.equivalentAmount)
        }
    }

    if (!isAirMco(product)) {
        if (isRefund(product)) {
            result = MiscUtil.negate(result)
        }
    }

    return result
}

def printPayment(List<BaseProduct> tickets) {
    rowHeight(15)
    int startRowIndex = getRowIndex()

    printPaymentHeader()
    int linesCount = printPaymentTable(tickets)
    printPaymentTotal(linesCount)

    int paymentTotalRowIndex = getRowIndex()

    moveTo(startRowIndex, 0)

    return paymentTotalRowIndex
}

def printPaymentHeader() {
    createStyle(name: "center-border-1", leftBorder:'MEDIUM', rightBorder:'MEDIUM', topBorder:'MEDIUM', bottomBorder:'MEDIUM', fontBold : true, parent: "center-border")
    createStyle(name: "center-border-2", leftBorder:'MEDIUM', rightBorder:'MEDIUM', topBorder:'MEDIUM', bottomBorder:'MEDIUM', parent: "center-border")

    def nextCellRow = {
        nextRow()
        19.times {
            nextColumn()
        }
    }

    nextCellRow(); text("Расчеты", "center-border-1", 7, 1)
    7.times {
        nextColumn()
    }

    nextCellRow(); text("Организация", "center-border-2", 6, 1)
    6.times {
        nextColumn()
    }
    text("Сумма", "center-border-1")
}

def printPaymentTable(List<BaseProduct> tickets) {
    createStyle(name: "center-border-1", leftBorder:'MEDIUM', parent: "center-border")
    createStyle(name: "center-border-2", rightBorder:'MEDIUM', parent: "center-border")

    def printPaymentLine = { Object[] paymentData ->
        nextRow()
        19.times {
            nextColumn()
        }

        String title = "FEE" != paymentData[3] ?
                TextUtil.nonNullStr((String) paymentData[0]) :
                "Сбор " + paymentData[0]

        text(title, "center-border-1", 6, 1)
        6.times {
            nextColumn()
        }

        number(paymentData[2], "center-border-2")
        nextColumn()
    }
    def printEmptyRow = {
        printPaymentLine(new Object[4])
    }

    int linesCount = 0

    def paymentData = collectPaymentData(tickets)
    paymentData.each {
        printPaymentLine(it)
        linesCount++
    }

    int minPaymentLines = 14
    if (paymentData.size() < minPaymentLines) {
        (minPaymentLines - paymentData.size()).times {
            printEmptyRow()
            linesCount++
        }
    }

    return linesCount
}

def printPaymentTotal(int linesCount) {
    createStyle(name: "center-1", topBorder:'MEDIUM', parent: "center")
    createStyle(name: "center-2", topBorder:'MEDIUM', bottomBorder:'MEDIUM', parent: "center")

    nextRow()
    19.times {
        nextColumn()
    }

    6.times {
        text("", "center-1")
        nextColumn()
    }

    formula("SUM(${cellIndex(-linesCount, 0)}:${cellIndex(-1, 0)})", "center-2")
}

def collectPaymentData(List<BaseProduct> tickets) {
    Map<String, Object[]> paymentData = new TreeMap<>()

    tickets.each {
        fillPaymentData(it, paymentData, "FEE")
        fillPaymentData(it, paymentData, "TICKET")
    }

    List<Object[]> result = new ArrayList<>(paymentData.values())
    result.sort(debtPaymentComparator())

    return result
}

def fillPaymentData(BaseProduct product, Map<String, Object[]> paymentData, String type) {
    String productKey = productKey(product)
    if (!productKey || !issuedBefore(product)) {
        return
    }

    BigDecimal paymentAmount = paymentAmount(product, type)

    if (!MiscUtil.isZero(paymentAmount, true)) {
        String key = type + productKey
        if (paymentData.containsKey(key)) {
            def payment = paymentData.get(key)
            if (!payment[1] || payment[1].after(MiscUtil.setDayEndTime(issueDate(product)))) {
                payment[1] = issueDate(product)
            }
            payment[2] = MiscUtil.sum((BigDecimal) payment[2], paymentAmount)
        } else {
            paymentData.put(key, [createClientTitle(product), issueDate(product), paymentAmount, type] as Object[])
        }
    }
}

def paymentAmount(BaseProduct product, String type) {
    BigDecimal result = BigDecimal.ZERO

    Date startDate = parameters['key-report-params']?.periodBegin
    Date endDate = parameters['key-report-params']?.periodEnd
    def fops = collectFops(product)
    if (fops.empty || !startDate || !endDate) {
        return result
    }

    fops.each {
        if (it.type != PaymentType.CASH
                && it.type != PaymentType.CREDIT_CARD
                && it.type != PaymentType.CREDIT_CARD_AGENCY) {
            return
        }
        if (!MiscUtil.isBetween(startDate, endDate, it.operationDate)) {
            return
        }
        if (type == "FEE" && GeneralProductHelper.isServiceFop(it)) {
            return
        }
        if (type == "TICKET" && !GeneralProductHelper.isServiceFop(it)) {
            return
        }

        result = MiscUtil.sum(result, it.equivalentAmount)
    }

    if (!isAirMco(product)) {
        if (isRefund(product)) {
            result = MiscUtil.negate(result)
        } else if (isExchange(product)) {
            result = MiscUtil.sub(result, paymentAmount(previousProduct(product), type))
        }
    }

    return result
}

def debtPaymentComparator() {
    def typeToInt = { Object[] o ->
        if (((String) o[3]) == "TICKET") {
            return 0
        } else if (((String) o[3]) == "FEE") {
            return 1
        } else {
            return 2
        }
    }

    return new Comparator<Object[]>() {
        int compare(final Object[] o1, final Object[] o2) {
            // Title
            int res = TextUtil.compare((String) o1[0], (String) o2[0], false, false)
            if (res != 0) {
                return res
            }

            // Date
            res = MiscUtil.compare((Date) o1[1], (Date) o2[1], false)
            if (res != 0) {
                return res
            }

            // Type
            res = Integer.compare(typeToInt(o1), typeToInt(o2))
            if (res != 0) {
                return res
            }

            // Amount
            return MiscUtil.compare((BigDecimal) o1[2], (BigDecimal) o1[2], false)
        }
    }
}

def createClientTitle(BaseProduct product) {
    return ("ФЛ" == client(product)) ?
            (client(product) + "(" + lastName(product) + ")") :
            client(product)
}

def issueDate(BaseProduct product) {
    return MiscUtil.clearTime(GeneralProductHelper.getHandler(product).findIssueDate(product))
}

def productKey(BaseProduct product) {
    String clientTitle = createClientTitle(product)
    String status = GeneralProductHelper.getStatus(product)?.name()
    String date = issueDate(product) ? new SimpleDateFormat("ddMMyy").format(issueDate(product)) : ""

    return clientTitle ? (clientTitle + status + date) : null
}

def printTotal(int paymentTotalRowIndex) {
    rowHeight(15)
    int startRowIndex = getRowIndex()
    int tableTotalLineNumber = startRowIndex
    int paymentTotalRowNumber = paymentTotalRowIndex + 1

    printTotalLabels()

    moveTo(startRowIndex, 0)

    printTotalValues(tableTotalLineNumber, paymentTotalRowNumber)
}

def printTotalLabels() {
    createStyle(name: "left-border-top", topBorder:'MEDIUM', parent: "left-border")
    createStyle(name: "left-border-bottom", bottomBorder: 'MEDIUM', parent: "left-border")

    nextRow(); text("Сальдо на начало дня", "left-border-top", 3, 1)
    nextRow(); text("остаток на начало дня", "left-border", 3, 1)
    nextRow(); text("Продано за наличный расчет", "left-border", 3, 1)
    nextRow(); text("Кредит долг", "left-border", 3, 1)
    nextRow(); text("Отдано кредиткой", "left-border", 3, 1)
    nextRow(); text("Сдано в кассу", "left-border", 3, 1)
    nextRow(); text("Возврат", "left-border", 3, 1)
    nextRow(); text("Возврат кред", "left-border", 3, 1)
    nextRow(); text("", "left-border", 3, 1)
    nextRow(); text("Инкассация", "left-border", 3, 1)
    nextRow(); text("", "left-border", 3, 1)
    nextRow(); text("Остаток на конец дня", "left-border", 3, 1)
    nextRow(); text("Чек  ККМ", "left-border", 3, 1)
    nextRow(); text("", "left-border", 3, 1)
    nextRow(); text("", "left-border", 3, 1)
    nextRow(); text("Сальдо на конец дня", "left-border-bottom", 3, 1)
}

def printTotalValues(int tableTotalLineNumber, int paymentTotalRowNumber) {
    def totalValueCellName = { int numberOfTotalValueRow ->
        return "D" + (tableTotalLineNumber + 1 + numberOfTotalValueRow)
    }

    createStyle(name: "left-bold-border-top", topBorder:'MEDIUM', parent: "left-bold-border")
    createStyle(name: "right-border-number-total", format: '0.00', parent: "right-border-number")
    createStyle(name: "right-border-number-red", fontColor: "RED", parent: "right-border-number-total")
    createStyle(name: "right-border-number-red-bottom", bottomBorder:'MEDIUM', parent: "right-border-number-red")

    def nextCellRow = {
        nextRow()
        3.times {
            nextColumn()
        }
    }
    def printEmpty = {
        text("", "left-bold-border", 4, 1)
    }

    String checkKKMFormula = "V" + tableTotalLineNumber + "+" + "Z" + paymentTotalRowNumber
    String collectionFormula = totalValueCellName(5) + "+" +
            totalValueCellName(6) + "+" + totalValueCellName(10)

    nextCellRow(); text("", "left-bold-border-top", 4, 1)
    nextCellRow(); printEmpty()
    nextCellRow(); formula("W" + tableTotalLineNumber, "right-border-number-total", 4, 1)
    nextCellRow(); printEmpty()
    nextCellRow(); formula("U" + tableTotalLineNumber, "right-border-number-total", 4, 1)
    nextCellRow(); printEmpty()
    nextCellRow(); formula("X" + tableTotalLineNumber, "right-border-number-total", 4, 1)
    nextCellRow(); printEmpty()
    nextCellRow(); printEmpty()
    nextCellRow(); printEmpty()
    nextCellRow(); printEmpty()
    nextCellRow(); formula(totalValueCellName(2) + "+" + checkKKMFormula +
            "-(" + collectionFormula + ")", "right-border-number-red", 4, 1)
    nextCellRow(); formula(checkKKMFormula, "right-border-number-red", 4, 1)
    nextCellRow(); printEmpty()
    nextCellRow(); printEmpty()
    nextCellRow(); formula(totalValueCellName(1) + "+" + totalValueCellName(3) + "+" +
            totalValueCellName(5) + "+" + totalValueCellName(7) +
            "-V" + tableTotalLineNumber + "-Z" + paymentTotalRowNumber, "right-border-number-red-bottom", 4, 1)
}

def printAgent() {
    EntityReference<Person> agentRef = (EntityReference<Person>) parameters['agent']
    EntityContainer<Person> agentCont = EntityStorage.get().resolve(agentRef)

    String agentName =
            agentCont ? PersonalLocalizableNameUtil.buildFullName(
                    agentCont.entity, true, LocaleHelper.RU_LOCALE, false) : "___________"

    4.times {
        nextRow(); rowHeight(15)
    }
    2.times {
        nextColumn()
    }
    text("Оператор " + agentName, "left", 3, 1)
}

def setAutoWidth() {
    nextRow()
    rowHeight(15)

    28.times {
        columnAutoWidth(true)
        nextColumn()
    }
}

page{"Реестр"} {
    List<BaseProduct> tickets = collectSortedTickets()

    printHead()
    printRegistry(tickets)
    printDebt(tickets)
    int paymentTotalRowIndex = printPayment(tickets)
    printTotal(paymentTotalRowIndex)
    printAgent()
    setAutoWidth()
}