Changeset - b613a5072cbd
[Not reviewed]
0 3 0
Brett Smith - 5 years ago 2019-11-11 19:30:05
brettcsmith@brettcsmith.org
Initial code to report VAT on invoices.
3 files changed with 26 insertions and 1 deletions:
0 comments (0 inline, 0 general)
pinaxcon/settings.py
Show inline comments
 
import os
 
import dj_database_url
 

	
 
from decimal import Decimal
 

	
 
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
 
PACKAGE_ROOT = os.path.abspath(os.path.dirname(__file__))
 
BASE_DIR = PACKAGE_ROOT
 

	
 
DEBUG = bool(int(os.environ.get("DJANGO_DEBUG", "1")))
 

	
 
UNPREPEND_WWW = bool(int(os.environ.get("DJANGO_UNPREPEND_WWW", "0")))
 

	
 
APPEND_SLASH = True
 

	
 
# HEROKU: Update database configuration with $DATABASE_URL.
 
DATABASES = {"default": {}}
 
import dj_database_url
 
db_from_env = dj_database_url.config()
 
DATABASES['default'].update(db_from_env)
 
if DEBUG and not DATABASES['default']:
 
    DATABASES['default'] = {
 
        "ENGINE": "django.db.backends.sqlite3",
 
        "NAME": os.path.join(PROJECT_ROOT, "dev.db"),
 
    }
 

	
 
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost").split()
 
CANONICAL_HOST = os.environ.get("DJANGO_CANONICAL_HOST", None)
 

	
 
# If DEFAULT_FROM_EMAIL is not set, email will most likely break in prod.
 
from_email = os.environ.get("DJANGO_DEFAULT_FROM_EMAIL", None)
 
if from_email is not None:
 
    DEFAULT_FROM_EMAIL = from_email
 
    SERVER_EMAIL = DEFAULT_FROM_EMAIL
 

	
 
THEME_CONTACT_EMAIL = os.environ.get("THEME_CONTACT_EMAIL", from_email)
 

	
 
# Local time zone for this installation. Choices can be found here:
 
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
 
# although not all choices may be available on all operating systems.
 
# On Unix systems, a value of None will cause Django to use the same
 
# timezone as the operating system.
 
# If running in a Windows environment this must be set to the same as your
 
# system time zone.
 
TIME_ZONE = os.environ.get("TZ")
 

	
 
# Set the email address that will receive errors.
 
admin_email = os.environ.get("DJANGO_ADMIN_EMAIL", None)
 
if admin_email is not None:
 
    ADMINS = [("Webmaster", admin_email)]
 

	
 
# Language code for this installation. All choices can be found here:
 
# http://www.i18nguy.com/unicode/language-identifiers.html
 
LANGUAGE_CODE = "en-us"
 

	
 
SITE_ID = int(os.environ.get("SITE_ID", 1))
 

	
 
# If you set this to False, Django will make some optimizations so as not
 
# to load the internationalization machinery.
 
USE_I18N = True
 

	
 
# If you set this to False, Django will not format dates, numbers and
 
# calendars according to the current locale.
 
USE_L10N = True
 

	
 
# If you set this to False, Django will not use timezone-aware datetimes.
 
USE_TZ = True
 

	
 
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
 
# trailing slash.
 
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
 
MEDIA_URL = "/site_media/media/"
 

	
 
# Absolute path to the directory static files should be collected to.
 
# Don"t put anything in this directory yourself; store your static files
 
# in apps" "static/" subdirectories and in STATICFILES_DIRS.
 
# Example: "/home/media/media.lawrence.com/static/"
 
STATIC_ROOT = os.path.join(PROJECT_ROOT, 'staticfiles')
 

	
 
# URL prefix for static files.
 
# Example: "http://media.lawrence.com/static/"
 
STATIC_URL = '/static/'
 

	
 
# Additional locations of static files
 
STATICFILES_DIRS = (
 
    os.path.join(PROJECT_ROOT, 'static'),
 
)
 

	
 
# List of finder classes that know how to find static files in
 
# various locations.
 
STATICFILES_FINDERS = [
 
    "django.contrib.staticfiles.finders.FileSystemFinder",
 
    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
 
    "compressor.finders.CompressorFinder",
 
]
 

	
 
try:
 
    _dynamic_storage_root = os.environ['DJANGO_DYNAMIC_STORAGE_ROOT']
 
except KeyError:
 
    # Absolute filesystem path to the directory that will hold user-uploaded files.
...
 
@@ -222,104 +223,111 @@ LOGGING = {
 
    },
 
    "handlers": {
 
        'console': {
 
            'level': os.environ.get('DJANGO_LOG_LEVEL', 'WARNING'),
 
            'class': 'logging.StreamHandler',
 
        },
 
        "mail_admins": {
 
            "level": "ERROR",
 
            "filters": ["require_debug_false"],
 
            "class": "django.utils.log.AdminEmailHandler"
 
        }
 
    },
 
    "loggers": {
 
        'django': {
 
            'handlers': ['console'],
 
            'propagate': True,
 
        },
 
        "django.request": {
 
            "handlers": ["mail_admins"],
 
            "level": "ERROR",
 
            "propagate": True,
 
        },
 
    }
 
}
 

	
 
COMPRESS_PRECOMPILERS = (
 
    ('text/x-scss', 'django_libsass.SassCompiler'),
 
)
 

	
 
FIXTURE_DIRS = [
 
    os.path.join(PROJECT_ROOT, "fixtures"),
 
]
 

	
 
# Heroku: Get email configuration from environment variables.
 

	
 
EMAIL_BACKEND = os.environ.get(
 
    "DJANGO_EMAIL_BACKEND",
 
    "django.core.mail.backends.{}.EmailBackend".format('console' if DEBUG else 'smtp'),
 
)
 
EMAIL_LOG_BACKEND = "email_log.backends.EmailBackend"
 
EMAIL_HOST = os.environ.get("DJANGO_EMAIL_HOST", "")
 
EMAIL_PORT = int(os.environ.get("DJANGO_EMAIL_PORT", 25))
 
EMAIL_HOST_USER = os.environ.get("DJANGO_EMAIL_HOST_USER", "")
 
EMAIL_HOST_PASSWORD = os.environ.get("DJANGO_EMAIL_HOST_PASSWORD", "")
 
EMAIL_USE_TLS = bool(int(os.environ.get("DJANGO_EMAIL_USE_TLS", "0")))
 
EMAIL_USE_SSL = bool(int(os.environ.get("DJANGO_EMAIL_USE_SSL", "0")))
 

	
 
ACCOUNT_LOGIN_URL = "nbpy_login"
 
LOGIN_URL = "nbpy_login"
 

	
 
# We need to explicitly switch on signups.
 
ACCOUNT_OPEN_SIGNUP = bool(int(os.environ.get("DJANGO_ACCOUNT_OPEN_SIGNUP", "0")))
 
ACCOUNT_EMAIL_UNIQUE = True
 
ACCOUNT_EMAIL_CONFIRMATION_REQUIRED = bool(int(os.environ.get(
 
    'DJANGO_ACCOUNT_CONFIRM_EMAIL',
 
    False if DEBUG else True,
 
)))
 
ACCOUNT_LOGIN_REDIRECT_URL = "dashboard"
 
ACCOUNT_LOGOUT_REDIRECT_URL = "home"
 
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 2
 
ACCOUNT_USE_AUTH_AUTHENTICATE = True
 
ACCOUNT_HOOKSET =  "pinaxcon.account_hooks.BetterAccountHookSet"
 

	
 
AUTHENTICATION_BACKENDS = [
 
    "symposion.teams.backends.TeamPermissionsBackend",
 
    "account.auth_backends.UsernameAuthenticationBackend",
 
]
 

	
 
CONFERENCE_ID = 1
 
PROPOSAL_FORMS = {
 
    "talk": "pinaxcon.proposals.forms.TalkProposalForm",
 
    "discussion": "pinaxcon.proposals.forms.TalkProposalForm",
 
}
 
PINAX_PAGES_HOOKSET = "pinaxcon.hooks.PinaxPagesHookSet"
 
PINAX_BOXES_HOOKSET = "pinaxcon.hooks.PinaxBoxesHookSet"
 

	
 
PINAX_STRIPE_PUBLIC_KEY = os.environ.get("STRIPE_PUBLIC_KEY", "your test public key")
 
PINAX_STRIPE_SECRET_KEY = os.environ.get("STRIPE_SECRET_KEY", "your test secret key")
 
TUOKCEHC_BASE_URL = os.environ.get("TUOKCEHC_BASE_URL", None)
 
PINAX_STRIPE_SEND_EMAIL_RECEIPTS = False
 

	
 
SYMPOSION_SPEAKER_MODEL = "pinaxcon.proposals.models.ConferenceSpeaker"
 
SYMPOSION_SPEAKER_FORM = "pinaxcon.proposals.forms.ConferenceSpeakerForm"
 

	
 
# Registrasion Attendee profile model
 
ATTENDEE_PROFILE_MODEL = "pinaxcon.registrasion.models.AttendeeProfile"
 
# Registrasion attendee profile form -- must act on ATTENDEE_PROFILE_FORM
 
# You only need to provide this if you're customising the form from the default
 
# ATTENDEE_PROFILE_FORM = "pinaxcon.registrasion.forms.ProfileForm"
 

	
 
# Ticket product category -- used to identify which products must be available
 
# in order to register.
 
TICKET_PRODUCT_CATEGORY = 1
 

	
 
INVOICE_CURRENCY = "USD"
 

	
 
# VAT rate is 21%
 
# When PINAX_VAT_RATE is set, this percentage of the invoice total is
 
# displayed as a line item as the amount of VAT paid.
 
INVOICE_VAT_RATE = Decimal(os.environ.get('PINAX_VAT_RATE') or 0)
 
if INVOICE_VAT_RATE > 1:
 
    INVOICE_VAT_RATE /= 100
 

	
 
MARKDOWN_DEUX_STYLES = {
 
    "default": {
 
        "safe_mode": False,
 
        "extras": {
 
            "tables": 1,
 
        }
 
    },
 
}
pinaxcon/templates/registrasion/invoice/details.html
Show inline comments
 
{% extends "registrasion/invoice/details_.html" %}
 

	
 
{% load nbpy_tags %}
 

	
 
{% comment %}
 
  Blocks that you can override:
 

	
 
  - heading
 
  - subheading
 
  - invoice_intro
 
  - extra_line_items
 
  - contact_info
 

	
 
{% endcomment %}
 

	
 
{% block heading %}
 
  {% if invoice.is_paid or invoice.is_refunded %}
 
    Registration Receipt
 
  {% else %}
 
    Pending Registration
 
  {% endif %}
 
{% endblock %}
 

	
 
{% block subheading %}
 
  CopyleftConf 2020, February 3, Brussels, Belgium
 
{% endblock %}
 

	
 
{% block invoice_intro %}
 
  {% if invoice.is_unpaid %}
 
    This is a registration summary for CopyleftConf 2020. It is not confirmed until paid in full.
 
  {% elif invoice.is_void %}
 
    This is a void registration summary for CopyleftConf 2020. It is provided for informational purposes only.
 
  {% elif invoice.is_refunded %}
 
    This is a refunded registration summary for CopyleftConf 2020. It is provided for informational purposes only.
 
  {% elif invoice.is_paid %}
 
    This is a confirmed registration summary for CopyleftConf 2020.
 
  {% endif %}
 
{% endblock %}
 

	
 
{% block extra_line_items_after_total %}
 
  {% vat_amount(invoice) as vat %}
 
  {% if vat %}
 
    <tr>
 
      <td colspan="3">Includes {{ vat_rate }} VAT:</td>
 
      <td class="text-right">${{ vat }}</td>
 
    </tr>
 
  {% endif %}
 
  {% donation_income invoice as donation %}
 
  {% if donation %}
 
    <tr>
 
      <td colspan="3">Includes donation eligible for tax deduction in the USA:</td>
 
      <td class="text-right">${{ donation }}</td>
 
    </tr>
 
  {% endif %}
 
{% endblock %}
 

	
 
{% block contact_info %}
 
  <p>Direct inquiries to <a href="mailto:contact@copyleftconf.org">contact@copyleftconf.org</a></p>
 
  <p>CopyleftConf is run by <a href="https://sfconservancy.org">Software Freedom Conservancy</a>, a 501(c)(3) not-for-profit public charity registered in New York. Software Freedom Conservancy's federal tax-exempt EIN is 41-2203632.</p>
 

	
 
  <strong>Mailing Address</strong>
 
  <address>
 
    Software Freedom Conservancy, Inc.<br>
 
    137 MONTAGUE ST STE 380<br>
 
    Brooklyn, NY 11201-3548<br>
 
  </address>
 
{% endblock %}
pinaxcon/templatetags/nbpy_tags.py
Show inline comments
 
from registrasion.models import commerce
 
from registrasion.controllers.category import CategoryController
 
from registrasion.controllers.item import ItemController
 
from registrasion.templatetags import registrasion_tags
 

	
 
from decimal import Decimal
 
from django import template
 
from django.conf import settings
 
from django.db.models import Sum
 
from urllib import urlencode  # TODO: s/urllib/six.moves.urllib/
 

	
 
register = template.Library()
 

	
 
CENTS_QUANT = Decimal('.01')
 
VAT_RATE = str(settings.INVOICE_VAT_RATE * 100).rstrip('.0') + '%'
 

	
 
@register.simple_tag
 
def vat_amount(invoice):
 
    return (invoice.value * settings.INVOICE_VAT_RATE).quantize(CENTS_QUANT)
 

	
 
@register.simple_tag
 
def vat_rate():
 
    return VAT_RATE
 

	
 
@register.simple_tag(takes_context=True)
 
def donation_income(context, invoice):
 
    ''' Calculates the donation income for a given invoice.
 

	
 
    Returns:
 
        the donation income.
 

	
 
    '''
 

	
 
    # 15% (FSA) goes to Conservancy; 85% is real goods
 

	
 
    fsa_rate = Decimal("0.85")
 
    rbi_full_ticket = Decimal("68.00")
 
    rbi_early_bird_discount = Decimal("-21.35")
 
    rbi = []
 

	
 
    for line in invoice.lineitem_set.all():
 
        if line.product.category.name == "Ticket":
 
            if line.product.name.startswith("Unaffiliated Individual"):
 
                # Includes full price & discounts
 
                rbi.append(line.total_price * fsa_rate)
 
            else:
 
                if line.total_price > 0:
 
                    rbi.append(rbi_full_ticket)
 
                elif line.total_price < 0:
 
                    rbi.append(rbi_early_bird_discount)
 
        elif line.product.category.name == "T-Shirt":
 
            rbi.append(line.total_price * fsa_rate)
 

	
 
    donation = max(Decimal('0'), (invoice.value - sum(rbi)))
 
    return donation.quantize(Decimal('.01'))
 
    return donation.quantize(CENTS_QUANT)
 

	
 

	
 
# TODO: include van/de/van der/de la/etc etc etc
 

	
 
@register.simple_tag
 
def name_split(name, split_characters=None):
 

	
 
    tokens = name.split()
 
    if split_characters is None or len(name) > split_characters:
 
        even_split = int((len(tokens) + 1) / 2)  # Round up.
 
    else:
 
        even_split = len(tokens)
 

	
 
    return {
 
        "first" : " ".join(tokens[:even_split]),
 
        "last" : " ".join(tokens[even_split:]),
 
    }
 

	
 
@register.simple_tag
 
def company_split(name):
 
    f =  name_split(name, 18)
 
    return f
 

	
 

	
 
@register.simple_tag(takes_context=True)
 
def special(context, user):
 
    organiser = user.groups.filter(name='Conference organisers').exists()
 
    try:
 
        speaker = user.speaker_profile.presentations.count() != 0
 
    except Exception:
 
        speaker = False
 
    tt = ticket_type(context)
 
    volunteer = "Volunteer" in tt
 

	
 
    if organiser:
 
        return "Organizer"
 
    elif speaker or "Speaker" in tt:
 
        return "Speaker"
 
    elif volunteer:
 
        return "Staff"
 
    else:
 
        return ""
 

	
 

	
 
CLEARED = set([
 
    "BeeWare Project",
 
    "Project Jupyter",
 
    "PSF Packaging WG / PyCon 2018 Chair",
 
    "PyCon Ukraine",
 
    "PyLadies PDX",
 
    "Recovered Silver",
 
    "Twisted",
 
    "@vmbrasseur",
 
])
 

	
 
@register.simple_tag
 
def affiliation(ticket, user):
 
    aff = user.attendee.attendeeprofilebase.attendeeprofile.company
 
    if "Individual" not in ticket or "Sponsor" in ticket:
 
        return aff
 
    elif ticket == "Individual Supporter" and aff in CLEARED:
 
        return aff
 
    else:
 
        return ""
 

	
 

	
 
@register.simple_tag(takes_context=True)
 
def ticket_type(context):
 

	
 
    items = registrasion_tags.items_purchased(context)
 
    for item in items:
 
        if item.product.category.name == "Ticket":
 
            return item.product.name
0 comments (0 inline, 0 general)