-
Notifications
You must be signed in to change notification settings - Fork 5
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
Settlement report #184
Settlement report #184
Changes from 6 commits
b67d4f2
cb988fd
206d0bc
a917fa2
9c2348a
ce5faed
374b917
4820a42
d708182
91f8f54
50ecd3c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,8 @@ class LINE_ITEM_STATUS(LabeledEnum): | |
#: a line item. Eg: a discount no longer applicable on a line item as a result of a cancellation | ||
VOID = (3, __("Void")) | ||
|
||
LINE_ITEM_STATUS.TRANSACTION = [LINE_ITEM_STATUS.CONFIRMED, LINE_ITEM_STATUS.VOID, LINE_ITEM_STATUS.CANCELLED] | ||
|
||
|
||
LineItemTuple = namedtuple('LineItemTuple', ['item_id', 'id', 'base_amount', 'discount_policy_id', 'discount_coupon_id', 'discounted_amount', 'final_amount']) | ||
|
||
|
@@ -66,7 +68,8 @@ class LineItem(BaseMixin, db.Model): | |
""" | ||
__tablename__ = 'line_item' | ||
__uuid_primary_key__ = True | ||
__table_args__ = (db.UniqueConstraint('customer_order_id', 'line_item_seq'),) | ||
__table_args__ = (db.UniqueConstraint('customer_order_id', 'line_item_seq'), | ||
db.UniqueConstraint('previous_id')) | ||
|
||
# line_item_seq is the relative number of the line item per order. | ||
line_item_seq = db.Column(db.Integer, nullable=False) | ||
|
@@ -78,6 +81,12 @@ class LineItem(BaseMixin, db.Model): | |
item_id = db.Column(None, db.ForeignKey('item.id'), nullable=False, index=True, unique=False) | ||
item = db.relationship(Item, backref=db.backref('line_items', cascade='all, delete-orphan')) | ||
|
||
previous_id = db.Column(None, db.ForeignKey('line_item.id'), nullable=True, index=True, unique=True) | ||
previous = db.relationship('LineItem', | ||
primaryjoin=('line_item.c.id==line_item.c.previous_id'), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Brackets not required. It's confusing as it appears as a tuple until you notice it ends with |
||
backref=db.backref('revision', uselist=False), | ||
remote_side='LineItem.id') | ||
|
||
discount_policy_id = db.Column(None, db.ForeignKey('discount_policy.id'), nullable=True, index=True, unique=False) | ||
discount_policy = db.relationship('DiscountPolicy', backref=db.backref('line_items')) | ||
|
||
|
@@ -325,6 +334,11 @@ def get_confirmed_line_items(self): | |
Order.get_confirmed_line_items = property(get_confirmed_line_items) | ||
|
||
|
||
def get_initial_line_items(self): | ||
return LineItem.query.filter(LineItem.order == self, LineItem.previous == None, LineItem.status.in_(LINE_ITEM_STATUS.TRANSACTION)) | ||
Order.get_initial_line_items = property(get_initial_line_items) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it's a property it shouldn't have the |
||
|
||
|
||
def get_from_item(cls, item, qty, coupon_codes=[]): | ||
""" | ||
Returns a list of (discount_policy, discount_coupon) tuples | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,7 @@ | |
from decimal import Decimal | ||
from coaster.utils import LabeledEnum, isoweek_datetime | ||
from isoweek import Week | ||
from baseframe import __ | ||
from baseframe import __, localize_timezone | ||
from boxoffice.models import db, BaseMixin, Order, ORDER_STATUS, MarkdownColumn, ItemCollection | ||
from ..extapi import RAZORPAY_PAYMENT_STATUS | ||
|
||
|
@@ -74,6 +74,8 @@ class PaymentTransaction(BaseMixin, db.Model): | |
internal_note = db.Column(db.Unicode(250), nullable=True) | ||
refund_description = db.Column(db.Unicode(250), nullable=True) | ||
note_to_user = MarkdownColumn('note_to_user', nullable=True) | ||
# Refund id issued by the payment gateway | ||
pg_refundid = db.Column(db.Unicode(80), nullable=True) | ||
|
||
|
||
def get_refund_transactions(self): | ||
|
@@ -167,3 +169,72 @@ def calculate_weekly_refunds(item_collection_ids, user_tz, year): | |
ordered_week_refunds[int(week_refund.sales_week)] = week_refund.sum | ||
|
||
return ordered_week_refunds | ||
|
||
def get_settled_transactions(date_range): | ||
settled_transactions = get_settlements(date_range) | ||
headers = ['settlement_id', 'transaction_type', 'order_id', 'payment_id', 'refund_id', 'item_collection', 'description', 'base_amount', 'discounted_amount', 'final_amount', 'order_paid_amount', 'transaction_date', 'settled_at', 'razorpay_fees', 'order_amount', 'credit', 'debit', 'receivable_amount', 'settlement_amount', 'buyer_fullname'] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Long line needs wrapping. |
||
rows = [] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add comment indicating content. |
||
external_transaction_msg = u"Transaction external to Boxoffice. Credited directly to Razorpay?" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PG-specific message. |
||
|
||
for settled_transaction in settled_transactions: | ||
if settled_transaction['type'] == 'settlement': | ||
rows.append({ | ||
'settlement_id': settled_transaction['entity_id'], | ||
'settlement_amount': settled_transaction['amount'], | ||
'settled_at': settled_transaction['settled_at'] | ||
|
||
}) | ||
elif settled_transaction['type'] == 'payment': | ||
payment = OnlinePayment.query.filter_by(pg_paymentid=settled_transaction['entity_id']).one_or_none() | ||
if payment: | ||
order = payment.order | ||
rows.append({ | ||
'settlement_id': settled_transaction['settlement_id'], | ||
'transaction_type': settled_transaction['type'], | ||
'order_id': order.id, | ||
'payment_id': settled_transaction['entity_id'], | ||
'razorpay_fees': settled_transaction['fee'], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PG reference shouldn't be here. |
||
'transaction_date': localize_timezone(order.paid_at, 'Asia/Kolkata'), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does Razorpay do IST as a documented feature of their API? Link to docs will help. |
||
'credit': settled_transaction['credit'], | ||
'buyer_fullname': order.buyer_fullname, | ||
'item_collection': order.item_collection.title | ||
}) | ||
for line_item in order.get_initial_line_items: | ||
rows.append({ | ||
'settlement_id': settled_transaction['settlement_id'], | ||
'payment_id': settled_transaction['entity_id'], | ||
'order_id': order.id, | ||
'item_collection': order.item_collection.title, | ||
'description': line_item.item.title, | ||
'base_amount': line_item.base_amount, | ||
'discounted_amount': line_item.discounted_amount, | ||
'final_amount': line_item.final_amount | ||
}) | ||
else: | ||
# Transaction outside of Boxoffice | ||
rows.append({ | ||
'settlement_id': settled_transaction['settlement_id'], | ||
'payment_id': settled_transaction['entity_id'], | ||
'credit': settled_transaction['credit'], | ||
'description': external_transaction_msg | ||
}) | ||
elif settled_transaction['type'] == 'refund': | ||
payment = OnlinePayment.query.filter_by(settled_transactionid=settled_transaction['payment_id']).one() | ||
refund = PaymentTransaction.query.filter(PaymentTransaction.online_payment == payment, | ||
PaymentTransaction.transaction_type == TRANSACTION_TYPE.REFUND, | ||
PaymentTransaction.pg_refundid == settled_transaction['entity_id'] | ||
).one() | ||
order = refund.order | ||
rows.append({ | ||
'settlement_id': settled_transaction['settlement_id'], | ||
'refund_id': settled_transaction['entity_id'], | ||
'payment_id': settled_transaction['payment_id'], | ||
'order_id': order.id, | ||
'razorpay_fees': settled_transaction['fee'], | ||
'debit': settled_transaction['debit'], | ||
'buyer_fullname': order.buyer_fullname, | ||
'description': refund.refund_description, | ||
'amount': refund.amount, | ||
'item_collection': order.item_collection.title | ||
}) | ||
return (headers, rows) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
"""add_previous_to_line_item | ||
|
||
Revision ID: 171fcb171759 | ||
Revises: 81f30d00706f | ||
Create Date: 2017-10-24 18:40:39.183620 | ||
|
||
""" | ||
|
||
# revision identifiers, used by Alembic. | ||
revision = '171fcb171759' | ||
down_revision = '81f30d00706f' | ||
|
||
from alembic import op | ||
import sqlalchemy as sa | ||
from sqlalchemy.dialects import postgresql | ||
import sqlalchemy_utils | ||
|
||
def upgrade(): | ||
op.add_column('line_item', sa.Column('previous_id', sqlalchemy_utils.types.uuid.UUIDType(), nullable=True)) | ||
op.create_index(op.f('ix_line_item_previous_id'), 'line_item', ['previous_id'], unique=True) | ||
op.create_foreign_key('line_item_id_fkey', 'line_item', 'line_item', ['previous_id'], ['id']) | ||
|
||
|
||
def downgrade(): | ||
op.drop_constraint('line_item_id_fkey', 'line_item', type_='foreignkey') | ||
op.drop_index(op.f('ix_line_item_previous_id'), table_name='line_item') | ||
op.drop_column('line_item', 'previous_id') |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
"""retroactively_migrate_previous_id_for_line_item | ||
|
||
Revision ID: 66b67130c901 | ||
Revises: 171fcb171759 | ||
Create Date: 2017-10-26 14:50:18.859247 | ||
|
||
""" | ||
|
||
# revision identifiers, used by Alembic. | ||
revision = '66b67130c901' | ||
down_revision = '171fcb171759' | ||
|
||
from collections import OrderedDict | ||
from alembic import op | ||
import sqlalchemy as sa | ||
from sqlalchemy.dialects import postgresql | ||
from sqlalchemy.sql import table, column | ||
import sqlalchemy_utils | ||
from boxoffice.models.order import ORDER_STATUS | ||
from boxoffice.models.line_item import LINE_ITEM_STATUS | ||
|
||
|
||
|
||
def find_nearest_timestamp(lst, timestamp): | ||
nearest_ts = None | ||
for ts in lst: | ||
diff = abs(timestamp - ts).total_seconds() | ||
if diff < 1: | ||
if nearest_ts and diff > abs(timestamp - nearest_ts).total_seconds(): | ||
# there's a timestamp that's closer | ||
pass | ||
else: | ||
nearest_ts = ts | ||
return nearest_ts | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be simplified to (source): nearest_ts = min(lst, key=lambda x: abs(x - timestamp).total_seconds())
if abs(nearest_ts - timestamp).total_seconds() < 1:
return nearest_ts |
||
|
||
def set_previous_keys_for_line_items(line_items): | ||
timestamped_line_items = OrderedDict() | ||
|
||
# Assemble the `timestamped_line_items` dictionary with the timestamp at which the line items were created | ||
# as the key, and the line items that were created at that time as the value (as a list) | ||
# Some line items may have been created a few milliseconds later, so the nearest timestamp | ||
# with a tolerance level of one second is searched for | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This depends on whether we used |
||
for line_item in line_items: | ||
ts_key = find_nearest_timestamp(timestamped_line_items.keys(), line_item.created_at) or line_item.created_at | ||
if not timestamped_line_items.get(ts_key): | ||
timestamped_line_items[ts_key] = [] | ||
timestamped_line_items[ts_key].append({ | ||
'id': line_item.id, | ||
'status': line_item.status, | ||
'item_id': line_item.item_id, | ||
'previous_id': None | ||
}) | ||
|
||
# The previous line item for a line item, is a line item that has an earlier timestamp with a void status | ||
# with the same item_id. Find it and set it | ||
used_line_item_ids = set() | ||
for idx, (timestamp, line_item_dicts) in enumerate(timestamped_line_items.items()): | ||
# 0th timestamps are root line items | ||
if idx > 0: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, I'm unclear on what There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The 0th group of |
||
for li_dict in timestamped_line_items[timestamp]: | ||
previous_li_dict = [previous_li_dict | ||
for previous_li_dict in timestamped_line_items[timestamped_line_items.keys()[idx-1]] | ||
if previous_li_dict['item_id'] == li_dict['item_id'] | ||
and previous_li_dict['id'] not in used_line_item_ids | ||
and previous_li_dict['status'] == LINE_ITEM_STATUS.VOID][0] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indent the two |
||
li_dict['previous_id'] = previous_li_dict['id'] | ||
used_line_item_ids.add(previous_li_dict['id']) | ||
|
||
return [li_dict for li_dicts in timestamped_line_items.values() for li_dict in li_dicts] | ||
|
||
order_table = table('customer_order', | ||
column('id', sqlalchemy_utils.types.uuid.UUIDType()), | ||
column('status', sa.Integer())) | ||
|
||
line_item_table = table('line_item', | ||
column('id', sqlalchemy_utils.types.uuid.UUIDType()), | ||
column('customer_order_id', sqlalchemy_utils.types.uuid.UUIDType()), | ||
column('item_id', sqlalchemy_utils.types.uuid.UUIDType()), | ||
column('previous_id', sqlalchemy_utils.types.uuid.UUIDType()), | ||
column('status', sa.Integer()), | ||
column('created_at', sa.Boolean())) | ||
|
||
def upgrade(): | ||
conn = op.get_bind() | ||
orders = conn.execute(sa.select([order_table.c.id]).where(order_table.c.status.in_(ORDER_STATUS.TRANSACTION)).select_from(order_table)) | ||
for order_id in [order.id for order in orders]: | ||
line_items = conn.execute(sa.select([line_item_table.c.id, | ||
line_item_table.c.item_id, | ||
line_item_table.c.status, | ||
line_item_table.c.created_at | ||
]).where(line_item_table.c.customer_order_id == order_id).order_by("created_at").select_from(line_item_table)) | ||
updated_line_item_dicts = set_previous_keys_for_line_items(line_items) | ||
for updated_line_item_dict in updated_line_item_dicts: | ||
if updated_line_item_dict['previous_id']: | ||
conn.execute(sa.update(line_item_table).where( | ||
line_item_table.c.id == updated_line_item_dict['id'] | ||
).values(previous_id=updated_line_item_dict['previous_id'])) | ||
|
||
|
||
def downgrade(): | ||
conn = op.get_bind() | ||
orders = conn.execute(sa.select([order_table.c.id]).where(order_table.c.status.in_(ORDER_STATUS.TRANSACTION)).select_from(order_table)) | ||
for order_id in [order.id for order in orders]: | ||
line_items = conn.execute(sa.select([line_item_table.c.id]).where( | ||
line_item_table.c.customer_order_id == order_id).order_by("created_at").select_from(line_item_table)) | ||
for line_item in line_items: | ||
conn.execute(sa.update(line_item_table).where( | ||
line_item_table.c.id == line_item.id | ||
).values(previous_id=None)) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
"""add_pg_refundid_to_transaction | ||
|
||
Revision ID: 81f30d00706f | ||
Revises: 1a22f5035244 | ||
Create Date: 2017-10-19 03:39:48.608087 | ||
|
||
""" | ||
|
||
# revision identifiers, used by Alembic. | ||
revision = '81f30d00706f' | ||
down_revision = '1a22f5035244' | ||
|
||
from alembic import op | ||
import sqlalchemy as sa | ||
from sqlalchemy.dialects import postgresql | ||
|
||
def upgrade(): | ||
op.add_column('payment_transaction', sa.Column('pg_refundid', sa.Unicode(length=80), nullable=True)) | ||
|
||
|
||
def downgrade(): | ||
op.drop_column('payment_transaction', 'pg_refundid') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a kludge. I'm adding a fix to Coaster to support this directly inside the class.