Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create invoices #199

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion piecash/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
factories,
)
from .business import Vendor, Customer, Employee, Address
from .business import Invoice, Job
from .business import Invoice, Job, Bill, Expensevoucher
from .business import Taxtable, TaxtableEntry
from .budget import Budget, BudgetAmount
from .kvp import slot
Expand Down
2 changes: 1 addition & 1 deletion piecash/business/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .invoice import Billterm, Entry, Invoice, Job, Order
from .invoice import Billterm, Entry, Invoice, Job, Order, Bill, Expensevoucher
from .tax import Taxtable, TaxtableEntry
from .person import Customer, Employee, Vendor, Address
857 changes: 803 additions & 54 deletions piecash/business/invoice.py

Large diffs are not rendered by default.

22 changes: 19 additions & 3 deletions piecash/business/person.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from sqlalchemy import Column, VARCHAR, INTEGER, BIGINT, ForeignKey, and_, event
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import composite, relation, foreign
from sqlalchemy.orm.attributes import get_history

from .._common import hybrid_property_gncnumeric, CallableList
from .._declbase import DeclarativeBaseGuid
Expand All @@ -14,7 +15,6 @@
(3, "USEGLOBAL")
]


class Address(object):
"""An Address object encapsulates information regarding an address in GnuCash.

Expand Down Expand Up @@ -71,7 +71,6 @@ def __eq__(self, other):
def __ne__(self, other):
return not self.__eq__(other)


class Person:
"""A mixin declaring common field for Customer, Vendor and Employee"""

Expand Down Expand Up @@ -164,6 +163,23 @@ def add(target, value, initiator):
value.owner_guid = target.guid
value._assign_id()

# add listeners to update the Billterm.refcount field
if owner_type and hasattr(cls, "term"):
event.listen(cls, "after_insert", cls._changed)
event.listen(cls, "after_update", cls._changed)
event.listen(cls, "after_delete", cls._deleted)

def _changed(mapper, connection, target):
# check if the term field changed, obtain new and old value, and update the refcount for the terms
newval, _, oldval = get_history(target, 'term')
if newval and (newval[0] is not None):
newval[0]._increase_refcount(connection)
if oldval and (oldval[0] is not None):
oldval[0]._decrease_refcount(connection)

def _deleted(mapper, connection, target):
if target.term:
target.term._decrease_refcount(connection)

class Customer(Person, DeclarativeBaseGuid):
"""
Expand Down Expand Up @@ -425,5 +441,5 @@ def __init__(

_counter_name = "counter_vendor"

PersonType = {Vendor: 4, Customer: 2, Employee: 5}

PersonType = {Vendor: 4, Customer: 2}
111 changes: 111 additions & 0 deletions piecash/business/tax.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
from .._declbase import DeclarativeBaseGuid, DeclarativeBase
from ..sa_extra import ChoiceType

from enum import Enum
import collections

class DiscountType(Enum):
value = "VALUE"
percent = "PERCENT"

class DiscountHow(Enum):
pretax = "PRETAX"
sametime = "SAMETIME"
posttax = "POSTTAX"

class Taxtable(DeclarativeBaseGuid):
__tablename__ = "taxtables"
Expand Down Expand Up @@ -58,8 +69,104 @@ def __str__(self):
)
else:
return "TaxTable<{}>".format(self.name)

# adjust the refcount field - called by e.g. Entry
def _increase_refcount(self, connection, increment=1):
r = connection.execute(
self.__table__.
select().
where(Taxtable.guid == self.guid))
refcountval = r.fetchone()[2]
connection.execute(
self.__table__.
update().
values(refcount=refcountval+increment).
where(Taxtable.guid == self.guid))

# adjust the refcount field - called by e.g. Entry
def _decrease_refcount(self, connection):
self._increase_refcount(connection, increment=-1)

def calculate_subtotal_and_tax(self, quantity, price, i_disc_how, i_disc_type, i_discount, taxincluded):
# 12 different permutations depending on i_disc_type (2), i_disc_how (3), taxincluded (2)
# Basically: pretax + tax - discount = subtotal + tax. What varies is the reference for tax computation, and whether the quantity * price already includes tax or not
# The Taxtable may contain multiple TaxtableEntries, each entry either a percent or a value. The tax is then:
# tax = sum(tax value) + pretax * sum(tax percent)
# A dict is returned with the amount of tax due to each tax account
tax = 0
subtotal = 0
pretax = 0
taxes = {}
if self.entries:
# First need sum(tax values) and sum(tax percent)
sum_tax_value = sum(entry.amount for entry in self.entries if entry.type=="value")
sum_tax_percent = sum(entry.amount for entry in self.entries if entry.type=="percentage") / 100

if taxincluded:
#tax included with quantity * price: need to back off the old tax before adding the new tax
#quantity * price = pretax + tax = pretax + sum_tax_value + pretax * sum_tax_percent = pretax * (1 + sum_tax_percent) + sum_tax_value
pretax = (quantity * price - sum_tax_value) / (1 + sum_tax_percent)
else:
pretax = quantity * price

if i_disc_how == DiscountHow.sametime and i_disc_type == DiscountType.percent:
#tax and discount based on pretax value.
subtotal = pretax * (1 - i_discount/100)
tax = sum_tax_value + pretax * sum_tax_percent
elif i_disc_how == DiscountHow.sametime and i_disc_type == DiscountType.value:
#price includes tax, discount and tax applied to pretax value
subtotal = pretax - i_discount
tax = sum_tax_value + pretax * sum_tax_percent
elif i_disc_how == DiscountHow.pretax and i_disc_type == DiscountType.percent:
#price includes a tax that needs backing off, and tax to be recomputed after discount applied
subtotal = pretax * (1 - i_discount/100)
tax = sum_tax_value + subtotal * sum_tax_percent
elif i_disc_how == DiscountHow.pretax and i_disc_type == DiscountType.value:
#price includes tax that needs backing off, and tax recomputed based on discounted price
subtotal = pretax - i_discount
tax = sum_tax_value + subtotal * sum_tax_percent
elif i_disc_how == DiscountHow.posttax and i_disc_type == DiscountType.percent and taxincluded:
#discount applied to pretax + tax
subtotal = pretax - quantity * price * i_discount/100
tax = sum_tax_value + pretax * sum_tax_percent
elif i_disc_how == DiscountHow.posttax and i_disc_type == DiscountType.percent and not taxincluded:
#discount calculated based on pretax + tax
tax = sum_tax_value + pretax * sum_tax_percent
subtotal = pretax - (pretax + tax) * i_discount/100
elif i_disc_how == DiscountHow.posttax and i_disc_type == DiscountType.value:
subtotal = pretax - i_discount
tax = sum_tax_value + pretax * sum_tax_percent

# get dict of accounts and tax due to each account
# tax = sum(tax value) + pretax * sum(tax percent)
if sum_tax_percent != 0:
pretax = (tax - sum_tax_value) / sum_tax_percent
else:
pretax = tax - sum_tax_value
for entry in self.entries:
if entry.type == "value":
taxes[entry.account] = taxes.get(entry.account, 0) + entry.amount
else:
taxes[entry.account] = taxes.get(entry.account, 0) + pretax*entry.amount/100

return subtotal, tax, taxes

def _table_entries(self):
return collections.Counter([entry.__str__() for entry in self.entries])

def create_copy_as_child(self):
# only copy a tax table if an existing copy doesn't exist; only differences in taxtable entries matter, i.e. amount, type and account.
# this is captured in TaxtableEntry.__string__
clone = None
if not any([self._table_entries() == child._table_entries() for child in self.children]):
entries = [entry.clone() for entry in self.entries]
clone = Taxtable(self.name, entries=entries)
clone.invisible = True
clone.parent_guid = self.guid
self.children.append(clone)

return clone

class TaxtableEntry(DeclarativeBase):
__tablename__ = "taxtable_entries"

Expand Down Expand Up @@ -91,3 +198,7 @@ def __init__(self, type, amount, account, taxtable=None):

def __str__(self):
return "TaxEntry<{} {} in {}>".format(self.amount, self.type, self.account.name)

def clone(self):
clone = TaxtableEntry(self.type, self.amount, self.account)
return clone
48 changes: 44 additions & 4 deletions piecash/core/book.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@
from .transaction import Split, Transaction
from .._common import CallableList, GnucashException
from .._declbase import DeclarativeBaseGuid
from ..business.invoice import Invoice
from ..business.invoice import Invoice, Bill, Expensevoucher
from ..sa_extra import kvp_attribute


class Book(DeclarativeBaseGuid):
"""
A Book represents a GnuCash document. It is created through one of the two factory functions
Expand Down Expand Up @@ -412,6 +411,24 @@ def invoices(self):

return CallableList(self.session.query(Invoice))

@property
def bills(self):
"""
gives easy access to all commodities in the book through a :class:`piecash.model_common.CallableList`
of :class:`piecash.core.commodity.Commodity`
"""

return CallableList(self.session.query(Bill))

@property
def expensevouchers(self):
"""
gives easy access to all commodities in the book through a :class:`piecash.model_common.CallableList`
of :class:`piecash.core.commodity.Commodity`
"""

return CallableList(self.session.query(Expensevoucher))

@property
def currencies(self):
"""
Expand Down Expand Up @@ -473,12 +490,35 @@ def employees(self):
@property
def taxtables(self):
"""
gives easy access to all commodities in the book through a :class:`piecash.model_common.CallableList`
gives easy access to taxtables in the book through a :class:`piecash.model_common.CallableList`
of :class:`piecash.business.tax.Taxtable`

Only retrieves 'parent' taxtables, i.e. taxtables with attribute invisible set to False.
Child taxtables (those with attribute invisible set to True) may be accessed via the parents.
"""
from ..business import Taxtable

return CallableList(self.session.query(Taxtable))
return CallableList(self.session.query(Taxtable).filter(Taxtable.invisible==False))

@property
def billterms(self):
"""
gives easy access to all terms in the book through a :class:`piecash.model_common.CallableList`
of :class:`piecash.business.invoice.Billterm`
"""
from ..business import Billterm

return CallableList(self.session.query(Billterm))

@property
def jobs(self):
"""
gives easy access to all jobs in the book through a :class:`piecash.model_common.CallableList`
of :class:`piecash.business.invoice.Job`
"""
from ..business import Job

return CallableList(self.session.query(Job))

@property
def query(self):
Expand Down
2 changes: 1 addition & 1 deletion piecash/kvp.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ class SlotGUID(SlotFrame):
_mapping_name_class = {
"from-sched-xaction": "piecash.core.transaction.ScheduledTransaction",
"account": "piecash.core.account.Account",
"invoice-guid": "piecash.business.invoice.Invoice",
"invoice-guid": "piecash.business.invoice.InvoiceBase",
"peer_guid": "piecash.core.transaction.Split",
"gains-split": "piecash.core.transaction.Split",
"gains-source": "piecash.core.transaction.Split",
Expand Down
Loading