Skip to content

Commit

Permalink
VAT management on invoices. Closes #190.
Browse files Browse the repository at this point in the history
  • Loading branch information
fgaudin committed May 10, 2011
1 parent 1327e99 commit d2bce56
Show file tree
Hide file tree
Showing 13 changed files with 700 additions and 191 deletions.
102 changes: 76 additions & 26 deletions accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,16 +244,19 @@ def invoice_footer(canvas, doc):
canvas.saveState()
canvas.setFont('Times-Roman', 10)
PAGE_WIDTH = defaultPageSize[0]
footer_text = "%s %s - SIRET : %s - %s, %s %s" % (user.first_name,
user.last_name,
user.get_profile().company_id,
user.get_profile().address.street.replace("\n", ", ").replace("\r", ""),
user.get_profile().address.zipcode,
user.get_profile().address.city)
footer_text = "%s %s - %s, %s %s" % (user.first_name,
user.last_name,
user.get_profile().address.street.replace("\n", ", ").replace("\r", ""),
user.get_profile().address.zipcode,
user.get_profile().address.city)
if user.get_profile().address.country:
footer_text = footer_text + u", %s" % (user.get_profile().address.country)

canvas.drawCentredString(PAGE_WIDTH / 2.0, 0.5 * inch, footer_text)
extra_info = u"SIRET : %s" % (user.get_profile().company_id)
if user.get_profile().vat_number:
extra_info = u"%s - N° TVA : %s" % (extra_info, user.get_profile().vat_number)
canvas.drawCentredString(PAGE_WIDTH / 2.0, 0.35 * inch, extra_info)
canvas.restoreState()

filename = ugettext('invoice_%(invoice_id)d.pdf') % {'invoice_id': self.invoice_id}
Expand Down Expand Up @@ -379,10 +382,14 @@ def invoice_footer(canvas, doc):
story.append(spacer3)

# invoice row list
data = [[ugettext('Label'), ugettext('Quantity'), ugettext('Unit price'), ugettext('Total')]]
rows = self.invoice_rows.all()
extra_rows = 0
label_width = 4.5 * inch
data = [[ugettext('Label'), ugettext('Quantity'), ugettext('Unit price'), ugettext('Total excl tax')]]
if user.get_profile().vat_number:
data[0].append(ugettext('VAT'))
label_width = 4.0 * inch
else:
label_width = 4.5 * inch
for row in rows:
label = row.label
if row.proposal.reference:
Expand All @@ -397,10 +404,17 @@ def invoice_footer(canvas, doc):
unit_price = unit_price.quantize(Decimal(1)) if unit_price == unit_price.to_integral() else unit_price.normalize()
total = row.quantity * row.unit_price
total = total.quantize(Decimal(1)) if total == total.to_integral() else total.normalize()
data.append([label, localize(quantity), "%s %s" % (localize(unit_price), "€".decode('utf-8')), "%s %s" % (localize(total), "€".decode('utf-8'))])

data_row = [label, localize(quantity), "%s %s" % (localize(unit_price), "€".decode('utf-8')), "%s %s" % (localize(total), "€".decode('utf-8'))]
if user.get_profile().vat_number:
data_row.append("%s%%" % (localize(row.vat_rate)))
data.append(data_row)
for extra_row in splitted_para.lines[1:]:
label = " ".join(extra_row[1])
data.append([label, '', '', ''])
if user.get_profile().vat_number:
data.append([label, '', '', '', ''])
else:
data.append([label, '', '', ''])
extra_rows = extra_rows + 1

row_count = len(rows) + extra_rows
Expand All @@ -415,19 +429,29 @@ def invoice_footer(canvas, doc):
max_row_count = max_row_count + normal_page_count

for i in range(max_row_count - row_count):
data.append(['', '', '', ''])

row_table = Table(data, [4.7 * inch, 0.8 * inch, 0.9 * inch, 0.8 * inch], (max_row_count + 1) * [0.3 * inch])
row_table.setStyle(TableStyle([('ALIGN', (0, 0), (-1, 0), 'CENTER'),
('ALIGN', (1, 0), (-1, -1), 'CENTER'),
('FONT', (0, 0), (-1, 0), 'Times-Bold'),
('BOX', (0, 0), (-1, 0), 0.25, colors.black),
('INNERGRID', (0, 0), (-1, 0), 0.25, colors.black),
('BOX', (0, 1), (0, -1), 0.25, colors.black),
('BOX', (1, 1), (1, -1), 0.25, colors.black),
('BOX', (2, 1), (2, -1), 0.25, colors.black),
('BOX', (3, 1), (3, -1), 0.25, colors.black),
]))
if user.get_profile().vat_number:
data.append(['', '', '', '', ''])
else:
data.append(['', '', '', ''])

if user.get_profile().vat_number:
row_table = Table(data, [4.2 * inch, 0.8 * inch, 0.9 * inch, 0.8 * inch, 0.5 * inch], (max_row_count + 1) * [0.3 * inch])
else:
row_table = Table(data, [4.7 * inch, 0.8 * inch, 0.9 * inch, 0.8 * inch], (max_row_count + 1) * [0.3 * inch])

row_style = [('ALIGN', (0, 0), (-1, 0), 'CENTER'),
('ALIGN', (1, 0), (-1, -1), 'CENTER'),
('FONT', (0, 0), (-1, 0), 'Times-Bold'),
('BOX', (0, 0), (-1, 0), 0.25, colors.black),
('INNERGRID', (0, 0), (-1, 0), 0.25, colors.black),
('BOX', (0, 1), (0, -1), 0.25, colors.black),
('BOX', (1, 1), (1, -1), 0.25, colors.black),
('BOX', (2, 1), (2, -1), 0.25, colors.black),
('BOX', (3, 1), (3, -1), 0.25, colors.black)]
if user.get_profile().vat_number:
row_style.append(('BOX', (4, 1), (4, -1), 0.25, colors.black))

row_table.setStyle(TableStyle(row_style))

story.append(row_table)

Expand All @@ -445,11 +469,37 @@ def invoice_footer(canvas, doc):
if self.owner.get_profile().bic:
left_block.append(Paragraph(_("BIC/SWIFT : %s") % (self.owner.get_profile().bic), styleNSmall))

if user.get_profile().vat_number:
right_block = [Paragraph(_("Total excl tax : %(amount)s %(currency)s") % {'amount': localize(invoice_amount), 'currency' : "€".decode('utf-8')}, styleN)]
vat_amounts = {}
for row in rows:
vat_rate = row.vat_rate
vat_amount = row.amount * vat_rate / 100
if vat_rate in vat_amounts:
vat_amounts[vat_rate] = vat_amounts[vat_rate] + vat_amount
else:
vat_amounts[vat_rate] = vat_amount
for vat_rate, vat_amount in vat_amounts.items():
vat_amount = round(vat_amount, 2)
#vat_amount = vat_amount.quantize(Decimal(1)) if vat_amount == vat_amount.to_integral() else vat_amount.normalize()
right_block.append(Paragraph(_("VAT %(vat_rate)s%% : %(vat_amount)s %(currency)s") % {'vat_rate': localize(vat_rate),
'vat_amount': localize(vat_amount),
'currency' : "€".decode('utf-8')},
styleN))

incl_tax_amount = invoice_amount + sum(vat_amounts.values())
#incl_tax_amount = incl_tax_amount.quantize(Decimal(1)) if incl_tax_amount == incl_tax_amount.to_integral() else incl_tax_amount.normalize()
incl_tax_amount = round(incl_tax_amount, 2)
right_block.append(Spacer(1, 0.25 * inch))
right_block.append(Paragraph(_("TOTAL incl tax : %(amount)s %(currency)s") % {'amount': localize(incl_tax_amount), 'currency' : "€".decode('utf-8')}, styleTotal))
else:
right_block = [Paragraph(_("TOTAL excl tax : %(amount)s %(currency)s") % {'amount': localize(invoice_amount), 'currency' : "€".decode('utf-8')}, styleTotal),
Spacer(1, 0.25 * inch),
Paragraph(u"TVA non applicable, art. 293 B du CGI", styleN)]

data = [[left_block,
'',
[Paragraph(_("TOTAL excl. VAT : %(amount)s %(currency)s") % {'amount': localize(invoice_amount), 'currency' : "€".decode('utf-8')}, styleTotal),
Spacer(1, 0.25 * inch),
Paragraph(u"TVA non applicable, art. 293 B du CGI", styleN)]], ]
right_block], ]

if self.execution_begin_date and self.execution_end_date:
data[0][0].insert(1, Paragraph(_("Execution dates : %(begin_date)s to %(end_date)s") % {'begin_date': localize(self.execution_begin_date), 'end_date' : localize(self.execution_end_date)}, styleN))
Expand Down
92 changes: 90 additions & 2 deletions accounts/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
ProposalRow
from accounts.models import INVOICE_STATE_EDITED, Invoice, InvoiceRow, \
INVOICE_STATE_SENT, InvoiceRowAmountError, PAYMENT_TYPE_CHECK, \
PAYMENT_TYPE_CASH, Expense, INVOICE_STATE_PAID
PAYMENT_TYPE_CASH, Expense, INVOICE_STATE_PAID, VAT_RATES_19_6
from contact.models import Country
from autoentrepreneur.models import UserProfile

class ExpensePermissionTest(TestCase):
fixtures = ['test_users']
Expand Down Expand Up @@ -798,7 +799,7 @@ def testDownloadPdf(self):
content = response.content.split("\n")
invariant_content = content[0:66] + content[67:110] + content[111:-1]
self.assertEquals(hashlib.md5("\n".join(invariant_content)).hexdigest(),
"15afee56ba684f4b97e334c386559b86")
"119afaaace168edcf2ddcc82297b4bfe")

def testInvoiceBookDownloadPdf(self):
"""
Expand Down Expand Up @@ -1344,6 +1345,93 @@ def testBug207(self):
response = self.client.get(reverse('invoice_download', kwargs={'id': i.id}))
self.assertEqual(response.status_code, 200)

def testVat(self):
profile = UserProfile.objects.get(user=1)
profile.vat_number = 'FR010123456789123'
profile.save()

i = Invoice.objects.create(customer_id=self.proposal.project.customer_id,
invoice_id=1,
state=INVOICE_STATE_EDITED,
amount='2',
edition_date=datetime.date(2010, 8, 31),
payment_date=datetime.date(2010, 9, 30),
paid_date=None,
payment_type=PAYMENT_TYPE_CHECK,
execution_begin_date=datetime.date(2010, 8, 1),
execution_end_date=datetime.date(2010, 8, 7),
penalty_date=datetime.date(2010, 10, 8),
penalty_rate='1.5',
discount_conditions='Nothing',
owner_id=1)

i_row = InvoiceRow.objects.create(proposal_id=self.proposal.id,
invoice_id=i.id,
label='Day of work',
category=ROW_CATEGORY_SERVICE,
quantity=1,
unit_price='1',
balance_payments=False,
vat_rate=VAT_RATES_19_6,
owner_id=1)
i_row = InvoiceRow.objects.create(proposal_id=self.proposal.id,
invoice_id=i.id,
label='Day of work',
category=ROW_CATEGORY_SERVICE,
quantity=1,
unit_price='1',
balance_payments=False,
vat_rate=VAT_RATES_19_6,
owner_id=1)
response = self.client.get(reverse('invoice_detail', kwargs={'id': i.id}))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['invoice'].get_vat(), Decimal('0.392'))
self.assertEqual(response.context['invoice'].amount_including_tax(), Decimal('2.392'))

def testDownloadPdfWithVat(self):
"""
Tests non-regression on pdf
"""
profile = User.objects.get(pk=1).get_profile()
profile.iban_bban = 'FR76 1234 1234 1234 1234 1234 123'
profile.bic = 'CCBPFRABCDE'
profile.vat_number = 'FR010123456789123'
profile.save()
i = Invoice.objects.create(customer_id=self.proposal.project.customer_id,
invoice_id=1,
state=INVOICE_STATE_EDITED,
amount='1000',
edition_date=datetime.date(2010, 8, 31),
payment_date=datetime.date(2010, 9, 30),
paid_date=None,
payment_type=PAYMENT_TYPE_CHECK,
execution_begin_date=datetime.date(2010, 8, 1),
execution_end_date=datetime.date(2010, 8, 7),
penalty_date=datetime.date(2010, 10, 8),
penalty_rate='1.5',
discount_conditions='Nothing',
owner_id=1)

i_row = InvoiceRow.objects.create(proposal_id=self.proposal.id,
invoice_id=i.id,
label='Day of work',
category=ROW_CATEGORY_SERVICE,
quantity=10,
unit_price='100',
balance_payments=False,
vat_rate=VAT_RATES_19_6,
owner_id=1)

response = self.client.get(reverse('invoice_download', kwargs={'id': i.id}))
self.assertEqual(response.status_code, 200)
f = open('/tmp/invoiceVat.pdf', 'w')
f.write(response.content)
f.close()
content = response.content.split("\n")
invariant_content = content[0:66] + content[67:110] + content[111:-1]
self.assertEquals(hashlib.md5("\n".join(invariant_content)).hexdigest(),
"142188046b08a8c9f39868815fa7bd11")

class InvoiceBug106Test(TransactionTestCase):
fixtures = ['test_users', 'test_contacts', 'test_projects']

Expand Down
Loading

0 comments on commit d2bce56

Please sign in to comment.