Add protocoin to Transaction model and editor

This commit is contained in:
Tanner Collin 2022-08-22 20:17:43 +00:00
parent 9d41af9eca
commit 81c9bd9c9b
4 changed files with 93 additions and 64 deletions

View File

@ -78,6 +78,7 @@ class Transaction(models.Model):
paypal_txn_id = models.CharField(max_length=17, blank=True, null=True, unique=True) 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_txn_type = models.CharField(max_length=64, blank=True, null=True)
paypal_payer_id = models.CharField(max_length=13, 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_type = models.TextField(blank=True, null=True)
report_memo = models.TextField(blank=True, null=True) report_memo = models.TextField(blank=True, null=True)

View File

@ -38,6 +38,7 @@ class TransactionSerializer(serializers.ModelSerializer):
'Member', 'Member',
'Clearing', 'Clearing',
'Cash', 'Cash',
'Protocoin',
]) ])
category = serializers.ChoiceField([ category = serializers.ChoiceField([
'Membership', 'Membership',
@ -49,6 +50,7 @@ class TransactionSerializer(serializers.ModelSerializer):
'Garage Sale', 'Garage Sale',
'Reimburse', 'Reimburse',
'Other', 'Other',
'Exchange',
]) ])
member_id = serializers.SerializerMethodField() member_id = serializers.SerializerMethodField()
member_name = serializers.SerializerMethodField() member_name = serializers.SerializerMethodField()
@ -58,8 +60,10 @@ class TransactionSerializer(serializers.ModelSerializer):
'Unmatched Purchase', 'Unmatched Purchase',
'User Flagged', 'User Flagged',
], allow_null=True, required=False) ], 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() 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: class Meta:
model = models.Transaction model = models.Transaction
@ -80,18 +84,26 @@ class TransactionSerializer(serializers.ModelSerializer):
member = get_object_or_404(models.Member, id=self.initial_data['member_id']) member = get_object_or_404(models.Member, id=self.initial_data['member_id'])
validated_data['user'] = member.user 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: 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']))) 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': if validated_data['account_type'] == 'PayPal':
msg = 'Manual PayPal transaction added:\n' + str(validated_data) msg = 'Manual PayPal transaction added:\n' + str(validated_data)
utils.alert_tanner(msg) 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 validated_data['account_type'] in ['Interac', 'Dream Pmt', 'Square Pmt', 'PayPal']:
if not validated_data.get('reference_number', None): if not validated_data.get('reference_number', None):
raise ValidationError(dict(reference_number='This field is required.')) raise ValidationError(dict(reference_number='This field is required.'))

View File

@ -26,7 +26,7 @@ const memberSorts = {
export function MembersDropdown(props) { export function MembersDropdown(props) {
const { token, name, onChange, value, initial } = props; const { token, name, onChange, value, initial } = props;
const [response, setResponse] = useState({ results: [] }); 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); const [search, setSearch] = useState(searchDefault);
useEffect(() => { useEffect(() => {
@ -59,7 +59,7 @@ export function MembersDropdown(props) {
value={value} value={value}
placeholder='Search for Member' placeholder='Search for Member'
onChange={onChange} 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'})}
/> />
); );

View File

@ -9,7 +9,7 @@ import { isAdmin, BasicTable, requester } from './utils.js';
import { NotFound } from './Misc.js'; import { NotFound } from './Misc.js';
export function TransactionEditor(props) { export function TransactionEditor(props) {
const { token, input, setInput, error, noMemberSearch } = props; const { token, input, setInput, error } = props;
const [prevInput] = useState(input); const [prevInput] = useState(input);
@ -32,6 +32,7 @@ export function TransactionEditor(props) {
//{ key: '5', text: 'Member Balance / Protocash', value: 'Member' }, //{ key: '5', text: 'Member Balance / Protocash', value: 'Member' },
{ key: '6', text: 'Membership Adjustment / Clearing', value: 'Clearing' }, { key: '6', text: 'Membership Adjustment / Clearing', value: 'Clearing' },
{ key: '7', text: 'PayPal', value: 'PayPal' }, { key: '7', text: 'PayPal', value: 'PayPal' },
{ key: '8', text: 'Protocoin', value: 'Protocoin' },
]; ];
const sourceOptions = [ const sourceOptions = [
@ -53,17 +54,18 @@ export function TransactionEditor(props) {
{ key: '0', text: 'Membership Dues', value: 'Membership' }, { key: '0', text: 'Membership Dues', value: 'Membership' },
{ key: '1', text: 'Course Fee', value: 'OnAcct' }, { key: '1', text: 'Course Fee', value: 'OnAcct' },
{ key: '2', text: 'Snacks / Pop / Coffee', value: 'Snacks' }, { key: '2', text: 'Snacks / Pop / Coffee', value: 'Snacks' },
{ key: '3', text: 'Donation', value: 'Donation' }, { key: '3', text: 'Donation (Explain in Memo)', value: 'Donation' },
{ key: '4', text: 'Consumables (Explain in memo)', value: 'Consumables' }, { key: '4', text: 'Consumables (Explain in Memo)', value: 'Consumables' },
{ key: '5', text: 'Purchase of Locker / Materials / Stock', value: 'Purchases' }, { key: '5', text: 'Purchase of Locker / Materials / Stock', value: 'Purchases' },
//{ key: '6', text: 'Auction, Garage Sale, Nearly Free Shelf', value: 'Garage Sale' }, { key: '6', text: 'Purchase of Protocoin', value: 'Exchange' },
{ key: '7', text: 'Reimbursement (Enter a negative value)', value: 'Reimburse' }, { key: '7', text: 'Reimbursement (Not for Refunds)', value: 'Reimburse' },
{ key: '8', text: 'Other (Explain in memo)', value: 'Other' }, { key: '8', text: 'Other (Explain in Memo)', value: 'Other' },
]; ];
return ( return (
<div className='transaction-editor'> <div className='transaction-editor'>
{!noMemberSearch && <Form.Field error={error.member_id}> <Form.Group widths='equal'>
<Form.Field error={error.member_id}>
<label>Member (search)</label> <label>Member (search)</label>
<MembersDropdown <MembersDropdown
token={token} token={token}
@ -71,21 +73,54 @@ export function TransactionEditor(props) {
onChange={handleValues} onChange={handleValues}
initial={input.member_name} initial={input.member_name}
/> />
</Form.Field>} </Form.Field>
<Form.Group widths='equal'>
<Form.Input <Form.Input
label='Date' label='Date'
fluid fluid
{...makeProps('date')} {...makeProps('date')}
/> />
</Form.Group>
<Form.Group widths='equal'>
<Form.Select
label='Payment Method'
fluid
options={accountOptions}
{...makeProps('account_type')}
onChange={handleValues}
/>
{input.account_type && (input.account_type === 'Protocoin' ?
<Form.Input <Form.Input
label='Amount' label='Protocoin Delta (+/-)'
fluid
{...makeProps('protocoin')}
/>
:
<Form.Input
label='Amount ($)'
fluid fluid
{...makeProps('amount')} {...makeProps('amount')}
/> />
)}
</Form.Group> </Form.Group>
{input?.account_type !== prevInput?.account_type && input?.account_type === 'PayPal' &&
<Message visible warning>
<Message.Header>Are you sure?</Message.Header>
<p>PayPal transactions are automatic. Double check there's no duplicate. They may take 24h to appear.</p>
</Message>
}
{input?.account_type !== prevInput?.account_type && input?.account_type === 'Protocoin' &&
<Message visible warning>
<Message.Header>Are you sure?</Message.Header>
<p>Protocoin spending transactions are automatic. Do you want "Purchase of Protocoin" category below?</p>
</Message>
}
<Form.Group widths='equal'>
<Form.Select <Form.Select
label='Category' label='Category'
fluid fluid
@ -94,35 +129,22 @@ export function TransactionEditor(props) {
onChange={handleValues} onChange={handleValues}
/> />
<Form.Select {input.category === 'Membership' &&
label='Payment Method / Account' <Form.Input
label='Membership Months (+/-)'
fluid fluid
options={accountOptions} {...makeProps('number_of_membership_months')}
{...makeProps('account_type')}
onChange={handleValues}
/> />
{input?.account_type !== prevInput?.account_type && input?.account_type === 'PayPal' &&
<Message visible warning>
<Message.Header>Are you sure?</Message.Header>
<p>PayPal transactions should be automatic. Double check there's no duplicate. They may take 24h to appear.</p>
</Message>
} }
{/* <Form.Group widths='equal'> {input.category === 'Exchange' &&
<Form.Input <Form.Input
label='Payment Method' label='Protocoin Purchased'
fluid fluid
{...makeProps('payment_method')} {...makeProps('amount')} // trick the user
/> />
<Form.Select }
label='Info Source' </Form.Group>
fluid
options={sourceOptions}
{...makeProps('info_source')}
onChange={handleValues}
/>
</Form.Group> */}
<Form.Group widths='equal'> <Form.Group widths='equal'>
<Form.Input <Form.Input
@ -131,18 +153,12 @@ export function TransactionEditor(props) {
{...makeProps('reference_number')} {...makeProps('reference_number')}
/> />
<Form.Input
label='Number of Membership Months'
fluid
{...makeProps('number_of_membership_months')}
/>
</Form.Group>
<Form.Input <Form.Input
label='Memo / Notes' label='Memo / Notes'
fluid fluid
{...makeProps('memo')} {...makeProps('memo')}
/> />
</Form.Group>
</div> </div>
); );
@ -435,7 +451,7 @@ export function TransactionDetail(props) {
<Header size='large'>Transaction Receipt</Header> <Header size='large'>Transaction Receipt</Header>
<Grid stackable columns={2}> <Grid stackable columns={2}>
<Grid.Column> <Grid.Column width={6}>
<TransactionTable user={user} transaction={transaction} /> <TransactionTable user={user} transaction={transaction} />
<div style={{ display: 'none' }}> <div style={{ display: 'none' }}>
@ -447,7 +463,7 @@ export function TransactionDetail(props) {
/> />
</Grid.Column> </Grid.Column>
<Grid.Column> <Grid.Column width={10}>
{isAdmin(user) ? {isAdmin(user) ?
<Segment padded> <Segment padded>
<EditTransaction transaction={transaction} setTransaction={setTransaction} {...props} /> <EditTransaction transaction={transaction} setTransaction={setTransaction} {...props} />