Skip to content

Commit

Permalink
Show VAT as included on invoices (#681)
Browse files Browse the repository at this point in the history
* Added explicit cast to avoid type mismatch error

* Show correct ticket price on invoices #661

This commit fixed the issue with invoices with discounts. The invoice now shows the original ticket price instead of the already discounted price to be consistent with the reservation summary and receipts.

* Show VAT as included on invoices #661

Invoices show VAT (in case VAT should be shown), but when the amount already includes it, the invoices don’t mention it. This commit makes invoices differentiate between included and not included VAT when displaying it.

* Show VAT as included on invoices #661

Invoices show VAT (in case VAT should be shown), but when the amount already includes it, the invoices don’t mention it. This commit makes invoices differentiate between included and not included VAT when displaying it.

* Fixed summary price calculation

* Added license to SummaryPriceContainerTest
  • Loading branch information
salmar authored and cbellone committed Sep 15, 2019
1 parent b1a7a66 commit 46fcbbb
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 55 deletions.
201 changes: 151 additions & 50 deletions src/main/java/alfio/manager/TicketReservationManager.java
Expand Up @@ -16,21 +16,140 @@
*/
package alfio.manager;

import static alfio.model.Audit.EntityType.RESERVATION;
import static alfio.model.Audit.EventType.EXTERNAL_INVOICE_NUMBER;
import static alfio.model.Audit.EventType.MATCHING_PAYMENT_FOUND;
import static alfio.model.BillingDocument.Type.CREDIT_NOTE;
import static alfio.model.BillingDocument.Type.INVOICE;
import static alfio.model.BillingDocument.Type.RECEIPT;
import static alfio.model.PromoCodeDiscount.categoriesOrNull;
import static alfio.model.TicketReservation.TicketReservationStatus.CANCELLED;
import static alfio.model.TicketReservation.TicketReservationStatus.EXTERNAL_PROCESSING_PAYMENT;
import static alfio.model.TicketReservation.TicketReservationStatus.IN_PAYMENT;
import static alfio.model.TicketReservation.TicketReservationStatus.OFFLINE_PAYMENT;
import static alfio.model.TicketReservation.TicketReservationStatus.PENDING;
import static alfio.model.TicketReservation.TicketReservationStatus.WAITING_EXTERNAL_CONFIRMATION;
import static alfio.model.system.ConfigurationKeys.ALLOW_FREE_TICKETS_CANCELLATION;
import static alfio.model.system.ConfigurationKeys.ASSIGNMENT_REMINDER_INTERVAL;
import static alfio.model.system.ConfigurationKeys.ASSIGNMENT_REMINDER_START;
import static alfio.model.system.ConfigurationKeys.AUTOMATIC_REMOVAL_EXPIRED_OFFLINE_PAYMENT;
import static alfio.model.system.ConfigurationKeys.BANK_ACCOUNT_NR;
import static alfio.model.system.ConfigurationKeys.BANK_ACCOUNT_OWNER;
import static alfio.model.system.ConfigurationKeys.BASE_URL;
import static alfio.model.system.ConfigurationKeys.ENABLE_TICKET_TRANSFER;
import static alfio.model.system.ConfigurationKeys.INVOICE_ADDRESS;
import static alfio.model.system.ConfigurationKeys.MAX_AMOUNT_OF_TICKETS_BY_RESERVATION;
import static alfio.model.system.ConfigurationKeys.NOTIFY_ALL_FAILED_PAYMENT_ATTEMPTS;
import static alfio.model.system.ConfigurationKeys.OFFLINE_REMINDER_HOURS;
import static alfio.model.system.ConfigurationKeys.OPTIONAL_DATA_REMINDER_ENABLED;
import static alfio.model.system.ConfigurationKeys.RESERVATION_MIN_TIMEOUT_AFTER_FAILED_PAYMENT;
import static alfio.model.system.ConfigurationKeys.RESERVATION_TIMEOUT;
import static alfio.model.system.ConfigurationKeys.SEND_TICKETS_AUTOMATICALLY;
import static alfio.model.system.ConfigurationKeys.VAT_NR;
import static alfio.util.MonetaryUtil.formatCents;
import static alfio.util.MonetaryUtil.unitToCents;
import static alfio.util.Wrappers.optionally;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.equalsIgnoreCase;
import static org.apache.commons.lang3.StringUtils.stripAll;
import static org.apache.commons.lang3.StringUtils.stripToEmpty;
import static org.apache.commons.lang3.StringUtils.stripToNull;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.apache.commons.lang3.StringUtils.trimToNull;
import static org.apache.commons.lang3.time.DateUtils.addHours;
import static org.apache.commons.lang3.time.DateUtils.truncate;

import java.math.BigDecimal;
import java.time.Clock;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;

import alfio.controller.api.support.TicketHelper;
import alfio.controller.form.UpdateTicketOwnerForm;
import alfio.manager.i18n.MessageSourceManager;
import alfio.manager.payment.BankTransferManager;
import alfio.manager.payment.PaymentSpecification;
import alfio.manager.support.*;
import alfio.manager.support.CategoryEvaluator;
import alfio.manager.support.FeeCalculator;
import alfio.manager.support.PartialTicketTextGenerator;
import alfio.manager.support.PaymentResult;
import alfio.manager.support.PaymentWebhookResult;
import alfio.manager.system.ConfigurationLevel;
import alfio.manager.system.ConfigurationManager;
import alfio.manager.system.Mailer;
import alfio.model.*;
import alfio.model.AdditionalService;
import alfio.model.AdditionalServiceItem;
import alfio.model.AdditionalServiceItem.AdditionalServiceItemStatus;
import alfio.model.AdditionalServiceText;
import alfio.model.Audit;
import alfio.model.BillingDocument;
import alfio.model.CustomerName;
import alfio.model.Event;
import alfio.model.EventAndOrganizationId;
import alfio.model.OrderSummary;
import alfio.model.PriceContainer;
import alfio.model.PromoCodeDiscount;
import alfio.model.PromoCodeDiscount.DiscountType;
import alfio.model.ReservationIdAndEventId;
import alfio.model.SpecialPrice;
import alfio.model.SpecialPrice.Status;
import alfio.model.SummaryPriceContainer;
import alfio.model.SummaryRow;
import alfio.model.Ticket;
import alfio.model.Ticket.TicketStatus;
import alfio.model.TicketCategory;
import alfio.model.TicketFieldValue;
import alfio.model.TicketReservation;
import alfio.model.TicketReservation.TicketReservationStatus;
import alfio.model.TicketReservationInfo;
import alfio.model.TicketReservationInvoicingAdditionalInfo;
import alfio.model.TicketReservationWithTransaction;
import alfio.model.TicketWithCategory;
import alfio.model.TotalPrice;
import alfio.model.decorator.AdditionalServiceItemPriceContainer;
import alfio.model.decorator.AdditionalServicePriceContainer;
import alfio.model.decorator.TicketPriceContainer;
Expand All @@ -41,63 +160,45 @@
import alfio.model.result.ErrorCode;
import alfio.model.result.Result;
import alfio.model.system.ConfigurationKeys;
import alfio.model.transaction.*;
import alfio.model.transaction.PaymentContext;
import alfio.model.transaction.PaymentMethod;
import alfio.model.transaction.PaymentProxy;
import alfio.model.transaction.Transaction;
import alfio.model.transaction.TransactionInitializationToken;
import alfio.model.transaction.capabilities.OfflineProcessor;
import alfio.model.transaction.capabilities.ServerInitiatedTransaction;
import alfio.model.transaction.capabilities.SignedWebhookHandler;
import alfio.model.user.Organization;
import alfio.model.user.Role;
import alfio.repository.*;
import alfio.repository.AdditionalServiceItemRepository;
import alfio.repository.AdditionalServiceRepository;
import alfio.repository.AdditionalServiceTextRepository;
import alfio.repository.AuditingRepository;
import alfio.repository.BillingDocumentRepository;
import alfio.repository.EventRepository;
import alfio.repository.InvoiceSequencesRepository;
import alfio.repository.PromoCodeDiscountRepository;
import alfio.repository.SpecialPriceRepository;
import alfio.repository.TicketCategoryDescriptionRepository;
import alfio.repository.TicketCategoryRepository;
import alfio.repository.TicketFieldRepository;
import alfio.repository.TicketRepository;
import alfio.repository.TicketReservationRepository;
import alfio.repository.TicketSearchRepository;
import alfio.repository.TransactionRepository;
import alfio.repository.user.OrganizationRepository;
import alfio.repository.user.UserRepository;
import alfio.util.*;
import alfio.util.ErrorsCode;
import alfio.util.EventUtil;
import alfio.util.Json;
import alfio.util.LocaleUtil;
import alfio.util.MonetaryUtil;
import alfio.util.ObjectDiffUtil;
import alfio.util.TemplateManager;
import alfio.util.TemplateResource;
import alfio.util.Wrappers;
import ch.digitalfondue.npjt.AffectedRowCountAndKey;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;

import java.math.BigDecimal;
import java.time.Clock;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static alfio.model.Audit.EntityType.RESERVATION;
import static alfio.model.Audit.EventType.EXTERNAL_INVOICE_NUMBER;
import static alfio.model.Audit.EventType.MATCHING_PAYMENT_FOUND;
import static alfio.model.BillingDocument.Type.*;
import static alfio.model.PromoCodeDiscount.categoriesOrNull;
import static alfio.model.TicketReservation.TicketReservationStatus.*;
import static alfio.model.system.ConfigurationKeys.*;
import static alfio.util.MonetaryUtil.formatCents;
import static alfio.util.MonetaryUtil.unitToCents;
import static alfio.util.Wrappers.optionally;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static java.util.stream.Collectors.*;
import static org.apache.commons.lang3.StringUtils.*;
import static org.apache.commons.lang3.time.DateUtils.addHours;
import static org.apache.commons.lang3.time.DateUtils.truncate;

@Component
@Transactional
Expand Down
10 changes: 8 additions & 2 deletions src/main/java/alfio/model/SummaryPriceContainer.java
Expand Up @@ -34,9 +34,15 @@ static int getSummaryPriceBeforeVatCts(List<? extends SummaryPriceContainer> ele
if(vatStatus == PriceContainer.VatStatus.NOT_INCLUDED_EXEMPT) {
return MonetaryUtil.centsToUnit(item.getSrcPriceCts(), currencyCode);
} else if(vatStatus == PriceContainer.VatStatus.INCLUDED_EXEMPT) {
return MonetaryUtil.centsToUnit(item.getSrcPriceCts(), currencyCode).add(vatStatus.extractRawVAT(centsToUnit(item.getSrcPriceCts(), item.getCurrencyCode()), item.getVatPercentageOrZero()));
var rawVat = vatStatus.extractRawVAT(centsToUnit(item.getSrcPriceCts(), item.getCurrencyCode()), item.getVatPercentageOrZero());
return MonetaryUtil.centsToUnit(item.getSrcPriceCts(), currencyCode).add(rawVat);
} else if(vatStatus == PriceContainer.VatStatus.INCLUDED) {
var rawVat = vatStatus.extractRawVAT(centsToUnit(item.getSrcPriceCts(), item.getCurrencyCode()), item.getVatPercentageOrZero());
return MonetaryUtil.centsToUnit(item.getSrcPriceCts(), currencyCode).subtract(rawVat);
} else {
return MonetaryUtil.centsToUnit(item.getSrcPriceCts(), currencyCode);
}
return MonetaryUtil.centsToUnit(item.getFinalPriceCts(), currencyCode).subtract(item.getRawVAT());

}).reduce(BigDecimal::add).map(p -> MonetaryUtil.unitToCents(p, currencyCode)).orElse(0);
}
}
6 changes: 3 additions & 3 deletions src/main/resources/alfio/templates/invoice.ms
Expand Up @@ -161,16 +161,16 @@
<tr>
<td class="text-center">{{amount}}</td>
<td>{{name}}</td>
<td class="text-right">{{price}}</td>
<td class="text-right">{{subTotal}}</td>
<td class="text-right">{{priceBeforeVat}}</td>
<td class="text-right">{{subTotalBeforeVat}}</td>
</tr>
{{/orderSummary.summary}}
</tbody>
<tfoot>
{{^orderSummary.free}}
{{#orderSummary.displayVat}}
<tr><th colspan="3" class="no-padding">{{#i18n}}reservation-page.vat [{{ticketReservation.usedVatPercent}}] [{{vatTranslation}}]{{/i18n}}</th><th class="text-right">{{orderSummary.totalVAT}}</th></tr>
{{/orderSummary.displayVat}}
{{/orderSummary.displayVat}}
{{^orderSummary.displayVat}}
<tr><th colspan="4" class="no-padding">{{#i18n}}invoice.vat-voided [{{vatTranslation}}]{{/i18n}}</th></tr>
{{/orderSummary.displayVat}}
Expand Down
78 changes: 78 additions & 0 deletions src/test/java/alfio/model/SummaryPriceContainerTest.java
@@ -0,0 +1,78 @@
/**
* This file is part of alf.io.
*
* alf.io is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* alf.io is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with alf.io. If not, see <http://www.gnu.org/licenses/>.
*/
package alfio.model;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.junit.jupiter.api.Test;

import alfio.model.decorator.TicketPriceContainer;

public class SummaryPriceContainerTest {

@Test
public void testSummaryPriceBeforeVatIncluded() {
List<TicketPriceContainer> items = IntStream.range(0, 10)
.mapToObj(i -> vatIncludedPriceContainer()).collect(Collectors.toList());

int summaryPrice = SummaryPriceContainer.getSummaryPriceBeforeVatCts(items);

assertThat(summaryPrice, is(equalTo(8264)));
}

@Test
public void testSummaryPriceBeforeVatExcluded() {
List<TicketPriceContainer> items = IntStream.range(0, 10)
.mapToObj(i -> vatExcludedPriceContainer()).collect(Collectors.toList());

int summaryPrice = SummaryPriceContainer.getSummaryPriceBeforeVatCts(items);

assertThat(summaryPrice, is(equalTo(10000)));
}

private TicketPriceContainer vatIncludedPriceContainer() {
var ticket = createPriceContainer();
when(ticket.getVatStatus()).thenReturn(PriceContainer.VatStatus.INCLUDED);

return ticket;
}

private TicketPriceContainer vatExcludedPriceContainer() {
var ticket = createPriceContainer();
when(ticket.getVatStatus()).thenReturn(PriceContainer.VatStatus.NOT_INCLUDED);

return ticket;
}

private TicketPriceContainer createPriceContainer() {
var ticket = mock(TicketPriceContainer.class);
when(ticket.getCurrencyCode()).thenReturn("EUR");
when(ticket.getSrcPriceCts()).thenReturn(1000);
when(ticket.getVatPercentageOrZero()).thenReturn(new BigDecimal("21.00"));

return ticket;
}
}

0 comments on commit 46fcbbb

Please sign in to comment.