def set_payment(self, splits): self.json["payment"] = Amount.zero() for job in self.json["jobs"]: credit = Amount.zero() for row in splits: if job["job.id"] == row["job.id"]: credit = row["credit"] break job["payment"] = credit self.json["payment"] += credit
def calculate_initial_refund(self): refund_total = Amount.zero() for form in self.formset: refund_total += form.refund_amount for form in self.formset: refund_total = form.consume_refund(refund_total) return refund_total
def calculate_totals(self, totals, condition=lambda form: True): for total in totals: total_field = ( total[:-4] + "total_diff_amount" if total.endswith("_diff") else total + "_total_amount" ) total_field = total_field.replace(".", "_") setattr(self, total_field, Amount.zero()) for form in self.formset: if condition(form): for total in totals: total_field = ( total[:-4] + "total_diff_amount" if total.endswith("_diff") else total + "_total_amount" ) if "." in total: state_name, attr_name = total.split(".") state = getattr(form, state_name) form_amount = getattr(state, attr_name + "_amount") else: form_amount = getattr(form, total + "_amount") total_field = total_field.replace(".", "_") total_amount = getattr(self, total_field) setattr(self, total_field, total_amount + form_amount)
def migrate_invoices(apps, schema_editor): from systori.apps.company.models import Company from systori.apps.document.models import Invoice from systori.apps.task.models import Job from systori.apps.accounting.report import create_invoice_report from systori.apps.accounting.constants import TAX_RATE from systori.lib.accounting.tools import Amount for company in Company.objects.all(): company.activate() for invoice in Invoice.objects.all(): invoice.json["debit"] = Amount(invoice.json["debit_net"], invoice.json["debit_tax"]) if "debits" not in invoice.json: invoice.json["jobs"] = [] invoice.save() continue invoice.json["jobs"] = invoice.json["debits"] del invoice.json["debits"] jobs = Job.objects.filter( id__in=[job["job.id"] for job in invoice.json["jobs"]]) tdate = date( *map(int, invoice.json["transactions"][-1]["date"].split("-"))) new_json = create_invoice_report(invoice.transaction, jobs, tdate) if (company.schema == "mehr_handwerk" and invoice.id not in [86, 111]) or ( company.schema == "montageservice_grad" and invoice.id not in [1]): assert new_json["debit"].gross == invoice.json["debit"].gross assert new_json["invoiced"].gross == invoice.json[ "debited_gross"] invoice.json.update(new_json) for job in invoice.json["jobs"]: taskgroup_total = Decimal("0.00") for taskgroup in job["taskgroups"]: taskgroup_total += taskgroup["total"] job["progress"] = Amount.from_net(taskgroup_total, TAX_RATE) invoiced_total = Amount.zero() for debit in invoice.json["job_debits"].get(job["job.id"], []): invoiced_total += debit["amount"] job["invoiced"] = invoiced_total job["debit"] = Amount(job["amount_net"], job["amount_tax"]) job["balance"] = Amount(job["balance_net"], job["balance_tax"]) job["estimate"] = Amount.from_net(job["estimate_net"], TAX_RATE) job["is_itemized"] = job["invoiced"].gross == job[ "progress"].gross invoice.save()
def clean(self): splits = Amount.zero() for form in self.formset: splits += form.split_amount if splits.gross != self.payment_value: raise forms.ValidationError( _("The sum of splits must equal the payment amount.") )
def init_amount(self, name): initial_amount = self.initial.get(name, Amount.zero()) self.initial[name + "_net"] = initial_amount.net self.initial[name + "_tax"] = initial_amount.tax initial_or_updated_amount = Amount( convert_field_to_value(self[name + "_net"]), convert_field_to_value(self[name + "_tax"]), ) setattr(self, name + "_amount", initial_or_updated_amount)
def set_adjustments(self, adjustments): self.json["adjustment"] = Amount.zero() self.json["corrected"] = Amount.zero() for job in self.json["jobs"]: adjustment = Amount.zero() corrected = Amount.zero() for row in adjustments: if job["job.id"] == row["job.id"]: adjustment = row["adjustment"] corrected = row["corrected"] break job["adjustment"] = adjustment self.json["adjustment"] += adjustment job["corrected"] = corrected self.json["corrected"] += corrected
def calculate_initial_values(self): if "invoiced" in self.initial: self.pre_txn.invoiced_amount = self.initial["invoiced"] self.pre_txn.invoiced_diff_amount = Amount.zero() if "corrected" not in self.initial: self.initial["corrected"] = self.pre_txn.invoiced_amount self.init_amount("corrected") if "adjustment" not in self.initial: self.initial["adjustment"] = ( self.corrected_amount - self.pre_txn.invoiced_amount ) self.init_amount("adjustment")
def calculate_initial_values(self): self.balance_amount = self.pre_txn.balance_amount if "invoiced" in self.initial: # when paying an invoice, the balance is taken from invoice instead of accouting system self.balance_amount = self.initial["invoiced"] self.init_amount("split") self.init_amount("discount") self.init_amount("adjustment") if self.balance_amount.gross < 0: self.balance_amount = Amount.zero() self.credit_amount = ( self.split_amount + self.discount_amount + self.adjustment_amount )
def calculate_accounting_state(self, state): state.estimate_amount = Amount.from_net(self.job.estimate, TAX_RATE) state.progress_amount = Amount.from_net(self.job.progress, TAX_RATE) state.progress_percent = self.job.progress_percent state.invoiced_amount = self.job.account.invoiced state.invoiced_percent = 0 if state.progress_amount.net > 0: state.invoiced_percent = ( state.invoiced_amount.net / state.progress_amount.net * 100 ) state.balance_amount = self.job.account.balance state.itemized_amount = state.progress_amount - state.invoiced_amount if state.itemized_amount.gross < 0: state.itemized_amount = Amount.zero()
def __init__(self, *args, **kwargs): super().__init__(*args, formset_class=RefundFormSet, **kwargs) if not self.instance.id: self.calculate_initial_refund() self.calculate_totals( [ "pre_txn.paid", "pre_txn.invoiced", "pre_txn.invoiced_diff", "pre_txn.progress", "pre_txn.progress_diff", "refund", "credit", ] ) self.customer_refund_amount = Amount.zero() if self.refund_total_amount.gross > self.credit_total_amount.gross: self.customer_refund_amount = ( self.refund_total_amount - self.credit_total_amount )
def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["status_filter"] = self.status_filter query = self.get_queryset() query = (query.prefetch_related("project").prefetch_related( "parent").filter(document_date__gte=date(2015, 9, 1)).order_by( "-document_date", "invoice_no")) months = OrderedDict() for invoice in query: doc_date = date(invoice.document_date.year, invoice.document_date.month, 1) month = months.setdefault(doc_date, { "invoices": [], "debit": Amount.zero() }) month["debit"] += invoice.json["debit"] month["invoices"].append(invoice) context["invoice_groups"] = months return context
def create_refund_report(txn): refund = {"txn": txn} jobs = {} for entry in txn.entries.all(): if not entry.account.is_receivable: continue job = jobs.setdefault( entry.job.id, { "job.id": entry.job.id, "code": entry.job.code, "name": entry.job.name, "amount": Amount.zero(), }, ) job["amount"] += entry.amount return refund
def calculate_initial_values(self): # on first load BooleanField.value() comes from self.initial and is an actual bool() type # on subsequent submits, value() returns a string from self.data, this makes # testing for truthyness difficult, so lets just always use the string version # unless you have self.cleaned_data available which is best self.is_invoiced_str = str(self["is_invoiced"].value()) self.is_override_str = str(self["is_override"].value()) if self.is_override_str == "False": if self.pre_txn.itemized_amount.net > 0: self.initial["debit"] = self.pre_txn.itemized_amount if "debit" not in self.initial: self.initial["debit"] = Amount.zero() self.init_amount("debit") self.post_txn = SimpleNamespace() self.post_txn.invoiced_amount = self.pre_txn.invoiced_amount + self.debit_amount self.post_txn.balance_amount = self.pre_txn.balance_amount + self.debit_amount self.is_itemized = True if self.post_txn.invoiced_amount != self.pre_txn.progress_amount: self.is_itemized = False
def test_serialize(self): invoice = Invoice.objects.create(project=self.project, letterhead=self.letterhead) invoice.json = {"jobs": [{"job": self.job}], "add_terms": False} pdf_type.invoice.serialize(invoice) self.maxDiff = None self.assertEqual( { "jobs": [{ "tasks": [], "groups": [{ "group.id": 2, "code": "01.01", "name": self.group.name, "description": "", "progress": Decimal("0.00"), "estimate": Decimal("0.0000"), "tasks": [{ "task.id": 1, "code": "01.01.001", "name": self.task.name, "description": "", "is_provisional": False, "variant_group": 0, "variant_serial": 0, "qty": Decimal("0.0000"), "complete": Decimal("0.0000"), "unit": "", "price": Decimal("0.0000"), "progress": Decimal("0.00"), "estimate": Decimal("0.0000"), "lineitems": [{ "lineitem.id": 1, "name": self.lineitem.name, "qty": Decimal("0.0000"), "unit": "", "price": Decimal("0.0000"), "estimate": Decimal("0.0000"), }], }], "groups": [], }], }], "add_terms": False, "debit": Amount.zero(), "invoiced": Amount.zero(), "paid": Amount.zero(), "unpaid": Amount.zero(), "payments": [], "job_debits": {}, }, invoice.json, )
def create_invoice_report(invoice_txn, jobs, transacted_on_or_before=None): """ :param invoice_txn: transaction that should be excluded from 'open claims' (unpaid amount) :param jobs: limit transaction details to specific set of jobs :param transacted_on_or_before: limit transactions up to and including a certain date :return: serializable data structure """ txns_query = ( Transaction.objects.filter(entries__job__in=jobs) .prefetch_related("entries__job__project") .prefetch_related("entries__account") .distinct() ) if transacted_on_or_before: txns_query = txns_query.filter(transacted_on__lte=transacted_on_or_before) transactions = list(txns_query) transactions.sort(key=_transaction_sort_key) report = { "invoiced": Amount.zero(), "paid": Amount.zero(), "payments": [], "job_debits": {}, "unpaid": Amount.zero(), "debit": Amount.zero(), } for txn in transactions: txn_dict = { "id": txn.id, "type": txn.transaction_type, "date": txn.transacted_on, "payment": Amount.zero(), "discount": Amount.zero(), "total": Amount.zero(), "jobs": {}, } job_debits = {} for entry in txn.entries.all(): # we only work with receivable accounts from here on out if not entry.account.is_receivable: continue if entry.job not in jobs: # skip jobs we're not interested in continue job_dict = txn_dict["jobs"].setdefault( entry.job.id, { "job.id": entry.job.id, "code": entry.job.code, "name": entry.job.name, "payment": Amount.zero(), "discount": Amount.zero(), "total": Amount.zero(), }, ) job_debit = job_debits.setdefault( entry.job.id, { "transaction_type": txn.transaction_type, "entry_type": None, "date": txn.transacted_on, "amount": Amount.zero(), }, ) if entry.entry_type == entry.PAYMENT: txn_dict["payment"] += entry.amount job_dict["payment"] += entry.amount elif entry.entry_type == entry.DISCOUNT: txn_dict["discount"] += entry.amount job_dict["discount"] += entry.amount if entry.entry_type in (entry.PAYMENT, entry.DISCOUNT): txn_dict["total"] += entry.amount job_dict["total"] += entry.amount if entry.entry_type in entry.TYPES_FOR_INVOICED_SUM: report["invoiced"] += entry.amount if txn.id == invoice_txn.id: report["debit"] += entry.amount assert job_debit["entry_type"] in ( None, entry.entry_type, entry.ADJUSTMENT, ) job_debit["entry_type"] = entry.entry_type job_debit["amount"] += entry.amount if entry.entry_type in entry.TYPES_FOR_PAID_SUM: report["paid"] += entry.amount if txn.transaction_type == txn.PAYMENT: report["payments"].append(txn_dict) for job_id, debit_dict in job_debits.items(): if debit_dict["amount"].gross != 0: debits = report["job_debits"].setdefault(job_id, []) debits.append(debit_dict) total_unpaid_balance = ( report["invoiced"] + report["paid"] ) # report['paid'] is a negative number report["unpaid"] = Amount.zero() if total_unpaid_balance.gross > report["debit"].gross: # There is more owed on the account than this invoice is trying to solicit. # This means we need to show the difference between what is being sought and what is owed # as unpaid amount above the debit line on invoice. report["unpaid"] = ( report["debit"] - total_unpaid_balance ) # big from small to get negative number return report
class InvoiceFactory(factory.django.DjangoModelFactory): json = {"debit": Amount.zero()} class Meta: model = Invoice
def migrate_payments(apps, schema_editor): from systori.apps.company.models import Company from systori.apps.document.models import Payment, DocumentSettings from systori.apps.task.models import Job from systori.apps.accounting.models import Transaction from systori.lib.accounting.tools import Amount print("payment migration..") for company in Company.objects.all(): company.activate() for t in Transaction.objects.filter( transaction_type=Transaction.PAYMENT): job = None job_dict = None jobs = {} split_t = Amount.zero() discount_t = Amount.zero() adjustment_t = Amount.zero() credit_t = Amount.zero() if t.id in [9, 154, 297, 307]: entry = t.entries.first() print( "Company: ", company.schema, ", Transaction ID:", t.id, ", Date:", t.transacted_on, ", Bank:", entry.account.name, ", Amount:", entry.amount.gross, ", Entries: ", t.entries.count(), ) continue bank_account = None for entry in t.entries.all(): if (entry.account.is_bank or entry.account.name == "VR Bank Rhein-Neckar eG"): bank_account = entry.account continue job = entry.job if job: job_dict = jobs.setdefault( job.id, { "job.id": job.id, "name": job.name, "split": Amount.zero(), "discount": Amount.zero(), "adjustment": Amount.zero(), "credit": Amount.zero(), }, ) if entry.entry_type == entry.PAYMENT: job_dict["split"] += entry.amount.negate split_t += entry.amount elif entry.entry_type == entry.DISCOUNT: job_dict["discount"] += entry.amount.negate discount_t += entry.amount elif entry.entry_type == entry.ADJUSTMENT: job_dict["adjustment"] += entry.amount.negate adjustment_t += entry.amount if entry.entry_type in ( entry.PAYMENT, entry.DISCOUNT, entry.ADJUSTMENT, ): job_dict["credit"] += entry.amount.negate credit_t += entry.amount assert job assert bank_account letterhead = DocumentSettings.get_for_language( "de").invoice_letterhead payment = Payment( project=job.project, document_date=t.transacted_on, transaction=t, letterhead=letterhead, ) payment.json.update({ "bank_account": bank_account.id, "date": t.transacted_on, "payment": credit_t.negate.gross, "discount": Decimal("0.00"), "split_total": split_t.negate, "discount_total": discount_t.negate, "adjustment_total": adjustment_t.negate, "credit_total": credit_t.negate, "jobs": jobs.values(), }) payment.save()