Repository: dulacp/django-accounting Branch: master Commit: 2e61776a653e Files: 169 Total size: 257.8 KB Directory structure: gitextract_lsm6zxq8/ ├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.rst ├── accounting/ │ ├── __init__.py │ ├── apps/ │ │ ├── __init__.py │ │ ├── books/ │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── apps.py │ │ │ ├── calculators.py │ │ │ ├── context_processors.py │ │ │ ├── forms.py │ │ │ ├── managers.py │ │ │ ├── middlewares.py │ │ │ ├── migrations/ │ │ │ │ ├── 0001_initial.py │ │ │ │ ├── 0002_auto_20141029_1606.py │ │ │ │ ├── 0003_auto_20141029_1606.py │ │ │ │ ├── 0004_auto_20141104_1026.py │ │ │ │ ├── 0005_auto_20150128_1458.py │ │ │ │ ├── 0006_auto_20150206_1836.py │ │ │ │ ├── 0007_auto_20150206_1836.py │ │ │ │ ├── 0008_auto_20150206_1843.py │ │ │ │ ├── 0009_auto_20150206_1850.py │ │ │ │ ├── 0010_auto_20150210_1449.py │ │ │ │ ├── 0011_auto_20150324_1430.py │ │ │ │ └── __init__.py │ │ │ ├── mixins.py │ │ │ ├── models.py │ │ │ ├── templatetags/ │ │ │ │ ├── __init__.py │ │ │ │ └── status_filters.py │ │ │ ├── urls.py │ │ │ ├── utils.py │ │ │ └── views.py │ │ ├── connect/ │ │ │ ├── __init__.py │ │ │ ├── middlewares.py │ │ │ ├── models.py │ │ │ ├── steps.py │ │ │ ├── urls.py │ │ │ └── views.py │ │ ├── context_processors.py │ │ ├── people/ │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── forms.py │ │ │ ├── migrations/ │ │ │ │ ├── 0001_initial.py │ │ │ │ ├── 0002_auto_20141029_1609.py │ │ │ │ ├── 0003_employee_payroll_tax_rate.py │ │ │ │ ├── 0004_auto_20141029_2306.py │ │ │ │ ├── 0005_auto_20141029_2308.py │ │ │ │ └── __init__.py │ │ │ ├── models.py │ │ │ ├── urls.py │ │ │ └── views.py │ │ └── reports/ │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── forms.py │ │ ├── migrations/ │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_auto_20150128_1458.py │ │ │ ├── 0003_auto_20150131_1902.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── urls.py │ │ ├── views.py │ │ └── wrappers.py │ ├── defaults.py │ ├── libs/ │ │ ├── __init__.py │ │ ├── checks.py │ │ ├── decorators.py │ │ ├── exceptions.py │ │ ├── fields.py │ │ ├── foundation.py │ │ ├── intervals.py │ │ ├── prices.py │ │ ├── templatetags/ │ │ │ ├── __init__.py │ │ │ ├── check_filters.py │ │ │ ├── check_tags.py │ │ │ ├── currency_filters.py │ │ │ ├── display_tags.py │ │ │ ├── distance_filters.py │ │ │ ├── float_filters.py │ │ │ ├── form_filters.py │ │ │ ├── form_tags.py │ │ │ ├── format_filters.py │ │ │ ├── introspection_filters.py │ │ │ ├── my_filters.py │ │ │ ├── nav.py │ │ │ └── url_tags.py │ │ └── utils.py │ ├── static/ │ │ └── accounting/ │ │ ├── css/ │ │ │ └── main.css │ │ └── js/ │ │ ├── books/ │ │ │ └── invoice_or_bill_create.js │ │ ├── jquery.formset.js │ │ └── main.js │ ├── templates/ │ │ └── accounting/ │ │ ├── _generics/ │ │ │ ├── check_tag.html │ │ │ ├── delete_entity.html │ │ │ └── form.html │ │ ├── _partials/ │ │ │ └── form_fields.html │ │ ├── base.html │ │ ├── books/ │ │ │ ├── _generics/ │ │ │ │ ├── sale_content.html │ │ │ │ ├── sale_detail.html │ │ │ │ └── sale_list.html │ │ │ ├── _partials/ │ │ │ │ ├── expense_claim_list.html │ │ │ │ ├── payment_list.html │ │ │ │ └── tax_rate_list.html │ │ │ ├── bill_create_or_update.html │ │ │ ├── bill_detail.html │ │ │ ├── bill_list.html │ │ │ ├── dashboard.html │ │ │ ├── estimate_create_or_update.html │ │ │ ├── estimate_detail.html │ │ │ ├── estimate_list.html │ │ │ ├── expense_claim_create_or_update.html │ │ │ ├── expense_claim_detail.html │ │ │ ├── expense_claim_list.html │ │ │ ├── invoice_create_or_update.html │ │ │ ├── invoice_detail.html │ │ │ ├── invoice_list.html │ │ │ ├── organization_create_or_update.html │ │ │ ├── organization_detail.html │ │ │ ├── organization_list.html │ │ │ ├── organization_selector.html │ │ │ ├── payment_create_or_update.html │ │ │ ├── tax_rate_create_or_update.html │ │ │ └── tax_rate_list.html │ │ ├── connect/ │ │ │ └── getting_started.html │ │ ├── layout.html │ │ ├── people/ │ │ │ ├── client_create_or_update.html │ │ │ ├── client_detail.html │ │ │ ├── client_list.html │ │ │ ├── employee_create_or_update.html │ │ │ ├── employee_detail.html │ │ │ └── employee_list.html │ │ └── reports/ │ │ ├── _partials/ │ │ │ └── period_form.html │ │ ├── financial_settings_update.html │ │ ├── invoice_details_report.html │ │ ├── pay_run_report.html │ │ ├── payrun_settings_update.html │ │ ├── profit_and_loss_report.html │ │ ├── report_list.html │ │ ├── settings_list.html │ │ └── tax_report.html │ ├── urls.py │ └── wsgi.py ├── lint.sh ├── requirements.txt ├── runtests.py ├── setup.cfg ├── setup.py └── tests/ ├── __init__.py ├── _site/ │ ├── __init__.py │ └── model_tests_app/ │ ├── __init__.py │ └── models.py ├── config.py ├── functional/ │ ├── __init__.py │ └── libs/ │ ├── __init__.py │ ├── context_processors_tests.py │ ├── template_tags_tests.py │ └── utils_tests.py ├── integration/ │ └── __init__.py ├── settings.py └── unit/ ├── __init__.py ├── books/ │ ├── __init__.py │ ├── bill_tests.py │ └── invoice_tests.py ├── clients/ │ ├── __init__.py │ ├── organization_tests.py │ └── user_tests.py └── libs/ ├── __init__.py └── prices_tests.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coveragerc ================================================ [run] source = accounting omit = *migrations* ================================================ FILE: .gitignore ================================================ ## Byte-compiled __pycache__/ *.py[cod] *.egg-info /dist/ /build/ ## Database *.sqlite3 ## Translations *.mo ## Components components/ ## Testing .coverage .noseids coverage.xml violations.txt nosetests.xml /htmlcov/* ================================================ FILE: .travis.yml ================================================ # Use the newer container-based infrastructure # http://docs.travis-ci.com/user/workers/container-based-infrastructure/ sudo: false # Cache pip downloads cache: directories: - $HOME/.pip-cache/ language: python python: - '3.3' - '3.4' install: - pip install -e . -r requirements.txt script: - make travis after_success: - coveralls ================================================ FILE: LICENSE ================================================ Copyright (c) 2013 Pierre Dulac (https://dulacp.com/) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ # These targets are not files .PHONY: contribute travis test lint coverage clean: # Remove files not in source control find . -type f -name "*.pyc" -delete rm -rf nosetests.xml coverage.xml htmlcov violations.txt todo: # Look for areas of the code that need updating when some event has taken place grep --exclude-dir=components -rnH TODO reqs grep --exclude-dir=components -rnH TODO accounting grep --exclude-dir=components -rnH TODO tests publish: git push --tag origin master rm -rf dist/* python setup.py sdist twine upload dist/* ## Testing lint: ./lint.sh test: ./runtests.py tests coverage: coverage run ./runtests.py --with-xunit tests coverage xml -i # This target is run on Travis.ci. We lint, test and build. # We don't call 'install' first as that is run as a separate part # of the Travis build process. travis: lint coverage ## Install / Upgrade install: pip install -r requirements.txt upgrade: pip install --upgrade -r requirements.txt ================================================ FILE: README.rst ================================================ django-accounting ================= In the beginning God created man, and the costs followed afterwards. |Build Status| |Coverage Status| Check the associated project `Accountant `__, a concrete integration of the *django-accounting* application that you can deploy with one click to Heroku. Requirements ------------ - Python 3.x - Django 1.7+ - `dj-static `__ Features supported ------------------ *with inspiration from already existing very good services (Xero, Freshbooks, etc)* Those crossed are not yet supported Books ~~~~~ - **Estimating** generate estimates that can lead to an invoice or not - **Invoicing** generate invoices - **Billing** share the maximum of logic with the invoicing system - **Payments** to track partial/complete payments of invoices and bills - **ExpenseClaim** for employees of organizations that used their personnal accounts - **Dashboard / Current balance** displayed the current balance - **Dashboard / Overdued invoices & bills** to track what's late Clients ~~~~~~~ - **Creation/Update** - [STRIKEOUT:**Deletion** inform the user of the cascade deletion of invoices and bills] - **Professional address** specify the address on the client model - **Linked to organization** to implicitly create the bill when cross-invoicing between organizations Reports ~~~~~~~ - **Profit and Loss** to know how much you've collected, and how much you've spent - **Tax Report** to know how much you need to keep for taxes declarations - **Payroll Report** to know how much you need to keep for payroll taxes - **Invoice details Report** to understand the calculations that lead to the tax report and the payroll report - [STRIKEOUT:**Expense claims Report** to compute what the company owes to whom quickly] Contact ------- | `Pierre Dulac `_ | `@_dulacp `_ License ------- Accounting is available under the MIT license. See the LICENSE file for more info. .. |Build Status| image:: https://travis-ci.org/dulacp/django-accounting.svg :target: https://travis-ci.org/dulacp/django-accounting .. |Coverage Status| image:: https://coveralls.io/repos/dulacp/django-accounting/badge.svg :target: https://coveralls.io/r/dulacp/django-accounting ================================================ FILE: accounting/__init__.py ================================================ import os # Use 'final' as the 4th element to indicate # a full release VERSION = (0, 2, 10) def get_short_version(): return '%s.%s' % (VERSION[0], VERSION[1]) def get_version(): version = '%s.%s' % (VERSION[0], VERSION[1]) # Append 3rd digit if > 0 if VERSION[2]: version = '%s.%s' % (version, VERSION[2]) return version # Cheeky setting that allows each template to be accessible by two paths. # Eg: the template 'accounting/templates/accounting/base.html' can be accessed # via both 'base.html' and 'accounting/base.html'. This allows Accounting's # templates to be extended by templates with the same filename ACCOUNTING_MAIN_TEMPLATE_DIR = os.path.join( os.path.dirname(os.path.abspath(__file__)), 'templates/accounting') ACCOUNTING_APPS = ( 'accounting', 'accounting.libs', 'accounting.apps.connect', 'accounting.apps.people', 'accounting.apps.books', 'accounting.apps.reports', # Third party apps that accounting depends on 'bootstrap3', 'django_select2', 'datetimewidget', ) ACCOUNTING_TEMPLATE_CONTEXT_PROCESSORS = ( 'accounting.apps.context_processors.metadata', 'accounting.apps.books.context_processors.organizations', ) ACCOUNTING_MIDDLEWARE_CLASSES = ( 'accounting.apps.books.middlewares.AutoSelectOrganizationMiddleware', ) def get_apps(): return ACCOUNTING_APPS ================================================ FILE: accounting/apps/__init__.py ================================================ ================================================ FILE: accounting/apps/books/__init__.py ================================================ default_app_config = 'accounting.apps.books.apps.FrenchBooksConfig' ================================================ FILE: accounting/apps/books/admin.py ================================================ from django.contrib import admin from django.contrib.contenttypes.admin import GenericTabularInline from . import models @admin.register(models.Organization) class OrganizationAdmin(admin.ModelAdmin): pass @admin.register(models.TaxRate) class TaxRateAdmin(admin.ModelAdmin): pass class PaymentInline(GenericTabularInline): model = models.Payment extra = 1 class EstimateLineInline(admin.TabularInline): model = models.EstimateLine extra = 1 @admin.register(models.Estimate) class EstimateAdmin(admin.ModelAdmin): inlines = ( EstimateLineInline, ) readonly = ( 'total_incl_tax', 'total_excl_tax', ) class InvoiceLineInline(admin.TabularInline): model = models.InvoiceLine extra = 1 @admin.register(models.Invoice) class InvoiceAdmin(admin.ModelAdmin): inlines = ( InvoiceLineInline, PaymentInline, ) readonly = ( 'total_incl_tax', 'total_excl_tax', ) class BillLineInline(admin.TabularInline): model = models.BillLine extra = 1 @admin.register(models.Bill) class BillAdmin(admin.ModelAdmin): inlines = ( BillLineInline, PaymentInline, ) readonly = ( 'total_incl_tax', 'total_excl_tax', ) class ExpenseClaimLineInline(admin.TabularInline): model = models.ExpenseClaimLine extra = 1 @admin.register(models.ExpenseClaim) class ExpenseClaimAdmin(admin.ModelAdmin): inlines = ( ExpenseClaimLineInline, PaymentInline, ) readonly = ( 'total_incl_tax', 'total_excl_tax', ) @admin.register(models.Payment) class PaymentAdmin(admin.ModelAdmin): pass ================================================ FILE: accounting/apps/books/apps.py ================================================ from django.apps import AppConfig class FrenchBooksConfig(AppConfig): name = "accounting.apps.books" verbose_name = "Accounting Books" ================================================ FILE: accounting/apps/books/calculators.py ================================================ from decimal import Decimal as D from accounting.libs.intervals import TimeInterval class SalePaymentLineProcessed(object): sale = None payment = None amount_excl_tax = None tax_rate = None def __init__(self, sale, payment): self.sale = sale self.payment = payment self.amount_excl_tax = D('0') def process(self): for line in self.sale.lines.all(): tax_rate = line.tax_rate line_factor = line.line_price_incl_tax / self.sale.total_incl_tax portion_amount = self.payment.amount * line_factor portion_amount_excl_tax = portion_amount / (D('1') + tax_rate.rate) if self.tax_rate is None: self.tax_rate = tax_rate elif self.tax_rate.pk != tax_rate.pk: raise NotImplementedError("the system doesn't support " "yet multiple tax rates " "into a same invoice") self.amount_excl_tax += portion_amount_excl_tax class ProfitsLossCalculator(object): """ Compute the profits value in the most effective way considering the sum type (collected / accurial) and the given period of time. """ # TODO support Accurial sum # SUM_TYPE_ACCURIAL = 'accurial' SUM_TYPE_COLLECTED = 'collected' SUM_TYPE_CHOICES = ( # SUM_TYPE_ACCURIAL, SUM_TYPE_COLLECTED, ) organization = None def __init__(self, organization, sum_type=SUM_TYPE_COLLECTED, start=None, end=None): assert sum_type in self.SUM_TYPE_CHOICES, "Not a supported sum type" self.organization = organization self.period = TimeInterval(start=start, end=end) def process_generator(self, sales_queryset): """ Generator that computes the profits/loss for each sale payment objects It's a complex machine because of the partial payments that need to be taken into account with the sum type Collected. So it yield a tuple similar to (sale, payment, amount) """ sales_queryset = sales_queryset.filter(organization=self.organization) if self.period.start: sales_queryset = (sales_queryset .filter(payments__date_paid__gte=self.period.start)) if self.period.end: sales_queryset = (sales_queryset .filter(payments__date_paid__lte=self.period.end)) # optimize the queryset sales_queryset = (sales_queryset .prefetch_related( 'lines', 'lines__tax_rate', 'payments') .distinct()) for sale in sales_queryset: for pay in sale.payments.all(): # NB: even with the queryset filters we can still get payments # outside the period interval [start, end], because # `self.payements.all()` is uncorrelated with the filters if self.period.start and pay.date_paid < self.period.start: continue if self.period.end and pay.date_paid > self.period.end: continue output = SalePaymentLineProcessed(sale, pay) output.process() yield output def total_collected(self): collected = D('0') invoices_queryset = self.organization.invoices.all() for output in self.process_generator(invoices_queryset): collected += output.amount_excl_tax return collected def total_expenses(self): expenses = D('0') bills_queryset = self.organization.bills.all() for output in self.process_generator(bills_queryset): expenses += output.amount_excl_tax return expenses def profits(self): return self.total_collected() - self.total_expenses() ================================================ FILE: accounting/apps/books/context_processors.py ================================================ from .utils import organization_manager def organizations(request): """ Add some generally useful metadata to the template context """ # selected organization orga = organization_manager.get_selected_organization(request) # all user authorized organizations if not request.user or not request.user.is_authenticated(): user_organizations = None else: user = request.user user_organizations = organization_manager.get_user_organizations(user) return { 'user_organizations': user_organizations, 'selected_organization': orga, } ================================================ FILE: accounting/apps/books/forms.py ================================================ from django.forms import ModelForm, BaseInlineFormSet from django.forms.models import inlineformset_factory from .models import ( Organization, TaxRate, Estimate, EstimateLine, Invoice, InvoiceLine, Bill, BillLine, ExpenseClaim, ExpenseClaimLine, Payment) from .utils import organization_manager from accounting.apps.people.models import Client, Employee from accounting.apps.people.forms import UserMultipleChoices from django_select2.fields import AutoModelSelect2Field from datetimewidget.widgets import DateWidget class RequiredFirstInlineFormSet(BaseInlineFormSet): """ Used to make empty formset forms required See http://stackoverflow.com/questions/2406537/django-formsets-\ make-first-required/4951032#4951032 """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if len(self.forms) > 0: first_form = self.forms[0] first_form.empty_permitted = False class SaleInlineLineFormSet(RequiredFirstInlineFormSet): def __init__(self, *args, **kwargs): orga = kwargs.pop('organization') super().__init__(*args, **kwargs) for f in self.forms: f.restrict_to_organization(orga) class ClientForOrganizationChoices(AutoModelSelect2Field): queryset = Client.objects.all() search_fields = ( 'name__icontains', ) def prepare_qs_params(self, request, search_term, search_fields): """restrict to the current selected organization""" params = super().prepare_qs_params(request, search_term, search_fields) orga = organization_manager.get_selected_organization(request) params['and']['organization'] = orga return params class EmployeeForOrganizationChoices(AutoModelSelect2Field): queryset = Employee.objects.all() search_fields = ( 'first_name__icontains', 'last_name__icontains', 'email__icontains', ) def prepare_qs_params(self, request, search_term, search_fields): """restrict to the current selected organization""" params = super().prepare_qs_params(request, search_term, search_fields) orga = organization_manager.get_selected_organization(request) params['and']['organization'] = orga return params class OrganizationForm(ModelForm): members = UserMultipleChoices(required=False) class Meta: model = Organization fields = ( "display_name", "legal_name", "members", ) class TaxRateForm(ModelForm): class Meta: model = TaxRate fields = ( "name", "rate", ) class RestrictLineFormToOrganizationMixin(object): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) instance = kwargs.get('instance', None) if instance: if isinstance(instance, InvoiceLine): organization = instance.invoice.organization elif isinstance(instance, BillLine): organization = instance.bill.organization elif isinstance(instance, ExpenseClaimLine): organization = instance.expense_claim.organization else: raise NotImplementedError("The mixin has been applied to a " "form model that is not supported") self.restrict_to_organization(organization) def restrict_to_organization(self, organization): self.fields['tax_rate'].queryset = organization.tax_rates.all() class EstimateForm(ModelForm): client = ClientForOrganizationChoices() class Meta: model = Estimate fields = ( "number", "client", "date_issued", "date_dued", ) widgets = { 'date_issued': DateWidget( attrs={'id': "id_date_issued"}, options={'clearBtn': 'false'}, usel10n=True, bootstrap_version=3), 'date_dued': DateWidget( attrs={'id': "id_date_dued"}, options={'clearBtn': 'false'}, usel10n=True, bootstrap_version=3), } class EstimateLineForm(RestrictLineFormToOrganizationMixin, ModelForm): class Meta: model = EstimateLine fields = ( "label", "description", "unit_price_excl_tax", "quantity", "tax_rate", ) EstimateLineFormSet = inlineformset_factory(Estimate, EstimateLine, form=EstimateLineForm, formset=SaleInlineLineFormSet, min_num=1, extra=0) class InvoiceForm(ModelForm): client = ClientForOrganizationChoices() class Meta: model = Invoice fields = ( "number", "client", "date_issued", "date_dued", ) widgets = { 'date_issued': DateWidget( attrs={'id': "id_date_issued"}, options={'clearBtn': 'false'}, usel10n=True, bootstrap_version=3), 'date_dued': DateWidget( attrs={'id': "id_date_dued"}, options={'clearBtn': 'false'}, usel10n=True, bootstrap_version=3), } class InvoiceLineForm(RestrictLineFormToOrganizationMixin, ModelForm): class Meta: model = InvoiceLine fields = ( "label", "description", "unit_price_excl_tax", "quantity", "tax_rate", ) InvoiceLineFormSet = inlineformset_factory(Invoice, InvoiceLine, form=InvoiceLineForm, formset=SaleInlineLineFormSet, min_num=1, extra=0) class BillForm(ModelForm): client = ClientForOrganizationChoices() class Meta: model = Bill fields = ( "number", "client", "date_issued", "date_dued", ) widgets = { 'date_issued': DateWidget( attrs={'id': "id_date_issued"}, options={'clearBtn': 'false'}, usel10n=True, bootstrap_version=3), 'date_dued': DateWidget( attrs={'id': "id_date_dued"}, options={'clearBtn': 'false'}, usel10n=True, bootstrap_version=3), } class BillLineForm(RestrictLineFormToOrganizationMixin, ModelForm): class Meta: model = BillLine fields = ( "label", "description", "unit_price_excl_tax", "quantity", "tax_rate", ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) BillLineFormSet = inlineformset_factory(Bill, BillLine, form=BillLineForm, formset=SaleInlineLineFormSet, min_num=1, extra=0) class ExpenseClaimForm(ModelForm): employee = EmployeeForOrganizationChoices() class Meta: model = ExpenseClaim fields = ( "number", "employee", "date_issued", "date_dued", ) widgets = { 'date_issued': DateWidget( attrs={'id': "id_date_issued"}, options={'clearBtn': 'false'}, usel10n=True, bootstrap_version=3), 'date_dued': DateWidget( attrs={'id': "id_date_dued"}, options={'clearBtn': 'false'}, usel10n=True, bootstrap_version=3), } class ExpenseClaimLineForm(RestrictLineFormToOrganizationMixin, ModelForm): class Meta: model = ExpenseClaimLine fields = ( "label", "description", "unit_price_excl_tax", "quantity", "tax_rate", ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) ExpenseClaimLineFormSet = inlineformset_factory(ExpenseClaim, ExpenseClaimLine, form=ExpenseClaimLineForm, formset=SaleInlineLineFormSet, min_num=1, extra=0) class PaymentForm(ModelForm): class Meta: model = Payment fields = ( "amount", "reference", "detail", "date_paid", ) widgets = { 'date_paid': DateWidget( attrs={'id': "id_date_paid"}, options={'clearBtn': 'false'}, usel10n=True, bootstrap_version=3,), } ================================================ FILE: accounting/apps/books/managers.py ================================================ from datetime import date from django.db import models from django.db.models import Sum class TotalQuerySetMixin(object): def _get_total(self, prop): return self.aggregate(sum=Sum(prop))["sum"] def total_paid(self): return self._get_total('payments__amount') class InvoiceQuerySetMixin(object): def dued(self): return self.filter(date_dued__lte=date.today()) class EstimateQuerySet(TotalQuerySetMixin, models.QuerySet): pass class InvoiceQuerySet(TotalQuerySetMixin, InvoiceQuerySetMixin, models.QuerySet): def turnover_excl_tax(self): return self._get_total('total_excl_tax') def turnover_incl_tax(self): return self._get_total('total_incl_tax') class BillQuerySet(TotalQuerySetMixin, InvoiceQuerySetMixin, models.QuerySet): def debts_excl_tax(self): return self._get_total('total_excl_tax') def debts_incl_tax(self): return self._get_total('total_incl_tax') class ExpenseClaimQuerySet(TotalQuerySetMixin, InvoiceQuerySetMixin, models.QuerySet): def debts_excl_tax(self): return self._get_total('total_excl_tax') def debts_incl_tax(self): return self._get_total('total_incl_tax') ================================================ FILE: accounting/apps/books/middlewares.py ================================================ from .utils import organization_manager class AutoSelectOrganizationMiddleware(object): def process_request(self, request): if not request.user or not request.user.is_authenticated(): return orga = organization_manager.get_selected_organization(request) if orga is not None: return user_orgas = organization_manager.get_user_organizations(request.user) if user_orgas.count(): orga = user_orgas.first() organization_manager.set_selected_organization(request, orga) ================================================ FILE: accounting/apps/books/migrations/0001_initial.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations import accounting.apps.books.utils from django.conf import settings import datetime import accounting.libs.checks import django.core.validators from decimal import Decimal class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( name='Organization', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, primary_key=True, auto_created=True)), ('display_name', models.CharField(help_text='Name that you communicate', max_length=150)), ('legal_name', models.CharField(help_text='Official name to appear on your reports, sales invoices and bills', max_length=150)), ('members', models.ManyToManyField(null=True, blank=True, related_name='organizations', to=settings.AUTH_USER_MODEL)), ('owner', models.ForeignKey(related_name='owned_organizations', to=settings.AUTH_USER_MODEL)), ], options={ }, bases=(models.Model,), ), ] ================================================ FILE: accounting/apps/books/migrations/0002_auto_20141029_1606.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations import django.core.validators import accounting.apps.books.utils from decimal import Decimal import datetime import accounting.libs.checks def next_invoice_number(): return 100 class Migration(migrations.Migration): dependencies = [ ('contenttypes', '0001_initial'), ('books', '0001_initial'), ] operations = [ migrations.CreateModel( name='Bill', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('number', models.CharField(default=next_invoice_number, max_length=6)), ('total_incl_tax', models.DecimalField(decimal_places=2, default=Decimal('0'), verbose_name='Total (inc. tax)', max_digits=12)), ('total_excl_tax', models.DecimalField(decimal_places=2, default=Decimal('0'), verbose_name='Total (excl. tax)', max_digits=12)), ('draft', models.BooleanField(default=False)), ('sent', models.BooleanField(default=False)), ('paid', models.BooleanField(default=False)), ('date_issued', models.DateField(default=datetime.date.today)), ('date_dued', models.DateField(null=True, help_text='The date when the total amount should have been collected', verbose_name='Due date', blank=True)), ('date_paid', models.DateField(null=True, blank=True)), ], options={ 'ordering': ('-date_issued', 'id'), }, bases=(accounting.libs.checks.CheckingModelMixin, models.Model), ), migrations.CreateModel( name='BillLine', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('label', models.CharField(max_length=255)), ('description', models.TextField(null=True, blank=True)), ('unit_price_excl_tax', models.DecimalField(decimal_places=2, max_digits=8)), ('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=8)), ], options={ }, bases=(models.Model,), ), migrations.CreateModel( name='Estimate', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('number', models.CharField(default=next_invoice_number, max_length=6)), ('total_incl_tax', models.DecimalField(decimal_places=2, default=Decimal('0'), verbose_name='Total (inc. tax)', max_digits=12)), ('total_excl_tax', models.DecimalField(decimal_places=2, default=Decimal('0'), verbose_name='Total (excl. tax)', max_digits=12)), ('draft', models.BooleanField(default=False)), ('sent', models.BooleanField(default=False)), ('paid', models.BooleanField(default=False)), ('date_issued', models.DateField(default=datetime.date.today)), ('date_dued', models.DateField(null=True, help_text='The date when the total amount should have been collected', verbose_name='Due date', blank=True)), ('date_paid', models.DateField(null=True, blank=True)), ], options={ 'ordering': ('-date_issued', 'id'), }, bases=(accounting.libs.checks.CheckingModelMixin, models.Model), ), migrations.CreateModel( name='EstimateLine', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('label', models.CharField(max_length=255)), ('description', models.TextField(null=True, blank=True)), ('unit_price_excl_tax', models.DecimalField(decimal_places=2, max_digits=8)), ('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=8)), ], options={ }, bases=(models.Model,), ), migrations.CreateModel( name='Invoice', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('number', models.CharField(default=next_invoice_number, max_length=6)), ('total_incl_tax', models.DecimalField(decimal_places=2, default=Decimal('0'), verbose_name='Total (inc. tax)', max_digits=12)), ('total_excl_tax', models.DecimalField(decimal_places=2, default=Decimal('0'), verbose_name='Total (excl. tax)', max_digits=12)), ('draft', models.BooleanField(default=False)), ('sent', models.BooleanField(default=False)), ('paid', models.BooleanField(default=False)), ('date_issued', models.DateField(default=datetime.date.today)), ('date_dued', models.DateField(null=True, help_text='The date when the total amount should have been collected', verbose_name='Due date', blank=True)), ('date_paid', models.DateField(null=True, blank=True)), ], options={ 'ordering': ('-date_issued', 'id'), }, bases=(accounting.libs.checks.CheckingModelMixin, models.Model), ), migrations.CreateModel( name='InvoiceLine', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('label', models.CharField(max_length=255)), ('description', models.TextField(null=True, blank=True)), ('unit_price_excl_tax', models.DecimalField(decimal_places=2, max_digits=8)), ('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=8)), ('invoice', models.ForeignKey(to='books.Invoice', related_name='lines')), ], options={ }, bases=(models.Model,), ), migrations.CreateModel( name='Payment', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('amount', models.DecimalField(decimal_places=2, verbose_name='Amount', max_digits=12)), ('detail', models.CharField(null=True, blank=True, max_length=255)), ('date_paid', models.DateField(default=datetime.date.today)), ('reference', models.CharField(null=True, blank=True, max_length=255)), ('object_id', models.PositiveIntegerField()), ('content_type', models.ForeignKey(to='contenttypes.ContentType')), ], options={ 'ordering': ('-date_paid',), }, bases=(models.Model,), ), migrations.CreateModel( name='TaxRate', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(max_length=50)), ('rate', models.DecimalField(decimal_places=5, validators=[django.core.validators.MinValueValidator(Decimal('0')), django.core.validators.MaxValueValidator(Decimal('1'))], max_digits=6)), ('organization', models.ForeignKey(related_name='tax_rates', to='books.Organization', verbose_name='Attached to Organization')), ], options={ }, bases=(models.Model,), ), migrations.AddField( model_name='invoiceline', name='tax_rate', field=models.ForeignKey(to='books.TaxRate'), preserve_default=True, ), ] ================================================ FILE: accounting/apps/books/migrations/0003_auto_20141029_1606.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ ('books', '0002_auto_20141029_1606'), ('people', '0001_initial'), ] operations = [ migrations.AddField( model_name='invoice', name='client', field=models.ForeignKey(to='people.Client', verbose_name='To Client'), preserve_default=True, ), migrations.AddField( model_name='invoice', name='organization', field=models.ForeignKey(related_name='invoices', to='books.Organization', verbose_name='From Organization'), preserve_default=True, ), migrations.AlterUniqueTogether( name='invoice', unique_together=set([('number', 'organization')]), ), migrations.AddField( model_name='estimateline', name='invoice', field=models.ForeignKey(to='books.Estimate', related_name='lines'), preserve_default=True, ), migrations.AddField( model_name='estimateline', name='tax_rate', field=models.ForeignKey(to='books.TaxRate'), preserve_default=True, ), migrations.AddField( model_name='estimate', name='client', field=models.ForeignKey(to='people.Client', verbose_name='To Client'), preserve_default=True, ), migrations.AddField( model_name='estimate', name='organization', field=models.ForeignKey(related_name='estimates', to='books.Organization', verbose_name='From Organization'), preserve_default=True, ), migrations.AlterUniqueTogether( name='estimate', unique_together=set([('number', 'organization')]), ), migrations.AddField( model_name='billline', name='bill', field=models.ForeignKey(to='books.Bill', related_name='lines'), preserve_default=True, ), migrations.AddField( model_name='billline', name='tax_rate', field=models.ForeignKey(to='books.TaxRate'), preserve_default=True, ), migrations.AddField( model_name='bill', name='client', field=models.ForeignKey(to='people.Client', verbose_name='From Client'), preserve_default=True, ), migrations.AddField( model_name='bill', name='organization', field=models.ForeignKey(related_name='bills', to='books.Organization', verbose_name='To Organization'), preserve_default=True, ), migrations.AlterUniqueTogether( name='bill', unique_together=set([('number', 'organization')]), ), ] ================================================ FILE: accounting/apps/books/migrations/0004_auto_20141104_1026.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations import accounting.apps.books.utils def next_invoice_number(): return 100 class Migration(migrations.Migration): dependencies = [ ('books', '0003_auto_20141029_1606'), ] operations = [ migrations.AlterModelOptions( name='bill', options={'ordering': ('-number',)}, ), migrations.AlterModelOptions( name='estimate', options={'ordering': ('-number',)}, ), migrations.AlterModelOptions( name='invoice', options={'ordering': ('-number',)}, ), migrations.AlterField( model_name='bill', name='number', field=models.CharField(max_length=6, db_index=True, default=next_invoice_number), ), migrations.AlterField( model_name='estimate', name='number', field=models.CharField(max_length=6, db_index=True, default=next_invoice_number), ), migrations.AlterField( model_name='invoice', name='number', field=models.CharField(max_length=6, db_index=True, default=next_invoice_number), ), ] ================================================ FILE: accounting/apps/books/migrations/0005_auto_20150128_1458.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ ('books', '0004_auto_20141104_1026'), ] operations = [ migrations.AlterField( model_name='bill', name='number', field=models.CharField(default=1, max_length=6, db_index=True), ), migrations.AlterField( model_name='estimate', name='number', field=models.CharField(default=1, max_length=6, db_index=True), ), migrations.AlterField( model_name='invoice', name='number', field=models.CharField(default=1, max_length=6, db_index=True), ), ] ================================================ FILE: accounting/apps/books/migrations/0006_auto_20150206_1836.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ ('books', '0005_auto_20150128_1458'), ] operations = [ migrations.AddField( model_name='bill', name='number_int', field=models.IntegerField(default=1, db_index=True), preserve_default=True, ), migrations.AddField( model_name='estimate', name='number_int', field=models.IntegerField(default=1, db_index=True), preserve_default=True, ), migrations.AddField( model_name='invoice', name='number_int', field=models.IntegerField(default=1, db_index=True), preserve_default=True, ), ] ================================================ FILE: accounting/apps/books/migrations/0007_auto_20150206_1836.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations def _migrate_sale_numbers(sales): for s in sales: s.number_int = int(s.number.strip()) s.save() def migrate_estimate_numbers(apps, schema_editor): Estimate = apps.get_model("books", "Estimate") _migrate_sale_numbers(Estimate.objects.all()) def migrate_invoice_numbers(apps, schema_editor): Invoice = apps.get_model("books", "Invoice") _migrate_sale_numbers(Invoice.objects.all()) def migrate_bill_numbers(apps, schema_editor): Bill = apps.get_model("books", "Bill") _migrate_sale_numbers(Bill.objects.all()) class Migration(migrations.Migration): dependencies = [ ('books', '0006_auto_20150206_1836'), ] operations = [ migrations.RunPython(migrate_estimate_numbers), migrations.RunPython(migrate_invoice_numbers), migrations.RunPython(migrate_bill_numbers) ] ================================================ FILE: accounting/apps/books/migrations/0008_auto_20150206_1843.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ ('books', '0007_auto_20150206_1836'), ] operations = [ migrations.AlterModelOptions( name='bill', options={'ordering': ('-number_int',)}, ), migrations.AlterModelOptions( name='estimate', options={'ordering': ('-number_int',)}, ), migrations.AlterModelOptions( name='invoice', options={'ordering': ('-number_int',)}, ), migrations.AlterUniqueTogether( name='bill', unique_together=set([('number_int', 'organization')]), ), migrations.RemoveField( model_name='bill', name='number', ), migrations.AlterUniqueTogether( name='estimate', unique_together=set([('number_int', 'organization')]), ), migrations.RemoveField( model_name='estimate', name='number', ), migrations.AlterUniqueTogether( name='invoice', unique_together=set([('number_int', 'organization')]), ), migrations.RemoveField( model_name='invoice', name='number', ), ] ================================================ FILE: accounting/apps/books/migrations/0009_auto_20150206_1850.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ ('books', '0008_auto_20150206_1843'), ] operations = [ migrations.AlterModelOptions( name='bill', options={'ordering': ('-number',)}, ), migrations.AlterModelOptions( name='estimate', options={'ordering': ('-number',)}, ), migrations.AlterModelOptions( name='invoice', options={'ordering': ('-number',)}, ), migrations.RenameField( model_name='bill', old_name='number_int', new_name='number', ), migrations.RenameField( model_name='estimate', old_name='number_int', new_name='number', ), migrations.RenameField( model_name='invoice', old_name='number_int', new_name='number', ), migrations.AlterUniqueTogether( name='bill', unique_together=set([('number', 'organization')]), ), migrations.AlterUniqueTogether( name='estimate', unique_together=set([('number', 'organization')]), ), migrations.AlterUniqueTogether( name='invoice', unique_together=set([('number', 'organization')]), ), ] ================================================ FILE: accounting/apps/books/migrations/0010_auto_20150210_1449.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ ('books', '0009_auto_20150206_1850'), ] operations = [ migrations.RemoveField( model_name='bill', name='draft', ), migrations.RemoveField( model_name='bill', name='paid', ), migrations.RemoveField( model_name='bill', name='sent', ), migrations.RemoveField( model_name='estimate', name='draft', ), migrations.RemoveField( model_name='estimate', name='paid', ), migrations.RemoveField( model_name='estimate', name='sent', ), migrations.RemoveField( model_name='invoice', name='draft', ), migrations.RemoveField( model_name='invoice', name='paid', ), migrations.RemoveField( model_name='invoice', name='sent', ), ] ================================================ FILE: accounting/apps/books/migrations/0011_auto_20150324_1430.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations from decimal import Decimal import datetime import accounting.libs.checks class Migration(migrations.Migration): dependencies = [ ('people', '0005_auto_20141029_2308'), ('books', '0010_auto_20150210_1449'), ] operations = [ migrations.CreateModel( name='ExpenseClaim', fields=[ ('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')), ('number', models.IntegerField(db_index=True, default=1)), ('total_incl_tax', models.DecimalField(verbose_name='Total (inc. tax)', max_digits=12, default=Decimal('0'), decimal_places=2)), ('total_excl_tax', models.DecimalField(verbose_name='Total (excl. tax)', max_digits=12, default=Decimal('0'), decimal_places=2)), ('date_issued', models.DateField(default=datetime.date.today)), ('date_dued', models.DateField(verbose_name='Due date', blank=True, null=True, help_text='The date when the total amount should have been collected')), ('date_paid', models.DateField(blank=True, null=True)), ('employee', models.ForeignKey(verbose_name='Paid by employee', to='people.Employee')), ('organization', models.ForeignKey(related_name='expense_claims', to='books.Organization', verbose_name='From Organization')), ], options={ 'ordering': ('-number',), }, bases=(accounting.libs.checks.CheckingModelMixin, models.Model), ), migrations.CreateModel( name='ExpenseClaimLine', fields=[ ('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')), ('label', models.CharField(max_length=255)), ('description', models.TextField(blank=True, null=True)), ('unit_price_excl_tax', models.DecimalField(max_digits=8, decimal_places=2)), ('quantity', models.DecimalField(max_digits=8, default=1, decimal_places=2)), ('expense_claim', models.ForeignKey(related_name='lines', to='books.ExpenseClaim')), ('tax_rate', models.ForeignKey(to='books.TaxRate')), ], options={ }, bases=(models.Model,), ), migrations.AlterUniqueTogether( name='expenseclaim', unique_together=set([('number', 'organization')]), ), ] ================================================ FILE: accounting/apps/books/migrations/__init__.py ================================================ ================================================ FILE: accounting/apps/books/mixins.py ================================================ from django.db.models.fields import FieldDoesNotExist from django.views import generic from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse from .utils import organization_manager class RestrictToSelectedOrganizationQuerySetMixin(object): """ To restrict objects to the current selected organization """ def get_restriction_filters(self): # check for the field meta = self.model._meta field, model, direct, m2m = meta.get_field_by_name('organization') # build the restriction orga = organization_manager.get_selected_organization(self.request) return {field.name: orga.pk} def get_queryset(self): filters = self.get_restriction_filters() queryset = super().get_queryset() queryset = queryset.filter(**filters) return queryset def get(self, request, *args, **kwargs): orga = organization_manager.get_selected_organization(request) if orga is None: return HttpResponseRedirect(reverse('books:organization-selector')) return super().get(request, *args, **kwargs) class RestrictToOrganizationFormRelationsMixin(object): """ To restrict relations choices to the organization linked instances """ relation_name = 'organization' def _restrict_fields_choices(self, model, organization, fields): for source in fields: field, m, direct, m2m = model._meta.get_field_by_name(source) rel = field.rel if not rel: # next field continue rel_model = rel.to try: rel_model._meta.get_field_by_name(self.relation_name) except FieldDoesNotExist: # next field continue form_field = fields[source] form_field.queryset = (form_field.choices.queryset .filter(**{self.relation_name: organization})) def restrict_fields_choices_to_organization(self, form, organization): assert organization is not None, "no organization to restrict to" model = form._meta.model self._restrict_fields_choices(model, organization, form.fields) class SaleListQuerySetMixin(object): def get_queryset(self): queryset = super().get_queryset() queryset = (queryset .select_related( 'organization') .prefetch_related( 'lines', 'lines__tax_rate')) try: # to raise the exception self.model._meta.get_field_by_name('client') queryset = queryset.select_related('client') except FieldDoesNotExist: pass try: # to raise the exception self.model._meta.get_field_by_name('payments') queryset = queryset.prefetch_related('payments') except FieldDoesNotExist: pass return queryset class AutoSetSelectedOrganizationMixin(object): def form_valid(self, form): obj = form.save(commit=False) orga = organization_manager.get_selected_organization(self.request) obj.organization = orga return super().form_valid(form) class AbstractSaleCreateUpdateMixin(RestrictToOrganizationFormRelationsMixin, object): formset_class = None def get_context_data(self, **kwargs): assert self.formset_class is not None, "No formset class specified" context = super().get_context_data(**kwargs) orga = organization_manager.get_selected_organization(self.request) if self.request.POST: context['line_formset'] = self.formset_class( self.request.POST, instance=self.object, organization=orga) else: context['line_formset'] = self.formset_class( instance=self.object, organization=orga) return context def get_form(self, form_class=None): """Restrict the form relations to the current organization""" form = super().get_form(form_class) orga = organization_manager.get_selected_organization(self.request) self.restrict_fields_choices_to_organization(form, orga) return form def form_valid(self, form): context = self.get_context_data() line_formset = context['line_formset'] if not line_formset.is_valid(): return super().form_invalid(form) self.object = form.save() line_formset.instance = self.object line_formset.save() # update totals self.object.compute_totals() return super().form_valid(form) class AbstractSaleDetailMixin(object): def get_queryset(self): queryset = super().get_queryset() queryset = queryset.select_related('organization') try: # to raise the exception self.model._meta.get_field_by_name('client') queryset = queryset.select_related('client') except FieldDoesNotExist: pass return queryset def get_object(self): # save some db queries by caching the fetched object if hasattr(self, '_object'): return getattr(self, '_object') obj = super().get_object() setattr(self, '_object', obj) return obj def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) obj = self.get_object() ctx["checklist"] = obj.full_check() ctx["lines"] = (obj.lines.all() .select_related( 'tax_rate')) return ctx class PaymentFormMixin(generic.edit.FormMixin): payment_form_class = None def get_context_data(self, **kwargs): assert self.payment_form_class is not None, \ "No formset class specified" self.object = self.get_object() context = super().get_context_data(**kwargs) form = self.get_form(self.payment_form_class) context['payment_form'] = form return context def post(self, request, *args, **kwargs): """ Handles POST requests, instantiating a form instance with the passed POST variables and then checked for validity. """ form = self.get_form(self.payment_form_class) if form.is_valid(): return self.form_valid(form) else: return self.form_invalid(form) def form_valid(self, form): self.object = self.get_object() # save payment payment = form.save(commit=False) payment.content_object = self.object payment.save() return super().form_valid(form) ================================================ FILE: accounting/apps/books/models.py ================================================ from decimal import Decimal as D from datetime import date from django.conf import settings from django.db import models from django.core.urlresolvers import reverse from django.core.validators import MaxValueValidator, MinValueValidator from django.contrib.contenttypes.fields import ( GenericForeignKey, GenericRelation) from django.contrib.contenttypes.models import ContentType from django.utils import timezone from accounting.libs import prices from accounting.libs.checks import CheckingModelMixin from accounting.libs.templatetags.currency_filters import currency_formatter from accounting.libs.templatetags.format_filters import percentage_formatter from .managers import ( EstimateQuerySet, InvoiceQuerySet, BillQuerySet, ExpenseClaimQuerySet) TWO_PLACES = D(10) ** -2 class Organization(models.Model): display_name = models.CharField(max_length=150, help_text="Name that you communicate") legal_name = models.CharField(max_length=150, help_text="Official name to appear on your reports, sales " "invoices and bills") owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="owned_organizations") members = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="organizations", blank=True, null=True) class Meta: pass def __str__(self): return self.legal_name def get_absolute_url(self): return reverse('books:organization-detail', args=[self.pk]) @property def turnover_excl_tax(self): return self.invoices.turnover_excl_tax() or D('0.00') @property def turnover_incl_tax(self): return self.invoices.turnover_incl_tax() or D('0.00') @property def debts_excl_tax(self): return self.bills.debts_excl_tax() or D('0.00') @property def debts_incl_tax(self): return self.bills.debts_incl_tax() or D('0.00') @property def profits(self): return self.turnover_excl_tax - self.debts_excl_tax @property def collected_tax(self): return self.turnover_incl_tax - self.turnover_excl_tax @property def deductible_tax(self): return self.debts_incl_tax - self.debts_excl_tax @property def tax_provisionning(self): return self.collected_tax - self.deductible_tax @property def overdue_total(self): due_invoices = self.invoices.dued() due_turnonver = due_invoices.turnover_incl_tax() total_paid = due_invoices.total_paid() return due_turnonver - total_paid class TaxRate(models.Model): """ Every transaction line item needs a Tax Rate. Tax Rates can have multiple Tax Components. For instance, you can have an item that is charged a Tax Rate called "City Import Tax (8%)" that has two components: - a city tax of 5% - an import tax of 3%. *inspired by Xero* """ organization = models.ForeignKey('books.Organization', related_name="tax_rates", verbose_name="Attached to Organization") name = models.CharField(max_length=50) rate = models.DecimalField(max_digits=6, decimal_places=5, validators=[MinValueValidator(D('0')), MaxValueValidator(D('1'))]) class Meta: pass def __str__(self): return "{} ({})".format(self.name, percentage_formatter(self.rate)) class AbstractSale(CheckingModelMixin, models.Model): number = models.IntegerField(default=1, db_index=True) # Total price needs to be stored with and wihtout taxes # because the tax percentage can vary depending on the associated lines total_incl_tax = models.DecimalField("Total (inc. tax)", decimal_places=2, max_digits=12, default=D('0')) total_excl_tax = models.DecimalField("Total (excl. tax)", decimal_places=2, max_digits=12, default=D('0')) # tracking date_issued = models.DateField(default=date.today) date_dued = models.DateField("Due date", blank=True, null=True, help_text="The date when the total amount " "should have been collected") date_paid = models.DateField(blank=True, null=True) class Meta: abstract = True class CheckingOptions: fields = ( 'total_incl_tax', 'total_excl_tax', 'date_dued', ) def __str__(self): return "#{} ({})".format(self.number, self.total_incl_tax) def get_detail_url(self): raise NotImplementedError def get_edit_url(self): raise NotImplementedError def compute_totals(self): self.total_excl_tax = self.get_total_excl_tax() self.total_incl_tax = self.get_total_incl_tax() def _get_total(self, prop): """ For executing a named method on each line of the basket and returning the total. """ total = D('0.00') line_queryset = self.lines.all() for line in line_queryset: total = total + getattr(line, prop) return total @property def total_tax(self): return self.total_incl_tax - self.total_excl_tax def get_total_excl_tax(self): return self._get_total('line_price_excl_tax') def get_total_incl_tax(self): return self._get_total('line_price_incl_tax') @property def total_paid(self): total = D('0') for p in self.payments.all(): total += p.amount return total @property def total_due_incl_tax(self): due = self.total_incl_tax due -= self.total_paid return due def is_fully_paid(self): paid = self.total_paid.quantize(TWO_PLACES) total = self.total_incl_tax.quantize(TWO_PLACES) return paid >= total def is_partially_paid(self): paid = self.total_paid.quantize(TWO_PLACES) total = self.total_incl_tax.quantize(TWO_PLACES) return paid and paid > 0 and paid < total @property def payroll_taxes(self): # TODO implement collected/accurial paid = self.total_paid payroll = D('0') for emp in self.organization.employees.all(): if not emp.salary_follows_profits: continue payroll += paid * emp.shares_percentage * emp.payroll_tax_rate return payroll def _check_total(self, check, total, computed_total): if total.quantize(TWO_PLACES) != computed_total.quantize(TWO_PLACES): check.mark_fail(level=check.LEVEL_ERROR, message="The computed amount isn't correct, it " "should be {}, please edit and save the " "{} to fix it.".format( currency_formatter(total), self._meta.verbose_name)) else: check.mark_pass() return check def check_total_excl_tax(self, check): total = self.get_total_excl_tax() return self._check_total(check, total, self.total_excl_tax) def check_total_incl_tax(self, check): total = self.get_total_incl_tax() return self._check_total(check, total, self.total_incl_tax) def check_date_dued(self, check): if self.date_dued is None: check.mark_fail(message="No due date specified") return check if self.total_excl_tax == D('0'): check.mark_fail(message="The invoice has no value") return check if self.is_fully_paid(): last_payment = self.payments.all().first() formatted_date = last_payment.date_paid.strftime('%B %d, %Y') check.mark_pass(message="Has been paid on the {}" .format(formatted_date)) return check if timezone.now().date() > self.date_dued: check.mark_fail(message="The due date has been exceeded.") else: check.mark_pass() return check class AbstractSaleLine(models.Model): label = models.CharField(max_length=255) description = models.TextField(blank=True, null=True) unit_price_excl_tax = models.DecimalField(max_digits=8, decimal_places=2) quantity = models.DecimalField(max_digits=8, decimal_places=2, default=1) class Meta: abstract = True def __str__(self): return self.label @property def unit_price(self): """Returns the `Price` instance representing the instance""" unit = self.unit_price_excl_tax tax = unit * self.tax_rate.rate p = prices.Price(settings.ACCOUNTING_DEFAULT_CURRENCY, unit, tax=tax) return p @property def line_price_excl_tax(self): return self.quantity * self.unit_price.excl_tax @property def line_price_incl_tax(self): return self.quantity * self.unit_price.incl_tax @property def taxes(self): return self.line_price_incl_tax - self.line_price_excl_tax def from_client(self): raise NotImplementedError def to_client(self): raise NotImplementedError class Estimate(AbstractSale): organization = models.ForeignKey('books.Organization', related_name="estimates", verbose_name="From Organization") client = models.ForeignKey('people.Client', verbose_name="To Client") objects = EstimateQuerySet.as_manager() class Meta: unique_together = (("number", "organization"),) ordering = ('-number',) def get_detail_url(self): return reverse('books:estimate-detail', args=[self.pk]) def get_edit_url(self): return reverse('books:estimate-edit', args=[self.pk]) def from_client(self): return self.organization def to_client(self): return self.client class EstimateLine(AbstractSaleLine): invoice = models.ForeignKey('books.Estimate', related_name="lines") tax_rate = models.ForeignKey('books.TaxRate') class Meta: pass class Invoice(AbstractSale): organization = models.ForeignKey('books.Organization', related_name="invoices", verbose_name="From Organization") client = models.ForeignKey('people.Client', verbose_name="To Client") payments = GenericRelation('books.Payment') objects = InvoiceQuerySet.as_manager() class Meta: unique_together = (("number", "organization"),) ordering = ('-number',) def get_detail_url(self): return reverse('books:invoice-detail', args=[self.pk]) def get_edit_url(self): return reverse('books:invoice-edit', args=[self.pk]) def from_client(self): return self.organization def to_client(self): return self.client class InvoiceLine(AbstractSaleLine): invoice = models.ForeignKey('books.Invoice', related_name="lines") tax_rate = models.ForeignKey('books.TaxRate') class Meta: pass class Bill(AbstractSale): organization = models.ForeignKey('books.Organization', related_name="bills", verbose_name="To Organization") client = models.ForeignKey('people.Client', verbose_name="From Client") payments = GenericRelation('books.Payment') objects = BillQuerySet.as_manager() class Meta: unique_together = (("number", "organization"),) ordering = ('-number',) def get_detail_url(self): return reverse('books:bill-detail', args=[self.pk]) def get_edit_url(self): return reverse('books:bill-edit', args=[self.pk]) def from_client(self): return self.client def to_client(self): return self.organization class BillLine(AbstractSaleLine): bill = models.ForeignKey('books.Bill', related_name="lines") tax_rate = models.ForeignKey('books.TaxRate') class Meta: pass class ExpenseClaim(AbstractSale): organization = models.ForeignKey('books.Organization', related_name="expense_claims", verbose_name="From Organization") employee = models.ForeignKey('people.Employee', verbose_name="Paid by employee") payments = GenericRelation('books.Payment') objects = ExpenseClaimQuerySet.as_manager() class Meta: unique_together = (("number", "organization"),) ordering = ('-number',) def get_detail_url(self): return reverse('books:expense_claim-detail', args=[self.pk]) def get_edit_url(self): return reverse('books:expense_claim-edit', args=[self.pk]) def from_client(self): return self.employee def to_client(self): return self.organization class ExpenseClaimLine(AbstractSaleLine): expense_claim = models.ForeignKey('books.ExpenseClaim', related_name="lines") tax_rate = models.ForeignKey('books.TaxRate') class Meta: pass class Payment(models.Model): amount = models.DecimalField("Amount", decimal_places=2, max_digits=12) detail = models.CharField(max_length=255, blank=True, null=True) date_paid = models.DateField(default=date.today) reference = models.CharField(max_length=255, blank=True, null=True) # relationship to an object content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') class Meta: ordering = ('-date_paid',) def __str__(self): if self.detail: return self.detail return "Payment of {}".format(currency_formatter(self.amount)) ================================================ FILE: accounting/apps/books/templatetags/__init__.py ================================================ ================================================ FILE: accounting/apps/books/templatetags/status_filters.py ================================================ from django import template register = template.Library() @register.filter('status_to_css_classname') def _invoice_or_bill_status_to_classname(invoice_or_bill): """ Return the appropriated css classname for the invoice/bill status """ if not invoice_or_bill.pass_full_checking(): checks = invoice_or_bill.full_check() for c in checks: if c.level == c.LEVEL_ERROR: return 'danger' return 'warning' if invoice_or_bill.is_fully_paid(): return 'success' elif invoice_or_bill.is_partially_paid(): return 'info' else: return '' ================================================ FILE: accounting/apps/books/urls.py ================================================ from django.conf.urls import patterns, url from . import views urlpatterns = patterns('', url(r'^$', views.DashboardView.as_view(), name="dashboard"), # Organizations url(r'^organization/$', views.OrganizationListView.as_view(), name="organization-list"), url(r'^organization/selector/$', views.OrganizationSelectorView.as_view(), name="organization-selector"), url(r'^organization/create/$', views.OrganizationCreateView.as_view(), name="organization-create"), url(r'^organization/(?P\d+)/edit/$', views.OrganizationUpdateView.as_view(), name="organization-edit"), url(r'^organization/(?P\d+)/detail/$', views.OrganizationDetailView.as_view(), name="organization-detail"), url(r'^organization/(?P\d+)/select/$', views.OrganizationSelectionView.as_view(), name="organization-select"), # Tax Rates url(r'^tax_rates/$', views.TaxRateListView.as_view(), name="tax_rate-list"), url(r'^tax_rates/create/$', views.TaxRateCreateView.as_view(), name="tax_rate-create"), url(r'^tax_rates/(?P\d+)/edit/$', views.TaxRateUpdateView.as_view(), name="tax_rate-edit"), url(r'^tax_rates/(?P\d+)/delete/$', views.TaxRateDeleteView.as_view(), name="tax_rate-delete"), # Estimates url(r'^estimate/$', views.EstimateListView.as_view(), name="estimate-list"), url(r'^estimate/create/$', views.EstimateCreateView.as_view(), name="estimate-create"), url(r'^estimate/(?P\d+)/edit/$', views.EstimateUpdateView.as_view(), name="estimate-edit"), url(r'^estimate/(?P\d+)/delete/$', views.EstimateDeleteView.as_view(), name="estimate-delete"), url(r'^estimate/(?P\d+)/detail/$', views.EstimateDetailView.as_view(), name="estimate-detail"), # Invoices url(r'^invoice/$', views.InvoiceListView.as_view(), name="invoice-list"), url(r'^invoice/create/$', views.InvoiceCreateView.as_view(), name="invoice-create"), url(r'^invoice/(?P\d+)/edit/$', views.InvoiceUpdateView.as_view(), name="invoice-edit"), url(r'^invoice/(?P\d+)/delete/$', views.InvoiceDeleteView.as_view(), name="invoice-delete"), url(r'^invoice/(?P\d+)/detail/$', views.InvoiceDetailView.as_view(), name="invoice-detail"), # Bills url(r'^bill/$', views.BillListView.as_view(), name="bill-list"), url(r'^bill/create/$', views.BillCreateView.as_view(), name="bill-create"), url(r'^bill/(?P\d+)/edit/$', views.BillUpdateView.as_view(), name="bill-edit"), url(r'^bill/(?P\d+)/delete/$', views.BillDeleteView.as_view(), name="bill-delete"), url(r'^bill/(?P\d+)/detail/$', views.BillDetailView.as_view(), name="bill-detail"), # ExpenseClaims url(r'^expense-claim/$', views.ExpenseClaimListView.as_view(), name="expense_claim-list"), url(r'^expense-claim/create/$', views.ExpenseClaimCreateView.as_view(), name="expense_claim-create"), url(r'^expense-claim/(?P\d+)/edit/$', views.ExpenseClaimUpdateView.as_view(), name="expense_claim-edit"), url(r'^expense-claim/(?P\d+)/delete/$', views.ExpenseClaimDeleteView.as_view(), name="expense_claim-delete"), url(r'^expense-claim/(?P\d+)/detail/$', views.ExpenseClaimDetailView.as_view(), name="expense_claim-detail"), # Payments url(r'^payment/(?P\d+)/edit/$', views.PaymentUpdateView.as_view(), name="payment-edit"), url(r'^payment/(?P\d+)/delete/$', views.PaymentDeleteView.as_view(), name="payment-delete"), ) ================================================ FILE: accounting/apps/books/utils.py ================================================ from django.db.models import Q class OrganizationManager(object): selected_organization_key = 'selected_organization_pk' def get_user_organizations(self, user): # To avoid circular imports from .models import Organization orgas = (Organization.objects .filter(Q(members=user) | Q(owner=user)) .distinct()) return orgas def set_selected_organization(self, request, organization): key = self.selected_organization_key request.session[key] = organization.pk def get_selected_organization(self, request): key = self.selected_organization_key if key not in request.session: return # To avoid circular imports from .models import Organization pk = request.session[key] organization = Organization.objects.get(pk=pk) return organization organization_manager = OrganizationManager() class BaseNumberGenerator(object): """ Simple object for generating sale numbers. """ def next_number(self, organization): raise NotImplementedError class EstimateNumberGenerator(BaseNumberGenerator): def next_number(self, organization): last = organization.estimates.all().order_by('-number').first() if last is not None: last_number = int(last.number) else: last_number = 0 return last_number + 1 class InvoiceNumberGenerator(BaseNumberGenerator): def next_number(self, organization): last = organization.invoices.all().order_by('-number').first() if last is not None: last_number = int(last.number) else: last_number = 0 return last_number + 1 class BillNumberGenerator(BaseNumberGenerator): def next_number(self, organization): last = organization.bills.all().order_by('-number').first() if last is not None: last_number = int(last.number) else: last_number = 0 return last_number + 1 class ExpenseClaimNumberGenerator(BaseNumberGenerator): def next_number(self, organization): last = organization.expense_claims.all().order_by('-number').first() if last is not None: last_number = int(last.number) else: last_number = 0 return last_number + 1 ================================================ FILE: accounting/apps/books/views.py ================================================ import logging from decimal import Decimal as D from django.views import generic from django.core.urlresolvers import reverse, reverse_lazy from django.db.models import Sum from django.http import HttpResponseRedirect from .mixins import ( RestrictToSelectedOrganizationQuerySetMixin, SaleListQuerySetMixin, AutoSetSelectedOrganizationMixin, AbstractSaleCreateUpdateMixin, AbstractSaleDetailMixin, PaymentFormMixin) from .models import ( Organization, TaxRate, Estimate, Invoice, Bill, ExpenseClaim, Payment) from .forms import ( OrganizationForm, TaxRateForm, EstimateForm, EstimateLineFormSet, InvoiceForm, InvoiceLineFormSet, BillForm, BillLineFormSet, ExpenseClaimForm, ExpenseClaimLineFormSet, PaymentForm) from .utils import ( organization_manager, EstimateNumberGenerator, InvoiceNumberGenerator, BillNumberGenerator, ExpenseClaimNumberGenerator) logger = logging.getLogger(__name__) class OrganizationSelectorView(generic.TemplateView): template_name = "books/organization_selector.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) user = self.request.user orgas = organization_manager.get_user_organizations(user) cumulated_turnovers = (orgas .aggregate(sum=Sum('invoices__total_excl_tax'))["sum"]) or D('0') cumulated_debts = (orgas .aggregate(sum=Sum('bills__total_excl_tax'))["sum"]) or D('0') cumulated_profits = cumulated_turnovers - cumulated_debts context["organizations_count"] = orgas.count() context["organizations_cumulated_turnovers"] = cumulated_turnovers context["organizations_cumulated_profits"] = cumulated_profits context["organizations_cumulated_active_days"] = 0 context["organizations"] = orgas context["last_invoices"] = Invoice.objects.all()[:10] return context class DashboardView(generic.DetailView): template_name = "books/dashboard.html" model = Organization context_object_name = "organization" def get_object(self): return organization_manager.get_selected_organization(self.request) def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) organization = self.get_object() ctx['invoices'] = (organization.invoices.all() .select_related( 'client', 'organization') .prefetch_related( 'lines', 'lines__tax_rate', 'payments') .distinct()) ctx['bills'] = (organization.bills.all() .select_related( 'client', 'organization') .prefetch_related( 'lines', 'lines__tax_rate', 'payments') .distinct()) return ctx def get(self, request, *args, **kwargs): orga = organization_manager.get_selected_organization(self.request) if orga is None: return HttpResponseRedirect(reverse('books:organization-selector')) return super().get(request, *args, **kwargs) class OrganizationListView(generic.ListView): template_name = "books/organization_list.html" model = Organization context_object_name = "organizations" def get_queryset(self): # only current authenticated user organizations return organization_manager.get_user_organizations(self.request.user) class OrganizationCreateView(generic.CreateView): template_name = "books/organization_create_or_update.html" model = Organization form_class = OrganizationForm success_url = reverse_lazy("books:organization-list") def form_valid(self, form): obj = form.save(commit=False) obj.owner = self.request.user return super().form_valid(form) class OrganizationUpdateView(generic.UpdateView): template_name = "books/organization_create_or_update.html" model = Organization form_class = OrganizationForm success_url = reverse_lazy("books:organization-list") def get_queryset(self): # only current authenticated user organizations return organization_manager.get_user_organizations(self.request.user) class OrganizationDetailView(generic.DetailView): template_name = "books/organization_detail.html" model = Organization context_object_name = "organization" def get_queryset(self): # only current authenticated user organizations return organization_manager.get_user_organizations(self.request.user) def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) organization = self.get_object() ctx['invoices'] = (organization.invoices.all() .select_related('client', 'organization') .prefetch_related('lines')) ctx['bills'] = (organization.bills.all() .select_related('client', 'organization') .prefetch_related('lines')) return ctx class OrganizationSelectionView(generic.DetailView): model = Organization def get_queryset(self): # only current authenticated user organizations return organization_manager.get_user_organizations(self.request.user) def post(self, request, *args, **kwargs): orga = self.get_object() organization_manager.set_selected_organization(self.request, orga) return HttpResponseRedirect(reverse('books:dashboard')) class TaxRateListView(RestrictToSelectedOrganizationQuerySetMixin, generic.ListView): template_name = "books/tax_rate_list.html" model = TaxRate context_object_name = "tax_rates" class TaxRateCreateView(AutoSetSelectedOrganizationMixin, generic.CreateView): template_name = "books/tax_rate_create_or_update.html" model = TaxRate form_class = TaxRateForm success_url = reverse_lazy("books:tax_rate-list") class TaxRateUpdateView(AutoSetSelectedOrganizationMixin, generic.UpdateView): template_name = "books/tax_rate_create_or_update.html" model = TaxRate form_class = TaxRateForm success_url = reverse_lazy("books:tax_rate-list") class TaxRateDeleteView(generic.DeleteView): template_name = "_generics/delete_entity.html" model = TaxRate success_url = reverse_lazy('books:tax_rate-list') class PaymentUpdateView(generic.UpdateView): template_name = "books/payment_create_or_update.html" model = Payment form_class = PaymentForm def get_success_url(self): related_obj = self.object.content_object if isinstance(related_obj, Invoice): return reverse("books:invoice-detail", args=[related_obj.pk]) elif isinstance(related_obj, Bill): return reverse("books:bill-detail", args=[related_obj.pk]) logger.warning("Unsupported related object '{}' for " "payment '{}'".format(self.object, related_obj)) return reverse("books:dashboard") class PaymentDeleteView(generic.DeleteView): template_name = "_generics/delete_entity.html" model = Payment success_url = reverse_lazy('books:invoice-list') class EstimateListView(RestrictToSelectedOrganizationQuerySetMixin, SaleListQuerySetMixin, generic.ListView): template_name = "books/estimate_list.html" model = Estimate context_object_name = "estimates" class EstimateCreateView(AutoSetSelectedOrganizationMixin, AbstractSaleCreateUpdateMixin, generic.CreateView): template_name = "books/bill_create_or_update.html" model = Estimate form_class = EstimateForm formset_class = EstimateLineFormSet success_url = reverse_lazy("books:estimate-list") def get_form(self, form_class=None): form = super().get_form(form_class) orga = organization_manager.get_selected_organization(self.request) self.restrict_fields_choices_to_organization(form, orga) return form def get_initial(self): initial = super().get_initial() orga = organization_manager.get_selected_organization(self.request) initial['number'] = EstimateNumberGenerator().next_number(orga) return initial class EstimateUpdateView(AutoSetSelectedOrganizationMixin, AbstractSaleCreateUpdateMixin, generic.UpdateView): template_name = "books/estimate_create_or_update.html" model = Estimate form_class = EstimateForm formset_class = EstimateLineFormSet success_url = reverse_lazy("books:estimate-list") class EstimateDeleteView(generic.DeleteView): template_name = "_generics/delete_entity.html" model = Estimate success_url = reverse_lazy('books:estimate-list') class EstimateDetailView(AbstractSaleDetailMixin, generic.DetailView): template_name = "books/estimate_detail.html" model = Estimate context_object_name = "estimate" def get_success_url(self): return reverse('books:estimate-detail', args=[self.object.pk]) class InvoiceListView(RestrictToSelectedOrganizationQuerySetMixin, SaleListQuerySetMixin, generic.ListView): template_name = "books/invoice_list.html" model = Invoice context_object_name = "invoices" class InvoiceCreateView(AutoSetSelectedOrganizationMixin, AbstractSaleCreateUpdateMixin, generic.CreateView): template_name = "books/invoice_create_or_update.html" model = Invoice form_class = InvoiceForm formset_class = InvoiceLineFormSet success_url = reverse_lazy("books:invoice-list") def get_form(self, form_class=None): form = super().get_form(form_class) orga = organization_manager.get_selected_organization(self.request) self.restrict_fields_choices_to_organization(form, orga) return form def get_initial(self): initial = super().get_initial() orga = organization_manager.get_selected_organization(self.request) initial['number'] = InvoiceNumberGenerator().next_number(orga) return initial class InvoiceUpdateView(AutoSetSelectedOrganizationMixin, AbstractSaleCreateUpdateMixin, generic.UpdateView): template_name = "books/invoice_create_or_update.html" model = Invoice form_class = InvoiceForm formset_class = InvoiceLineFormSet success_url = reverse_lazy("books:invoice-list") class InvoiceDeleteView(generic.DeleteView): template_name = "_generics/delete_entity.html" model = Invoice success_url = reverse_lazy('books:invoice-list') class InvoiceDetailView(PaymentFormMixin, AbstractSaleDetailMixin, generic.DetailView): template_name = "books/invoice_detail.html" model = Invoice context_object_name = "invoice" payment_form_class = PaymentForm def get_success_url(self): return reverse('books:invoice-detail', args=[self.object.pk]) class BillListView(RestrictToSelectedOrganizationQuerySetMixin, SaleListQuerySetMixin, generic.ListView): template_name = "books/bill_list.html" model = Bill context_object_name = "bills" class BillCreateView(AutoSetSelectedOrganizationMixin, AbstractSaleCreateUpdateMixin, generic.CreateView): template_name = "books/bill_create_or_update.html" model = Bill form_class = BillForm formset_class = BillLineFormSet success_url = reverse_lazy("books:bill-list") def get_form(self, form_class=None): form = super().get_form(form_class) orga = organization_manager.get_selected_organization(self.request) self.restrict_fields_choices_to_organization(form, orga) return form def get_initial(self): initial = super().get_initial() orga = organization_manager.get_selected_organization(self.request) initial['number'] = BillNumberGenerator().next_number(orga) return initial class BillUpdateView(AutoSetSelectedOrganizationMixin, AbstractSaleCreateUpdateMixin, generic.UpdateView): template_name = "books/bill_create_or_update.html" model = Bill form_class = BillForm formset_class = BillLineFormSet success_url = reverse_lazy("books:bill-list") class BillDeleteView(generic.DeleteView): template_name = "_generics/delete_entity.html" model = Bill success_url = reverse_lazy('books:bill-list') class BillDetailView(PaymentFormMixin, AbstractSaleDetailMixin, generic.DetailView): template_name = "books/bill_detail.html" model = Bill context_object_name = "bill" payment_form_class = PaymentForm def get_success_url(self): return reverse('books:bill-detail', args=[self.object.pk]) class ExpenseClaimListView(RestrictToSelectedOrganizationQuerySetMixin, SaleListQuerySetMixin, generic.ListView): template_name = "books/expense_claim_list.html" model = ExpenseClaim context_object_name = "expense_claims" class ExpenseClaimCreateView(AutoSetSelectedOrganizationMixin, AbstractSaleCreateUpdateMixin, generic.CreateView): template_name = "books/expense_claim_create_or_update.html" model = ExpenseClaim form_class = ExpenseClaimForm formset_class = ExpenseClaimLineFormSet success_url = reverse_lazy("books:expense_claim-list") def get_form(self, form_class=None): form = super().get_form(form_class) orga = organization_manager.get_selected_organization(self.request) self.restrict_fields_choices_to_organization(form, orga) return form def get_initial(self): initial = super().get_initial() orga = organization_manager.get_selected_organization(self.request) initial['number'] = ExpenseClaimNumberGenerator().next_number(orga) return initial class ExpenseClaimUpdateView(AutoSetSelectedOrganizationMixin, AbstractSaleCreateUpdateMixin, generic.UpdateView): template_name = "books/expense_claim_create_or_update.html" model = ExpenseClaim form_class = ExpenseClaimForm formset_class = ExpenseClaimLineFormSet success_url = reverse_lazy("books:expense_claim-list") class ExpenseClaimDeleteView(generic.DeleteView): template_name = "_generics/delete_entity.html" model = ExpenseClaim success_url = reverse_lazy('books:expense_claim-list') class ExpenseClaimDetailView(PaymentFormMixin, AbstractSaleDetailMixin, generic.DetailView): template_name = "books/expense_claim_detail.html" model = ExpenseClaim context_object_name = "expense_claim" payment_form_class = PaymentForm def get_success_url(self): return reverse('books:expense_claim-detail', args=[self.object.pk]) ================================================ FILE: accounting/apps/connect/__init__.py ================================================ ================================================ FILE: accounting/apps/connect/middlewares.py ================================================ class ForceGettingStartedMiddleware(object): def process_request(self, request): pass ================================================ FILE: accounting/apps/connect/models.py ================================================ ================================================ FILE: accounting/apps/connect/steps.py ================================================ import logging from django.core.urlresolvers import reverse from django.core.exceptions import ValidationError from accounting.apps.books.utils import organization_manager from accounting.apps.reports.models import ( BusinessSettings, FinancialSettings, PayRunSettings) logger = logging.getLogger(__name__) class StepOptions(object): """ Meta class options for a `BaseStep` subclass """ def __init__(self, meta): self.name = getattr(meta, 'name', None) assert isinstance(self.name, str), \ '`name` must be a string instance' self.description = getattr(meta, 'description', "") assert isinstance(self.description, str), \ '`description` must be a string instance' class BaseStep(object): """ Abstract class to subclass to create a getting started step """ user = None _completion = None _options_class = StepOptions class StepOptions: name = "" description = None def __init__(self, user): super().__init__() self.opts = self._options_class(getattr(self, 'StepOptions', None)) self.user = user def completed(self, request): if self._completion is None: self._completion = self.check_completion(request) return self._completion def is_completed(self): """pre computed value, to be called in templates""" if self._completion is None: logger.error("`completed` needs to be run before using " "this method") return False return self._completion def check_completion(self, request): """ Implement the logic of the step and returns a boolean """ raise NotImplementedError def get_action_url(self): """Returns the url to complete the step""" pass class CreateOrganizationStep(BaseStep): """ At least one organization has been created """ class StepOptions: name = "Create an Organization" description = "the organization is the foundation of the accounting " \ "system, tell Accountant-x more about it" def check_completion(self, request): orgas = organization_manager.get_user_organizations(request.user) count = orgas.count() return count > 0 def get_action_url(self): return reverse('books:organization-create') class ConfigureTaxRatesStep(BaseStep): """ At least one tax rate has been added (even if the rate is 0) """ class StepOptions: name = "Configure Tax Rates" description = "even if you are not subject to tax collecting rules " \ "you should create a 0% tax entry" def check_completion(self, request): orga = organization_manager.get_selected_organization(request) if orga is None: return False count = orga.tax_rates.all().count() return count > 0 def get_action_url(self): return reverse('books:tax_rate-create') class ConfigureBusinessSettingsStep(BaseStep): """ The associated business settings has been completed """ class StepOptions: name = "Configure Business Settings" description = "for now there is not much thing, but please create it" def check_completion(self, request): orga = organization_manager.get_selected_organization(request) if orga is None: return False try: settings = orga.business_settings settings.full_clean() except BusinessSettings.DoesNotExist: return False except ValidationError: return False return True def get_action_url(self): return reverse('reports:settings-business') class ConfigureFinancialSettingsStep(BaseStep): class StepOptions: name = "Configure Financial Settings" description = "tell Accountant-x what is your financial rulling rule" def check_completion(self, request): orga = organization_manager.get_selected_organization(request) if orga is None: return False try: settings = orga.financial_settings settings.full_clean() except FinancialSettings.DoesNotExist: return False except ValidationError: return False return True def get_action_url(self): return reverse('reports:settings-financial') class AddEmployeesStep(BaseStep): class StepOptions: name = "Add Employees" description = "add at least one *employee*, even if you are giving " \ "yourself a salary that follows the profits" def check_completion(self, request): orga = organization_manager.get_selected_organization(request) if orga is None: return False count = orga.employees.all().count() return count > 0 def get_action_url(self): return reverse('people:employee-create') class ConfigurePayRunSettingsStep(BaseStep): class StepOptions: name = "Configure Pay Run Settings" description = "tell to Accountant-x how you distribute salaries" def check_completion(self, request): orga = organization_manager.get_selected_organization(request) if orga is None: return False try: settings = orga.payrun_settings settings.full_clean() except PayRunSettings.DoesNotExist: return False except ValidationError: return False return True def get_action_url(self): return reverse('reports:settings-payrun') class AddFirstClientStep(BaseStep): class StepOptions: name = "Add the first Client" description = "close to the first invoice" def check_completion(self, request): orga = organization_manager.get_selected_organization(request) if orga is None: return False count = orga.clients.all().count() return count > 0 def get_action_url(self): return reverse('people:client-create') class AddFirstInvoiceStep(BaseStep): class StepOptions: name = "Add the first Invoice" description = "finally create it !" def check_completion(self, request): orga = organization_manager.get_selected_organization(request) if orga is None: return False count = orga.invoices.all().count() return count > 0 def get_action_url(self): return reverse('books:invoice-create') ================================================ FILE: accounting/apps/connect/urls.py ================================================ from django.conf.urls import patterns, url from . import views urlpatterns = patterns('', url(r'^$', views.RootRedirectionView.as_view(), name="root"), # Step by step url(r'^getting-started/$', views.GettingStartedView.as_view(), name="getting-started") ) ================================================ FILE: accounting/apps/connect/views.py ================================================ from django.views import generic from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse from accounting.apps.books.models import Organization from .steps import ( CreateOrganizationStep, ConfigureTaxRatesStep, ConfigureBusinessSettingsStep, ConfigureFinancialSettingsStep, AddEmployeesStep, ConfigurePayRunSettingsStep, AddFirstClientStep, AddFirstInvoiceStep) class RootRedirectionView(generic.View): """ Redirect to the books if an organization is already configured Otherwise we begin the step by step creation process to help the user begin and configure his books """ def get(self, *args, **kwargs): if Organization.objects.all().count(): return HttpResponseRedirect(reverse('books:dashboard')) class GettingStartedView(generic.TemplateView): template_name = "connect/getting_started.html" def get_steps(self, request): user = request.user steps = steps = [ CreateOrganizationStep(user), ConfigureTaxRatesStep(user), ConfigureBusinessSettingsStep(user), ConfigureFinancialSettingsStep(user), AddEmployeesStep(user), ConfigurePayRunSettingsStep(user), AddFirstClientStep(user), AddFirstInvoiceStep(user), ] return steps def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) request = self.request steps = self.get_steps(self.request) def uncomplete_filter(s): return not s.completed(request) uncompleted_steps = list(filter(uncomplete_filter, steps)) try: next_step = next(s for s in uncompleted_steps) except StopIteration: next_step = None ctx['steps'] = steps ctx['next_step'] = next_step ctx['all_steps_completed'] = bool(next_step is None) return ctx def post(self, request, *args, **kwargs): steps = self.get_steps(request) uncompleted_steps = filter(lambda s: not s.completed(request), steps) if not len(uncompleted_steps): return super().post(request, *args, **kwargs) # unmark the session as getting started request.sessions['getting_started_done'] = True return HttpResponseRedirect(reverse('books:dashboard')) ================================================ FILE: accounting/apps/context_processors.py ================================================ from django.conf import settings def metadata(request): """ Add some generally useful metadata to the template context """ return { 'display_version': getattr(settings, 'DISPLAY_VERSION', 'N/A'), 'display_short_version': getattr(settings, 'DISPLAY_SHORT_VERSION', 'N/A'), 'version': getattr(settings, 'VERSION', 'N/A'), } ================================================ FILE: accounting/apps/people/__init__.py ================================================ ================================================ FILE: accounting/apps/people/admin.py ================================================ from django.contrib import admin from . import models @admin.register(models.Client) class ClientAdmin(admin.ModelAdmin): pass @admin.register(models.Employee) class EmployeeAdmin(admin.ModelAdmin): pass ================================================ FILE: accounting/apps/people/forms.py ================================================ from django.forms import ModelForm from django.contrib.auth import get_user_model from .models import Client, Employee from django_select2.fields import ( AutoModelSelect2Field, AutoModelSelect2MultipleField) class ClientForm(ModelForm): class Meta: model = Client fields = ( "name", "address_line_1", "address_line_2", "city", "postal_code", "country", ) class EmployeeForm(ModelForm): class Meta: model = Employee fields = ( "first_name", "last_name", "email", "payroll_tax_rate", "salary_follows_profits", "shares_percentage", ) # TODO: avoid calling this in the global scope, can lead to circular imports User = get_user_model() class UserChoices(AutoModelSelect2Field): queryset = User.objects.all() search_fields = ( 'first_name__icontains', 'last_name__icontains', 'username__icontains', 'email__icontains', ) class UserMultipleChoices(AutoModelSelect2MultipleField): queryset = User.objects.all() search_fields = ( 'first_name__icontains', 'last_name__icontains', 'username__icontains', 'email__icontains', ) ================================================ FILE: accounting/apps/people/migrations/0001_initial.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations from decimal import Decimal import django.core.validators class Migration(migrations.Migration): dependencies = [ ('books', '0002_auto_20141029_1606'), ] operations = [ migrations.CreateModel( name='Client', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(max_length=150)), ('address_line_1', models.CharField(max_length=128)), ('address_line_2', models.CharField(null=True, blank=True, max_length=128)), ('city', models.CharField(max_length=64)), ('postal_code', models.CharField(max_length=7)), ('country', models.CharField(max_length=50)), ('organization', models.ForeignKey(null=True, related_name='orgas', to='books.Organization', blank=True)), ], options={ }, bases=(models.Model,), ), migrations.CreateModel( name='Employee', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('first_name', models.CharField(max_length=150)), ('last_name', models.CharField(max_length=150)), ('email', models.EmailField(max_length=254)), ('salaries_follow_profits', models.BooleanField(default=False)), ('shares_percentage', models.DecimalField(decimal_places=5, validators=[django.core.validators.MinValueValidator(Decimal('0')), django.core.validators.MaxValueValidator(Decimal('1'))], max_digits=6)), ('organization', models.ForeignKey(to='books.Organization', related_name='employees')), ], options={ }, bases=(models.Model,), ), ] ================================================ FILE: accounting/apps/people/migrations/0002_auto_20141029_1609.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ ('people', '0001_initial'), ] operations = [ migrations.RenameField( model_name='employee', old_name='salaries_follow_profits', new_name='salary_follows_profits', ), ] ================================================ FILE: accounting/apps/people/migrations/0003_employee_payroll_tax_rate.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations from decimal import Decimal import django.core.validators class Migration(migrations.Migration): dependencies = [ ('people', '0002_auto_20141029_1609'), ] operations = [ migrations.AddField( model_name='employee', name='payroll_tax_rate', field=models.DecimalField(default=0, decimal_places=5, validators=[django.core.validators.MinValueValidator(Decimal('0')), django.core.validators.MaxValueValidator(Decimal('1'))], max_digits=6), preserve_default=False, ), ] ================================================ FILE: accounting/apps/people/migrations/0004_auto_20141029_2306.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations def _link_to_first_organization(apps, schema_editor): Client = apps.get_model("people", "Client") Client.objects.update(organization_id=1) class Migration(migrations.Migration): dependencies = [ ('people', '0003_employee_payroll_tax_rate'), ] operations = [ migrations.RunPython(_link_to_first_organization) ] ================================================ FILE: accounting/apps/people/migrations/0005_auto_20141029_2308.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ ('people', '0004_auto_20141029_2306'), ] operations = [ migrations.AlterField( model_name='client', name='organization', field=models.ForeignKey(related_name='clients', to='books.Organization'), ), ] ================================================ FILE: accounting/apps/people/migrations/__init__.py ================================================ ================================================ FILE: accounting/apps/people/models.py ================================================ from decimal import Decimal as D from django.db import models from django.core.validators import MinValueValidator, MaxValueValidator class Client(models.Model): name = models.CharField(max_length=150) # address address_line_1 = models.CharField(max_length=128) address_line_2 = models.CharField(max_length=128, blank=True, null=True) city = models.CharField(max_length=64) postal_code = models.CharField(max_length=7) country = models.CharField(max_length=50) organization = models.ForeignKey('books.Organization', related_name="clients") class Meta: pass def __str__(self): return self.name def active_address_fields(self): """ Return the non-empty components of the address """ fields = [self.address_line_1, self.address_line_2, self.city, self.postal_code, self.country] fields = [f.strip() for f in fields if f] return fields def full_address(self, separator="\n"): return separator.join(filter(bool, self.active_address_fields())) class Employee(models.Model): first_name = models.CharField(max_length=150) last_name = models.CharField(max_length=150) email = models.EmailField(max_length=254) payroll_tax_rate = models.DecimalField( max_digits=6, decimal_places=5, validators=[ MinValueValidator(D('0')), MaxValueValidator(D('1')) ] ) salary_follows_profits = models.BooleanField(default=False) shares_percentage = models.DecimalField( max_digits=6, decimal_places=5, validators=[ MinValueValidator(D('0')), MaxValueValidator(D('1')) ] ) organization = models.ForeignKey('books.Organization', related_name="employees") class Meta: pass def __str__(self): return "{}".format(self.composite_name) @property def composite_name(self): return "{} {}".format(self.first_name, self.last_name) ================================================ FILE: accounting/apps/people/urls.py ================================================ from django.conf.urls import patterns, url from . import views urlpatterns = patterns('', # Clients url(r'^client/$', views.ClientListView.as_view(), name="client-list"), url(r'^client/create/$', views.ClientCreateView.as_view(), name="client-create"), url(r'^client/(?P\d+)/edit/$', views.ClientUpdateView.as_view(), name="client-edit"), url(r'^client/(?P\d+)/detail/$', views.ClientDetailView.as_view(), name="client-detail"), # Employees url(r'^employee/$', views.EmployeeListView.as_view(), name="employee-list"), url(r'^employee/create/$', views.EmployeeCreateView.as_view(), name="employee-create"), url(r'^employee/(?P\d+)/edit/$', views.EmployeeUpdateView.as_view(), name="employee-edit"), url(r'^employee/(?P\d+)/detail/$', views.EmployeeDetailView.as_view(), name="employee-detail"), ) ================================================ FILE: accounting/apps/people/views.py ================================================ from django.views import generic from django.core.urlresolvers import reverse from accounting.apps.books.mixins import ( RestrictToSelectedOrganizationQuerySetMixin, AutoSetSelectedOrganizationMixin) from .models import Client, Employee from .forms import ClientForm, EmployeeForm class ClientListView(RestrictToSelectedOrganizationQuerySetMixin, generic.ListView): template_name = "people/client_list.html" model = Client context_object_name = "clients" class ClientCreateView(AutoSetSelectedOrganizationMixin, generic.CreateView): template_name = "people/client_create_or_update.html" model = Client form_class = ClientForm def get_success_url(self): return reverse("people:client-list") class ClientUpdateView(RestrictToSelectedOrganizationQuerySetMixin, AutoSetSelectedOrganizationMixin, generic.UpdateView): template_name = "people/client_create_or_update.html" model = Client form_class = ClientForm def get_success_url(self): return reverse("people:client-list") class ClientDetailView(RestrictToSelectedOrganizationQuerySetMixin, generic.DetailView): template_name = "people/client_detail.html" model = Client context_object_name = "client" class EmployeeListView(RestrictToSelectedOrganizationQuerySetMixin, generic.ListView): template_name = "people/employee_list.html" model = Employee context_object_name = "employees" class EmployeeCreateView(AutoSetSelectedOrganizationMixin, generic.CreateView): template_name = "people/employee_create_or_update.html" model = Employee form_class = EmployeeForm def get_success_url(self): return reverse("people:employee-list") class EmployeeUpdateView(RestrictToSelectedOrganizationQuerySetMixin, AutoSetSelectedOrganizationMixin, generic.UpdateView): template_name = "people/employee_create_or_update.html" model = Employee form_class = EmployeeForm def get_success_url(self): return reverse("people:employee-list") class EmployeeDetailView(RestrictToSelectedOrganizationQuerySetMixin, generic.DetailView): template_name = "people/employee_detail.html" model = Employee context_object_name = "employee" ================================================ FILE: accounting/apps/reports/__init__.py ================================================ ================================================ FILE: accounting/apps/reports/admin.py ================================================ from django.contrib import admin from . import models @admin.register(models.FinancialSettings) class FinancialSettingsAdmin(admin.ModelAdmin): pass ================================================ FILE: accounting/apps/reports/forms.py ================================================ from datetime import timedelta from django import forms from .models import ( BusinessSettings, FinancialSettings, PayRunSettings) class BusinessSettingsForm(forms.ModelForm): class Meta: model = BusinessSettings fields = ( "business_type", ) class FinancialSettingsForm(forms.ModelForm): class Meta: model = FinancialSettings fields = ( "financial_year_end_day", "financial_year_end_month", "tax_id_number", "tax_id_display_name", "tax_period", ) class PayRunSettingsForm(forms.ModelForm): class Meta: model = PayRunSettings fields = ( "salaries_follow_profits", "payrun_period", ) class TimePeriodForm(forms.Form): date_from = forms.DateField(required=False, label="From") date_to = forms.DateField(required=False, label="To") _filters = None _description = None def _determine_filter_metadata(self): self._filters = {} self._description = "All orders" if self.errors: return date_from = self.cleaned_data['date_from'] date_to = self.cleaned_data['date_to'] if date_from and date_to: # We want to include end date so we adjust the date we # use with the 'range' function. self._filters = { 'date_placed__range': [ date_from, date_to + timedelta(days=1) ] } self._description = ("Between {} and {}" .format(date_from, date_to)) elif date_from and not date_to: self._filters = {'date_placed__gte': date_from} self._description = "Since {}".format(date_from) elif not date_from and date_to: self._filters = {'date_placed__lte': date_to} self._description = "Until {}".format(date_to) else: self._filters = {} self._description = "From the begining to now" def get_filters(self): if self._filters is None: self._determine_filter_metadata() return self._filters def get_filter_description(self): if self._description is None: self._determine_filter_metadata() return self._description ================================================ FILE: accounting/apps/reports/migrations/0001_initial.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations import django.core.validators class Migration(migrations.Migration): dependencies = [ ('books', '0003_auto_20141029_1606'), ] operations = [ migrations.CreateModel( name='BusinessSettings', fields=[ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), ('business_type', models.CharField(choices=[('sole_proprietorship', 'Sole Proprietorship')], max_length=50)), ('organization', models.OneToOneField(related_name='business_settings', to='books.Organization', null=True, blank=True)), ], options={ }, bases=(models.Model,), ), migrations.CreateModel( name='FinancialSettings', fields=[ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), ('financial_year_end_day', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(31)])), ('financial_year_end_month', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(12)])), ('tax_id_number', models.CharField(null=True, blank=True, max_length=150)), ('tax_id_display_name', models.CharField(null=True, blank=True, max_length=150)), ('tax_period', models.CharField(verbose_name='Tax Period', choices=[('monthly', '1 month'), ('bimonthly', '2 months'), ('quarter', '3 months'), ('half', '6 months'), ('year', '1 year')], max_length=20)), ('organization', models.OneToOneField(related_name='financial_settings', to='books.Organization', null=True, blank=True)), ], options={ }, bases=(models.Model,), ), migrations.CreateModel( name='PayRunSettings', fields=[ ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), ('salaries_follow_profits', models.BooleanField(default=False)), ('payrun_period', models.CharField(verbose_name='Payrun Period', default='monthly', choices=[('monthly', 'monthly')], max_length=20)), ('organization', models.OneToOneField(related_name='payrun_settings', to='books.Organization', null=True, blank=True)), ], options={ }, bases=(models.Model,), ), ] ================================================ FILE: accounting/apps/reports/migrations/0002_auto_20150128_1458.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations import django.core.validators class Migration(migrations.Migration): dependencies = [ ('reports', '0001_initial'), ] operations = [ migrations.AlterField( model_name='financialsettings', name='financial_year_end_day', field=models.PositiveSmallIntegerField(default=31, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(31)]), ), migrations.AlterField( model_name='financialsettings', name='financial_year_end_month', field=models.PositiveSmallIntegerField(default=12, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(12)]), ), ] ================================================ FILE: accounting/apps/reports/migrations/0003_auto_20150131_1902.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ ('reports', '0002_auto_20150128_1458'), ] operations = [ migrations.AlterField( model_name='businesssettings', name='business_type', field=models.CharField(choices=[('sole_proprietorship', 'Sole Proprietorship'), ('partnership', 'Partnership'), ('corporation', 'Corporation')], max_length=50), preserve_default=True, ), ] ================================================ FILE: accounting/apps/reports/migrations/__init__.py ================================================ ================================================ FILE: accounting/apps/reports/models.py ================================================ from django.db import models from django.core.validators import MinValueValidator, MaxValueValidator class BusinessSettings(models.Model): BUSINESS_TYPE_SOLE_PROPRIETORSHIP = 'sole_proprietorship' BUSINESS_TYPE_PARTNERSHIP = 'partnership' BUSINESS_TYPE_CORPORATION = 'corporation' BUSINESS_TYPE_CHOICES = ( (BUSINESS_TYPE_SOLE_PROPRIETORSHIP, "Sole Proprietorship"), (BUSINESS_TYPE_PARTNERSHIP, "Partnership"), (BUSINESS_TYPE_CORPORATION, "Corporation"), ) business_type = models.CharField(max_length=50, choices=BUSINESS_TYPE_CHOICES) # optionnaly linked to an organization # for automated behaviors during cross-organizations invoicing organization = models.OneToOneField('books.Organization', related_name="business_settings", blank=True, null=True) class Meta: pass class FinancialSettings(models.Model): financial_year_end_day = models.PositiveSmallIntegerField(default=31, validators=[ MinValueValidator(1), MaxValueValidator(31) ]) financial_year_end_month = models.PositiveSmallIntegerField(default=12, validators=[ MinValueValidator(1), MaxValueValidator(12) ]) tax_id_number = models.CharField(max_length=150, blank=True, null=True) tax_id_display_name = models.CharField(max_length=150, blank=True, null=True) TAX_PERIOD_MONTHLY = 'monthly' # 1 month TAX_PERIOD_BIMONTHLY = 'bimonthly' # 2 months TAX_PERIOD_QUARTER = 'quarter' # 3 months TAX_PERIOD_HALF = 'half' # 6 months TAX_PERIOD_YEAR = 'year' # 12 months TAX_PERIOD_CHOICES = ( (TAX_PERIOD_MONTHLY, "1 month"), (TAX_PERIOD_BIMONTHLY, "2 months"), (TAX_PERIOD_QUARTER, "3 months"), (TAX_PERIOD_HALF, "6 months"), (TAX_PERIOD_YEAR, "1 year"), ) tax_period = models.CharField("Tax Period", max_length=20, choices=TAX_PERIOD_CHOICES) # optionnaly linked to an organization # for automated behaviors during cross-organizations invoicing organization = models.OneToOneField('books.Organization', related_name="financial_settings", blank=True, null=True) class Meta: pass class PayRunSettings(models.Model): salaries_follow_profits = models.BooleanField(default=False) PAYRUN_MONTHLY = 'monthly' # 1 month # PAYRUN_QUARTER = 'quarter' # 3 months PAYRUN_CHOICES = ( (PAYRUN_MONTHLY, "monthly"), ) payrun_period = models.CharField("Payrun Period", max_length=20, choices=PAYRUN_CHOICES, default=PAYRUN_MONTHLY) # optionnaly linked to an organization # for automated behaviors during cross-organizations invoicing organization = models.OneToOneField('books.Organization', related_name="payrun_settings", blank=True, null=True) class Meta: pass ================================================ FILE: accounting/apps/reports/urls.py ================================================ from django.conf.urls import patterns, url from . import views urlpatterns = patterns('', # Reports url(r'^report/$', views.ReportListView.as_view(), name="report-list"), url(r'^report/tax/$', views.TaxReportView.as_view(), name="tax-report"), url(r'^report/profitloss/$', views.ProfitAndLossReportView.as_view(), name="profit-and-loss-report"), url(r'^report/payrun/$', views.PayRunReportView.as_view(), name="pay-run-report"), url(r'^report/invoicedetails/$', views.InvoiceDetailsView.as_view(), name="invoice-details-report"), # Settings url(r'^settings/$', views.SettingsListView.as_view(), name="settings-list"), url(r'^settings/business/$', views.BusinessSettingsUpdateView.as_view(), name="settings-business"), url(r'^settings/financial/$', views.FinancialSettingsUpdateView.as_view(), name="settings-financial"), url(r'^settings/payrun/$', views.PayRunSettingsUpdateView.as_view(), name="settings-payrun"), ) ================================================ FILE: accounting/apps/reports/views.py ================================================ from datetime import date from django.views import generic from django.core.urlresolvers import reverse from django.utils import timezone from dateutil.relativedelta import relativedelta from accounting.apps.books.utils import organization_manager from accounting.libs.intervals import TimeInterval from .models import ( BusinessSettings, FinancialSettings, PayRunSettings) from .forms import ( BusinessSettingsForm, FinancialSettingsForm, PayRunSettingsForm, TimePeriodForm) from .wrappers import ( TaxReport, ProfitAndLossReport, PayRunReport, InvoiceDetailsReport) class TimePeriodFormMixin(object): period = None def get_initial(self): initial = super().get_initial() # currrent quarter now = timezone.now() start = date( year=now.year, month=(now.month - ((now.month - 1) % 3)), day=1 ) end = start + relativedelta(months=3) initial['date_from'] = start initial['date_to'] = end return initial def get_form_kwargs(self): kwargs = super().get_form_kwargs() if self.request.GET: kwargs.update({ 'data': self.request.GET, }) return kwargs def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) form = ctx['form'] if form.is_valid(): start = form.cleaned_data['date_from'] end = form.cleaned_data['date_to'] ctx['form_title'] = form.get_filter_description() else: start = end = None ctx['form_title'] = "Time Interval" if self.period is None: self.period = TimeInterval(start=start, end=end) return ctx class ReportListView(generic.TemplateView): template_name = "reports/report_list.html" class SettingsListView(generic.TemplateView): template_name = "reports/settings_list.html" class GenericSettingsMixin(object): def get_object(self): orga = organization_manager.get_selected_organization(self.request) try: settings = self.model.objects.get(organization=orga) except self.model.DoesNotExist: settings = self.model.objects.create(organization=orga) return settings def get_success_url(self): return reverse("reports:settings-list") class BusinessSettingsUpdateView(GenericSettingsMixin, generic.UpdateView): template_name = "reports/financial_settings_update.html" model = BusinessSettings form_class = BusinessSettingsForm class FinancialSettingsUpdateView(GenericSettingsMixin, generic.UpdateView): template_name = "reports/financial_settings_update.html" model = FinancialSettings form_class = FinancialSettingsForm class PayRunSettingsUpdateView(GenericSettingsMixin, generic.UpdateView): template_name = "reports/payrun_settings_update.html" model = PayRunSettings form_class = PayRunSettingsForm class TaxReportView(TimePeriodFormMixin, generic.FormView): template_name = "reports/tax_report.html" form_class = TimePeriodForm def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) orga = organization_manager.get_selected_organization(self.request) report = TaxReport(orga, start=self.period.start, end=self.period.end) report.generate() ctx['tax_summaries'] = report.tax_summaries.values() return ctx class ProfitAndLossReportView(generic.TemplateView): template_name = "reports/profit_and_loss_report.html" def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) orga = organization_manager.get_selected_organization(self.request) # currrent quarter now = timezone.now() start = date( year=now.year, month=(now.month - ((now.month - 1) % 3)), day=1 ) end = start + relativedelta(months=3) report = ProfitAndLossReport(orga, start=start, end=end) report.generate() ctx['summaries'] = report.summaries ctx['total_summary'] = report.total_summary return ctx class PayRunReportView(TimePeriodFormMixin, generic.FormView): template_name = "reports/pay_run_report.html" form_class = TimePeriodForm def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) orga = organization_manager.get_selected_organization(self.request) report = PayRunReport(orga, start=self.period.start, end=self.period.end) report.generate() ctx['summaries'] = report.summaries.values() ctx['total_payroll_taxes'] = report.total_payroll_taxes return ctx class InvoiceDetailsView(TimePeriodFormMixin, generic.FormView): template_name = "reports/invoice_details_report.html" form_class = TimePeriodForm def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) orga = organization_manager.get_selected_organization(self.request) report = InvoiceDetailsReport(orga, start=self.period.start, end=self.period.end) report.generate() ctx['invoices'] = report.invoices ctx['tax_rates'] = report.tax_rates ctx['payrun_settings'] = orga.payrun_settings return ctx ================================================ FILE: accounting/apps/reports/wrappers.py ================================================ from decimal import Decimal as D from collections import defaultdict, OrderedDict from dateutil.relativedelta import relativedelta from accounting.apps.books.models import Invoice, Bill from accounting.apps.books.calculators import ProfitsLossCalculator from accounting.libs.intervals import TimeInterval class BaseReport(object): title = None period = None def __init__(self, title, start, end): self.title = title self.period = TimeInterval(start, end) def generate(self): raise NotImplementedError class TaxRateSummary(object): tax_rate = None taxable_amount = D('0') expenses_amount = D('0') @property def collected_taxes(self): return self.tax_rate.rate * self.taxable_amount @property def deductible_taxes(self): return self.tax_rate.rate * self.expenses_amount @property def net_amount(self): return self.taxable_amount - self.expenses_amount @property def net_taxes(self): return self.tax_rate.rate * self.net_amount class TaxReport(BaseReport): # TODO implement 'Billed (Accrual) / Collected (Cash based)' organization = None tax_summaries = None def __init__(self, organization, start, end): super().__init__("Tax Report", start, end) self.organization = organization self.tax_summaries = defaultdict(TaxRateSummary) def generate(self): invoice_queryset = Invoice.objects.all() bill_queryset = Bill.objects.all() self.generate_for_sales(invoice_queryset) self.generate_for_sales(bill_queryset) def generate_for_sales(self, sales_queryset): calculator = ProfitsLossCalculator(self.organization, start=self.period.start, end=self.period.end) for output in calculator.process_generator(sales_queryset): summary = self.tax_summaries[output.tax_rate.pk] summary.tax_rate = output.tax_rate if isinstance(output.sale, Invoice): summary.taxable_amount += output.amount_excl_tax elif isinstance(output.sale, Bill): summary.expenses_amount += output.amount_excl_tax else: raise ValueError("Unsupported type of sale {}" .format(output.sale.__class__)) class ProfitAndLossSummary(object): grouping_date = None sales_amount = D('0') expenses_amount = D('0') @property def net_profit(self): return self.sales_amount - self.expenses_amount class ProfitAndLossReport(BaseReport): # TODO implement 'Billed (Accrual) / Collected (Cash based)' organization = None summaries = None total_summary = None RESOLUTION_MONTHLY = 'monthly' RESOLUTION_CHOICES = ( RESOLUTION_MONTHLY, ) group_by_resolution = RESOLUTION_MONTHLY def __init__(self, organization, start, end): super().__init__("Profit and Loss", start, end) self.organization = organization self.summaries = {} steps_interval = relativedelta(end, start) assert self.group_by_resolution in self.RESOLUTION_CHOICES, \ "No a resolution choice" if self.group_by_resolution == self.RESOLUTION_MONTHLY: for step in range(0, steps_interval.months): key_date = start + relativedelta(months=step) self.summaries[key_date] = ProfitAndLossSummary() else: raise ValueError("Unsupported resolution {}" .format(self.group_by_resolution)) self.total_summary = ProfitAndLossSummary() def group_by_date(self, date): if self.group_by_resolution == self.RESOLUTION_MONTHLY: grouping_date = date.replace(day=1) else: raise ValueError("Unsupported resolution {}" .format(self.group_by_resolution)) return grouping_date def generate(self): invoice_queryset = Invoice.objects.all() bill_queryset = Bill.objects.all() self.generate_for_sales(invoice_queryset) self.generate_for_sales(bill_queryset) # order the results self.summaries = OrderedDict(sorted(self.summaries.items())) # compute totals for summary in self.summaries.values(): self.total_summary.sales_amount += summary.sales_amount self.total_summary.expenses_amount += summary.expenses_amount def generate_for_sales(self, sales_queryset): calculator = ProfitsLossCalculator(self.organization, start=self.period.start, end=self.period.end) for output in calculator.process_generator(sales_queryset): key_date = self.group_by_date(output.payment.date_paid) summary = self.summaries[key_date] if isinstance(output.sale, Invoice): summary.sales_amount += output.amount_excl_tax elif isinstance(output.sale, Bill): summary.expenses_amount += output.amount_excl_tax else: raise ValueError("Unsupported type of sale {}" .format(output.sale.__class__)) class PayRunSummary(object): payroll_tax_rate = None total_excl_tax = D('0') @property def payroll_taxes(self): return self.payroll_tax_rate * self.total_excl_tax class PayRunReport(BaseReport): organization = None summaries = None total_payroll_taxes = D('0') def __init__(self, organization, start, end): super().__init__("Pay Run Report", start, end) self.organization = organization self.summaries = defaultdict(PayRunSummary) def generate(self): employee_queryset = self.organization.employees.all() self.generate_for_employees(employee_queryset) def generate_for_employees(self, employee_queryset): total_payroll_taxes = D('0') calculator = ProfitsLossCalculator(self.organization, start=self.period.start, end=self.period.end) for emp in employee_queryset: summary = self.summaries[emp.composite_name] summary.employee = emp summary.payroll_tax_rate = emp.payroll_tax_rate if emp.salary_follows_profits: # TODO compute profits based on the period interval profits = calculator.profits() summary.total_excl_tax = profits * emp.shares_percentage else: raise ValueError("Salary not indexed on the profits " "are not supported yet") total_payroll_taxes += summary.payroll_taxes # Total payroll self.total_payroll_taxes = total_payroll_taxes class InvoiceDetailsReport(BaseReport): organization = None invoices = None tax_rates = None def __init__(self, organization, start, end): super().__init__("Pay Run Report", start, end) self.organization = organization self.tax_rates = organization.tax_rates.all() def generate(self): invoice_queryset = self.organization.invoices.all() self.generate_for_invoices(invoice_queryset) def generate_for_invoices(self, invoice_queryset): invoice_queryset = (invoice_queryset .filter(payments__date_paid__range=[ self.period.start, self.period.end ])) # optimize the query invoice_queryset = (invoice_queryset .select_related( 'organization') .prefetch_related( 'lines', 'lines__tax_rate', 'payments', 'organization__employees',) .distinct()) self.invoices = invoice_queryset ================================================ FILE: accounting/defaults.py ================================================ ACCOUNTING_DEFAULT_CURRENCY = "EUR" ================================================ FILE: accounting/libs/__init__.py ================================================ ================================================ FILE: accounting/libs/checks.py ================================================ from django.core.validators import EMPTY_VALUES from django.utils.datastructures import SortedDict class PrimaryKeyRelatedField(object): pass class CheckResult(object): """ Stands for a checking result of a model field """ RESULT_NEUTRAL = 'neutral' RESULT_FAILED = 'failed' RESULT_PASSED = 'passed' RESULT_CHOICES = ( (RESULT_NEUTRAL, "Neutral"), (RESULT_FAILED, "Failed"), (RESULT_PASSED, "Passed"), ) LEVEL_WARNING = 'warning' LEVEL_ERROR = 'error' LEVEL_CHOICES = ( (LEVEL_WARNING, "Warning"), (LEVEL_ERROR, "Error"), ) def __init__(self, field, result=None, level=None, message=None): self.field = field self.message = message if not result: result = self.RESULT_NEUTRAL self.result = result if not level: level = self.LEVEL_WARNING self.level = level def mark_fail(self, level=LEVEL_WARNING, message=None): self.result = self.RESULT_FAILED self.level = level self.message = message def mark_pass(self, message=None): self.result = self.RESULT_PASSED self.message = message @property def has_failed(self): return self.result == self.RESULT_FAILED @property def has_passed(self): return self.result == self.RESULT_PASSED class CheckingModelOptions(object): """ Meta class options for `CheckingModelMixin` """ def __init__(self, meta): self.fields = getattr(meta, 'fields', ()) assert isinstance(self.fields, (list, tuple)), \ '`fields` must be a list or tuple' self.exclude = getattr(meta, 'exclude', ()) assert isinstance(self.exclude, (list, tuple)), \ '`exclude` must be a list or tuple' class CheckingModelMixin(object): _options_class = CheckingModelOptions def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.opts = self._options_class(getattr(self, 'CheckingOptions', None)) def has_custom_check_for_field(self, field_name): return hasattr(self, 'check_%s' % field_name) def get_check_for_field(self, field_name, checking_fields=None): if checking_fields is None: checking_fields = self.get_checking_fields() if field_name not in checking_fields: raise AttributeError("Field '%s' not checkable" % field_name) field = checking_fields.get(field_name) check = CheckResult(field=field) # custom check method if self.has_custom_check_for_field(field_name): return getattr(self, 'check_%s' % field_name)(check) # default check if isinstance(field, PrimaryKeyRelatedField): value = getattr(self, field_name).all() has_failed = bool(value.count() == 0) else: value = getattr(self, field_name) has_failed = bool(value in EMPTY_VALUES) if has_failed: check.mark_fail() else: check.mark_pass() return check def get_checking_fields(self, special_exclude=['id']): """ Returns the set of fields on which we perform checkings """ ret = SortedDict() for f in self._meta.fields: # avoid special_exclude fields if f.attname in special_exclude: continue ret[f.attname] = f # Deal with reverse relationships reverse_rels = self._meta.get_all_related_objects() # reverse_rels += self._meta.get_all_related_many_to_many_objects() for relation in reverse_rels: accessor_name = relation.get_accessor_name() to_many = relation.field.rel.multiple if not self.opts.fields or accessor_name not in self.opts.fields: continue if not to_many: raise NotImplementedError ret[accessor_name] = PrimaryKeyRelatedField() # If 'fields' is specified, use those fields, in that order. if self.opts.fields: new = SortedDict() for key in self.opts.fields: new[key] = ret[key] ret = new # Remove anything in 'exclude' if self.opts.exclude: for key in self.opts.exclude: ret.pop(key, None) return ret def check_fields(self): """ First `self.clean_fields` is called to ensure data integrity Checks all fields and return a list of `CheckResult` instances """ # TODO try to reintegrate this one without # increase the db queries too much # self.clean_fields() checks = [] fields = self.get_checking_fields() for key, field in fields.items(): check = self.get_check_for_field(key, checking_fields=fields) checks.append(check) return checks def full_check(self): """ Calls `self.check_fields`, `self.check` in that order NB: no need to call `self.full_clean` because the above methods already made those calls internally """ # basic field checks checks = self.check_fields() # special checks additional_checks = self.check_additionnals() checks.extend(additional_checks) return checks def check_additionnals(self): """Additional checks that the user can implement""" return [] def _raw_checking_completion(self): """ Useful for additional checking completion computations """ checks = self.full_check() completed = sum(1 for c in checks if c.has_passed) return completed, len(checks) def checking_completion(self): """ Compute the percentage of checking completed on the model instance """ completed, total = self._raw_checking_completion() return float(completed) / total def full_checking_completion(self): """ Calls `self.checking_completion` This method should be used to do checking completion on related objects """ completion = self.checking_completion() return completion def pass_full_checking(self): completion = self.full_checking_completion() return completion == 1.0 ================================================ FILE: accounting/libs/decorators.py ================================================ # encoding: utf-8 def composed(*decs): """ Compose multiple decorators Example : >>> @composed(dec1, dec2) ... def some(f): ... pass """ def deco(f): for dec in reversed(decs): f = dec(f) return f return deco def order_fields(*field_list): def decorator(form): original_init = form.__init__ def init(self, *args, **kwargs): original_init(self, *args, **kwargs) for field in field_list[::-1]: self.fields.insert(0, field, self.fields.pop(field)) form.__init__ = init return form return decorator def memoize(func): """ Memoization decorator for a function taking one or more arguments. """ class memodict(dict): def __getitem__(self, *key): return dict.__getitem__(self, key) def __missing__(self, key): ret = self[key] = func(*key) return ret return memodict().__getitem__ ================================================ FILE: accounting/libs/exceptions.py ================================================ ================================================ FILE: accounting/libs/fields.py ================================================ from django.db import models import uuid class UUIDField(models.CharField): def __init__(self, *args, **kwargs): kwargs['max_length'] = kwargs.get('max_length', 64 ) kwargs['blank'] = True models.CharField.__init__(self, *args, **kwargs) def _generate_uuid(self): return str(uuid.uuid4()) def pre_save(self, model_instance, add): if add or not getattr(model_instance, self.attname): value = self._generate_uuid() setattr(model_instance, self.attname, value) return value else: return super(models.CharField, self).pre_save(model_instance, add) ================================================ FILE: accounting/libs/foundation.py ================================================ """ Python helpers """ from collections import Mapping def update(d, u, depth=-1): """ Recursively merge or update dict-like objects. >>> update({'k1': {'k2': 2}}, {'k1': {'k2': {'k3': 3}}, 'k4': 4}) {'k1': {'k2': {'k3': 3}}, 'k4': 4} """ for k, v in u.iteritems(): if isinstance(v, Mapping) and not depth == 0: r = update(d.get(k, {}), v, depth=max(depth - 1, -1)) d[k] = r elif isinstance(d, Mapping): d[k] = u[k] else: d = {k: u[k]} return d ================================================ FILE: accounting/libs/intervals.py ================================================ from datetime import date class TimeInterval(object): start = None end = None def __init__(self, start, end): assert start is None or isinstance(start, date), \ "start should be a date instance" assert end is None or isinstance(end, date), \ "end should be a date instance" self.start = start self.end = end ================================================ FILE: accounting/libs/prices.py ================================================ class TaxNotKnown(Exception): """ Exception for when a tax-inclusive price is requested but we don't know what the tax applicable is (yet). """ class Price(object): """ Simple price class that encapsulates a price and its tax information Attributes: incl_tax (Decimal): Price including taxes excl_tax (Decimal): Price excluding taxes tax (Decimal): Tax amount is_tax_known (bool): Whether tax is known currency (str): 3 character currency code """ def __init__(self, currency, excl_tax, incl_tax=None, tax=None): self.currency = currency self.excl_tax = excl_tax if incl_tax is not None: self.incl_tax = incl_tax self.is_tax_known = True elif tax is not None: self.incl_tax = excl_tax + tax self.is_tax_known = True else: self.incl_tax = None self.is_tax_known = False def _get_tax(self): return self.incl_tax - self.excl_tax def _set_tax(self, value): self.incl_tax = self.excl_tax + value self.is_tax_known = True tax = property(_get_tax, _set_tax) def __repr__(self): if self.is_tax_known: return "%s(currency=%r, excl_tax=%r, incl_tax=%r, tax=%r)" % ( self.__class__.__name__, self.currency, self.excl_tax, self.incl_tax, self.tax) return "%s(currency=%r, excl_tax=%r)" % ( self.__class__.__name__, self.currency, self.excl_tax) def __eq__(self, other): """ Two price objects are equal if currency, price.excl_tax and tax match. """ return (self.currency == other.currency and self.excl_tax == other.excl_tax and self.incl_tax == other.incl_tax) ================================================ FILE: accounting/libs/templatetags/__init__.py ================================================ ================================================ FILE: accounting/libs/templatetags/check_filters.py ================================================ from django import template from accounting.libs.checks import CheckingModelMixin register = template.Library() @register.filter def check(obj, field_name=None): """ Can check an entire model or just a single model field """ if isinstance(obj, CheckingModelMixin): if not field_name: check = obj.full_check() else: check = obj.get_check_for_field(field_name) else: return None return check @register.filter('level_to_css_classname') def _check_level_to_classname(check): """ Return the appropriated css classname for the check level """ if check.has_failed: if check.level == check.LEVEL_ERROR: return 'danger' elif check.level == check.LEVEL_WARNING: return 'warning' else: return 'info' return 'default' @register.filter('level_to_glyphicon') def _check_level_to_glyphicon(check): """ Return the appropriated glyph icon for the check level """ if check.has_failed: if check.level == check.LEVEL_ERROR: return 'minus-sign' elif check.level == check.LEVEL_WARNING: return 'exclamation-sign' else: return 'question-sign' return 'ok' ================================================ FILE: accounting/libs/templatetags/check_tags.py ================================================ from django import template from classytags.core import Options from classytags.arguments import Argument from classytags.helpers import InclusionTag register = template.Library() @register.tag class Check(InclusionTag): name = 'render_check' template = '_generics/check_tag.html' options = Options( Argument('check'), ) def get_context(self, context, check): context.update({ 'check': check }) return context ================================================ FILE: accounting/libs/templatetags/currency_filters.py ================================================ # encoding: utf-8 from decimal import Decimal as D, InvalidOperation from django import template from django.conf import settings from django.utils.translation import to_locale, get_language from babel.numbers import format_currency register = template.Library() @register.filter(name='currency') def currency_formatter(value, currency=None): """ Format decimal value as currency """ try: value = D(value) except (TypeError, InvalidOperation): return "" # Using Babel's currency formatting # http://babel.pocoo.org/docs/api/numbers/#babel.numbers.format_currency currency = currency or settings.ACCOUNTING_DEFAULT_CURRENCY kwargs = { 'currency': currency, 'format': getattr(settings, 'CURRENCY_FORMAT', None), 'locale': to_locale(get_language()), } return format_currency(value, **kwargs) ================================================ FILE: accounting/libs/templatetags/display_tags.py ================================================ from django import template register = template.Library() ================================================ FILE: accounting/libs/templatetags/distance_filters.py ================================================ from django import template from django.contrib.gis.measure import Distance register = template.Library() @register.filter def has_distance(search_object): return bool(search_object._point_of_origin) @register.filter def distance(dist): if isinstance(dist, Distance): d = dist.m unit = "m" if d > 1000: d = dist.km unit = "km" ctx = { 'distance': float('%.2g' % d), 'unit': unit, } return u"%(distance)s %(unit)s" % ctx ================================================ FILE: accounting/libs/templatetags/float_filters.py ================================================ from django.template import Library from django.utils.numberformat import format register = Library() @register.filter(name="float_dot") def do_float_dot(value, decimal_pos=4): return format(value or 0, ".", decimal_pos) do_float_dot.is_safe = True ================================================ FILE: accounting/libs/templatetags/form_filters.py ================================================ from django import template from django.forms import ModelForm, BaseFormSet from django.forms.forms import BoundField register = template.Library() @register.filter def css_class(field): if isinstance(field, BoundField): field = field.field return field.widget.__class__.__name__.lower() @register.filter def is_disabled(field): if isinstance(field, BoundField): field = field.field return 'disabled' in field.widget.attrs @register.filter def is_readonly(field): if isinstance(field, BoundField): field = field.field return ('readonly' in field.widget.attrs and field.widget.attrs.get('readonly') is True) @register.filter def get_form_model_verbose_name(instance): if isinstance(instance, ModelForm): return instance._meta.model._meta.verbose_name.title() if isinstance(instance, BaseFormSet): return instance.model._meta.verbose_name_plural.title() return '' ================================================ FILE: accounting/libs/templatetags/form_tags.py ================================================ from django import template register = template.Library() @register.tag def annotate_form_field(parser, token): """ Set an attribute on a form field with the widget type This means templates can use the widget type to render things differently if they want to. Django doesn't make this available by default. """ args = token.split_contents() if len(args) < 2: raise template.TemplateSyntaxError( "annotate_form_field tag requires a form field to be passed") return FormFieldNode(args[1]) class FormFieldNode(template.Node): def __init__(self, field_str): self.field = template.Variable(field_str) def render(self, context): field = self.field.resolve(context) field.widget_type = field.field.widget.__class__.__name__ return '' ================================================ FILE: accounting/libs/templatetags/format_filters.py ================================================ import datetime from django import template from django.utils.translation import ugettext_lazy as _ from django.utils.timezone import now as django_now from django.utils.translation import to_locale, get_language from babel.numbers import format_percent register = template.Library() @register.filter('percentage') def percentage_formatter(value): if value or value == 0: kwargs = { 'locale': to_locale(get_language()), 'format': "#,##0.00 %", } return format_percent(value, **kwargs) @register.filter def smartdate(value): if isinstance(value, datetime.datetime): now = django_now() else: now = datetime.date.today() timedelta = value - now format = _(u"%(delta)s %(unit)s") delta = abs(timedelta.days) if delta > 30: delta = int(delta / 30) unit = _(u"mois") else: unit = _(u"jours") ctx = { 'delta': delta, 'unit': unit, } return format % ctx ================================================ FILE: accounting/libs/templatetags/introspection_filters.py ================================================ from django import template from django.forms import ModelForm, BaseFormSet from django.db.models import Model from django_select2.fields import ( AutoModelSelect2Field, AutoModelSelect2MultipleField) register = template.Library() @register.filter def get_model_verbose_name(instance): if isinstance(instance, Model): return instance._meta.verbose_name.title() return '' @register.filter def get_form_model_verbose_name(instance): if isinstance(instance, ModelForm): return instance._meta.model._meta.verbose_name.title() if isinstance(instance, BaseFormSet): return instance.model._meta.verbose_name_plural.title() return '' @register.filter def is_select2_field(form, field): select2_classes = (AutoModelSelect2Field, AutoModelSelect2MultipleField) res = any(isinstance(field.field, cls) for cls in select2_classes) return res ================================================ FILE: accounting/libs/templatetags/my_filters.py ================================================ from django import template register = template.Library() @register.filter(name='times') def times(number): return range(number) @register.filter def get_object(l, index): return l[index] @register.filter def get_item(d, key, default=None): if default: d.get(key, None) return d.get(key) ================================================ FILE: accounting/libs/templatetags/nav.py ================================================ # encoding: utf-8 import re from django import template register = template.Library() @register.simple_tag def active(request, pattern, exact_match=False): if exact_match: if not pattern.startswith('^'): pattern = '^' + pattern if not pattern.endswith('$'): pattern = pattern + '$' if hasattr(request, 'path') and re.search(pattern, request.path): return 'active' return '' ================================================ FILE: accounting/libs/templatetags/url_tags.py ================================================ # encoding: utf-8 from django import template from django.http import QueryDict from classytags.core import Tag, Options from classytags.arguments import MultiKeywordArgument, MultiValueArgument register = template.Library() class QueryParameters(Tag): name = 'query' options = Options( MultiKeywordArgument('kwa'), ) def render_tag(self, context, kwa): q = QueryDict('').copy() q.update(kwa) return q.urlencode() register.tag(QueryParameters) class GetParameters(Tag): """ {% get_parameters [except_field, ] %} """ name = 'get_parameters' options = Options( MultiValueArgument('except_fields', required=False), ) def render_tag(self, context, except_fields): try: # If there's an exception (500), default context_processors may not # be called. request = context['request'] except KeyError: return context getvars = request.GET.copy() for field in except_fields: if field in getvars: del getvars[field] return getvars.urlencode() register.tag(GetParameters) ================================================ FILE: accounting/libs/utils.py ================================================ import os import uuid import datetime import random import hashlib import copy import decimal def banker_round(decimal_value): """ Force the value to be rounded with the `ROUND_HALF_EVEN` method, also called the Banking Rounding due to the heavy use in the banking system """ return decimal_value.quantize(decimal.Decimal('0.01'), rounding=decimal.ROUND_HALF_EVEN) def random_token(extra=None, hash_func=hashlib.sha256): """ Extracted from `django-user-accounts` """ if extra is None: extra = [] bits = extra + [str(random.SystemRandom().getrandbits(512))] return hash_func("".join(bits)).hexdigest() def create_hash(string, hash_func=hashlib.sha256): """ Create a 10-caracters string hash """ _hash = hash_func(string) return _hash.hexdigest()[:10] def nested_hash(data): """ Make a hash from a nested dictionnary """ if isinstance(data, (set, tuple, list)): return tuple(nested_hash(d) for d in data) elif not isinstance(data, dict): return data new_data = copy.deepcopy(data) for k, v in new_data.items(): new_data[k] = nested_hash(v) return hash(tuple(frozenset(sorted(new_data.items())))) def unique_filename(path): """ Return a unique filename, which is usefull for image upload for instance """ def _unique_path(obj, name): parts = name.split('.') extension = parts[-1] directory_path = os.path.normpath( datetime.datetime.now().strftime(path)) unique_name = "{0}.{1}".format(uuid.uuid4(), extension) return os.path.join(directory_path, unique_name) return _unique_path def queryset_iterator(queryset, chunksize=1000, reverse=False): """ Execute the request by chunks to avoid database memory error """ ordering = '-' if reverse else '' queryset = queryset.order_by(ordering + 'pk') last_pk = None new_items = True while new_items: new_items = False chunk = queryset if last_pk is not None: func = 'lt' if reverse else 'gt' chunk = chunk.filter(**{'pk__' + func: last_pk}) chunk = chunk[:chunksize] row = None for row in chunk: yield row if row is not None: last_pk = row.pk new_items = True ================================================ FILE: accounting/static/accounting/css/main.css ================================================ /* * Base structure */ body { padding-top: 0px; } /* * Utils */ .overflow-box { overflow: auto; } .table .empty-line > td { border-top: none; } .table .empty-line + tr > td { border-top: none; } .table .empty-cell { border-top: none; } /* * Global add-ons */ .sub-header { padding-bottom: 10px; border-bottom: 1px solid #eee; } /* * Top navigation * Hide default border to remove 1px line. */ .navbar-fixed-top { border: 0; } .navbar-inverse { background: #222; } .navbar-nav > li { border-right: 1px solid rgba(255, 255, 255, 0.15); } .navbar-nav > li > a { padding-left: 2em; padding-right: 2em; } .navbar-nav > li > a:hover { background: rgba(255, 255, 255, 0.06) !important; } @media (min-width: 768px) { .nav .breadcrumb { margin: 7px 10px 7px 0px; } } /* * Sidebar */ /* Hide for mobile, show later */ .sidebar { display: none; } @media (min-width: 768px) { .sidebar { position: fixed; top: 0px; bottom: 0; left: 0; z-index: 1000; display: block; padding: 80px 20px 20px 20px; overflow-x: hidden; overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ background-color: #f5f5f5; border-right: 1px solid #eee; } } /* Sidebar navigation */ .nav-sidebar { font-weight: 500; margin-right: -21px; /* 20px padding + 1px border */ margin-bottom: 20px; margin-left: -20px; } .nav-sidebar > li > a { padding-right: 20px; padding-left: 20px; } .nav-sidebar > .active > a, .nav-sidebar > .active > a:hover, .nav-sidebar > .active > a:focus { color: #fff; background-color: #428bca; } /* * Main content */ .main { padding: 20px; } @media (min-width: 768px) { .main { padding-right: 40px; padding-left: 40px; } } .main .page-header { margin-top: 0; } /* * Placeholder dashboard ideas */ .placeholders { margin-bottom: 30px; text-align: center; } .placeholders h4 { margin-bottom: 0; } .placeholder { margin-bottom: 20px; } .placeholder img { display: inline-block; border-radius: 50%; } /* * Figure */ .figure { margin-bottom: 1em; padding: 0.8em 0.5em 1.5em 0.5em; background: white; background: rgba(255, 255, 255, 0.50); border: 1px solid rgba(0, 0, 0, 0.06); border-radius: 4px; } /* * Panels */ .panel-link { display: block; } .panel-link:hover { background: #f0f0f0; text-decoration: none; } /* * Check list */ .check-list { } .check-item { margin-right: 10px; } /* * Form labels */ .label-inlineblock { display: inline-block; } .control-label-block { display: block; } ================================================ FILE: accounting/static/accounting/js/books/invoice_or_bill_create.js ================================================ $(function() { $('.formset-form').formset({ prefix: 'lines', addText: 'add another line', deleteText: 'remove line', addCssClass: 'btn btn-primary btn-sm', deleteCssClass: 'btn btn-danger btn-sm', added: function($row) { // update line title var index = $row.index('.formset-form') + 1; $row.find('.counter').text(index); } }); }) ================================================ FILE: accounting/static/accounting/js/jquery.formset.js ================================================ /** * jQuery Dynamic Formset * Inspired from the jQuery Formset plugin of Stanislaus Madueke */ ;(function($) { $.fn.formset = function(opts) { var options = $.extend({}, $.fn.formset.defaults, opts), flatExtraClasses = options.extraClasses.join(' '), totalForms = $('#id_' + options.prefix + '-TOTAL_FORMS'), maxForms = $('#id_' + options.prefix + '-MAX_NUM_FORMS'), childElementSelector = 'input,select,textarea,label,div', $$ = $(this), applyExtraClasses = function(row, ndx) { if (options.extraClasses) { row.removeClass(flatExtraClasses); row.addClass(options.extraClasses[ndx % options.extraClasses.length]); } }, updateElementIndex = function(elem, prefix, ndx) { var idRegex = new RegExp(prefix + '-(\\d+|__prefix__)-'), replacement = prefix + '-' + ndx + '-'; if (elem.attr("for")) elem.attr("for", elem.attr("for").replace(idRegex, replacement)); if (elem.attr('id')) elem.attr('id', elem.attr('id').replace(idRegex, replacement)); if (elem.attr('name')) elem.attr('name', elem.attr('name').replace(idRegex, replacement)); }, hasChildElements = function(row) { return row.find(childElementSelector).length > 0; }, showAddButton = function() { return maxForms.length == 0 || // For Django versions pre 1.2 (maxForms.val() == '' || (maxForms.val() - totalForms.val() > 0)); }, insertDeleteLink = function(row) { var delCssSelector = options.deleteCssClass.trim().replace(/\s+/g, '.'), addCssSelector = options.addCssClass.trim().replace(/\s+/g, '.'); if (row.is('TR')) { // If the forms are laid out in table rows, insert // the remove button into the last table cell: row.children(':last').append('' + options.deleteText + ''); } else if (row.is('UL') || row.is('OL')) { // If they're laid out as an ordered/unordered list, // insert an
  • after the last list item: row.append('
  • ' + options.deleteText +'
  • '); } else { // Otherwise, just insert the remove button as the // last child element of the form's container: row.append('' + options.deleteText +''); } row.find('a.' + delCssSelector).click(function() { var row = $(this).parents('.' + options.formCssClass), del = row.find('input:hidden[id $= "-DELETE"]'), buttonRow = row.siblings("a." + addCssSelector + ', .' + options.formCssClass + '-add'), forms; if (del.length) { // We're dealing with an inline formset. // Rather than remove this form from the DOM, we'll mark it as deleted // and hide it, then let Django handle the deleting: del.val('on'); row.hide(); forms = $('.' + options.formCssClass).not(':hidden'); } else { row.remove(); // Update the TOTAL_FORMS count: forms = $('.' + options.formCssClass).not('.formset-custom-template'); totalForms.val(forms.length); } for (var i=0, formCount=forms.length; i