diff --git a/apiserver/apiserver/api/models.py b/apiserver/apiserver/api/models.py index a34780f..4398bad 100644 --- a/apiserver/apiserver/api/models.py +++ b/apiserver/apiserver/api/models.py @@ -78,6 +78,7 @@ class Transaction(models.Model): paypal_txn_id = models.CharField(max_length=17, blank=True, null=True, unique=True) paypal_txn_type = models.CharField(max_length=64, blank=True, null=True) paypal_payer_id = models.CharField(max_length=13, blank=True, null=True) + protocoin = models.DecimalField(max_digits=7, decimal_places=2) report_type = models.TextField(blank=True, null=True) report_memo = models.TextField(blank=True, null=True) diff --git a/apiserver/apiserver/api/serializers.py b/apiserver/apiserver/api/serializers.py index b6c9d98..89eb61f 100644 --- a/apiserver/apiserver/api/serializers.py +++ b/apiserver/apiserver/api/serializers.py @@ -38,6 +38,7 @@ class TransactionSerializer(serializers.ModelSerializer): 'Member', 'Clearing', 'Cash', + 'Protocoin', ]) category = serializers.ChoiceField([ 'Membership', @@ -49,6 +50,7 @@ class TransactionSerializer(serializers.ModelSerializer): 'Garage Sale', 'Reimburse', 'Other', + 'Exchange', ]) member_id = serializers.SerializerMethodField() member_name = serializers.SerializerMethodField() @@ -58,8 +60,10 @@ class TransactionSerializer(serializers.ModelSerializer): 'Unmatched Purchase', 'User Flagged', ], allow_null=True, required=False) - number_of_membership_months = serializers.IntegerField(max_value=36, min_value=-36) + number_of_membership_months = serializers.IntegerField(max_value=36, min_value=-36, default=0) recorder = serializers.SerializerMethodField() + amount = serializers.DecimalField(max_digits=None, decimal_places=2, default=0) + protocoin = serializers.DecimalField(max_digits=None, decimal_places=2, default=0) class Meta: model = models.Transaction @@ -80,18 +84,26 @@ class TransactionSerializer(serializers.ModelSerializer): member = get_object_or_404(models.Member, id=self.initial_data['member_id']) validated_data['user'] = member.user - if validated_data['account_type'] != 'Clearing': + if validated_data['account_type'] == 'Protocoin' and validated_data['category'] == 'Exchange': + raise ValidationError(dict(category='Can\'t purchase Protocoin with Protocoin.')) + + if validated_data['category'] == 'Exchange': + if validated_data['amount'] == 0: + raise ValidationError(dict(category='Can\'t purchase 0 Protocoin.')) + validated_data['protocoin'] = validated_data['amount'] + + if validated_data['account_type'] not in ['Clearing', 'Protocoin']: if validated_data['amount'] == 0: raise ValidationError(dict(account_type='Can\'t have a $0.00 {} transaction. Do you want "Membership Adjustment"?'.format(validated_data['account_type']))) - if validated_data['category'] != 'Reimburse': - if validated_data['amount'] < 0: - raise ValidationError(dict(category='Can\'t have a negative {} transaction. Do you want "Reimbursement"?'.format(validated_data['category']))) - if validated_data['account_type'] == 'PayPal': msg = 'Manual PayPal transaction added:\n' + str(validated_data) utils.alert_tanner(msg) + if validated_data['account_type'] == 'Protocoin': + msg = 'Manual Protocoin transaction added:\n' + str(validated_data) + utils.alert_tanner(msg) + if validated_data['account_type'] in ['Interac', 'Dream Pmt', 'Square Pmt', 'PayPal']: if not validated_data.get('reference_number', None): raise ValidationError(dict(reference_number='This field is required.')) diff --git a/webclient/src/Members.js b/webclient/src/Members.js index 939d6a9..9af98aa 100644 --- a/webclient/src/Members.js +++ b/webclient/src/Members.js @@ -26,7 +26,7 @@ const memberSorts = { export function MembersDropdown(props) { const { token, name, onChange, value, initial } = props; const [response, setResponse] = useState({ results: [] }); - const searchDefault = {seq: 0, q: initial || ''}; + const searchDefault = {seq: 0, q: initial || '', sort: 'newest_active'}; const [search, setSearch] = useState(searchDefault); useEffect(() => { @@ -59,7 +59,7 @@ export function MembersDropdown(props) { value={value} placeholder='Search for Member' onChange={onChange} - onSearchChange={(e, v) => setSearch({seq: parseInt(e.timeStamp), q: v.searchQuery})} + onSearchChange={(e, v) => setSearch({seq: parseInt(e.timeStamp), q: v.searchQuery, sort: 'newest_active'})} /> ); diff --git a/webclient/src/Transactions.js b/webclient/src/Transactions.js index e648a95..49d0198 100644 --- a/webclient/src/Transactions.js +++ b/webclient/src/Transactions.js @@ -9,7 +9,7 @@ import { isAdmin, BasicTable, requester } from './utils.js'; import { NotFound } from './Misc.js'; export function TransactionEditor(props) { - const { token, input, setInput, error, noMemberSearch } = props; + const { token, input, setInput, error } = props; const [prevInput] = useState(input); @@ -32,6 +32,7 @@ export function TransactionEditor(props) { //{ key: '5', text: 'Member Balance / Protocash', value: 'Member' }, { key: '6', text: 'Membership Adjustment / Clearing', value: 'Clearing' }, { key: '7', text: 'PayPal', value: 'PayPal' }, + { key: '8', text: 'Protocoin', value: 'Protocoin' }, ]; const sourceOptions = [ @@ -53,76 +54,97 @@ export function TransactionEditor(props) { { key: '0', text: 'Membership Dues', value: 'Membership' }, { key: '1', text: 'Course Fee', value: 'OnAcct' }, { key: '2', text: 'Snacks / Pop / Coffee', value: 'Snacks' }, - { key: '3', text: 'Donation', value: 'Donation' }, - { key: '4', text: 'Consumables (Explain in memo)', value: 'Consumables' }, + { key: '3', text: 'Donation (Explain in Memo)', value: 'Donation' }, + { key: '4', text: 'Consumables (Explain in Memo)', value: 'Consumables' }, { key: '5', text: 'Purchase of Locker / Materials / Stock', value: 'Purchases' }, - //{ key: '6', text: 'Auction, Garage Sale, Nearly Free Shelf', value: 'Garage Sale' }, - { key: '7', text: 'Reimbursement (Enter a negative value)', value: 'Reimburse' }, - { key: '8', text: 'Other (Explain in memo)', value: 'Other' }, + { key: '6', text: 'Purchase of Protocoin', value: 'Exchange' }, + { key: '7', text: 'Reimbursement (Not for Refunds)', value: 'Reimburse' }, + { key: '8', text: 'Other (Explain in Memo)', value: 'Other' }, ]; return (
- {!noMemberSearch && - - - } - + + + + + - - + + - + {input.account_type && (input.account_type === 'Protocoin' ? + + : + + )} + {input?.account_type !== prevInput?.account_type && input?.account_type === 'PayPal' && Are you sure? -

PayPal transactions should be automatic. Double check there's no duplicate. They may take 24h to appear.

+

PayPal transactions are automatic. Double check there's no duplicate. They may take 24h to appear.

} - {/* - + {input?.account_type !== prevInput?.account_type && input?.account_type === 'Protocoin' && + + Are you sure? +

Protocoin spending transactions are automatic. Do you want "Purchase of Protocoin" category below?

+
+ } + + - */} + + {input.category === 'Membership' && + + } + + {input.category === 'Exchange' && + + } +
- -
); }; @@ -435,7 +451,7 @@ export function TransactionDetail(props) {
Transaction Receipt
- +
@@ -447,7 +463,7 @@ export function TransactionDetail(props) { /> - + {isAdmin(user) ?