Skip to content

Commit

Permalink
Merge pull request #560 from liberapay/expenses-1
Browse files Browse the repository at this point in the history
Implement Invoices - Part 1
  • Loading branch information
Changaco authored Mar 30, 2017
2 parents bbdd90a + 9a85f35 commit 10b956c
Show file tree
Hide file tree
Showing 19 changed files with 1,001 additions and 5 deletions.
12 changes: 12 additions & 0 deletions emails/new_invoice.spt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{{ _("Invoice from {0} on Liberapay", sender_name) }}

[---] text/html
<p>{{ _(
"{sender_name} has submitted an invoice for a payment of {amount}.",
sender_name=sender_name, amount=Money(invoice.amount, 'EUR')
) }}</p>

<p>{{ _("Description: {0}", invoice.description) }}</p>

<p><a href="{{ participant.url('invoices/%s' % invoice.id) }}"
style="{{ button_style('primary') }}">{{ _("View the invoice") }}</a></p>
4 changes: 3 additions & 1 deletion js/forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ Liberapay.forms.setInvalid = function($input, invalid) {
Liberapay.forms.jsSubmit = function() {
function submit(e) {
e.preventDefault();
var $form = $(this.form || this);
var form = this.form || this;
if (form.reportValidity && form.reportValidity() == false) return;
var $form = $(form);
var target = $form.attr('action');
var js_only = target == 'javascript:';
var data = $form.serializeArray();
Expand Down
17 changes: 17 additions & 0 deletions liberapay/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,23 @@ def check_bits(bits):
FEE_PAYOUT_WARN = Decimal('0.03') # warn user when fee exceeds 3%
FEE_VAT = Decimal('0.17') # 17% (Luxembourg rate)

INVOICE_DOC_MAX_SIZE = 5000000
INVOICE_DOCS_EXTS = ['pdf', 'jpeg', 'jpg', 'png']
INVOICE_DOCS_LIMIT = 10

INVOICE_NATURES = {
'expense': _("Expense Report"),
}

INVOICE_STATUSES = {
'pre': _("Draft"),
'new': _("Sent (awaiting approval)"),
'retracted': _("Retracted"),
'accepted': _("Accepted (awaiting payment)"),
'paid': _("Paid"),
'rejected': _("Rejected"),
}

JINJA_ENV_COMMON = dict(
trim_blocks=True, lstrip_blocks=True,
line_statement_prefix='%',
Expand Down
41 changes: 41 additions & 0 deletions liberapay/models/participant.py
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,47 @@ def get_communities(self):
""", (self.id,))


# Invoices
# ========

def can_invoice(self, other):
if self.kind != 'individual' or other.kind != 'organization':
return False
return bool(self.allow_invoices and other.allow_invoices)

def update_invoice_status(self, invoice_id, new_status, message=None):
if new_status in ('canceled', 'new', 'retracted'):
column = 'sender'
elif new_status in ('accepted', 'paid', 'rejected'):
column = 'addressee'
else:
raise ValueError(new_status)
if new_status in ('new', 'canceled'):
old_status = 'pre'
elif new_status == 'paid':
old_status = 'accepted'
else:
old_status = 'new'
with self.db.get_cursor() as c:
p_id = self.id
r = c.one("""
UPDATE invoices
SET status = %(new_status)s
WHERE id = %(invoice_id)s
AND status = %(old_status)s
AND {0} = %(p_id)s
RETURNING id
""".format(column), locals())
if not r:
return False
c.run("""
INSERT INTO invoice_events
(invoice, participant, status, message)
VALUES (%s, %s, %s, %s)
""", (invoice_id, self.id, new_status, message))
return True


# More Random Stuff
# =================

Expand Down
2 changes: 2 additions & 0 deletions liberapay/utils/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ def iter_payday_events(db, participant, year=None):
event['wallet_delta'] = -event['amount']
if event['status'] == 'succeeded':
balance -= event['wallet_delta']
if event['context'] == 'expense':
event['invoice_url'] = participant.path('invoices/%s' % event['invoice'])
event['kind'] = kind

day_events.append(event)
Expand Down
4 changes: 3 additions & 1 deletion liberapay/utils/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ def autolink(self, link, is_email):


renderer = CustomRenderer(flags=m.HTML_SKIP_HTML)
md = m.Markdown(renderer, extensions=('autolink', 'strikethrough', 'no-intra-emphasis'))
md = m.Markdown(renderer, extensions=(
'autolink', 'strikethrough', 'no-intra-emphasis', 'tables',
))


def render(markdown):
Expand Down
4 changes: 4 additions & 0 deletions liberapay/wireup.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ class AppConf(object):
openstreetmap_id=str,
openstreetmap_secret=str,
password_rounds=int,
s3_endpoint=str,
s3_public_access_key=str,
s3_secret_key=str,
s3_region=str,
send_newsletters_every=int,
socket_timeout=float,
smtp_host=str,
Expand Down
45 changes: 45 additions & 0 deletions sql/branch.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
-- ALTER TYPE ... ADD cannot run inside a transaction block
ALTER TYPE transfer_context ADD VALUE 'expense';

BEGIN;

CREATE TYPE invoice_nature AS ENUM ('expense');

CREATE TYPE invoice_status AS ENUM
('pre', 'canceled', 'new', 'retracted', 'accepted', 'paid', 'rejected');

CREATE TABLE invoices
( id serial PRIMARY KEY
, ctime timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP
, sender bigint NOT NULL REFERENCES participants
, addressee bigint NOT NULL REFERENCES participants
, nature invoice_nature NOT NULL
, amount numeric(35,2) NOT NULL CHECK (amount > 0)
, description text NOT NULL
, details text
, documents jsonb NOT NULL
, status invoice_status NOT NULL
);

CREATE TABLE invoice_events
( id serial PRIMARY KEY
, invoice int NOT NULL REFERENCES invoices
, participant bigint NOT NULL REFERENCES participants
, ts timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP
, status invoice_status NOT NULL
, message text
);

ALTER TABLE participants ADD COLUMN allow_invoices boolean;

ALTER TABLE transfers
ADD COLUMN invoice int REFERENCES invoices,
ADD CONSTRAINT expense_chk CHECK (NOT (context='expense' AND invoice IS NULL));

INSERT INTO app_conf VALUES
('s3_endpoint', '""'::jsonb),
('s3_public_access_key', '""'::jsonb),
('s3_secret_key', '""'::jsonb),
('s3_region', '"eu-west-1"'::jsonb);

END;
14 changes: 13 additions & 1 deletion style/base/base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ h2, h3, h4, h5, h6 {
0% { background-color: rgba(yellow, 0.7); }
}

section.profile-statement, section.community-sidebar {
section.profile-statement, section.community-sidebar, section.invoice-details {
h1 {
font-size: $font-size-h3;
margin: $font-size-h3 0 10px;
Expand Down Expand Up @@ -351,6 +351,18 @@ section.community-sidebar {
max-width: 100%;
}
}
section.invoice-details {
border-left: 2px solid $gray-light;
margin: 0 0 1em 1.5em;
padding-left: 0.5em;

table {
td, th {
border: 1px solid #ccc;
padding: 0.3em 0.4em;
}
}
}

#notification-area-bottom, #notification-area-top {
text-align: center;
Expand Down
Loading

0 comments on commit 10b956c

Please sign in to comment.