diff --git a/accounts/forms.py b/accounts/forms.py
index e266192..8ad42ef 100644
--- a/accounts/forms.py
+++ b/accounts/forms.py
@@ -75,3 +75,4 @@ def __init__(self, *args, **kwargs):
self.fields['balance_payments'].widget.attrs['class'] = 'balance-payments-field'
self.fields['quantity'].widget.attrs['class'] = 'quantity-field'
self.fields['unit_price'].widget.attrs['class'] = 'unit-price-field'
+ self.fields['detail'].widget.attrs['class'] = 'row-detail'
diff --git a/accounts/migrations/0016_auto__add_field_invoicerow_comment.py b/accounts/migrations/0016_auto__add_field_invoicerow_comment.py
new file mode 100644
index 0000000..e7d1502
--- /dev/null
+++ b/accounts/migrations/0016_auto__add_field_invoicerow_comment.py
@@ -0,0 +1,162 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'InvoiceRow.comment'
+ db.add_column('accounts_invoicerow', 'comment', self.gf('django.db.models.fields.TextField')(null=True, blank=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'InvoiceRow.comment'
+ db.delete_column('accounts_invoicerow', 'comment')
+
+
+ models = {
+ 'accounts.expense': {
+ 'Meta': {'object_name': 'Expense', '_ormbases': ['core.OwnedObject']},
+ 'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '12', 'decimal_places': '2'}),
+ 'date': ('django.db.models.fields.DateField', [], {'db_index': 'True'}),
+ 'description': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'payment_type': ('django.db.models.fields.IntegerField', [], {}),
+ 'reference': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'supplier': ('django.db.models.fields.CharField', [], {'max_length': '70', 'null': 'True', 'blank': 'True'})
+ },
+ 'accounts.invoice': {
+ 'Meta': {'ordering': "['invoice_id']", 'object_name': 'Invoice', '_ormbases': ['core.OwnedObject']},
+ 'amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'}),
+ 'customer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contact.Contact']", 'null': 'True', 'blank': 'True'}),
+ 'discount_conditions': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
+ 'edition_date': ('django.db.models.fields.DateField', [], {'db_index': 'True'}),
+ 'execution_begin_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'execution_end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'invoice_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'paid_date': ('django.db.models.fields.DateField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+ 'payment_date': ('django.db.models.fields.DateField', [], {'db_index': 'True'}),
+ 'payment_type': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'penalty_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'penalty_rate': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '4', 'decimal_places': '2', 'blank': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'})
+ },
+ 'accounts.invoicerow': {
+ 'Meta': {'ordering': "['id']", 'object_name': 'InvoiceRow'},
+ 'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'}),
+ 'balance_payments': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'category': ('django.db.models.fields.IntegerField', [], {}),
+ 'comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'invoice': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invoice_rows'", 'to': "orm['accounts.Invoice']"}),
+ 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'proposal': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'invoice_rows'", 'null': 'True', 'to': "orm['project.Proposal']"}),
+ 'quantity': ('django.db.models.fields.DecimalField', [], {'max_digits': '5', 'decimal_places': '1'}),
+ 'unit_price': ('django.db.models.fields.DecimalField', [], {'max_digits': '12', 'decimal_places': '2'}),
+ 'vat_rate': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '4', 'decimal_places': '1', 'blank': 'True'})
+ },
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contact.address': {
+ 'Meta': {'object_name': 'Address', '_ormbases': ['core.OwnedObject']},
+ 'city': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'country': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contact.Country']", 'null': 'True', 'blank': 'True'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'street': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'zipcode': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': 'True'})
+ },
+ 'contact.contact': {
+ 'Meta': {'object_name': 'Contact', '_ormbases': ['core.OwnedObject']},
+ 'address': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contact.Address']"}),
+ 'comment': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'company_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'blank': 'True'}),
+ 'contact_type': ('django.db.models.fields.IntegerField', [], {}),
+ 'contacts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'contacts_rel_+'", 'null': 'True', 'to': "orm['contact.Contact']"}),
+ 'email': ('django.db.models.fields.EmailField', [], {'default': "''", 'max_length': '75', 'blank': 'True'}),
+ 'firstname': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'function': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}),
+ 'legal_form': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'representative': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'representative_function': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'})
+ },
+ 'contact.country': {
+ 'Meta': {'ordering': "['country_name']", 'object_name': 'Country'},
+ 'country_code2': ('django.db.models.fields.CharField', [], {'max_length': '2'}),
+ 'country_code3': ('django.db.models.fields.CharField', [], {'max_length': '3'}),
+ 'country_name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'core.ownedobject': {
+ 'Meta': {'object_name': 'OwnedObject'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'unique': 'True', 'max_length': '36'})
+ },
+ 'project.project': {
+ 'Meta': {'object_name': 'Project', '_ormbases': ['core.OwnedObject']},
+ 'customer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contact.Contact']"}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'})
+ },
+ 'project.proposal': {
+ 'Meta': {'ordering': "['begin_date', 'update_date']", 'object_name': 'Proposal', '_ormbases': ['core.OwnedObject']},
+ 'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'}),
+ 'begin_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'contract_content': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'contract_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
+ 'end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'expiration_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'payment_delay': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'payment_delay_other': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'payment_delay_type_other': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['project.Project']"}),
+ 'reference': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True', 'blank': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'}),
+ 'update_date': ('django.db.models.fields.DateField', [], {})
+ }
+ }
+
+ complete_apps = ['accounts']
diff --git a/accounts/migrations/0017_auto__del_field_invoicerow_comment__add_field_invoicerow_detail.py b/accounts/migrations/0017_auto__del_field_invoicerow_comment__add_field_invoicerow_detail.py
new file mode 100644
index 0000000..b978d8d
--- /dev/null
+++ b/accounts/migrations/0017_auto__del_field_invoicerow_comment__add_field_invoicerow_detail.py
@@ -0,0 +1,168 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Deleting field 'InvoiceRow.comment'
+ db.delete_column('accounts_invoicerow', 'comment')
+
+ # Adding field 'InvoiceRow.detail'
+ db.add_column('accounts_invoicerow', 'detail', self.gf('django.db.models.fields.TextField')(null=True, blank=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Adding field 'InvoiceRow.comment'
+ db.add_column('accounts_invoicerow', 'comment', self.gf('django.db.models.fields.TextField')(null=True, blank=True), keep_default=False)
+
+ # Deleting field 'InvoiceRow.detail'
+ db.delete_column('accounts_invoicerow', 'detail')
+
+
+ models = {
+ 'accounts.expense': {
+ 'Meta': {'object_name': 'Expense', '_ormbases': ['core.OwnedObject']},
+ 'amount': ('django.db.models.fields.DecimalField', [], {'max_digits': '12', 'decimal_places': '2'}),
+ 'date': ('django.db.models.fields.DateField', [], {'db_index': 'True'}),
+ 'description': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'payment_type': ('django.db.models.fields.IntegerField', [], {}),
+ 'reference': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'supplier': ('django.db.models.fields.CharField', [], {'max_length': '70', 'null': 'True', 'blank': 'True'})
+ },
+ 'accounts.invoice': {
+ 'Meta': {'ordering': "['invoice_id']", 'object_name': 'Invoice', '_ormbases': ['core.OwnedObject']},
+ 'amount': ('django.db.models.fields.DecimalField', [], {'default': '0', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'}),
+ 'customer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contact.Contact']", 'null': 'True', 'blank': 'True'}),
+ 'discount_conditions': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
+ 'edition_date': ('django.db.models.fields.DateField', [], {'db_index': 'True'}),
+ 'execution_begin_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'execution_end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'invoice_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'paid_date': ('django.db.models.fields.DateField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
+ 'payment_date': ('django.db.models.fields.DateField', [], {'db_index': 'True'}),
+ 'payment_type': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'penalty_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'penalty_rate': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '4', 'decimal_places': '2', 'blank': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'})
+ },
+ 'accounts.invoicerow': {
+ 'Meta': {'ordering': "['id']", 'object_name': 'InvoiceRow'},
+ 'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'}),
+ 'balance_payments': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'category': ('django.db.models.fields.IntegerField', [], {}),
+ 'detail': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'invoice': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invoice_rows'", 'to': "orm['accounts.Invoice']"}),
+ 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'proposal': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'invoice_rows'", 'null': 'True', 'to': "orm['project.Proposal']"}),
+ 'quantity': ('django.db.models.fields.DecimalField', [], {'max_digits': '5', 'decimal_places': '1'}),
+ 'unit_price': ('django.db.models.fields.DecimalField', [], {'max_digits': '12', 'decimal_places': '2'}),
+ 'vat_rate': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '4', 'decimal_places': '1', 'blank': 'True'})
+ },
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contact.address': {
+ 'Meta': {'object_name': 'Address', '_ormbases': ['core.OwnedObject']},
+ 'city': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'country': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contact.Country']", 'null': 'True', 'blank': 'True'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'street': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'zipcode': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': 'True'})
+ },
+ 'contact.contact': {
+ 'Meta': {'object_name': 'Contact', '_ormbases': ['core.OwnedObject']},
+ 'address': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contact.Address']"}),
+ 'comment': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'company_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'blank': 'True'}),
+ 'contact_type': ('django.db.models.fields.IntegerField', [], {}),
+ 'contacts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'contacts_rel_+'", 'null': 'True', 'to': "orm['contact.Contact']"}),
+ 'email': ('django.db.models.fields.EmailField', [], {'default': "''", 'max_length': '75', 'blank': 'True'}),
+ 'firstname': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'function': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}),
+ 'legal_form': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'representative': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'representative_function': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'})
+ },
+ 'contact.country': {
+ 'Meta': {'ordering': "['country_name']", 'object_name': 'Country'},
+ 'country_code2': ('django.db.models.fields.CharField', [], {'max_length': '2'}),
+ 'country_code3': ('django.db.models.fields.CharField', [], {'max_length': '3'}),
+ 'country_name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'core.ownedobject': {
+ 'Meta': {'object_name': 'OwnedObject'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'unique': 'True', 'max_length': '36'})
+ },
+ 'project.project': {
+ 'Meta': {'object_name': 'Project', '_ormbases': ['core.OwnedObject']},
+ 'customer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contact.Contact']"}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'})
+ },
+ 'project.proposal': {
+ 'Meta': {'ordering': "['begin_date', 'update_date']", 'object_name': 'Proposal', '_ormbases': ['core.OwnedObject']},
+ 'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'}),
+ 'begin_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'contract_content': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'contract_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
+ 'end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'expiration_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'payment_delay': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'payment_delay_other': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'payment_delay_type_other': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['project.Project']"}),
+ 'reference': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True', 'blank': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'}),
+ 'update_date': ('django.db.models.fields.DateField', [], {})
+ }
+ }
+
+ complete_apps = ['accounts']
diff --git a/accounts/models.py b/accounts/models.py
index 0f722d5..45dea2f 100644
--- a/accounts/models.py
+++ b/accounts/models.py
@@ -2,26 +2,20 @@
from decimal import Decimal
from django.utils.formats import localize
import datetime
-from custom_canvas import NumberedCanvas
-from reportlab.platypus import Paragraph, Frame, Spacer, BaseDocTemplate, PageTemplate
-from reportlab.lib.styles import ParagraphStyle
-from reportlab.rl_config import defaultPageSize
+from reportlab.platypus import Paragraph, Spacer
from reportlab.lib.units import inch
-from reportlab.lib.enums import TA_CENTER
-from reportlab.platypus import Table, TableStyle, Image
-from reportlab.lib import colors
+from reportlab.platypus import Table, TableStyle
from django.db import models, connection
from core.models import OwnedObject
from django.utils.translation import ugettext_lazy as _, ugettext
-from contact.models import Contact, CONTACT_TYPE_COMPANY
+from contact.models import Contact
from django.core.urlresolvers import reverse
from project.models import Row, Proposal, update_row_amount, \
ROW_CATEGORY_SERVICE, ROW_CATEGORY, PROPOSAL_STATE_ACCEPTED, ProposalRow
from django.db.models.aggregates import Sum, Min, Max
from django.db.models.signals import post_save, pre_save, post_delete
-from django.conf import settings
from django.core.validators import MaxValueValidator
-from django.db.models.expressions import F
+from accounts.utils.pdf import InvoiceTemplate
PAYMENT_TYPE_CASH = 1
PAYMENT_TYPE_BANK_CARD = 2
@@ -259,279 +253,47 @@ def amount_including_tax(self):
return self.amount + self.get_vat()
def to_pdf(self, user, response):
- def invoice_footer(canvas, doc):
- canvas.saveState()
- canvas.setFont('Times-Roman', 10)
- PAGE_WIDTH = defaultPageSize[0]
- 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}
response['Content-Disposition'] = 'attachment; filename=%s' % (filename)
- doc = BaseDocTemplate(response, title=ugettext('Invoice #%(invoice_id)d') % {'invoice_id': self.invoice_id}, leftMargin=0.5 * inch, rightMargin=0.5 * inch)
- frameT = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height + 0.5 * inch, id='normal')
- doc.addPageTemplates([PageTemplate(id='all', frames=frameT, onPage=invoice_footer), ])
-
- styleH = ParagraphStyle({})
- styleH.fontSize = 14
- styleH.leading = 16
- styleH.borderPadding = (5,) * 4
-
- styleCustomer = ParagraphStyle({})
- styleCustomer.fontSize = 12
- styleCustomer.leading = 14
- styleCustomer.borderPadding = (5,) * 4
-
- styleTotal = ParagraphStyle({})
- styleTotal.fontSize = 14
- styleTotal.leading = 16
- styleTotal.borderColor = colors.black
- styleTotal.borderWidth = 0.5
- styleTotal.borderPadding = (5,) * 4
-
- styleH2 = ParagraphStyle({})
- styleH2.fontSize = 14
- styleH2.leading = 16
-
-
- styleTitle = ParagraphStyle({})
- styleTitle.fontSize = 14
- styleTitle.fontName = "Times-Bold"
-
- styleN = ParagraphStyle({})
- styleN.fontSize = 12
- styleN.leading = 14
-
- styleNSmall = ParagraphStyle({})
- styleNSmall.fontSize = 8
- styleNSmall.leading = 14
-
- styleF = ParagraphStyle({})
- styleF.fontSize = 10
- styleF.alignment = TA_CENTER
-
- styleLabel = ParagraphStyle({})
-
- story = []
-
- data = []
- user_header_content = """
- %s %s
- %s
- %s %s
- %s
- Siret : %s
- """ % (user.first_name,
- user.last_name,
- user.get_profile().address.street.replace("\n", "
"),
- user.get_profile().address.zipcode,
- user.get_profile().address.city,
- user.get_profile().address.country or '',
- user.get_profile().company_id)
-
- customer_header_content = """
-
- %s
- %s
- %s %s
- %s
- """
-
- if user.get_profile().logo_file:
- user_header = Image("%s%s" % (settings.FILE_UPLOAD_DIR, user.get_profile().logo_file))
- else:
- user_header = Paragraph(user_header_content, styleH)
-
- customer_header = customer_header_content % (self.customer.name,
- self.customer.address.street.replace("\n", "
"),
- self.customer.address.zipcode,
- self.customer.address.city,
- self.customer.address.country or '')
-
- customer_header = Paragraph(customer_header, styleCustomer)
-
- data.append([user_header,
- '',
- customer_header])
-
- t1 = Table(data, [3.5 * inch, 0.7 * inch, 3.1 * inch], [1.9 * inch])
- table_style = [('VALIGN', (0, 0), (-1, -1), 'TOP'), ]
- if user.get_profile().logo_file:
- table_style.append(('TOPPADDING', (0, 0), (0, 0), 0))
- table_style.append(('LEFTPADDING', (0, 0), (0, 0), 0))
-
- t1.setStyle(TableStyle(table_style))
-
- story.append(t1)
-
- spacer1 = Spacer(doc.width, 0.25 * inch)
- story.append(spacer1)
+ invoice_template = InvoiceTemplate(response, user)
- data = []
- msg = u"Dispensé d'immatriculation au registre du commerce et des sociétés (RCS) et au répertoire des métiers (RM)"
- data.append([Paragraph(msg, styleN),
- '',
- Paragraph("
" + _("Date : %s") % (localize(self.edition_date)), styleH2)])
+ invoice_template.init_doc(ugettext('Invoice #%(invoice_id)d') % {'invoice_id': self.invoice_id})
+ invoice_template.add_headers(self, self.customer, self.edition_date)
+ invoice_template.add_title(_("INVOICE #%d") % (self.invoice_id))
- t2 = Table(data, [3.5 * inch, 0.3 * inch, 3.5 * inch], [0.7 * inch])
- t2.setStyle(TableStyle([('VALIGN', (0, 0), (-1, -1), 'TOP'), ]))
-
- story.append(t2)
-
- spacer2 = Spacer(doc.width, 0.25 * inch)
- story.append(spacer2)
-
- story.append(Paragraph(_("INVOICE #%d") % (self.invoice_id), styleTitle))
+ # proposal row list
+ rows = self.invoice_rows.all()
+ invoice_template.add_rows(rows)
- spacer3 = Spacer(doc.width, 0.1 * inch)
- story.append(spacer3)
+ # total amount on the right side of footer
+ right_block = invoice_template.get_total_amount(self.amount, rows)
- # invoice row list
- rows = self.invoice_rows.all()
- extra_rows = 0
- 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 and row.proposal.reference:
- label = u"%s - [%s]" % (label, row.proposal.reference)
- para = Paragraph(label, styleLabel)
- para.width = label_width
- splitted_para = para.breakLines(label_width)
- label = " ".join(splitted_para.lines[0][1])
- quantity = row.quantity
- quantity = quantity.quantize(Decimal(1)) if quantity == quantity.to_integral() else quantity.normalize()
- unit_price = row.unit_price
- 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_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:
- if row.vat_rate:
- data_row.append("%s%%" % (localize(row.vat_rate)))
- else:
- data_row.append("-")
- data.append(data_row)
- for extra_row in splitted_para.lines[1:]:
- label = " ".join(extra_row[1])
- if user.get_profile().vat_number:
- data.append([label, '', '', '', ''])
- else:
- data.append([label, '', '', ''])
- extra_rows = extra_rows + 1
-
- row_count = len(rows) + extra_rows
- if row_count <= 16:
- max_row_count = 16
- else:
- first_page_count = 21
- normal_page_count = 33
- last_page_count = 27
- max_row_count = first_page_count + ((row_count - first_page_count) // normal_page_count * normal_page_count) + last_page_count
- if row_count - first_page_count - ((row_count - first_page_count) // normal_page_count * normal_page_count) > last_page_count:
- max_row_count = max_row_count + normal_page_count
-
- for i in range(max_row_count - row_count):
- 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)
-
- spacer4 = Spacer(doc.width, 0.35 * inch)
- story.append(spacer4)
invoice_amount = self.amount
invoice_amount = invoice_amount.quantize(Decimal(1)) if invoice_amount == invoice_amount.to_integral() else invoice_amount.normalize()
- left_block = [Paragraph(_("Payment date : %s") % (localize(self.payment_date)), styleN),
- Paragraph(_("Penalty begins on : %s") % (localize(self.penalty_date) or ''), styleN),
- Paragraph(_("Penalty rate : %s") % (localize(self.penalty_rate) or ''), styleN),
- Paragraph(_("Discount conditions : %s") % (self.discount_conditions or ''), styleN)]
+ left_block = [Paragraph(_("Payment date : %s") % (localize(self.payment_date)), InvoiceTemplate.styleN),
+ Paragraph(_("Penalty begins on : %s") % (localize(self.penalty_date) or ''), InvoiceTemplate.styleN),
+ Paragraph(_("Penalty rate : %s") % (localize(self.penalty_rate) or ''), InvoiceTemplate.styleN),
+ Paragraph(_("Discount conditions : %s") % (self.discount_conditions or ''), InvoiceTemplate.styleN)]
if self.owner.get_profile().iban_bban:
- left_block.append(Spacer(doc.width, 0.2 * inch))
- left_block.append(Paragraph(_("IBAN/BBAN : %s") % (self.owner.get_profile().iban_bban), styleNSmall))
+ left_block.append(Spacer(invoice_template.doc.width, 0.2 * inch))
+ left_block.append(Paragraph(_("IBAN/BBAN : %s") % (self.owner.get_profile().iban_bban), InvoiceTemplate.styleNSmall))
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 or 0
- vat_amount = row.amount * vat_rate / 100
- if vat_rate:
- 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)]
+ left_block.append(Paragraph(_("BIC/SWIFT : %s") % (self.owner.get_profile().bic), InvoiceTemplate.styleNSmall))
data = [[left_block,
'',
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))
+ 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)}, InvoiceTemplate.styleN))
footer_table = Table(data, [4.5 * inch, 0.3 * inch, 2.5 * inch], [1 * inch])
footer_table.setStyle(TableStyle([('VALIGN', (0, 0), (-1, -1), 'TOP'), ]))
- story.append(footer_table)
+ invoice_template.append_to_story(footer_table)
- doc.build(story, canvasmaker=NumberedCanvas)
+ invoice_template.build()
return response
diff --git a/accounts/tests.py b/accounts/tests.py
index ae9860b..b10f0fa 100644
--- a/accounts/tests.py
+++ b/accounts/tests.py
@@ -16,6 +16,7 @@
PAYMENT_TYPE_CASH, Expense, INVOICE_STATE_PAID
from contact.models import Country, Contact
from autoentrepreneur.models import UserProfile
+from django.contrib.webdesign import lorem_ipsum
class ExpensePermissionTest(TestCase):
fixtures = ['test_users']
@@ -411,12 +412,14 @@ def testGetAddFromProposal(self):
category=ROW_CATEGORY_SERVICE,
quantity=12,
unit_price='100',
+ detail="detail 1",
owner_id=1)
p_row2 = ProposalRow.objects.create(proposal_id=self.proposal.id,
label='Discount',
category=ROW_CATEGORY_SERVICE,
quantity=3,
unit_price='150',
+ detail="detail 2",
owner_id=1)
response = self.client.get(reverse('invoice_add_from_proposal', kwargs={'customer_id': self.proposal.project.customer_id,
@@ -429,12 +432,14 @@ def testGetAddFromProposal(self):
'unit_price': Decimal('150.00'),
'label': u'Discount',
'proposal': self.proposal,
+ 'detail': "detail 2",
'quantity': Decimal('3.0')}
expected_row2 = {'category': 1,
'balance_payments': True,
'unit_price': Decimal('100.00'),
'label': u'Day of work',
'proposal': self.proposal,
+ 'detail': "detail 1",
'quantity': Decimal('12.0')}
row1 = response.context['invoicerowformset'].forms[0].initial
@@ -900,7 +905,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(),
- "6451df912dc90e90df3865f8e0ab8dea")
+ "8d6ca232060a96f8ad6855bdce59dc2c")
def testInvoiceBookDownloadPdf(self):
"""
@@ -1573,7 +1578,78 @@ def testDownloadPdfWithVat(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(),
- "0acee8461a531bcb58403f8775f0f041")
+ "5cbe0bd475ed4aa5f46ce91d06f56992")
+
+ def testDownloadPdfWithRowDetail(self):
+ """
+ Tests non-regression on pdf with row detail set
+ """
+ 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()
+ customer_address = self.proposal.project.customer.address
+ customer_address.street = u"128 boulevard des champs élysées\nEspace ZAC du champs de mars\nBP 140"
+ customer_address.zipcode = '75001'
+ customer_address.city = 'Paris CEDEX 1'
+ customer_address.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,
+ detail=lorem_ipsum.COMMON_P,
+ 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,
+ detail=lorem_ipsum.COMMON_P,
+ 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,
+ detail=lorem_ipsum.COMMON_P,
+ owner_id=1)
+
+ response = self.client.get(reverse('invoice_download', kwargs={'id': i.id}))
+ self.assertEqual(response.status_code, 200)
+ f = open('/tmp/invoice_row_detail.pdf', 'w')
+ f.write(response.content)
+ f.close()
+ content = response.content.split("\n")
+ invariant_content = content[0:95] + content[96:152] + content[153:-1]
+ self.assertEquals(hashlib.md5("\n".join(invariant_content)).hexdigest(),
+ "f65bdc5502bf75def1ed0fae9ff1b399")
def testCanCreateInvoiceWithoutProposal(self):
contacts = Contact.objects.filter(name='New customer').count()
diff --git a/accounts/utils/__init__.py b/accounts/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/accounts/utils/pdf.py b/accounts/utils/pdf.py
new file mode 100644
index 0000000..ed74348
--- /dev/null
+++ b/accounts/utils/pdf.py
@@ -0,0 +1,18 @@
+from project.utils.pdf import ProposalTemplate
+from reportlab.lib.styles import ParagraphStyle
+from reportlab.lib.units import inch
+
+class InvoiceTemplate(ProposalTemplate):
+ styleNSmall = ParagraphStyle({})
+ styleNSmall.fontSize = 8
+ styleNSmall.leading = 14
+
+ def __init__(self, response, user):
+ super(InvoiceTemplate, self).__init__(response, user)
+ self.space_before_footer = 0.35 * inch
+
+ def get_label(self, row):
+ label = row.label
+ if row.proposal and row.proposal.reference:
+ label = u"%s - [%s]" % (label, row.proposal.reference)
+ return label
diff --git a/accounts/views.py b/accounts/views.py
index 6ae9d7f..17ead93 100644
--- a/accounts/views.py
+++ b/accounts/views.py
@@ -419,6 +419,7 @@ def invoice_create_or_edit(request, id=None, customer_id=None, proposal_id=None)
'balance_payments': True,
'category': proposal_row.category,
'quantity': proposal_row.quantity,
+ 'detail': proposal_row.detail,
'unit_price': proposal_row.unit_price})
InvoiceRowFormSet.extra = len(initial_row_data) + 1
diff --git a/media/css/style.css b/media/css/style.css
index 0a2dded..477b1b3 100644
--- a/media/css/style.css
+++ b/media/css/style.css
@@ -515,6 +515,11 @@ body.login {
text-align:right;
}
+.row-detail {
+ width:500px;
+ height:30px;
+}
+
.empty-field {
color: gray;
text-align:left;
diff --git a/media/img/edit.png b/media/img/edit.png
new file mode 100755
index 0000000..0fef9a9
Binary files /dev/null and b/media/img/edit.png differ
diff --git a/project/forms.py b/project/forms.py
index 45ab5cc..cda4e8f 100644
--- a/project/forms.py
+++ b/project/forms.py
@@ -79,3 +79,4 @@ def __init__(self, *args, **kwargs):
super(ProposalRowForm, self).__init__(*args, **kwargs)
self.fields['quantity'].widget.attrs['class'] = 'quantity-field'
self.fields['unit_price'].widget.attrs['class'] = 'unit-price-field'
+ self.fields['detail'].widget.attrs['class'] = 'row-detail'
diff --git a/project/migrations/0022_auto__add_field_proposalrow_comment.py b/project/migrations/0022_auto__add_field_proposalrow_comment.py
new file mode 100644
index 0000000..81a2257
--- /dev/null
+++ b/project/migrations/0022_auto__add_field_proposalrow_comment.py
@@ -0,0 +1,142 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'ProposalRow.comment'
+ db.add_column('project_proposalrow', 'comment', self.gf('django.db.models.fields.TextField')(null=True, blank=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'ProposalRow.comment'
+ db.delete_column('project_proposalrow', 'comment')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contact.address': {
+ 'Meta': {'object_name': 'Address', '_ormbases': ['core.OwnedObject']},
+ 'city': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'country': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contact.Country']", 'null': 'True', 'blank': 'True'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'street': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'zipcode': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': 'True'})
+ },
+ 'contact.contact': {
+ 'Meta': {'object_name': 'Contact', '_ormbases': ['core.OwnedObject']},
+ 'address': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contact.Address']"}),
+ 'comment': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'company_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'blank': 'True'}),
+ 'contact_type': ('django.db.models.fields.IntegerField', [], {}),
+ 'contacts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'contacts_rel_+'", 'null': 'True', 'to': "orm['contact.Contact']"}),
+ 'email': ('django.db.models.fields.EmailField', [], {'default': "''", 'max_length': '75', 'blank': 'True'}),
+ 'firstname': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'function': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}),
+ 'legal_form': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'representative': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'representative_function': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'})
+ },
+ 'contact.country': {
+ 'Meta': {'ordering': "['country_name']", 'object_name': 'Country'},
+ 'country_code2': ('django.db.models.fields.CharField', [], {'max_length': '2'}),
+ 'country_code3': ('django.db.models.fields.CharField', [], {'max_length': '3'}),
+ 'country_name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'core.ownedobject': {
+ 'Meta': {'object_name': 'OwnedObject'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'unique': 'True', 'max_length': '36'})
+ },
+ 'project.contract': {
+ 'Meta': {'object_name': 'Contract', '_ormbases': ['core.OwnedObject']},
+ 'content': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'contract_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
+ 'customer': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contracts'", 'to': "orm['contact.Contact']"}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'update_date': ('django.db.models.fields.DateField', [], {})
+ },
+ 'project.project': {
+ 'Meta': {'object_name': 'Project', '_ormbases': ['core.OwnedObject']},
+ 'customer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contact.Contact']"}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'})
+ },
+ 'project.proposal': {
+ 'Meta': {'ordering': "['begin_date', 'update_date']", 'object_name': 'Proposal', '_ormbases': ['core.OwnedObject']},
+ 'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'}),
+ 'begin_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'contract_content': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'contract_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
+ 'end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'expiration_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'payment_delay': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'payment_delay_other': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'payment_delay_type_other': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['project.Project']"}),
+ 'reference': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True', 'blank': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'}),
+ 'update_date': ('django.db.models.fields.DateField', [], {})
+ },
+ 'project.proposalrow': {
+ 'Meta': {'object_name': 'ProposalRow'},
+ 'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'}),
+ 'category': ('django.db.models.fields.IntegerField', [], {}),
+ 'comment': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': True}),
+ 'proposal': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'proposal_rows'", 'to': "orm['project.Proposal']"}),
+ 'quantity': ('django.db.models.fields.DecimalField', [], {'max_digits': '5', 'decimal_places': '1'}),
+ 'unit_price': ('django.db.models.fields.DecimalField', [], {'max_digits': '12', 'decimal_places': '2'}),
+ 'vat_rate': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '4', 'decimal_places': '1', 'blank': 'True'})
+ }
+ }
+
+ complete_apps = ['project']
diff --git a/project/migrations/0023_auto__del_field_proposalrow_comment__add_field_proposalrow_detail.py b/project/migrations/0023_auto__del_field_proposalrow_comment__add_field_proposalrow_detail.py
new file mode 100644
index 0000000..a04d290
--- /dev/null
+++ b/project/migrations/0023_auto__del_field_proposalrow_comment__add_field_proposalrow_detail.py
@@ -0,0 +1,148 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Deleting field 'ProposalRow.comment'
+ db.delete_column('project_proposalrow', 'comment')
+
+ # Adding field 'ProposalRow.detail'
+ db.add_column('project_proposalrow', 'detail', self.gf('django.db.models.fields.TextField')(null=True, blank=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Adding field 'ProposalRow.comment'
+ db.add_column('project_proposalrow', 'comment', self.gf('django.db.models.fields.TextField')(null=True, blank=True), keep_default=False)
+
+ # Deleting field 'ProposalRow.detail'
+ db.delete_column('project_proposalrow', 'detail')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contact.address': {
+ 'Meta': {'object_name': 'Address', '_ormbases': ['core.OwnedObject']},
+ 'city': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'country': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contact.Country']", 'null': 'True', 'blank': 'True'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'street': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'zipcode': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': 'True'})
+ },
+ 'contact.contact': {
+ 'Meta': {'object_name': 'Contact', '_ormbases': ['core.OwnedObject']},
+ 'address': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contact.Address']"}),
+ 'comment': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'company_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'blank': 'True'}),
+ 'contact_type': ('django.db.models.fields.IntegerField', [], {}),
+ 'contacts': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'contacts_rel_+'", 'null': 'True', 'to': "orm['contact.Contact']"}),
+ 'email': ('django.db.models.fields.EmailField', [], {'default': "''", 'max_length': '75', 'blank': 'True'}),
+ 'firstname': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'function': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}),
+ 'legal_form': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'representative': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
+ 'representative_function': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'})
+ },
+ 'contact.country': {
+ 'Meta': {'ordering': "['country_name']", 'object_name': 'Country'},
+ 'country_code2': ('django.db.models.fields.CharField', [], {'max_length': '2'}),
+ 'country_code3': ('django.db.models.fields.CharField', [], {'max_length': '3'}),
+ 'country_name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'core.ownedobject': {
+ 'Meta': {'object_name': 'OwnedObject'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'unique': 'True', 'max_length': '36'})
+ },
+ 'project.contract': {
+ 'Meta': {'object_name': 'Contract', '_ormbases': ['core.OwnedObject']},
+ 'content': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'contract_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
+ 'customer': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'contracts'", 'to': "orm['contact.Contact']"}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'update_date': ('django.db.models.fields.DateField', [], {})
+ },
+ 'project.project': {
+ 'Meta': {'object_name': 'Project', '_ormbases': ['core.OwnedObject']},
+ 'customer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contact.Contact']"}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'})
+ },
+ 'project.proposal': {
+ 'Meta': {'ordering': "['begin_date', 'update_date']", 'object_name': 'Proposal', '_ormbases': ['core.OwnedObject']},
+ 'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'}),
+ 'begin_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'contract_content': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+ 'contract_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
+ 'end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'expiration_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': 'True'}),
+ 'payment_delay': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'payment_delay_other': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'payment_delay_type_other': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['project.Project']"}),
+ 'reference': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True', 'blank': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {'default': '1', 'db_index': 'True'}),
+ 'update_date': ('django.db.models.fields.DateField', [], {})
+ },
+ 'project.proposalrow': {
+ 'Meta': {'object_name': 'ProposalRow'},
+ 'amount': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '12', 'decimal_places': '2', 'blank': 'True'}),
+ 'category': ('django.db.models.fields.IntegerField', [], {}),
+ 'detail': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'ownedobject_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['core.OwnedObject']", 'unique': 'True', 'primary_key': True}),
+ 'proposal': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'proposal_rows'", 'to': "orm['project.Proposal']"}),
+ 'quantity': ('django.db.models.fields.DecimalField', [], {'max_digits': '5', 'decimal_places': '1'}),
+ 'unit_price': ('django.db.models.fields.DecimalField', [], {'max_digits': '12', 'decimal_places': '2'}),
+ 'vat_rate': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '4', 'decimal_places': '1', 'blank': 'True'})
+ }
+ }
+
+ complete_apps = ['project']
diff --git a/project/models.py b/project/models.py
index 46ff14f..4e197a9 100644
--- a/project/models.py
+++ b/project/models.py
@@ -1,14 +1,9 @@
# -*- coding: utf-8 -*-
import unicodedata
-from custom_canvas import NumberedCanvas
-from reportlab.platypus import Paragraph, Frame, Spacer, BaseDocTemplate, PageTemplate
-from reportlab.lib.styles import ParagraphStyle
-from reportlab.rl_config import defaultPageSize
+from reportlab.platypus import Paragraph
from reportlab.lib.units import inch
-from reportlab.lib.enums import TA_CENTER
-from reportlab.platypus import Table, TableStyle, Image
-from reportlab.lib import colors
+from reportlab.platypus import Table, TableStyle
from decimal import Decimal
from django.db import models
@@ -23,6 +18,7 @@
import ho.pisa as pisa
from django.core.files.storage import FileSystemStorage
from django.db.models.query_utils import Q
+from project.utils.pdf import ProposalTemplate
store = FileSystemStorage(location=settings.FILE_UPLOAD_DIR)
@@ -264,267 +260,37 @@ def to_pdf(self, user, response):
"""
Generate a PDF file for the proposal
"""
- def proposal_footer(canvas, doc):
- canvas.saveState()
- canvas.setFont('Times-Roman', 10)
- PAGE_WIDTH = defaultPageSize[0]
- 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('proposal_%(id)d.pdf') % {'id': self.id}
response['Content-Disposition'] = 'attachment; filename=%s' % (filename)
- doc = BaseDocTemplate(response, title=ugettext('Proposal %(reference)s') % {'reference': self.reference}, leftMargin=0.5 * inch, rightMargin=0.5 * inch)
- frameT = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height + 0.5 * inch, id='normal')
- doc.addPageTemplates([PageTemplate(id='all', frames=frameT, onPage=proposal_footer), ])
-
- styleH = ParagraphStyle({})
- styleH.fontSize = 14
- styleH.leading = 16
- styleH.borderPadding = (5,) * 4
-
- styleCustomer = ParagraphStyle({})
- styleCustomer.fontSize = 12
- styleCustomer.leading = 14
- styleCustomer.borderPadding = (5,) * 4
-
- styleTotal = ParagraphStyle({})
- styleTotal.fontSize = 14
- styleTotal.leading = 16
- styleTotal.borderColor = colors.black
- styleTotal.borderWidth = 0.5
- styleTotal.borderPadding = (5,) * 4
-
- styleH2 = ParagraphStyle({})
- styleH2.fontSize = 14
- styleH2.leading = 16
-
-
- styleTitle = ParagraphStyle({})
- styleTitle.fontSize = 14
- styleTitle.fontName = "Times-Bold"
-
- styleN = ParagraphStyle({})
- styleN.fontSize = 12
- styleN.leading = 14
-
- styleF = ParagraphStyle({})
- styleF.fontSize = 10
- styleF.alignment = TA_CENTER
-
- styleLabel = ParagraphStyle({})
-
- story = []
-
- data = []
- user_header_content = """
- %s %s
- %s
- %s %s
- %s
- SIRET : %s
- """
- user_header_content = user_header_content % (user.first_name,
- user.last_name,
- user.get_profile().address.street.replace("\n", "
"),
- user.get_profile().address.zipcode,
- user.get_profile().address.city,
- user.get_profile().address.country or '',
- user.get_profile().company_id)
-
- if user.get_profile().phonenumber:
- user_header_content = "%s%s
" % (user_header_content, user.get_profile().phonenumber)
- if user.get_profile().professional_email:
- user_header_content = "%s%s
" % (user_header_content, user.get_profile().professional_email)
-
- customer_header_content = """
-
- %s
- %s
- %s %s
- %s
- """
-
- if user.get_profile().logo_file:
- user_header = Image("%s%s" % (settings.FILE_UPLOAD_DIR, user.get_profile().logo_file))
- else:
- user_header = Paragraph(user_header_content, styleH)
-
- data.append([user_header,
- '',
- Paragraph(customer_header_content % (self.project.customer.name,
- self.project.customer.address.street.replace("\n", "
"),
- self.project.customer.address.zipcode,
- self.project.customer.address.city,
- self.project.customer.address.country or ''), styleCustomer)])
+ proposal_template = ProposalTemplate(response, user)
- t1 = Table(data, [3.5 * inch, 0.7 * inch, 3.1 * inch], [1.9 * inch])
-
- table_style = [('VALIGN', (0, 0), (-1, -1), 'TOP'), ]
-
- if user.get_profile().logo_file:
- table_style.append(('TOPPADDING', (0, 0), (0, 0), 0))
- table_style.append(('LEFTPADDING', (0, 0), (0, 0), 0))
-
- t1.setStyle(TableStyle(table_style))
-
- story.append(t1)
-
- spacer1 = Spacer(doc.width, 0.25 * inch)
- story.append(spacer1)
-
- data = []
- msg = u"Dispensé d'immatriculation au registre du commerce et des sociétés (RCS) et au répertoire des métiers (RM)"
- data.append([Paragraph(msg, styleN),
- '',
- Paragraph("
" + _("Date : %s") % (localize(self.update_date)), styleH2)])
-
- t2 = Table(data, [3.5 * inch, 0.3 * inch, 3.5 * inch], [0.7 * inch])
- t2.setStyle(TableStyle([('VALIGN', (0, 0), (-1, -1), 'TOP'), ]))
-
- story.append(t2)
-
- spacer2 = Spacer(doc.width, 0.25 * inch)
- story.append(spacer2)
-
- story.append(Paragraph(_("PROPOSAL %s") % (self.reference), styleTitle))
-
- spacer3 = Spacer(doc.width, 0.1 * inch)
- story.append(spacer3)
+ proposal_template.init_doc(ugettext('Proposal %(reference)s') % {'reference': self.reference})
+ proposal_template.add_headers(self, self.project.customer, self.update_date)
+ proposal_template.add_title(_("PROPOSAL %s") % (self.reference))
# proposal row list
rows = self.proposal_rows.all()
- extra_rows = 0
- 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:
- para = Paragraph(row.label, styleLabel)
- para.width = label_width
- splitted_para = para.breakLines(label_width)
- label = " ".join(splitted_para.lines[0][1])
- quantity = row.quantity
- quantity = quantity.quantize(Decimal(1)) if quantity == quantity.to_integral() else quantity.normalize()
- unit_price = row.unit_price
- 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_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:
- if row.vat_rate:
- data_row.append("%s%%" % (localize(row.vat_rate)))
- else:
- data_row.append('-')
- data.append(data_row)
- for extra_row in splitted_para.lines[1:]:
- label = " ".join(extra_row[1])
- if user.get_profile().vat_number:
- data.append([label, '', '', '', ''])
- else:
- data.append([label, '', '', ''])
- extra_rows = extra_rows + 1
-
- row_count = len(rows) + extra_rows
- if row_count <= 16:
- max_row_count = 16
- else:
- first_page_count = 21
- normal_page_count = 33
- last_page_count = 27
- max_row_count = first_page_count + ((row_count - first_page_count) // normal_page_count * normal_page_count) + last_page_count
- if row_count - first_page_count - ((row_count - first_page_count) // normal_page_count * normal_page_count) > last_page_count:
- max_row_count = max_row_count + normal_page_count
-
- for i in range(max_row_count - row_count):
- 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)
-
- spacer4 = Spacer(doc.width, 0.55 * inch)
- story.append(spacer4)
- proposal_amount = self.amount
- proposal_amount = proposal_amount.quantize(Decimal(1)) if proposal_amount == proposal_amount.to_integral() else proposal_amount.normalize()
-
- if user.get_profile().vat_number:
- right_block = [Paragraph(_("Total excl tax : %(amount)s %(currency)s") % {'amount': localize(proposal_amount), 'currency' : "€".decode('utf-8')}, styleN)]
- vat_amounts = {}
- for row in rows:
- vat_rate = row.vat_rate or 0
- vat_amount = row.amount * vat_rate / 100
- if vat_rate:
- 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 = proposal_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(proposal_amount), 'currency' : "€".decode('utf-8')}, styleTotal),
- Spacer(1, 0.25 * inch),
- Paragraph(u"TVA non applicable, art. 293 B du CGI", styleN)]
+ proposal_template.add_rows(rows)
+ # total amount on the right side of footer
+ right_block = proposal_template.get_total_amount(self.amount, rows)
- data = [[[Paragraph(_("Proposal valid through : %s") % (localize(self.expiration_date) or ''), styleN),
- Paragraph(_("Payment delay : %s") % (self.get_payment_delay()), styleN)],
+ # left side of footer
+ data = [[[Paragraph(_("Proposal valid through : %s") % (localize(self.expiration_date) or ''), ProposalTemplate.styleN),
+ Paragraph(_("Payment delay : %s") % (self.get_payment_delay()), ProposalTemplate.styleN)],
'',
right_block], ]
if self.begin_date and self.end_date:
- data[0][0].append(Paragraph(_("Execution dates : %(begin_date)s to %(end_date)s") % {'begin_date': localize(self.begin_date), 'end_date' : localize(self.end_date)}, styleN))
+ data[0][0].append(Paragraph(_("Execution dates : %(begin_date)s to %(end_date)s") % {'begin_date': localize(self.begin_date), 'end_date' : localize(self.end_date)}, ProposalTemplate.styleN))
footer_table = Table(data, [4.5 * inch, 0.3 * inch, 2.5 * inch], [1 * inch])
footer_table.setStyle(TableStyle([('VALIGN', (0, 0), (-1, -1), 'TOP'), ]))
- story.append(footer_table)
+ proposal_template.append_to_story(footer_table)
- doc.build(story, canvasmaker=NumberedCanvas)
+ proposal_template.build()
return response
@@ -622,6 +388,7 @@ class Row(OwnedObject):
unit_price = models.DecimalField(max_digits=12, decimal_places=2, verbose_name=_('Unit price'))
amount = models.DecimalField(max_digits=12, decimal_places=2, blank=True, null=True, verbose_name=_('Amount'))
vat_rate = models.DecimalField(choices=VAT_RATES, decimal_places=1, max_digits=4, verbose_name=_('Vat'), blank=True, null=True)
+ detail = models.TextField(verbose_name=_('Detail'), blank=True, null=True)
class Meta:
abstract = True
diff --git a/project/tests.py b/project/tests.py
index aa0dce8..f32ab25 100644
--- a/project/tests.py
+++ b/project/tests.py
@@ -15,6 +15,7 @@
import datetime
import hashlib
from django.contrib.auth.models import User
+from django.contrib.webdesign import lorem_ipsum
class ContractPermissionTest(TestCase):
fixtures = ['test_users', 'test_contacts']
@@ -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(),
- "1cf71b609bd3b39291bc6663af5014b5")
+ "489e25b4c6a9c868301776b0883bc0b6")
def testContractDownloadPdf(self):
"""
@@ -1012,7 +1013,66 @@ def testDownloadPdfWithVat(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(),
- "6fd59f103a1f3379c26467bcfa12e212")
+ "c892095693e8cd4470738b36b60b6a84")
+
+ def testDownloadPdfWithRowDetail(self):
+ """
+ Tests non-regression on pdf with detail on rows
+ """
+ profile = User.objects.get(pk=1).get_profile()
+ profile.vat_number = 'FR010123456789123'
+ profile.save()
+ customer_address = Contact.objects.get(project=30).address
+ customer_address.street = u"128 boulevard des champs élysées\nEspace ZAC du champs de mars\nBP 140"
+ customer_address.zipcode = '75001'
+ customer_address.city = 'Paris CEDEX 1'
+ customer_address.save()
+
+ p = Proposal.objects.create(project_id=30,
+ update_date=datetime.date(2011, 2, 5),
+ state=PROPOSAL_STATE_DRAFT,
+ begin_date=datetime.date(2010, 8, 1),
+ end_date=datetime.date(2010, 8, 15),
+ contract_content='Content of contract',
+ amount=1000,
+ reference='XXX',
+ expiration_date=datetime.date(2010, 8, 2),
+ owner_id=1)
+
+ p_row = ProposalRow.objects.create(proposal_id=p.id,
+ label='Day of work',
+ category=ROW_CATEGORY_SERVICE,
+ quantity=10,
+ unit_price='100',
+ vat_rate=VAT_RATES_19_6,
+ detail=lorem_ipsum.COMMON_P,
+ owner_id=1)
+ p_row = ProposalRow.objects.create(proposal_id=p.id,
+ label='Day of work',
+ category=ROW_CATEGORY_SERVICE,
+ quantity=10,
+ unit_price='100',
+ vat_rate=VAT_RATES_19_6,
+ detail=lorem_ipsum.COMMON_P,
+ owner_id=1)
+ p_row = ProposalRow.objects.create(proposal_id=p.id,
+ label='Day of work',
+ category=ROW_CATEGORY_SERVICE,
+ quantity=10,
+ unit_price='100',
+ vat_rate=VAT_RATES_19_6,
+ detail=lorem_ipsum.COMMON_P,
+ owner_id=1)
+
+ response = self.client.get(reverse('proposal_download', kwargs={'id': p.id}))
+ self.assertEqual(response.status_code, 200)
+ f = open('/tmp/proposal_row_detail.pdf', 'w')
+ f.write(response.content)
+ f.close()
+ content = response.content.split("\n")
+ invariant_content = content[0:95] + content[96:152] + content[153:-1]
+ self.assertEquals(hashlib.md5("\n".join(invariant_content)).hexdigest(),
+ "3af10992262c39ea7d83e54248797b1a")
class Bug31Test(TestCase):
fixtures = ['test_dashboard_product_sales']
diff --git a/project/utils/__init__.py b/project/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/project/utils/pdf.py b/project/utils/pdf.py
new file mode 100644
index 0000000..a8f8ea5
--- /dev/null
+++ b/project/utils/pdf.py
@@ -0,0 +1,317 @@
+# -*- coding: utf-8 -*-
+
+from decimal import Decimal
+from reportlab.platypus import Table, TableStyle, Image, Paragraph
+from django.conf import settings
+from reportlab.lib.styles import ParagraphStyle
+from reportlab.lib.units import inch
+from reportlab.platypus.flowables import Spacer
+from django.utils.formats import localize
+from django.utils.translation import ugettext_lazy as _, ugettext
+from reportlab.platypus.doctemplate import BaseDocTemplate, PageTemplate
+from reportlab.platypus.frames import Frame
+from reportlab.rl_config import defaultPageSize
+from reportlab.lib import colors
+from reportlab.lib.enums import TA_CENTER
+from custom_canvas import NumberedCanvas
+
+class ProposalTemplate(object):
+
+ styleH = ParagraphStyle({})
+ styleH.fontSize = 14
+ styleH.leading = 16
+ styleH.borderPadding = (5,) * 4
+
+ styleH2 = ParagraphStyle({})
+ styleH2.fontSize = 14
+ styleH2.leading = 16
+
+ styleCustomer = ParagraphStyle({})
+ styleCustomer.fontSize = 12
+ styleCustomer.leading = 14
+ styleCustomer.borderPadding = (5,) * 4
+
+ styleN = ParagraphStyle({})
+ styleN.fontSize = 12
+ styleN.leading = 14
+
+ styleTotal = ParagraphStyle({})
+ styleTotal.fontSize = 14
+ styleTotal.leading = 16
+ styleTotal.borderColor = colors.black
+ styleTotal.borderWidth = 0.5
+ styleTotal.borderPadding = (5,) * 4
+
+ styleTitle = ParagraphStyle({})
+ styleTitle.fontSize = 14
+ styleTitle.fontName = "Helvetica-Bold"
+
+ styleF = ParagraphStyle({})
+ styleF.fontSize = 10
+ styleF.alignment = TA_CENTER
+
+ styleLabel = ParagraphStyle({})
+
+ styleDetail = ParagraphStyle({})
+ styleDetail.fontName = 'Helvetica-Oblique'
+ styleDetail.textColor = colors.gray
+
+ def __init__(self, response, user):
+ self.response = response
+ self.user = user
+ self.doc = None
+ self.story = []
+ self.space_before_footer = 0.55 * inch
+
+ def append_to_story(self, data):
+ self.story.append(data)
+
+ def build(self):
+ self.doc.build(self.story, canvasmaker=NumberedCanvas)
+
+ def init_doc(self, title):
+
+ def proposal_footer(canvas, doc):
+ canvas.saveState()
+ canvas.setFont('Helvetica', 10)
+ PAGE_WIDTH = defaultPageSize[0]
+ footer_text = "%s %s - %s, %s %s" % (self.user.first_name,
+ self.user.last_name,
+ self.user.get_profile().address.street.replace("\n", ", ").replace("\r", ""),
+ self.user.get_profile().address.zipcode,
+ self.user.get_profile().address.city)
+ if self.user.get_profile().address.country:
+ footer_text = footer_text + u", %s" % (self.user.get_profile().address.country)
+
+ canvas.drawCentredString(PAGE_WIDTH / 2.0, 0.5 * inch, footer_text)
+ extra_info = u"SIRET : %s" % (self.user.get_profile().company_id)
+ if self.user.get_profile().vat_number:
+ extra_info = u"%s - N° TVA : %s" % (extra_info, self.user.get_profile().vat_number)
+ canvas.drawCentredString(PAGE_WIDTH / 2.0, 0.35 * inch, extra_info)
+ canvas.restoreState()
+
+ self.doc = BaseDocTemplate(self.response, title=title, leftMargin=0.5 * inch, rightMargin=0.5 * inch)
+ frameT = Frame(self.doc.leftMargin, self.doc.bottomMargin, self.doc.width, self.doc.height + 0.5 * inch, id='normal')
+ self.doc.addPageTemplates([PageTemplate(id='all', frames=frameT, onPage=proposal_footer), ])
+
+ def add_headers(self, proposal, customer, document_date):
+
+ data = []
+ user_header_content = """
+ %s %s
+ %s
+ %s %s
+ %s
+ SIRET : %s
+ """
+ user_header_content = user_header_content % (self.user.first_name,
+ self.user.last_name,
+ self.user.get_profile().address.street.replace("\n", "
"),
+ self.user.get_profile().address.zipcode,
+ self.user.get_profile().address.city,
+ self.user.get_profile().address.country or '',
+ self.user.get_profile().company_id)
+
+ if self.user.get_profile().phonenumber:
+ user_header_content = "%s%s
" % (user_header_content, self.user.get_profile().phonenumber)
+ if self.user.get_profile().professional_email:
+ user_header_content = "%s%s
" % (user_header_content, self.user.get_profile().professional_email)
+
+ customer_header_content = """
+
+ %s
+ %s
+ %s %s
+ %s
+ """
+
+ if self.user.get_profile().logo_file:
+ user_header = Image("%s%s" % (settings.FILE_UPLOAD_DIR, self.user.get_profile().logo_file))
+ else:
+ user_header = Paragraph(user_header_content, self.styleH)
+
+ data.append([user_header,
+ '',
+ Paragraph(customer_header_content % (customer.name,
+ customer.address.street.replace("\n", "
"),
+ customer.address.zipcode,
+ customer.address.city,
+ customer.address.country or ''), self.styleCustomer)])
+
+ t1 = Table(data, [3.5 * inch, 0.7 * inch, 3.1 * inch], [1.9 * inch])
+
+ table_style = [('VALIGN', (0, 0), (-1, -1), 'TOP'), ]
+
+ if self.user.get_profile().logo_file:
+ table_style.append(('TOPPADDING', (0, 0), (0, 0), 0))
+ table_style.append(('LEFTPADDING', (0, 0), (0, 0), 0))
+
+ t1.setStyle(TableStyle(table_style))
+
+ self.story.append(t1)
+
+ self.story.append(Spacer(self.doc.width, 0.25 * inch))
+
+ data = []
+ msg = u"Dispensé d'immatriculation au registre du commerce et des sociétés (RCS) et au répertoire des métiers (RM)"
+ data.append([Paragraph(msg, self.styleN),
+ '',
+ Paragraph("
" + _("Date : %s") % (localize(document_date)), self.styleH2)])
+
+ t2 = Table(data, [3.5 * inch, 0.3 * inch, 3.5 * inch], [0.7 * inch])
+ t2.setStyle(TableStyle([('VALIGN', (0, 0), (-1, -1), 'TOP'), ]))
+
+ self.story.append(t2)
+
+ self.story.append(Spacer(self.doc.width, 0.25 * inch))
+
+ def add_title(self, title):
+ self.story.append(Paragraph(title, self.styleTitle))
+
+ spacer = Spacer(self.doc.width, 0.1 * inch)
+ self.story.append(spacer)
+
+ def add_row_detail(self, data, row, label_width):
+ extra_rows = 0
+ if row.detail:
+ for line in row.detail.split("\n"):
+ para = Paragraph(line, self.styleDetail)
+ para.width = label_width
+ splitted_para = para.breakLines(label_width)
+ for detail_row in splitted_para.lines:
+ detail = " ".join(detail_row[1])
+ data.append((detail,))
+ extra_rows += 1
+ return extra_rows
+
+ def add_row_table(self, data, row_count, extra_style=[]):
+ if row_count <= 16:
+ max_row_count = 16
+ else:
+ first_page_count = 21
+ normal_page_count = 33
+ last_page_count = 27
+ max_row_count = first_page_count + ((row_count - first_page_count) // normal_page_count * normal_page_count) + last_page_count
+ if row_count - first_page_count - ((row_count - first_page_count) // normal_page_count * normal_page_count) > last_page_count:
+ max_row_count = max_row_count + normal_page_count
+
+ for i in range(max_row_count - row_count):
+ if self.user.get_profile().vat_number:
+ data.append(['', '', '', '', ''])
+ else:
+ data.append(['', '', '', ''])
+
+ if self.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), 'Helvetica-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)]
+
+ row_style += extra_style
+
+ if self.user.get_profile().vat_number:
+ row_style.append(('BOX', (4, 1), (4, -1), 0.25, colors.black))
+
+ row_table.setStyle(TableStyle(row_style))
+
+ self.story.append(row_table)
+
+ def get_label(self, row):
+ return row.label
+
+ def add_rows(self, rows):
+ row_count = 0
+ extra_rows = 0
+ extra_style = []
+ data = []
+ data.append([ugettext('Label'), ugettext('Quantity'), ugettext('Unit price'), ugettext('Total excl tax')])
+ if self.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:
+ row_count += 1
+ label = self.get_label(row)
+ para = Paragraph(label, ProposalTemplate.styleLabel)
+ para.width = label_width
+ splitted_para = para.breakLines(label_width)
+ label = " ".join(splitted_para.lines[0][1])
+ quantity = row.quantity
+ quantity = quantity.quantize(Decimal(1)) if quantity == quantity.to_integral() else quantity.normalize()
+ unit_price = row.unit_price
+ 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_row = [label, localize(quantity), "%s %s" % (localize(unit_price), "€".decode('utf-8')), "%s %s" % (localize(total), "€".decode('utf-8'))]
+ if self.user.get_profile().vat_number:
+ if row.vat_rate:
+ data_row.append("%s%%" % (localize(row.vat_rate)))
+ else:
+ data_row.append('-')
+ data.append(data_row)
+
+ for extra_row in splitted_para.lines[1:]:
+ label = " ".join(extra_row[1])
+ if self.user.get_profile().vat_number:
+ data.append([label, '', '', '', ''])
+ else:
+ data.append([label, '', '', ''])
+ extra_rows += 1
+
+ extra_detail_rows = self.add_row_detail(data, row, label_width)
+ if extra_detail_rows:
+ extra_style.append(('FONT',
+ (0, row_count + extra_rows + 1),
+ (0, row_count + extra_rows + extra_detail_rows),
+ self.styleDetail.fontName))
+ extra_style.append(('TEXTCOLOR',
+ (0, row_count + extra_rows + 1),
+ (0, row_count + extra_rows + extra_detail_rows),
+ self.styleDetail.textColor))
+ extra_rows += extra_detail_rows
+
+ self.add_row_table(data, row_count + extra_rows, extra_style)
+ self.story.append(Spacer(self.doc.width, self.space_before_footer))
+
+ def get_total_amount(self, amount, rows):
+ amount = amount.quantize(Decimal(1)) if amount == amount.to_integral() else amount.normalize()
+
+ if self.user.get_profile().vat_number:
+ total_amount = [Paragraph(_("Total excl tax : %(amount)s %(currency)s") % {'amount': localize(amount), 'currency' : "€".decode('utf-8')}, ProposalTemplate.styleN)]
+ vat_amounts = {}
+ for row in rows:
+ vat_rate = row.vat_rate or 0
+ vat_amount = row.amount * vat_rate / 100
+ if vat_rate:
+ 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()
+ total_amount.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')},
+ ProposalTemplate.styleN))
+
+ incl_tax_amount = 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)
+ total_amount.append(Spacer(1, 0.25 * inch))
+ total_amount.append(Paragraph(_("TOTAL incl tax : %(amount)s %(currency)s") % {'amount': localize(incl_tax_amount), 'currency' : "€".decode('utf-8')}, ProposalTemplate.styleTotal))
+ else:
+ total_amount = [Paragraph(_("TOTAL excl tax : %(amount)s %(currency)s") % {'amount': localize(amount), 'currency' : "€".decode('utf-8')}, ProposalTemplate.styleTotal),
+ Spacer(1, 0.25 * inch),
+ Paragraph(u"TVA non applicable, art. 293 B du CGI", ProposalTemplate.styleN)]
+
+ return total_amount
diff --git a/templates/invoice/detail.html b/templates/invoice/detail.html
index 9278ed3..8477ce4 100644
--- a/templates/invoice/detail.html
+++ b/templates/invoice/detail.html
@@ -87,6 +87,7 @@