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 @@

{% trans "Detail rows" %}

{% if user.get_profile.vat_number %} {% trans "VAT rate" %} {% endif %} + {% trans "Detail" %} @@ -102,6 +103,7 @@

{% trans "Detail rows" %}

{% if user.get_profile.vat_number %} {{ invoicerow.vat_rate|default:'' }} {% endif %} + {{ invoicerow.detail|default:''|linebreaksbr }} {% endfor %} diff --git a/templates/invoice/edit.html b/templates/invoice/edit.html index 3569869..583a9e1 100644 --- a/templates/invoice/edit.html +++ b/templates/invoice/edit.html @@ -43,12 +43,21 @@ } jQuery('.invoice-row input:text').labelInside(); + jQuery('.invoice-row textarea').labelInside(); + jQuery('.row-detail.empty-field').hide(); jQuery('.proposal-field').proposalOverview('{% trans "Proposal" %}', '{% url proposal_detail id=0 %}'); jQuery('.balance_payments_label').rowHelp(); jQuery('.balance-payments-field').balanceAll(); + jQuery('.delete-checkbox').children('input').attr('checked', false); + + jQuery('.form-row').delegate('.show-detail', 'click', function(e){ + e.preventDefault(); + jQuery(e.currentTarget).siblings('.row-detail.empty-field').show(); + }); jQuery('#invoice-edit-form').submit(function() { jQuery('.invoice-row input.empty-field:text').val(''); + jQuery('.invoice-row textarea.empty-field').val(''); return true; }); @@ -67,6 +76,9 @@ jQuery('input:text',row).focusin(); jQuery('input',row).val(''); jQuery('input:text',row).focusout(); + jQuery('textarea',row).focusin(); + jQuery('textarea',row).val(''); + jQuery('textarea',row).focusout(); jQuery('input',row).attr('checked', false); jQuery('option',row).attr('selected', false); jQuery("option[value='']",row).attr('selected', true); @@ -93,11 +105,13 @@ && jQuery('.invoice-row:hidden').length && !jQuery('.invoice-row:hidden').children('input[type="hidden"]').first().val()) { jQuery('.invoice-row:hidden').show(); + jQuery('.row-detail.empty-field').hide(); } else { new_row = jQuery(e.currentTarget).parent('.inline-formset').prev('.invoice-row').clone(); jQuery(e.currentTarget).parent('.inline-formset').before(new_row); jQuery('ul.errorlist', new_row).remove(); jQuery('input',new_row).val(''); + jQuery('textarea',new_row).val(''); jQuery('input',new_row).attr('checked', false); jQuery('option',new_row).attr('selected', false); jQuery("option[value='']",new_row).attr('selected', true); @@ -108,6 +122,8 @@ }); jQuery('input:text', new_row).labelInside(); + jQuery('textarea', new_row).labelInside(); + jQuery('.row-detail.empty-field', new_row).hide(); jQuery('.proposal-field', new_row).proposalOverview('{% trans "Proposal" %}', '{% url proposal_detail id=0 %}'); jQuery('.balance_payments_label', new_row).rowHelp(); jQuery('.balance-payments-field', new_row).balanceAll(); @@ -186,7 +202,11 @@ {{ form.quantity.errors|format_errors:form.quantity.label }} {{ form.unit_price.errors|format_errors:form.unit_price.label }} {{ form.vat_rate.errors|format_errors:form.vat_rate.label }} + {{ form.detail.errors|format_errors:form.detail.label }} {{ form.ownedobject_ptr }}{{ form.label.label }} : {{ form.label }} {{ form.proposal }} {{ form.balance_payments.label }} : {{ form.balance_payments }} {{ form.category }} {{ form.quantity.label }} : {{ form.quantity }} {{ form.unit_price.label }} : {{ form.unit_price }}{% if user.get_profile.vat_number %} {{ form.vat_rate }}{% endif %}{{ form.DELETE }} + {% trans +
+ {{ form.detail.label }} : {{ form.detail }} {% endfor %}
diff --git a/templates/proposal/detail.html b/templates/proposal/detail.html index 642fea7..5fe8ce7 100644 --- a/templates/proposal/detail.html +++ b/templates/proposal/detail.html @@ -69,6 +69,7 @@

{% trans "Detail rows" %}

{% if user.get_profile.vat_number %} {% trans "VAT rate" %} {% endif %} + {% trans "Detail" %} @@ -82,6 +83,7 @@

{% trans "Detail rows" %}

{% if user.get_profile.vat_number %} {{ proposalrow.vat_rate|default:'' }} {% endif %} + {{ proposalrow.detail|default:''|linebreaksbr }} {% endfor %} diff --git a/templates/proposal/edit.html b/templates/proposal/edit.html index 24952aa..341e71c 100644 --- a/templates/proposal/edit.html +++ b/templates/proposal/edit.html @@ -29,9 +29,18 @@ }); jQuery('.proposal-row input:text').labelInside(); + jQuery('.proposal-row textarea').labelInside(); + jQuery('.row-detail.empty-field').hide(); + jQuery('.delete-checkbox').children('input').attr('checked', false); + + jQuery('.form-row').delegate('.show-detail', 'click', function(e){ + e.preventDefault(); + jQuery(e.currentTarget).siblings('.row-detail.empty-field').show(); + }); jQuery('#proposal-edit-form').submit(function() { jQuery('.proposal-row input.empty-field:text').val(''); + jQuery('.proposal-row textarea.empty-field').val(''); return true; }); @@ -53,6 +62,9 @@ jQuery('input:text',row).focusin(); jQuery('input',row).val(''); jQuery('input:text',row).focusout(); + jQuery('textarea',row).focusin(); + jQuery('textarea',row).val(''); + jQuery('textarea',row).focusout(); jQuery('input',row).attr('checked', false); jQuery('option',row).attr('selected', false); jQuery("option[value='']",row).attr('selected', true); @@ -79,11 +91,13 @@ && jQuery('.proposal-row:hidden').length && !jQuery('.proposal-row:hidden').children('input[type="hidden"]').first().val()) { jQuery('.proposal-row:hidden').show(); + jQuery('.row-detail.empty-field').hide(); } else { new_row = jQuery(e.currentTarget).parent('.inline-formset').prev('.proposal-row').clone(); jQuery(e.currentTarget).parent('.inline-formset').before(new_row); jQuery('ul.errorlist', new_row).remove(); jQuery('input',new_row).val(''); + jQuery('textarea',new_row).val(''); jQuery('input',new_row).attr('checked', false); jQuery('option',new_row).attr('selected', false); jQuery("option[value='']",new_row).attr('selected', true); @@ -94,6 +108,8 @@ }); jQuery('input:text', new_row).labelInside(); + jQuery('textarea', new_row).labelInside(); + jQuery('.row-detail.empty-field', new_row).hide(); jQuery(new_row).show(); jQuery('#id_proposal_rows-TOTAL_FORMS').val(parseInt(jQuery('#id_proposal_rows-TOTAL_FORMS').val(), 10) + 1); @@ -211,7 +227,11 @@ {{ form.quantity.errors|format_errors:form.quantity.label }} {{ form.unit_price.errors|format_errors:form.unit_price.label }} {{ form.vat_rate.errors|format_errors:form.vat_rate.label }} + {{ form.detail.errors|format_errors:form.detail.label }} {{ form.ownedobject_ptr }}{{ form.label.label }} : {{ form.label }} {{ form.category }} {{ form.quantity.label }} : {{ form.quantity }} {{ form.unit_price.label }} : {{ form.unit_price }} {% if user.get_profile.vat_number %} {{ form.vat_rate }}{% endif %}{{ form.DELETE }} + {% trans +
+ {{ form.detail.label }} : {{ form.detail }}
{% endfor %}