Exemplo n.º 1
0
 def get_value(self, data):
     amount = super(MoneyField, self).get_value(data)
     currency = data.get(get_currency_field_name(self.field_name), None)
     if currency:
         return Money(amount, currency)
     return amount
Exemplo n.º 2
0
INTEGER_FIELD = ModelWithVanillaMoneyField._meta.get_field('integer')


def get_args(value, field):
    """
    Constructs arguments for `display_for_field`.
    """
    if VERSION < (1, 9):
        return value, field
    return value, field, ''


@pytest.mark.parametrize(
    'value, kwargs, expected',
    (
        (Money(10, 'RUB'), {}, '10.00 руб.'),  # Issue 232
        (Money(1234), {
            'USE_L10N': True,
            'USE_THOUSAND_SEPARATOR': True
        }, '1,234.00 XYZ'),  # Issue 220
        (Money(1000, 'SAR'), {
            'USE_I18N': True,
            'LANGUAGE_CODE': 'en-us'
        }, 'ر.س1,000.00'),  # Issue 196
        (Money(1000, 'PLN'), {}, '1,000.00 zł'),  # Issue 102
        (Money('3.33', 'EUR'), {
            'USE_I18N': True,
            'LANGUAGE_CODE': 'de-de'
        }, '3.33 €'),  # Issue 90
    ))
def test_display_for_field(settings, value, kwargs, expected):
Exemplo n.º 3
0
 def test_creation(self):
     o = Operation('deposit', 100)
     self.assertIsInstance(o, Operation)
     self.assertEqual(o.amount, Money(100, settings.CURRENCY))
Exemplo n.º 4
0
 def _zero_balance(self):
     """Get a balance for this account with all currencies set to zero"""
     return Balance([Money("0", currency) for currency in self.currencies])
Exemplo n.º 5
0
    def test_failed_second_intent_succeeds(self):
        with mock.patch(
            'stripe.Webhook.construct_event',
            return_value=MockEvent(
                'payment_intent.payment_failed', {'object': {'id': self.intent.intent_id}}
            )
        ):
            response = self.client.post(
                self.webhook,
                HTTP_STRIPE_SIGNATURE='some signature'
            )
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            # Stripe might send double failed webhooks
            response = self.client.post(
                self.webhook,
                HTTP_STRIPE_SIGNATURE='some signature'
            )
            self.assertEqual(response.status_code, status.HTTP_200_OK)

        self.intent.refresh_from_db()
        payment = self.intent.payment

        donation = Donation.objects.get(pk=self.donation.pk)

        self.assertEqual(donation.status, 'failed')
        self.assertEqual(payment.status, 'failed')
        self.donation.refresh_from_db()
        self.assertEqual(self.donation.status, 'failed')

        second_intent = StripePaymentIntentFactory.create(donation=self.donation, intent_id='some-other-id')
        with open('bluebottle/funding_stripe/tests/files/intent_webhook_success.json') as hook_file:
            data = json.load(hook_file)
            data['object']['id'] = second_intent.intent_id

        transfer = stripe.Transfer(data['object']['charges']['data'][0]['transfer'])
        transfer.update({
            'id': data['object']['charges']['data'][0]['transfer'],
            'amount': 2500,
            'currency': 'eur'
        })

        with mock.patch(
            'stripe.Webhook.construct_event',
            return_value=MockEvent(
                'payment_intent.succeeded', data
            )
        ):
            with mock.patch(
                'stripe.Transfer.retrieve',
                return_value=transfer
            ):
                response = self.client.post(
                    self.webhook,
                    HTTP_STRIPE_SIGNATURE='some signature'
                )
                self.assertEqual(response.status_code, status.HTTP_200_OK)

        second_intent.refresh_from_db()
        self.assertEqual(second_intent.payment.pk, payment.pk)

        payment.refresh_from_db()
        donation.refresh_from_db()

        self.assertEqual(donation.status, 'succeeded')
        self.assertEqual(donation.payout_amount, Money(25, 'EUR'))
        self.assertEqual(payment.status, 'succeeded')
class TestMoneyField:

    if IS_DRF_3:
        from rest_framework.fields import empty
    else:
        empty = None

    def get_serializer(self, model_class, instance=None, data=empty):

        if IS_DRF_3:
            class Serializer(serializers.ModelSerializer):
                class Meta:
                    model = model_class
                    fields = '__all__'
        else:
            class Serializer(serializers.ModelSerializer):
                class Meta:
                    model = model_class

        return Serializer(instance=instance, data=data)

    @pytest.mark.parametrize(
        'model_class, create_kwargs, expected', (
            (NullMoneyFieldModel, {'field': None}, {'field': None, 'field_currency': 'USD'}),
            (
                NullMoneyFieldModel,
                {'field': Money(10, 'USD')},
                {'field': '10.00' if IS_DRF_3 else 10, 'field_currency': 'USD'}
            ),
            (
                ModelWithVanillaMoneyField,
                {'money': Money(10, 'USD')},
                {
                    'integer': 0,
                    'money': '10.00' if IS_DRF_3 else 10,
                    'money_currency': 'USD',
                    'second_money': '0.00' if IS_DRF_3 else 0,
                    'second_money_currency': 'EUR'}
            ),
        )
    )
    def test_to_representation(self, model_class, create_kwargs, expected):
        instance = model_class.objects.create(**create_kwargs)
        expected['id'] = instance.id
        serializer = self.get_serializer(model_class, instance=instance)
        assert serializer.data == expected

    @pytest.mark.parametrize(
        'model_class, field, value, expected', (
            (NullMoneyFieldModel, 'field', None, None),
            (NullMoneyFieldModel, 'field', Money(10, 'USD'), Money(10, 'USD')),
            (ModelWithVanillaMoneyField, 'money', Money(10, 'USD'), Money(10, 'USD')),
            (ModelWithVanillaMoneyField, 'money', 10, Money(10, 'XYZ')),
        )
    )
    def test_to_internal_value(self, model_class, field, value, expected):
        serializer = self.get_serializer(model_class, data={field: value})
        assert serializer.is_valid()
        instance = serializer.save()
        assert getattr(instance, field) == expected

    def test_invalid_value(self):
        serializer = self.get_serializer(ModelWithVanillaMoneyField, data={'money': None})
        assert not serializer.is_valid()
        error_text = 'This field may not be null.' if IS_DRF_3 else 'This field is required.'
        assert serializer.errors == {'money': [error_text]}

    @pytest.mark.parametrize(
        'body, expected', (
            ({'field': '10', 'field_currency': 'EUR'}, Money(10, 'EUR')),
            ({'field': '12.20', 'field_currency': 'GBP'}, Money(12.20, 'GBP')),
            ({'field': '15.15', 'field_currency': 'USD'}, Money(15.15, 'USD')),
        ),
    )
    def test_post_put_values(self, body, expected):
        serializer = self.get_serializer(NullMoneyFieldModel, data=body)
        serializer.is_valid()
        if IS_DRF_3:
            assert serializer.validated_data['field'] == expected
        else:
            assert Money(serializer.data['field'], serializer.data['field_currency']) == expected
Exemplo n.º 7
0
 def __init__(self, client: str, currency: str = settings.CURRENCY):
     self.currency = currency
     self.client = client
     self.current_balance = Money(0, self.currency)
     self.operations = deque()
Exemplo n.º 8
0
 def test_it_should_compute_the_account_balance_in_multiple_currencies(self):
     account = Account.objects.create(owner=self.user, currency='CHF')
     Charge.objects.create(account=account, amount=Money(10, 'CHF'), product_code='ACHARGE')
     Charge.objects.create(account=account, amount=Money(-3, 'EUR'), product_code='ACREDIT')
     with self.assertNumQueries(2):
         assert account.balance() == Total(-10, 'CHF', 3, 'EUR')
Exemplo n.º 9
0
 def test_it_can_create_charge_with_both_ad_hoc_label_and_product_code(self):
     charge = Charge.objects.create(account=self.account, amount=Money(10, 'CHF'),
                                    product_code='ACHARGE', ad_hoc_label='hai')
     charge.full_clean()
Exemplo n.º 10
0
 def test_objects_creation(self):
     SimpleModel.objects.create(money=Money("100.0", 'USD'))
     self.assertEqual(SimpleModel.objects.count(), 1)
Exemplo n.º 11
0
 def test_payments_should_ignore_refunds(self):
     Transaction.objects.create(account=self.account, success=True, amount=Money(-10, 'CHF'))
     with self.assertNumQueries(1):
         qs = Transaction.successful.payments()
         assert not qs.exists()
Exemplo n.º 12
0
    def testSaving(self):

        somemoney = Money("100.0")

        model = ModelWithVanillaMoneyField(money=somemoney)
        model.save()

        retrieved = ModelWithVanillaMoneyField.objects.get(pk=model.pk)

        self.assertEquals(somemoney.currency, retrieved.money.currency)
        self.assertEquals(somemoney, retrieved.money)

        # Try setting the value directly
        retrieved.money = Money(1, moneyed.DKK)
        retrieved.save()
        retrieved = ModelWithVanillaMoneyField.objects.get(pk=model.pk)

        self.assertEquals(Money(1, moneyed.DKK), retrieved.money)

        object = BaseModel.objects.create()
        self.assertEquals(Money(0, 'USD'), object.first_field)
        object = BaseModel.objects.create(first_field='111.2')
        self.assertEquals(Money('111.2', 'USD'), object.first_field)
        object = BaseModel.objects.create(first_field=Money('123', 'PLN'))
        self.assertEquals(Money('123', 'PLN'), object.first_field)

        object = ModelWithDefaultAsDecimal.objects.create()
        self.assertEquals(Money('0.01', 'CHF'), object.money)
        object = ModelWithDefaultAsInt.objects.create()
        self.assertEquals(Money('123', 'GHS'), object.money)
        object = ModelWithDefaultAsString.objects.create()
        self.assertEquals(Money('123', 'PLN'), object.money)
        object = ModelWithDefaultAsStringWithCurrency.objects.create()
        self.assertEquals(Money('123', 'USD'), object.money)
        object = ModelWithDefaultAsFloat.objects.create()
        self.assertEquals(Money('12.05', 'PLN'), object.money)
        object = ModelWithDefaultAsMoney.objects.create()
        self.assertEquals(Money('0.01', 'RUB'), object.money)
Exemplo n.º 13
0
def it_should_transform_money_to_netaxept_representation():
    money = Money(10, 'NOK')
    assert _money_to_netaxept_amount(money) == 1000
    assert _money_to_netaxept_currency(money) == 'NOK'
Exemplo n.º 14
0
def current_value(fundname, num_shares):
    # returns user's money position of given fund
    quote = getQuote(fundname)
    price = Money(quote, currency='USD')
    value = num_shares * price
    return value
Exemplo n.º 15
0
 def test_it_should_compute_the_invoice_due_when_there_are_transactions(self):
     invoice = Invoice.objects.create(account=self.account, due_date=date.today())
     Charge.objects.create(account=self.account, invoice=invoice, amount=Money(10, 'CHF'), product_code='ACHARGE')
     Transaction.objects.create(account=self.account, invoice=invoice, amount=Money(8, 'CHF'), success=True)
     with self.assertNumQueries(2):
         assert invoice.due() == Total(2, 'CHF')
Exemplo n.º 16
0
 def test_it_must_have_ad_hoc_code_or_product_code(self):
     charge = Charge.objects.create(account=self.account, amount=Money(10, 'CHF'))
     with raises(ValidationError):
         charge.full_clean()
Exemplo n.º 17
0
 def testNonExistentCurrency(self):
     m = Money(Decimal(10), moneyed.EUR)
     form = MoneyForm({"money_0": m.amount, "money_1": m.currency})
     self.assertFalse(form.is_valid())
Exemplo n.º 18
0
 def test_it_can_mark_charge_as_deleted(self):
     Charge.objects.create(account=self.account, amount=Money(10, 'CHF'),
                           product_code='ACHARGE', deleted=True)
Exemplo n.º 19
0
 def setUp(self):
     self.mortgage_payment = Money(amount=666.00, currency="USD")
Exemplo n.º 20
0
 def test_uninvoiced_paymnents_should_return_uninvoiced_payment(self):
     Transaction.objects.create(account=self.account, success=True, amount=Money(10, 'CHF'))
     with self.assertNumQueries(1):
         qs = Transaction.successful.uninvoiced(account_id=self.account.pk).payments()
         assert qs.exists()
class AssignmentForm(forms.Form):
    title = _("Assignment")
    layout = hg.BaseElement(
        _layout.datatable.DataTable(
            columns=(
                _layout.datatable.DataTableColumn(_("Date"), hg.C("row.date"),
                                                  None),
                _layout.datatable.DataTableColumn(_("Note"), hg.C("row.note"),
                                                  None),
                _layout.datatable.DataTableColumn(_("Account"),
                                                  hg.C("row.debitaccount"),
                                                  None),
                _layout.datatable.DataTableColumn(_("Cost Center"),
                                                  hg.C("row.creditaccount"),
                                                  None),
                _layout.datatable.DataTableColumn(
                    _("Person Number"), hg.C("row.person.personnumber"), None),
                _layout.datatable.DataTableColumn(_("Donor Number"),
                                                  hg.C("row.donornumber"),
                                                  None),
                _layout.datatable.DataTableColumn(
                    _("Assignment state"),
                    hg.If(
                        hg.C("row.person"),
                        _layout.icon.Icon(
                            "checkmark--filled",
                            size=16,
                            style="fill: currentColor; color: green;",
                        ),
                        _layout.icon.Icon(
                            "warning",
                            size=16,
                            style="fill: currentColor; color: red;",
                        ),
                    ),
                    None,
                ),
                _layout.datatable.DataTableColumn(_("Amount"),
                                                  hg.C("row.amount_formatted"),
                                                  None),
            ),
            row_iterator=hg.C("contributions"),
        ).with_toolbar(_("Overview of contributions to import"),
                       hg.C("importfile")),
        hg.DIV(
            hg.DIV(
                hg.DIV(
                    hg.F(lambda c: len(c["contributions"])),
                    " ",
                    _("contributions"),
                    _class="bx--batch-summary",
                ),
                hg.DIV(
                    _("Sum"),
                    hg.SPAN("|",
                            style="margin-left: 1rem; margin-right: 1rem"),
                    hg.F(lambda c: sum([
                        convert_money(
                            Money(contr.amount, contr.currency),
                            global_preferences["contributions__currency"],
                        ) for contr in c["contributions"]
                    ]) or Money(0, global_preferences["contributions__currency"
                                                      ])),
                    style=
                    "position: absolute; right: 0; margin-right: 1rem; color: #ffffff",
                ),
                _class="bx--batch-actions--active bx--batch-actions",
            ),
            _class="bx--table-toolbar",
            style="margin-bottom: 4rem",
        ),
        hg.DIV(style="margin-bottom: 2rem"),
        hg.If(
            hg.C("unassigned_contributions"),
            _layout.notification.InlineNotification(
                hg.BaseElement(
                    hg.F(lambda c: len([
                        c for c in c.get("contributions", ()) if not c.person
                    ])),
                    _(" contributions could not been assigned"),
                ),
                _("Please make sure that each entry has contributor number "
                  "which matches with a person number and do the import again"
                  ),
                kind="error",
                lowcontrast=True,
                action=(
                    _("Cancel import"),
                    "window.location = window.location.pathname + '?reset=1'",
                ),
                style="max-width: 100%",
            ),
            _layout.notification.InlineNotification(
                _("Assignment complete"),
                _("Continue in order to complete the import"),
                kind="success",
                style="max-width: 100%",
            ),
        ),
    )
Exemplo n.º 22
0
 def test_it_can_reverse(self):
     the_charge = Charge.objects.create(account=self.account, amount=Money(10, 'CHF'), product_code='ACHARGE')
     Charge.objects.create(account=self.account, amount=Money(-10, 'CHF'), product_code='REVERSAL',
                           reverses=the_charge)
Exemplo n.º 23
0
def assign_funds_to_invoice(invoice_id: str) -> bool:
    """
    Uses the available funds on the account (credits and payments) to pay the given invoice.
    :param invoice_id: The id of the invoice.
    :return: True if the invoice status is paid.

    A lot of side effects may occur in the database:
    - Funds (either payments or credits) may get assigned to the invoice.
    - The invoice status may change.
    - Credits entities may be created.
    """

    logger.info('assign-funds-to-invoice', invoice_id=invoice_id)
    invoice = Invoice.objects.get(pk=invoice_id)
    account_id = invoice.account_id

    #
    # Precondition. Don't touch invoices that are not PENDING
    #
    if invoice.status != Invoice.PENDING:
        logger.info('assign-funds-to-invoice.status-is-not-pending',
                    invoice_id=invoice_id)
        return False

    #
    # Precondition: Only handle invoices in a single currency
    #
    invoice_due_monies = invoice.due().monies()
    if len(invoice_due_monies) != 1:
        logger.info('assign-funds-to-invoice.more-than-one-currency',
                    invoice_id=invoice_id)
        return False
    invoice_due_amount = invoice_due_monies[0].amount
    invoice_due_currency = invoice_due_monies[0].currency

    #
    # 1. Collect funds as long as long as we need them
    #
    if invoice_due_amount > 0:

        payments = Transaction.successful \
            .payments() \
            .uninvoiced(account_id=account_id) \
            .in_currency(invoice_due_currency) \
            .order_by('created')

        credits = Charge.objects \
            .credits() \
            .uninvoiced(account_id=account_id) \
            .in_currency(invoice_due_currency) \
            .order_by('created')

        funds = list(credits) + list(payments)
        for fund in funds:
            contributed_amount = abs(
                fund.amount.amount
            )  # 'abs' because credits have a negative value
            logger.info('assign-funds-to-invoice.assigning-fund',
                        invoice_id=invoice_id,
                        fund_type=type(fund).__name__,
                        fund_id=str(fund.pk),
                        contributed_amount=contributed_amount)
            fund.invoice_id = invoice_id
            fund.save()
            invoice_due_amount -= contributed_amount
            if invoice_due_amount <= 0:
                break

    #
    # 2. Mark invoice paid if nothing is due.
    #
    if invoice_due_amount <= 0:
        logger.info('assign-funds-to-invoice.mark-paid',
                    invoice_id=invoice_id,
                    invoice_due_amount=invoice_due_amount)
        invoice.status = Invoice.PAID
        invoice.save()

    #
    # 3. Carry forward any overpaid money.
    #
    if invoice_due_amount < 0:
        overpayment = Money(abs(invoice_due_amount), invoice_due_currency)
        logger.info('assign-funds-to-invoice.handling-overpayment',
                    invoice_id=invoice_id,
                    overpayment=overpayment)
        with transaction.atomic():
            Charge.objects.create(account_id=account_id,
                                  amount=overpayment,
                                  product_code=CARRIED_FORWARD,
                                  invoice_id=invoice_id)
            Charge.objects.create(account_id=account_id,
                                  amount=-overpayment,
                                  product_code=CREDIT_REMAINING)

    return invoice.status == Invoice.PAID
Exemplo n.º 24
0
 def setUp(self):
     user = User.objects.create_user('a-username')
     account = Account.objects.create(owner=user, currency='CHF')
     self.charge = Charge.objects.create(account=account, amount=Money(10, 'CHF'), product_code='ACHARGE')
Exemplo n.º 25
0
def make_prices():
    service = Service.objects.get(pk=4)
    country = Country.objects.get(pk=1)
    service.prices.all().delete()

    # SUCCESS: add base price
    price = Price()
    price.service = service
    price.price = Money(120, EUR)
    price.full_clean()
    price.save()

    # ERROR: add another base price
    with pytest.raises(ValidationError) as e:
        price.pk = None
        price.full_clean()
        price.save()
    assert 'Base price for this country already exists' in e.value.messages

    # SUCCESS: add base country price
    price.pk = None
    price.country = country
    price.price = Money(75, EUR)
    price.full_clean()
    price.save()

    # ERROR: add another base price
    with pytest.raises(ValidationError) as e:
        price.pk = None
        price.full_clean()
        price.save()
    assert 'Base price for this country already exists' in e.value.messages
    assert service.prices.all().count() == 2

    # SUCCESS: add base period price 5-10
    price.pk = None
    price.country = None
    price.price = Money(40, EUR)
    price.period_from = 5
    price.period_to = 10
    price.full_clean()
    price.save()

    # ERROR: add another period price 5-10
    with pytest.raises(ValidationError) as e:
        price.pk = None
        price.full_clean()
        price.save()
    assert 'Price with this period already exists' in e.value.messages

    # SUCCESS: add country period price 5-10
    price.pk = None
    price.country = country
    price.price = Money(122, EUR)
    price.for_unit = False
    price.full_clean()
    price.save()

    # ERROR: add another period price 6-7
    with pytest.raises(ValidationError) as e:
        price.pk = None
        price.country = None
        price.period_from = 6
        price.period_to = 7
        price.full_clean()
        price.save()
    assert 'Price with this period already exists' in e.value.messages

    # ERROR: add another period price 7-14
    with pytest.raises(ValidationError) as e:
        price.pk = None
        price.country = None
        price.period_from = 7
        price.period_to = 14
        price.full_clean()
        price.save()
    assert 'Price with this period already exists' in e.value.messages

    # ERROR: add another period price 2-7
    with pytest.raises(ValidationError) as e:
        price.pk = None
        price.country = None
        price.period_from = 2
        price.period_to = 7
        price.full_clean()
        price.save()
    assert 'Price with this period already exists' in e.value.messages

    # ERROR: add another period price 10-12
    with pytest.raises(ValidationError) as e:
        price.pk = None
        price.country = None
        price.period_from = 10
        price.period_to = 12
        price.full_clean()
        price.save()
    assert 'Price with this period already exists' in e.value.messages

    # SUCCESS: add base period price 11-∞
    price.pk = None
    price.price = Money(15, EUR)
    price.period_from = 11
    price.for_unit = True
    price.period_to = None
    price.full_clean()
    price.save()

    # SUCCESS: add base period price ∞-4
    price.pk = None
    price.price = Money(15, EUR)
    price.period_from = None
    price.period_to = 4
    price.full_clean()
    price.save()

    # ERROR: add another period price 22-33
    with pytest.raises(ValidationError) as e:
        price.pk = None
        price.country = None
        price.period_from = 22
        price.period_to = 33
        price.full_clean()
        price.save()
    assert 'Price with this period already exists' in e.value.messages

    # ERROR: add another period price 1-3
    with pytest.raises(ValidationError) as e:
        price.pk = None
        price.country = None
        price.period_from = 1
        price.period_to = 3
        price.full_clean()
        price.save()
    assert 'Price with this period already exists' in e.value.messages

    # ERROR: add another country period price 5-11
    with pytest.raises(ValidationError) as e:
        price.pk = None
        price.country = country
        price.period_from = 5
        price.period_to = 11
        price.full_clean()
        price.save()
    assert 'Price with this period already exists' in e.value.messages

    # SUCCESS: add another country period price 11-20
    price.pk = None
    price.country = country
    price.price = Money(35, EUR)
    price.period_from = 11
    price.period_to = 20
    price.full_clean()
    price.save()

    price.pk = None
    price.country = Country.objects.get(pk=2)
    price.price = Money(2300, RUB)
    price.full_clean()
    price.save()

    assert service.prices.all().count() == 8
Exemplo n.º 26
0
 def test_uninvoiced_payments_should_ignore_invoiced_transactions(self):
     Invoice.objects.create(id=1, account=self.account, due_date=date.today())
     Transaction.objects.create(account=self.account, success=True, invoice_id=1, amount=Money(10, 'CHF'))
     with self.assertNumQueries(1):
         qs = Transaction.successful.uninvoiced(account_id=self.account.pk).payments()
         assert not qs.exists()
Exemplo n.º 27
0
def currency_exchange(
    source,
    source_amount,
    destination,
    destination_amount,
    trading_account,
    fee_destination=None,
    fee_amount=None,
    date=None,
    description=None,
):
    """Exchange funds from one currency to another

    Use this method to represent a real world currency transfer. Note this
    process doesn't care about exchange rates, only about the value
    of currency going in and out of the transaction.

    You can also record any exchange fees by syphoning off funds to
    ``fee_account`` of amount ``fee_amount``. Note
    that the free currency must be the same as the source currency.

    Examples:

        For example, imagine our Canadian bank has obligingly transferred 120 CAD into our US bank account.
        We sent CAD 120, and received USD 100. We were also changed 1.50 CAD in fees.

        We can represent this exchange in Hordak as follows::

            from hordak.utilities.currency import currency_exchange

            currency_exchange(
                # Source account and amount
                source=cad_cash,
                source_amount=Money(120, 'CAD'),
                # Destination account and amount
                destination=usd_cash,
                destination_amount=Money(100, 'USD'),
                # Trading account the exchange will be done through
                trading_account=trading,
                # We also incur some fees
                fee_destination=banking_fees,
                fee_amount=Money(1.50, 'CAD')
            )

        We should now find that:

         1. ``cad_cash.balance()`` has decreased by ``CAD 120``
         2. ``usd_cash.balance()`` has increased by ``USD 100``
         3. ``banking_fees.balance()`` is ``CAD 1.50``
         4. ``trading_account.balance()`` is ``USD 100, CAD -120``

        You can perform ``trading_account.normalise()`` to discover your unrealised gains/losses
        on currency traded through that account.

    Args:
        source (Account): The account the funds will be taken from
        source_amount (Money): A ``Money`` instance containing the inbound amount and currency.
        destination (Account): The account the funds will be placed into
        destination_amount (Money): A ``Money`` instance containing the outbound amount and currency
        trading_account (Account): The trading account to be used.
            The normalised balance of this account will indicate
            gains/losses you have made as part of your activity via this account.
            Note that the normalised balance fluctuates with the current exchange rate.
        fee_destination (Account): Your exchange may incur fees. Specifying this will move incurred fees
            into this account (optional).
        fee_amount (Money): The amount and currency of any incurred fees (optional).
        description (str): Description for the transaction.
            Will default to describing funds in/out & fees (optional).
        date (datetime.date): The date on which the transaction took place. Defaults to today (optional).

    Returns:
        (Transaction): The transaction created

    See Also:
        You can see the above example in practice in
        ``CurrencyExchangeTestCase.test_fees`` in `test_currency.py`_.

    .. _test_currency.py:
        https://github.com/adamcharnock/django-hordak/blob/master/hordak/tests/utilities/test_currency.py
    """
    from hordak.models import Account, Leg, Transaction

    if trading_account.type != Account.TYPES.trading:
        raise TradingAccountRequiredError(
            "Account {} must be a trading account".format(trading_account))

    if (fee_destination
            or fee_amount) and not (fee_destination and fee_amount):
        raise RuntimeError(
            "You must specify either neither or both fee_destination and fee_amount."
        )

    if fee_amount is None:
        # If fees are not specified then set fee_amount to be zero
        fee_amount = Money(0, source_amount.currency)
    else:
        # If we do have fees then make sure the fee currency matches the source currency
        if fee_amount.currency != source_amount.currency:
            raise InvalidFeeCurrency(
                "Fee amount currency ({}) must match source amount currency ({})"
                .format(fee_amount.currency, source_amount.currency))

    # Checks over and done now. Let's create the transaction
    with db_transaction.atomic():
        transaction = Transaction.objects.create(
            date=date or datetime.date.today(),
            description=description
            or "Exchange of {} to {}, incurring {} fees".format(
                source_amount,
                destination_amount,
                "no" if fee_amount is None else fee_amount,
            ),
        )

        # Source currency into trading account
        Leg.objects.create(transaction=transaction,
                           account=source,
                           amount=source_amount)
        Leg.objects.create(
            transaction=transaction,
            account=trading_account,
            amount=-(source_amount - fee_amount),
        )

        # Any fees
        if fee_amount and fee_destination:
            Leg.objects.create(
                transaction=transaction,
                account=fee_destination,
                amount=-fee_amount,
                description="Fees",
            )

        # Destination currency out of trading account
        Leg.objects.create(transaction=transaction,
                           account=trading_account,
                           amount=destination_amount)
        Leg.objects.create(transaction=transaction,
                           account=destination,
                           amount=-destination_amount)

    return transaction
Exemplo n.º 28
0
 def test_it_should_compute_the_invoice_due_in_multiple_currencies(self):
     invoice = Invoice.objects.create(account=self.account, due_date=date.today())
     Charge.objects.create(account=self.account, invoice=invoice, amount=Money(10, 'CHF'), product_code='ACHARGE')
     Charge.objects.create(account=self.account, invoice=invoice, amount=Money(-3, 'EUR'), product_code='ACREDIT')
     with self.assertNumQueries(2):
         assert invoice.due() == Total(10, 'CHF', -3, 'EUR')
Exemplo n.º 29
0
    def search_and_match(self, manufacturer_part, quantity=1, currency=None):
        manufacturer = manufacturer_part.manufacturer
        manufacturer_part_number = manufacturer_part.manufacturer_part_number
        if manufacturer:
            manufacturer_list = self.api.get_manufacturer_list()
            # TODO: possibly get manufacturer id from manufacturer list, do a fuzzy lookup using manufacturer name
            #  to reduce results
            mfg_id = manufacturer_list[
                manufacturer.
                name] if manufacturer.name in manufacturer_list else None
            if mfg_id:
                results = self.api.search_part_and_manufacturer(
                    part_number=manufacturer_part_number,
                    manufacturer_id=mfg_id)
            else:
                results = self.api.search_part(
                    part_number=manufacturer_part_number)
        else:
            results = self.api.search_part(
                part_number=manufacturer_part_number)

        mouser_parts = []
        optimal_part = None
        seller_parts = []
        for part in results['Parts']:
            seller = Seller(name='Mouser')
            try:
                quantity_available = [
                    int(s) for s in part['Availability'].split()
                    if s.isdigit()
                ][0]
                mouser_part = {
                    'part_number': part['ManufacturerPartNumber'],
                    'manufacturer': part['Manufacturer'],
                    'description': part['Description'],
                    'data_sheet': part['DataSheetUrl'],
                    'stock': part['Availability'],
                    'stock_parsed': quantity_available,
                    'lead_time': part['LeadTime'],
                    'seller_parts': [],
                    'product_detail_url': part['ProductDetailUrl'],
                }

                lead_time_days = [
                    int(s) for s in part['LeadTime'].split() if s.isdigit()
                ][0]  # TODO: Make sure it's actually days
                for pb in part['PriceBreaks']:
                    moq = int(pb['Quantity'])
                    unit_price_raw = parse_number(pb['Price'])
                    unit_currency = pb['Currency']
                    unit_cost = Money(unit_price_raw, unit_currency)
                    if currency:
                        unit_cost = convert_money(unit_cost, currency)
                    seller_part = SellerPart(
                        seller=seller,
                        manufacturer_part=manufacturer_part,
                        minimum_order_quantity=moq,
                        minimum_pack_quantity=1,
                        data_source='Mouser',
                        unit_cost=unit_cost,
                        lead_time_days=lead_time_days,
                        nre_cost=Money(0, currency),
                        ncnr=True)
                    mouser_part['seller_parts'].append(seller_part.as_dict())
                    seller_parts.append(seller_part)
                mouser_parts.append(mouser_part)
            except (KeyError, AttributeError, IndexError):
                continue
        local_seller_parts = list(manufacturer_part.seller_parts())
        seller_parts.extend(local_seller_parts)
        return {
            'mouser_parts': mouser_parts,
            'optimal_seller_part': SellerPart.optimal(seller_parts, quantity),
        }
Exemplo n.º 30
0
 def to_internal_value(self, data):
     if isinstance(data, Money):
         amount = super(MoneyField, self).to_internal_value(data.amount)
         return Money(amount, data.currency)
     return super(MoneyField, self).to_internal_value(data)