Ejemplo n.º 1
0
 def response_role_based_access(self):
     """
     Perform Role Based Access Downgrade
     - Archive custom roles.
     - Set user roles using custom roles to Read Only.
     - Reset initial roles to standard permissions.
     """
     custom_roles = [r.get_id for r in UserRole.get_custom_roles_by_domain(self.domain.name)]
     if not custom_roles:
         return True
     if self.verbose:
         for role in custom_roles:
             log_accounting_info("Archiving Custom Role %s" % role)
     # temporarily disable this part of the downgrade until we
     # have a better user experience for notifying the downgraded user
     # read_only_role = UserRole.get_read_only_role_by_domain(self.domain.name)
     # web_users = WebUser.by_domain(self.domain.name)
     # for web_user in web_users:
     #     if web_user.get_domain_membership(self.domain.name).role_id in custom_roles:
     #         web_user.set_role(self.domain.name, read_only_role.get_qualified_id())
     #         web_user.save()
     # for cc_user in CommCareUser.by_domain(self.domain.name):
     #     if cc_user.get_domain_membership(self.domain.name).role_id in custom_roles:
     #         cc_user.set_role(self.domain.name, 'none')
     #         cc_user.save()
     UserRole.archive_custom_roles_for_domain(self.domain.name)
     UserRole.reset_initial_roles_for_domain(self.domain.name)
     return True
Ejemplo n.º 2
0
def send_credits_on_hq_report():
    if settings.SAAS_REPORTING_EMAIL and settings.IS_SAAS_ENVIRONMENT:
        yesterday = datetime.date.today() - datetime.timedelta(days=1)
        credits_report = CreditsAutomatedReport()
        credits_report.send_report(settings.SAAS_REPORTING_EMAIL)
        log_accounting_info("Sent credits on hq report as of {}".format(
            yesterday.isoformat()))
Ejemplo n.º 3
0
    def _pay_invoice(self, invoice):
        log_accounting_info("[Autopay] Autopaying invoice {}".format(invoice.id))
        amount = invoice.balance.quantize(Decimal(10) ** -2)
        if not amount:
            return

        auto_payer = invoice.subscription.account.auto_pay_user
        payment_method = StripePaymentMethod.objects.get(web_user=auto_payer)
        autopay_card = payment_method.get_autopay_card(invoice.subscription.account)
        if autopay_card is None:
            return

        try:
            with transaction.atomic():
                payment_record = PaymentRecord.create_record(payment_method, 'temp_transaction_id', amount)
                invoice.pay_invoice(payment_record)
                invoice.subscription.account.last_payment_method = LastPayment.CC_AUTO
                invoice.account.save()
                transaction_id = payment_method.create_charge(
                    autopay_card,
                    amount_in_dollars=amount,
                    description='Auto-payment for Invoice %s' % invoice.invoice_number,
                )
        except stripe.error.CardError:
            self._handle_card_declined(invoice, payment_method)
        except payment_method.STRIPE_GENERIC_ERROR as e:
            self._handle_card_errors(invoice, e)
        else:
            payment_record.transaction_id = transaction_id
            payment_record.save()
            self._send_payment_receipt(invoice, payment_record)
Ejemplo n.º 4
0
def send_bookkeeper_email(month=None, year=None, emails=None):
    today = datetime.date.today()

    # now, make sure that we send out LAST month's invoices if we did
    # not specify a month or year.
    today = get_previous_month_date_range(today)[0]

    month = month or today.month
    year = year or today.year

    from corehq.apps.accounting.interface import InvoiceInterface
    request = HttpRequest()
    params = urlencode((
        ('report_filter_statement_period_use_filter', 'on'),
        ('report_filter_statement_period_month', month),
        ('report_filter_statement_period_year', year),
    ))
    request.GET = QueryDict(params)
    request.couch_user = FakeUser(
        domain="hqadmin",
        username="******",
    )
    invoice = InvoiceInterface(request)
    invoice.is_rendered_as_email = True
    first_of_month = datetime.date(year, month, 1)
    email_context = {
        'month': first_of_month.strftime("%B"),
    }
    email_content = render_to_string(
        'accounting/email/bookkeeper.html', email_context)
    email_content_plaintext = render_to_string(
        'accounting/email/bookkeeper.txt', email_context)

    format_dict = Format.FORMAT_DICT[Format.CSV]
    excel_attachment = {
        'title': 'Invoices_%(period)s.%(extension)s' % {
            'period': first_of_month.strftime('%B_%Y'),
            'extension': format_dict['extension'],
        },
        'mimetype': format_dict['mimetype'],
        'file_obj': invoice.excel_response,
    }

    emails = emails or settings.BOOKKEEPER_CONTACT_EMAILS
    for email in emails:
        send_HTML_email(
            "Invoices for %s" % datetime.date(year, month, 1).strftime(USER_MONTH_FORMAT),
            email,
            email_content,
            email_from=settings.DEFAULT_FROM_EMAIL,
            text_content=email_content_plaintext,
            file_attachments=[excel_attachment],
        )

    log_accounting_info(
        "Sent Bookkeeper Invoice Summary for %(month)s "
        "to %(emails)s." % {
            'month': first_of_month.strftime(USER_MONTH_FORMAT),
            'emails': ", ".join(emails)
        })
Ejemplo n.º 5
0
    def pay_autopayable_invoices(self, date_due):
        """ Pays the full balance of all autopayable invoices on date_due """
        autopayable_invoices = Invoice.autopayable_invoices(date_due)
        for invoice in autopayable_invoices:
            log_accounting_info("[Autopay] Autopaying invoice {}".format(invoice.id))
            amount = invoice.balance.quantize(Decimal(10) ** -2)
            if not amount:
                continue

            auto_payer = invoice.subscription.account.auto_pay_user
            payment_method = StripePaymentMethod.objects.get(web_user=auto_payer)
            autopay_card = payment_method.get_autopay_card(invoice.subscription.account)
            if autopay_card is None:
                continue

            try:
                payment_record = payment_method.create_charge(
                    autopay_card,
                    amount_in_dollars=amount,
                    description='Auto-payment for Invoice %s' % invoice.invoice_number,
                )
            except stripe.error.CardError:
                self._handle_card_declined(invoice, payment_method)
                continue
            except payment_method.STRIPE_GENERIC_ERROR as e:
                self._handle_card_errors(invoice, e)
                continue
            else:
                with transaction.atomic():
                    invoice.pay_invoice(payment_record)
                    invoice.subscription.account.last_payment_method = LastPayment.CC_AUTO
                    invoice.account.save()

                self._send_payment_receipt(invoice, payment_record)
Ejemplo n.º 6
0
def generate_invoices(based_on_date=None):
    """
    Generates all invoices for the past month.
    """
    today = based_on_date or datetime.date.today()
    invoice_start, invoice_end = get_previous_month_date_range(today)
    log_accounting_info(
        "Starting up invoices for %(start)s - %(end)s" % {
            'start': invoice_start.strftime(USER_DATE_FORMAT),
            'end': invoice_end.strftime(USER_DATE_FORMAT),
        })
    all_domain_ids = [d['id'] for d in Domain.get_all(include_docs=False)]
    for domain_doc in iter_docs(Domain.get_db(), all_domain_ids):
        domain = Domain.wrap(domain_doc)
        try:
            invoice_factory = DomainInvoiceFactory(invoice_start, invoice_end,
                                                   domain)
            invoice_factory.create_invoices()
            log_accounting_info("Sent invoices for domain %s" % domain.name)
        except CreditLineError as e:
            log_accounting_error("There was an error utilizing credits for "
                                 "domain %s: %s" % (domain.name, e))
        except InvoiceError as e:
            log_accounting_error("Could not create invoice for domain %s: %s" %
                                 (domain.name, e))
        except InvoiceAlreadyCreatedError as e:
            log_accounting_error("Invoice already existed for domain %s: %s" %
                                 (domain.name, e))
        except Exception as e:
            log_accounting_error("Error occurred while creating invoice for "
                                 "domain %s: %s" % (domain.name, e))

    if not settings.UNIT_TESTING:
        _invoicing_complete_soft_assert(False, "Invoicing is complete!")
Ejemplo n.º 7
0
def send_bookkeeper_email(month=None, year=None, emails=None):
    today = datetime.date.today()

    # now, make sure that we send out LAST month's invoices if we did
    # not specify a month or year.
    today = get_previous_month_date_range(today)[0]

    month = month or today.month
    year = year or today.year

    from corehq.apps.accounting.interface import InvoiceInterface
    request = HttpRequest()
    params = urlencode((
        ('report_filter_statement_period_use_filter', 'on'),
        ('report_filter_statement_period_month', month),
        ('report_filter_statement_period_year', year),
    ))
    request.GET = QueryDict(params)
    request.couch_user = FakeUser(
        domain="hqadmin",
        username="******",
    )
    invoice = InvoiceInterface(request)
    invoice.is_rendered_as_email = True
    first_of_month = datetime.date(year, month, 1)
    email_context = {
        'month': first_of_month.strftime("%B"),
    }
    email_content = render_to_string(
        'accounting/bookkeeper_email.html', email_context)
    email_content_plaintext = render_to_string(
        'accounting/bookkeeper_email_plaintext.html', email_context)

    format_dict = Format.FORMAT_DICT[Format.CSV]
    excel_attachment = {
        'title': 'Invoices_%(period)s.%(extension)s' % {
            'period': first_of_month.strftime('%B_%Y'),
            'extension': format_dict['extension'],
        },
        'mimetype': format_dict['mimetype'],
        'file_obj': invoice.excel_response,
    }

    emails = emails or settings.BOOKKEEPER_CONTACT_EMAILS
    for email in emails:
        send_HTML_email(
            "Invoices for %s" % datetime.date(year, month, 1).strftime(USER_MONTH_FORMAT),
            email,
            email_content,
            email_from=settings.DEFAULT_FROM_EMAIL,
            text_content=email_content_plaintext,
            file_attachments=[excel_attachment],
        )

    log_accounting_info(
        "Sent Bookkeeper Invoice Summary for %(month)s "
        "to %(emails)s." % {
            'month': first_of_month.strftime(USER_MONTH_FORMAT),
            'emails': ", ".join(emails)
        })
Ejemplo n.º 8
0
    def _pay_invoice(self, invoice):
        log_accounting_info("[Autopay] Autopaying invoice {}".format(
            invoice.id))
        amount = invoice.balance.quantize(Decimal(10)**-2)
        if not amount:
            return

        auto_payer = invoice.subscription.account.auto_pay_user
        payment_method = StripePaymentMethod.objects.get(web_user=auto_payer)
        autopay_card = payment_method.get_autopay_card(
            invoice.subscription.account)
        if autopay_card is None:
            return

        try:
            with transaction.atomic():
                payment_record = PaymentRecord.create_record(
                    payment_method, 'temp_transaction_id', amount)
                invoice.pay_invoice(payment_record)
                invoice.subscription.account.last_payment_method = LastPayment.CC_AUTO
                invoice.account.save()
                transaction_id = payment_method.create_charge(
                    autopay_card,
                    amount_in_dollars=amount,
                    description='Auto-payment for Invoice %s' %
                    invoice.invoice_number,
                )
        except stripe.error.CardError as e:
            self._handle_card_declined(invoice, e)
        except payment_method.STRIPE_GENERIC_ERROR as e:
            self._handle_card_errors(invoice, e)
        else:
            payment_record.transaction_id = transaction_id
            payment_record.save()
            self._send_payment_receipt(invoice, payment_record)
Ejemplo n.º 9
0
    def _create_invoice_for_subscription(self, subscription):
        def _get_invoice_start(sub, date_start):
            return max(sub.date_start, date_start)

        def _get_invoice_end(sub, date_end):
            if sub.date_end is not None and sub.date_end <= date_end:
                # Since the Subscription is actually terminated on date_end
                # have the invoice period be until the day before date_end.
                return sub.date_end - datetime.timedelta(days=1)
            else:
                return date_end

        if subscription.is_trial:
            # Don't create invoices for trial subscriptions
            log_accounting_info(
                "Skipping invoicing for Subscription %s because it's a trial."
                % subscription.pk)
            return

        if (subscription.skip_invoicing_if_no_feature_charges and
                not subscription.plan_version.feature_charges_exist_for_domain(
                    self.domain)):
            log_accounting_info(
                "Skipping invoicing for Subscription %s because there are no feature charges."
                % subscription.pk)
            return

        invoice_start = _get_invoice_start(subscription, self.date_start)
        invoice_end = _get_invoice_end(subscription, self.date_end)

        with transaction.atomic():
            invoice = self._generate_invoice(subscription, invoice_start,
                                             invoice_end)
            record = BillingRecord.generate_record(invoice)
        if record.should_send_email:
            try:
                if invoice.subscription.service_type == SubscriptionType.IMPLEMENTATION:
                    if self.recipients:
                        for email in self.recipients:
                            record.send_email(contact_email=email)
                    elif invoice.account.dimagi_contact:
                        record.send_email(
                            contact_email=invoice.account.dimagi_contact,
                            cc_emails=[settings.ACCOUNTS_EMAIL])
                    else:
                        record.send_email(
                            contact_email=settings.ACCOUNTS_EMAIL)
                else:
                    for email in self.recipients or invoice.contact_emails:
                        record.send_email(contact_email=email)
            except InvoiceEmailThrottledError as e:
                if not self.logged_throttle_error:
                    log_accounting_error(e.message)
                    self.logged_throttle_error = True
        else:
            record.skipped_email = True
            record.save()

        return invoice
Ejemplo n.º 10
0
 def get_response(self):
     response = []
     for priv in self.privileges:
         if self.verbose:
             log_accounting_info("Applying %s %s." % (priv, self.action_type))
         message = getattr(self, 'response_%s' % priv)
         if message is not None:
             response.append(message)
     return response
Ejemplo n.º 11
0
 def response_role_based_access(self):
     """
     Perform Role Based Access Upgrade
     - Un-archive custom roles.
     """
     if self.verbose:
         num_archived_roles = len(UserRole.by_domain(self.domain.name,
                                                     is_archived=True))
         if num_archived_roles:
             log_accounting_info("Re-Activating %d archived roles." % num_archived_roles)
     UserRole.unarchive_roles_for_domain(self.domain.name)
     return True
Ejemplo n.º 12
0
    def handle(self, verbose=False, testing=False, *args, **options):
        log_accounting_info(
            'Bootstrapping standard plans. Custom plans will have to be created via the admin UIs.'
        )

        if testing:
            log_accounting_info("Initializing Plans and Roles for Testing")
            config = BOOTSTRAP_CONFIG_TESTING
        else:
            config = BOOTSTRAP_CONFIG

        ensure_plans(config, verbose=verbose, apps=default_apps)
Ejemplo n.º 13
0
    def handle(self, dry_run=False, verbose=False, testing=False, *args, **options):
        log_accounting_info(
            'Bootstrapping standard plans. Custom plans will have to be created via the admin UIs.'
        )

        if testing:
            log_accounting_info("Initializing Plans and Roles for Testing")
            config = BOOTSTRAP_CONFIG_TESTING
        else:
            config = BOOTSTRAP_CONFIG

        ensure_plans(config, dry_run=dry_run, verbose=verbose, apps=default_apps)
Ejemplo n.º 14
0
    def _create_invoice_for_subscription(self, subscription):
        def _get_invoice_start(sub, date_start):
            if sub.date_start > date_start:
                return sub.date_start
            else:
                return date_start

        def _get_invoice_end(sub, date_end):
            if sub.date_end is not None and sub.date_end <= date_end:
                # Since the Subscription is actually terminated on date_end
                # have the invoice period be until the day before date_end.
                return sub.date_end - datetime.timedelta(days=1)
            else:
                return date_end

        if subscription.is_trial:
            # Don't create invoices for trial subscriptions
            log_accounting_info(
                "Skipping invoicing for Subscription %s because it's a trial."
                % subscription.pk
            )
            return

        if (
            subscription.skip_invoicing_if_no_feature_charges
            and not subscription.plan_version.feature_charges_exist_for_domain(self.domain)
        ):
            log_accounting_info(
                "Skipping invoicing for Subscription %s because there are no feature charges."
                % subscription.pk
            )
            return

        invoice_start = _get_invoice_start(subscription, self.date_start)
        invoice_end = _get_invoice_end(subscription, self.date_end)

        with transaction.atomic():
            invoice = self._generate_invoice(subscription, invoice_start, invoice_end)
            record = BillingRecord.generate_record(invoice)
        if record.should_send_email:
            try:
                record.send_email(contact_emails=self.recipients)
            except InvoiceEmailThrottledError as e:
                if not self.logged_throttle_error:
                    log_accounting_error(e.message)
                    self.logged_throttle_error = True
        else:
            record.skipped_email = True
            record.save()

        return invoice
Ejemplo n.º 15
0
def _ensure_product_and_rate(monthly_fee, edition, verbose, apps):
    """
    Ensures that all the necessary SoftwareProducts and SoftwareProductRates are created for the plan.
    """
    SoftwareProduct = apps.get_model('accounting', 'SoftwareProduct')
    SoftwareProductRate = apps.get_model('accounting', 'SoftwareProductRate')

    if verbose:
        log_accounting_info('Ensuring Products and Product Rates')

    product = SoftwareProduct(name='CommCare %s' % edition)

    # TODO - remove after product_type column is dropped and migrations are squashed
    if hasattr(product, 'product_type'):
        product.product_type = SoftwareProductType.COMMCARE

    if edition == SoftwarePlanEdition.ENTERPRISE:
        product.name = "Dimagi Only %s" % product.name

    product_rate = SoftwareProductRate(monthly_fee=monthly_fee)
    try:
        product = SoftwareProduct.objects.get(name=product.name)
        if verbose:
            log_accounting_info(
                "Product '%s' already exists. Using existing product to add rate."
                % product.name)
    except SoftwareProduct.DoesNotExist:
        product.save()
        if verbose:
            log_accounting_info("Creating Product: %s" % product)
    if verbose:
        log_accounting_info("Corresponding product rate of $%d created." %
                            product_rate.monthly_fee)
    product_rate.product = product
    return product, product_rate
Ejemplo n.º 16
0
def _ensure_product_and_rate(product_rate, edition, verbose, apps):
    """
    Ensures that all the necessary SoftwareProducts and SoftwareProductRates are created for the plan.
    """
    SoftwareProduct = apps.get_model('accounting', 'SoftwareProduct')
    SoftwareProductRate = apps.get_model('accounting', 'SoftwareProductRate')

    if verbose:
        log_accounting_info('Ensuring Products and Product Rates')

    product_type = SoftwareProductType.COMMCARE
    product = SoftwareProduct(name='%s %s' % (product_type, edition),
                              product_type=product_type)
    if edition == SoftwarePlanEdition.ENTERPRISE:
        product.name = "Dimagi Only %s" % product.name

    product_rate = SoftwareProductRate(**product_rate)
    try:
        product = SoftwareProduct.objects.get(name=product.name)
        if verbose:
            log_accounting_info(
                "Product '%s' already exists. Using existing product to add rate."
                % product.name)
    except SoftwareProduct.DoesNotExist:
        product.save()
        if verbose:
            log_accounting_info("Creating Product: %s" % product)
    if verbose:
        log_accounting_info("Corresponding product rate of $%d created." %
                            product_rate.monthly_fee)
    product_rate.product = product
    return product, product_rate
Ejemplo n.º 17
0
def _ensure_features(edition, dry_run, verbose, apps):
    """
    Ensures that all the Features necessary for the plans are created.
    """
    Feature = apps.get_model('accounting', 'Feature')

    if verbose:
        log_accounting_info('Ensuring Features for plan: %s' % edition)

    features = []
    for feature_type in FEATURE_TYPES:
        feature = Feature(name='%s %s' % (feature_type, edition),
                          feature_type=feature_type)
        if edition == SoftwarePlanEdition.ENTERPRISE:
            feature.name = "Dimagi Only %s" % feature.name
        if dry_run:
            log_accounting_info("[DRY RUN] Creating Feature: %s" % feature)
        else:
            try:
                feature = Feature.objects.get(name=feature.name)
                if verbose:
                    log_accounting_info(
                        "Feature '%s' already exists. Using existing feature to add rate."
                        % feature.name)
            except Feature.DoesNotExist:
                feature.save()
                if verbose:
                    log_accounting_info("Creating Feature: %s" % feature)
        features.append(feature)
    return features
Ejemplo n.º 18
0
def _ensure_features(edition, dry_run, verbose, apps):
    """
    Ensures that all the Features necessary for the plans are created.
    """
    Feature = apps.get_model('accounting', 'Feature')

    if verbose:
        log_accounting_info('Ensuring Features for plan: %s' % edition)

    features = []
    for feature_type in FEATURE_TYPES:
        feature = Feature(name='%s %s' % (feature_type, edition), feature_type=feature_type)
        if edition == SoftwarePlanEdition.ENTERPRISE:
            feature.name = "Dimagi Only %s" % feature.name
        if dry_run:
            log_accounting_info("[DRY RUN] Creating Feature: %s" % feature)
        else:
            try:
                feature = Feature.objects.get(name=feature.name)
                if verbose:
                    log_accounting_info(
                        "Feature '%s' already exists. Using existing feature to add rate."
                        % feature.name
                    )
            except Feature.DoesNotExist:
                feature.save()
                if verbose:
                    log_accounting_info("Creating Feature: %s" % feature)
        features.append(feature)
    return features
Ejemplo n.º 19
0
 def create_invoices(self):
     subscriptions = self._get_subscriptions()
     self._ensure_full_coverage(subscriptions)
     for subscription in subscriptions:
         try:
             if subscription.account.is_customer_billing_account:
                 log_accounting_info("Skipping invoice for subscription: %s, because it is part of a Customer "
                                     "Billing Account." % (subscription))
             elif should_create_invoice(subscription, self.domain, self.date_start, self.date_end):
                 self._create_invoice_for_subscription(subscription)
         except InvoiceAlreadyCreatedError as e:
             log_accounting_error(
                 "Invoice already existed for domain %s: %s" % (self.domain.name, e),
                 show_stack_trace=True,
             )
Ejemplo n.º 20
0
 def create_invoices(self):
     subscriptions = self._get_subscriptions()
     self._ensure_full_coverage(subscriptions)
     for subscription in subscriptions:
         try:
             if subscription.account.is_customer_billing_account:
                 log_accounting_info("Skipping invoice for subscription: %s, because it is part of a Customer "
                                     "Billing Account." % (subscription))
             elif should_create_invoice(subscription, self.domain, self.date_start, self.date_end):
                 self._create_invoice_for_subscription(subscription)
         except InvoiceAlreadyCreatedError as e:
             log_accounting_error(
                 "Invoice already existed for domain %s: %s" % (self.domain.name, e),
                 show_stack_trace=True,
             )
Ejemplo n.º 21
0
def update_exchange_rates(app_id=settings.OPEN_EXCHANGE_RATES_API_ID):
    try:
        log_accounting_info("Updating exchange rates...")
        rates = json.load(urllib2.urlopen(
            'https://openexchangerates.org/api/latest.json?app_id=%s' % app_id))['rates']
        default_rate = float(rates[Currency.get_default().code])
        for code, rate in rates.items():
            currency, _ = Currency.objects.get_or_create(code=code)
            currency.rate_to_default = float(rate) / default_rate
            currency.save()
            log_accounting_info("Exchange rate for %(code)s updated %(rate)f." % {
                'code': currency.code,
                'rate': currency.rate_to_default,
            })
    except Exception as e:
        log_accounting_error(e.message)
Ejemplo n.º 22
0
def _ensure_feature_rates(feature_rates, features, edition, verbose, apps):
    """
    Ensures that all the FeatureRates necessary for the plans are created.
    """
    FeatureRate = apps.get_model('accounting', 'FeatureRate')

    if verbose:
        log_accounting_info('Ensuring Feature Rates')

    db_feature_rates = []
    for feature in features:
        feature_rate = FeatureRate(**feature_rates[feature.feature_type])
        feature_rate.feature = feature
        if verbose:
            log_accounting_info("Creating rate for feature '%s': %s" % (feature.name, feature_rate))
        db_feature_rates.append(feature_rate)
    return db_feature_rates
Ejemplo n.º 23
0
def _ensure_feature_rates(feature_rates, features, edition, verbose, apps):
    """
    Ensures that all the FeatureRates necessary for the plans are created.
    """
    FeatureRate = apps.get_model('accounting', 'FeatureRate')

    if verbose:
        log_accounting_info('Ensuring Feature Rates')

    db_feature_rates = []
    for feature in features:
        feature_rate = FeatureRate(**feature_rates[feature.feature_type])
        feature_rate.feature = feature
        if verbose:
            log_accounting_info("Creating rate for feature '%s': %s" % (feature.name, feature_rate))
        db_feature_rates.append(feature_rate)
    return db_feature_rates
Ejemplo n.º 24
0
 def get_response(self):
     log_accounting_info('get_response for %s' % str(self))
     responses = []
     for privilege in self.privileges:
         log_accounting_info('handling privilege=%s' % str(privilege))
         try:
             response = self.privilege_to_response_function()[privilege](self.domain, self.new_plan_version)
             log_accounting_info('completed handling privilege=%s' % str(privilege))
         except ResourceConflict:
             log_accounting_info('ResourceConflict handling privilege=%s' % str(privilege))
             # Something else updated the domain. Reload and try again.
             self.domain = Domain.get_by_name(self.domain.name)
             response = self.privilege_to_response_function()[privilege](self.domain, self.new_plan_version)
             log_accounting_info('completed handling privilege=%s after ResourceConflict' % str(privilege))
         if response is not None:
             responses.append(response)
     return responses
Ejemplo n.º 25
0
 def ensure_full_coverage(self, subscriptions):
     plan_version = DefaultProductPlan.get_default_plan_by_domain(
         self.domain, edition=SoftwarePlanEdition.COMMUNITY
     ).plan.get_version()
     if not plan_version.feature_charges_exist_for_domain(self.domain):
         return
     community_ranges = self.get_community_ranges(subscriptions)
     if not community_ranges:
         return
     do_not_invoice = any([s.do_not_invoice for s in subscriptions])
     account = BillingAccount.get_or_create_account_by_domain(
         self.domain.name,
         created_by=self.__class__.__name__,
         created_by_invoicing=True,
         entry_point=EntryPoint.SELF_STARTED,
     )[0]
     if account.date_confirmed_extra_charges is None:
         log_accounting_info(
             "Did not generate invoice because date_confirmed_extra_charges "
             "was null for domain %s" % self.domain.name
         )
         do_not_invoice = True
     if not BillingContactInfo.objects.filter(account=account).exists():
         # No contact information exists for this account.
         # This shouldn't happen, but if it does, we can't continue
         # with the invoice generation.
         raise BillingContactInfoError(
             "Project %s has incurred charges, but does not have their "
             "Billing Contact Info filled out." % self.domain.name
         )
     # First check to make sure none of the existing subscriptions is set
     # to do not invoice. Let's be on the safe side and not send a
     # community invoice out, if that's the case.
     for c in community_ranges:
         # create a new community subscription for each
         # date range that the domain did not have a subscription
         community_subscription = Subscription(
             account=account,
             plan_version=plan_version,
             subscriber=self.subscriber,
             date_start=c[0],
             date_end=c[1],
             do_not_invoice=do_not_invoice,
         )
         community_subscription.save()
         subscriptions.append(community_subscription)
Ejemplo n.º 26
0
def should_create_invoice(subscription, domain, invoice_start, invoice_end):
    if subscription.is_trial:
        log_accounting_info("Skipping invoicing for Subscription %s because it's a trial." % subscription.pk)
        return False
    if subscription.skip_invoicing_if_no_feature_charges and not \
            subscription.plan_version.feature_charges_exist_for_domain(domain):
        log_accounting_info(
            "Skipping invoicing for Subscription %s because there are no feature charges."
            % subscription.pk
        )
        return False
    if subscription.date_start > invoice_end:
        # No invoice gets created if the subscription didn't start in the previous month.
        return False
    if subscription.date_end and subscription.date_end <= invoice_start:
        # No invoice gets created if the subscription ended before the invoicing period.
        return False
    return True
Ejemplo n.º 27
0
def should_create_invoice(subscription, domain, invoice_start, invoice_end):
    if subscription.is_trial:
        log_accounting_info("Skipping invoicing for Subscription %s because it's a trial." % subscription.pk)
        return False
    if subscription.skip_invoicing_if_no_feature_charges and not \
            subscription.plan_version.feature_charges_exist_for_domain(domain):
        log_accounting_info(
            "Skipping invoicing for Subscription %s because there are no feature charges."
            % subscription.pk
        )
        return False
    if subscription.date_start > invoice_end:
        # No invoice gets created if the subscription didn't start in the previous month.
        return False
    if subscription.date_end and subscription.date_end <= invoice_start:
        # No invoice gets created if the subscription ended before the invoicing period.
        return False
    return True
Ejemplo n.º 28
0
def update_exchange_rates(app_id=settings.OPEN_EXCHANGE_RATES_API_ID):
    try:
        log_accounting_info("Updating exchange rates...")
        rates = json.load(urllib2.urlopen(
            'https://openexchangerates.org/api/latest.json?app_id=%s' % app_id))['rates']
        default_rate = float(rates[Currency.get_default().code])
        for code, rate in rates.items():
            currency, _ = Currency.objects.get_or_create(code=code)
            currency.rate_to_default = float(rate) / default_rate
            currency.save()
            log_accounting_info("Exchange rate for %(code)s updated %(rate)f." % {
                'code': currency.code,
                'rate': currency.rate_to_default,
            })
    except Exception as e:
        log_accounting_error(
            "Error updating exchange rates: %s" % e.message,
            show_stack_trace=True,
        )
Ejemplo n.º 29
0
def _ensure_product_rate(monthly_fee, edition, verbose, apps):
    """
    Ensures that all the necessary SoftwareProductRates are created for the plan.
    """
    if verbose:
        log_accounting_info('Ensuring Product Rates')

    SoftwareProductRate = apps.get_model('accounting', 'SoftwareProductRate')

    product_name = 'CommCare %s' % edition
    if edition == SoftwarePlanEdition.ENTERPRISE:
        product_name = "Dimagi Only %s" % product_name

    product_rate = SoftwareProductRate(monthly_fee=monthly_fee)
    try:
        # TODO - remove after squashing migrations
        SoftwareProduct = apps.get_model('accounting', 'SoftwareProduct')
        product = SoftwareProduct(name=product_name, product_type=SoftwareProductType.COMMCARE)
        if hasattr(product, 'product_type'):
            product.product_type = SoftwareProductType.COMMCARE
        try:
            product = SoftwareProduct.objects.get(name=product.name)
            if verbose:
                log_accounting_info(
                    "Product '%s' already exists. Using existing product to add rate."
                    % product.name
                )
        except SoftwareProduct.DoesNotExist:
            if verbose:
                log_accounting_info("Creating Product: %s" % product)
            product.save()
        product_rate.product = product

        if verbose:
            log_accounting_info("Corresponding product rate of $%d created." % product_rate.monthly_fee)

        return product, product_rate

    except LookupError:
        product_rate.name = product_name
        if verbose:
            log_accounting_info("Corresponding product rate of $%d created." % product_rate.monthly_fee)
        return None, product_rate  # TODO - don't return tuple after squashing migrations
Ejemplo n.º 30
0
 def response_outbound_sms(self):
     """
     Reminder rules will be deactivated.
     """
     try:
         for reminder in self._active_reminders:
             reminder.active = False
             reminder.save()
             if self.verbose:
                 log_accounting_info(
                     "Deactivated Reminder %s [%s]"
                     % (reminder.nickname, reminder._id)
                 )
     except Exception:
         log_accounting_error(
             "Failed to downgrade outbound sms for domain %s."
             % self.domain.name
         )
         return False
     return True
Ejemplo n.º 31
0
    def _ensure_full_coverage(self, subscriptions):
        plan_version = DefaultProductPlan.get_default_plan_by_domain(
            self.domain, edition=SoftwarePlanEdition.COMMUNITY
        ).plan.get_version()
        if not plan_version.feature_charges_exist_for_domain(self.domain):
            return

        community_ranges = self._get_community_ranges(subscriptions)
        if not community_ranges:
            return

        # First check to make sure none of the existing subscriptions is set
        # to do not invoice. Let's be on the safe side and not send a
        # community invoice out, if that's the case.
        do_not_invoice = any([s.do_not_invoice for s in subscriptions])

        account = BillingAccount.get_or_create_account_by_domain(
            self.domain.name,
            created_by=self.__class__.__name__,
            entry_point=EntryPoint.SELF_STARTED,
        )[0]
        if account.date_confirmed_extra_charges is None:
            log_accounting_info(
                "Did not generate invoice because date_confirmed_extra_charges "
                "was null for domain %s" % self.domain.name
            )
            do_not_invoice = True

        for start_date, end_date in community_ranges:
            # create a new community subscription for each
            # date range that the domain did not have a subscription
            community_subscription = Subscription(
                account=account,
                plan_version=plan_version,
                subscriber=self.subscriber,
                date_start=start_date,
                date_end=end_date,
                do_not_invoice=do_not_invoice,
            )
            community_subscription.save()
            subscriptions.append(community_subscription)
Ejemplo n.º 32
0
def _ensure_product_rate(monthly_fee, edition, verbose, apps):
    """
    Ensures that all the necessary SoftwareProductRates are created for the plan.
    """
    if verbose:
        log_accounting_info('Ensuring Product Rates')

    SoftwareProductRate = apps.get_model('accounting', 'SoftwareProductRate')

    product_name = 'CommCare %s' % edition
    if edition == SoftwarePlanEdition.ENTERPRISE:
        product_name = "Dimagi Only %s" % product_name

    product_rate = SoftwareProductRate(monthly_fee=monthly_fee)
    try:
        # TODO - remove after squashing migrations
        SoftwareProduct = apps.get_model('accounting', 'SoftwareProduct')
        product = SoftwareProduct(name=product_name, product_type='CommCare')
        try:
            product = SoftwareProduct.objects.get(name=product.name)
            if verbose:
                log_accounting_info(
                    "Product '%s' already exists. Using existing product to add rate."
                    % product.name
                )
        except SoftwareProduct.DoesNotExist:
            if verbose:
                log_accounting_info("Creating Product: %s" % product)
            product.save()
        product_rate.product = product

        if verbose:
            log_accounting_info("Corresponding product rate of $%d created." % product_rate.monthly_fee)

        return product, product_rate

    except LookupError:
        product_rate.name = product_name
        if verbose:
            log_accounting_info("Corresponding product rate of $%d created." % product_rate.monthly_fee)
        return None, product_rate  # TODO - don't return tuple after squashing migrations
Ejemplo n.º 33
0
 def response_inbound_sms(self):
     """
     All Reminder rules utilizing "survey" will be deactivated.
     """
     try:
         surveys = filter(lambda x: x.method in [METHOD_IVR_SURVEY, METHOD_SMS_SURVEY], self._active_reminders)
         for survey in surveys:
             survey.active = False
             survey.save()
             if self.verbose:
                 log_accounting_info(
                     "Deactivated Survey %s [%s]"
                     % (survey.nickname, survey._id)
                 )
     except Exception:
         log_accounting_error(
             "Failed to downgrade outbound sms for domain %s."
             % self.domain.name
         )
         return False
     return True
Ejemplo n.º 34
0
def update_exchange_rates():
    app_id = settings.OPEN_EXCHANGE_RATES_API_ID
    if app_id:
        try:
            log_accounting_info("Updating exchange rates...")
            rates = json.load(six.moves.urllib.request.urlopen(
                'https://openexchangerates.org/api/latest.json?app_id=%s' % app_id))['rates']
            default_rate = float(rates[Currency.get_default().code])
            for code, rate in rates.items():
                currency, _ = Currency.objects.get_or_create(code=code)
                currency.rate_to_default = float(rate) / default_rate
                currency.save()
                log_accounting_info("Exchange rate for %(code)s updated %(rate)f." % {
                    'code': currency.code,
                    'rate': currency.rate_to_default,
                })
        except Exception as e:
            log_accounting_error(
                "Error updating exchange rates: %s" % six.text_type(e),
                show_stack_trace=True,
            )
Ejemplo n.º 35
0
    def _create_invoice_for_subscription(self, subscription):
        def _get_invoice_start(sub, date_start):
            if sub.date_start > date_start:
                return sub.date_start
            else:
                return date_start

        def _get_invoice_end(sub, date_end):
            if (
                sub.date_end is not None
                and sub.date_end <= date_end
            ):
                # Since the Subscription is actually terminated on date_end
                # have the invoice period be until the day before date_end.
                return sub.date_end - datetime.timedelta(days=1)
            else:
                return date_end

        if subscription.is_trial:
            # Don't create invoices for trial subscriptions
            log_accounting_info(
                "Skipping invoicing for Subscription %s because it's a trial."
                % subscription.pk
            )
            return

        invoice_start = _get_invoice_start(subscription, self.date_start)
        invoice_end = _get_invoice_end(subscription, self.date_end)

        with transaction.atomic():
            invoice = self._generate_invoice(subscription, invoice_start, invoice_end)
            record = BillingRecord.generate_record(invoice)
        try:
            record.send_email()
        except InvoiceEmailThrottledError as e:
            if not self.logged_throttle_error:
                log_accounting_error(e.message)
                self.logged_throttle_error = True

        return invoice
Ejemplo n.º 36
0
    def _ensure_full_coverage(self, subscriptions):
        plan_version = DefaultProductPlan.get_default_plan(
            edition=SoftwarePlanEdition.COMMUNITY).plan.get_version()
        if not plan_version.feature_charges_exist_for_domain(self.domain):
            return

        community_ranges = self._get_community_ranges(subscriptions)
        if not community_ranges:
            return

        # First check to make sure none of the existing subscriptions is set
        # to do not invoice. Let's be on the safe side and not send a
        # community invoice out, if that's the case.
        do_not_invoice = any([s.do_not_invoice for s in subscriptions])

        account = BillingAccount.get_or_create_account_by_domain(
            self.domain.name,
            created_by=self.__class__.__name__,
            entry_point=EntryPoint.SELF_STARTED,
        )[0]
        if account.date_confirmed_extra_charges is None:
            log_accounting_info(
                "Did not generate invoice because date_confirmed_extra_charges "
                "was null for domain %s" % self.domain.name)
            do_not_invoice = True

        for start_date, end_date in community_ranges:
            # create a new community subscription for each
            # date range that the domain did not have a subscription
            community_subscription = Subscription(
                account=account,
                plan_version=plan_version,
                subscriber=self.subscriber,
                date_start=start_date,
                date_end=end_date,
                do_not_invoice=do_not_invoice,
            )
            community_subscription.save()
            subscriptions.append(community_subscription)
Ejemplo n.º 37
0
def generate_invoices(based_on_date=None):
    """
    Generates all invoices for the past month.
    """
    today = based_on_date or datetime.date.today()
    invoice_start, invoice_end = get_previous_month_date_range(today)
    log_accounting_info("Starting up invoices for %(start)s - %(end)s" % {
        'start': invoice_start.strftime(USER_DATE_FORMAT),
        'end': invoice_end.strftime(USER_DATE_FORMAT),
    })
    all_domain_ids = [d['id'] for d in Domain.get_all(include_docs=False)]
    for domain_doc in iter_docs(Domain.get_db(), all_domain_ids):
        domain = Domain.wrap(domain_doc)
        try:
            invoice_factory = DomainInvoiceFactory(
                invoice_start, invoice_end, domain)
            invoice_factory.create_invoices()
            log_accounting_info("Sent invoices for domain %s" % domain.name)
        except CreditLineError as e:
            log_accounting_error(
                "There was an error utilizing credits for "
                "domain %s: %s" % (domain.name, e)
            )
        except InvoiceError as e:
            log_accounting_error(
                "Could not create invoice for domain %s: %s" % (domain.name, e)
            )
        except InvoiceAlreadyCreatedError as e:
            log_accounting_error(
                "Invoice already existed for domain %s: %s" % (domain.name, e)
            )
        except Exception as e:
            log_accounting_error(
                "Error occurred while creating invoice for "
                "domain %s: %s" % (domain.name, e)
            )

    if not settings.UNIT_TESTING:
        _invoicing_complete_soft_assert(False, "Invoicing is complete!")
Ejemplo n.º 38
0
def generate_invoices(based_on_date=None, check_existing=False, is_test=False):
    """
    Generates all invoices for the past month.
    """
    today = based_on_date or datetime.date.today()
    invoice_start, invoice_end = utils.get_previous_month_date_range(today)
    log_accounting_info("Starting up invoices for %(start)s - %(end)s" % {
        'start': invoice_start.strftime(USER_DATE_FORMAT),
        'end': invoice_end.strftime(USER_DATE_FORMAT),
    })
    all_domain_ids = [d['id'] for d in Domain.get_all(include_docs=False)]
    for domain_doc in iter_docs(Domain.get_db(), all_domain_ids):
        domain = Domain.wrap(domain_doc)
        if (check_existing and
            Invoice.objects.filter(
                subscription__subscriber__domain=domain,
                date_created__gte=today).count() != 0):
            pass
        elif is_test:
            log_accounting_info("Ready to create invoice for domain %s" % domain.name)
        else:
            try:
                invoice_factory = DomainInvoiceFactory(
                    invoice_start, invoice_end, domain)
                invoice_factory.create_invoices()
                log_accounting_info("Sent invoices for domain %s" % domain.name)
            except CreditLineError as e:
                log_accounting_error(
                    "There was an error utilizing credits for "
                    "domain %s: %s" % (domain.name, e)
                )
            except BillingContactInfoError as e:
                log_accounting_error("BillingContactInfoError: %s" % e)
            except InvoiceError as e:
                log_accounting_error(
                    "Could not create invoice for domain %s: %s" % (
                    domain.name, e
                ))
            except InvoiceAlreadyCreatedError as e:
                log_accounting_error(
                    "Invoice already existed for domain %s: %s" % (
                    domain.name, e
                ))
            except Exception as e:
                log_accounting_error(
                    "Error occurred while creating invoice for "
                    "domain %s: %s" % (domain.name, e)
                )
Ejemplo n.º 39
0
def weekly_digest():
    today = datetime.date.today()
    in_forty_days = today + datetime.timedelta(days=40)

    ending_in_forty_days = filter(
        lambda sub: not sub.is_renewed,
        Subscription.objects.filter(
            date_end__lte=in_forty_days,
            date_end__gte=today,
            is_active=True,
            is_trial=False,
            account__dimagi_contact__isnull=True,
        ))

    if not ending_in_forty_days:
        log_accounting_info(
            "Did not send summary of ending subscriptions because "
            "there are none.")
        return

    table = [[
        "Project Space",
        "Account",
        "Plan",
        "Salesforce Contract ID",
        "Dimagi Contact",
        "Start Date",
        "End Date",
        "Receives Invoice",
        "Created By",
    ]]

    def _fmt_row(sub):
        try:
            created_by_adj = SubscriptionAdjustment.objects.filter(
                subscription=sub,
                reason=SubscriptionAdjustmentReason.CREATE).order_by(
                    'date_created')[0]
            created_by = dict(SubscriptionAdjustmentMethod.CHOICES).get(
                created_by_adj.method, "Unknown")
        except (IndexError, SubscriptionAdjustment.DoesNotExist):
            created_by = "Unknown"
        return [
            sub.subscriber.domain,
            "%s (%s)" % (sub.account.name, sub.account.id),
            sub.plan_version.plan.name,
            sub.salesforce_contract_id,
            sub.account.dimagi_contact,
            sub.date_start,
            sub.date_end,
            "No" if sub.do_not_invoice else "YES",
            created_by,
        ]

    table.extend([_fmt_row(sub) for sub in ending_in_forty_days])

    file_to_attach = StringIO()
    export_from_tables([['End in 40 Days', table]], file_to_attach,
                       Format.XLS_2007)

    email_context = {
        'today': today.isoformat(),
        'forty_days': in_forty_days.isoformat(),
    }
    email_content = render_to_string('accounting/digest_email.html',
                                     email_context)
    email_content_plaintext = render_to_string('accounting/digest_email.txt',
                                               email_context)

    format_dict = Format.FORMAT_DICT[Format.XLS_2007]
    file_attachment = {
        'title': 'Subscriptions_%(start)s_%(end)s.xls' % {
            'start': today.isoformat(),
            'end': in_forty_days.isoformat(),
        },
        'mimetype': format_dict['mimetype'],
        'file_obj': file_to_attach,
    }
    from_email = "Dimagi Accounting <%s>" % settings.DEFAULT_FROM_EMAIL
    env = ("[{}] ".format(settings.SERVER_ENVIRONMENT.upper())
           if settings.SERVER_ENVIRONMENT != "production" else "")
    email_subject = "{}Subscriptions ending in 40 Days from {}".format(
        env, today.isoformat())
    send_HTML_email(
        email_subject,
        settings.ACCOUNTS_EMAIL,
        email_content,
        email_from=from_email,
        text_content=email_content_plaintext,
        file_attachments=[file_attachment],
    )

    log_accounting_info("Sent summary of ending subscriptions from %(today)s" %
                        {
                            'today': today.isoformat(),
                        })
Ejemplo n.º 40
0
def ensure_plans(config, dry_run, verbose, apps):
    DefaultProductPlan = apps.get_model('accounting', 'DefaultProductPlan')
    SoftwarePlan = apps.get_model('accounting', 'SoftwarePlan')
    SoftwarePlanVersion = apps.get_model('accounting', 'SoftwarePlanVersion')
    Role = apps.get_model('django_prbac', 'Role')

    for product_type in PRODUCT_TYPES:
        for plan_key, plan_deets in config.iteritems():
            edition, is_trial = plan_key
            features = _ensure_features(edition, dry_run, verbose, apps)
            try:
                role = _ensure_role(plan_deets['role'], apps)
            except Role.DoesNotExist:
                return

            product, product_rate = _ensure_product_and_rate(
                plan_deets['product_rate'],
                product_type,
                edition,
                dry_run=dry_run,
                verbose=verbose,
                apps=apps,
            )
            feature_rates = _ensure_feature_rates(
                plan_deets['feature_rates'],
                features,
                edition,
                dry_run=dry_run,
                verbose=verbose,
                apps=apps,
            )

            software_plan = SoftwarePlan(
                name='%s Edition' % product.name,
                edition=edition,
                visibility=SoftwarePlanVisibility.PUBLIC)

            if dry_run:
                log_accounting_info("[DRY RUN] Creating Software Plan: %s" %
                                    software_plan.name)
            else:
                try:
                    software_plan = SoftwarePlan.objects.get(
                        name=software_plan.name)
                    if verbose:
                        log_accounting_info(
                            "Plan '%s' already exists. Using existing plan to add version."
                            % software_plan.name)
                except SoftwarePlan.DoesNotExist:
                    software_plan.save()
                    if verbose:
                        log_accounting_info("Creating Software Plan: %s" %
                                            software_plan.name)

                software_plan_version = SoftwarePlanVersion(role=role,
                                                            plan=software_plan)

                # must save before assigning many-to-many relationship
                if hasattr(SoftwarePlanVersion, 'product_rates'):
                    software_plan_version.save()

                product_rate.save()
                if hasattr(SoftwarePlanVersion, 'product_rates'):
                    software_plan_version.product_rates.add(product_rate)
                elif hasattr(SoftwarePlanVersion, 'product_rate'):
                    software_plan_version.product_rate = product_rate
                else:
                    raise AccountingError(
                        'SoftwarePlanVersion does not have product_rate or product_rates field'
                    )

                # must save before assigning many-to-many relationship
                if hasattr(SoftwarePlanVersion, 'product_rate'):
                    software_plan_version.save()

                for feature_rate in feature_rates:
                    feature_rate.save()
                    software_plan_version.feature_rates.add(feature_rate)
                software_plan_version.save()

            default_product_plan = DefaultProductPlan(
                product_type=product.product_type,
                edition=edition,
                is_trial=is_trial)
            if dry_run:
                log_accounting_info(
                    "[DRY RUN] Setting plan as default for product '%s' and edition '%s'."
                    % (product.product_type, default_product_plan.edition))
            else:
                try:
                    default_product_plan = DefaultProductPlan.objects.get(
                        product_type=product.product_type,
                        edition=edition,
                        is_trial=is_trial)
                    if verbose:
                        log_accounting_info(
                            "Default for product '%s' and edition '%s' already exists."
                            % (product.product_type,
                               default_product_plan.edition))
                except DefaultProductPlan.DoesNotExist:
                    default_product_plan.plan = software_plan
                    default_product_plan.save()
                    if verbose:
                        log_accounting_info(
                            "Setting plan as default for product '%s' and edition '%s'."
                            % (product.product_type,
                               default_product_plan.edition))
Ejemplo n.º 41
0
def generate_invoices(based_on_date=None):
    """
    Generates all invoices for the past month.
    """
    today = based_on_date or datetime.date.today()
    invoice_start, invoice_end = get_previous_month_date_range(today)
    log_accounting_info(
        "Starting up invoices for %(start)s - %(end)s" % {
            'start': invoice_start.strftime(USER_DATE_FORMAT),
            'end': invoice_end.strftime(USER_DATE_FORMAT),
        })
    all_domain_ids = [d['id'] for d in Domain.get_all(include_docs=False)]
    for domain_doc in iter_docs(Domain.get_db(), all_domain_ids):
        domain_obj = Domain.wrap(domain_doc)
        if not domain_obj.is_active:
            continue
        try:
            invoice_factory = DomainInvoiceFactory(invoice_start, invoice_end,
                                                   domain_obj)
            invoice_factory.create_invoices()
            log_accounting_info("Sent invoices for domain %s" %
                                domain_obj.name)
        except CreditLineError as e:
            log_accounting_error(
                "There was an error utilizing credits for "
                "domain %s: %s" % (domain_obj.name, e),
                show_stack_trace=True,
            )
        except InvoiceError as e:
            log_accounting_error(
                "Could not create invoice for domain %s: %s" %
                (domain_obj.name, e),
                show_stack_trace=True,
            )
        except Exception as e:
            log_accounting_error(
                "Error occurred while creating invoice for "
                "domain %s: %s" % (domain_obj.name, e),
                show_stack_trace=True,
            )
    all_customer_billing_accounts = BillingAccount.objects.filter(
        is_customer_billing_account=True)
    for account in all_customer_billing_accounts:
        try:
            if account.invoicing_plan == InvoicingPlan.QUARTERLY:
                customer_invoice_start = invoice_start - relativedelta(
                    months=2)
            elif account.invoicing_plan == InvoicingPlan.YEARLY:
                customer_invoice_start = invoice_start - relativedelta(
                    months=11)
            else:
                customer_invoice_start = invoice_start
            invoice_factory = CustomerAccountInvoiceFactory(
                account=account,
                date_start=customer_invoice_start,
                date_end=invoice_end)
            invoice_factory.create_invoice()
        except CreditLineError as e:
            log_accounting_error(
                "There was an error utilizing credits for "
                "domain %s: %s" % (domain_obj.name, e),
                show_stack_trace=True,
            )
        except InvoiceError as e:
            log_accounting_error(
                "Could not create invoice for domain %s: %s" %
                (domain_obj.name, e),
                show_stack_trace=True,
            )
        except Exception as e:
            log_accounting_error(
                "Error occurred while creating invoice for "
                "domain %s: %s" % (domain_obj.name, e),
                show_stack_trace=True,
            )

    if not settings.UNIT_TESTING:
        _invoicing_complete_soft_assert(False, "Invoicing is complete!")
Ejemplo n.º 42
0
def ensure_plans(config, dry_run, verbose, apps):
    DefaultProductPlan = apps.get_model('accounting', 'DefaultProductPlan')
    SoftwarePlan = apps.get_model('accounting', 'SoftwarePlan')
    SoftwarePlanVersion = apps.get_model('accounting', 'SoftwarePlanVersion')
    Role = apps.get_model('django_prbac', 'Role')

    for product_type in PRODUCT_TYPES:
        for plan_key, plan_deets in config.iteritems():
            edition, is_trial = plan_key
            features = _ensure_features(edition, dry_run, verbose, apps)
            try:
                role = _ensure_role(plan_deets['role'], apps)
            except Role.DoesNotExist:
                return

            product, product_rate = _ensure_product_and_rate(
                plan_deets['product_rate'], product_type, edition,
                dry_run=dry_run, verbose=verbose, apps=apps,
            )
            feature_rates = _ensure_feature_rates(
                plan_deets['feature_rates'], features, edition,
                dry_run=dry_run, verbose=verbose, apps=apps,
            )

            software_plan = SoftwarePlan(
                name='%s Edition' % product.name, edition=edition, visibility=SoftwarePlanVisibility.PUBLIC
            )

            if dry_run:
                log_accounting_info("[DRY RUN] Creating Software Plan: %s" % software_plan.name)
            else:
                try:
                    software_plan = SoftwarePlan.objects.get(name=software_plan.name)
                    if verbose:
                        log_accounting_info(
                            "Plan '%s' already exists. Using existing plan to add version." % software_plan.name
                        )
                except SoftwarePlan.DoesNotExist:
                    software_plan.save()
                    if verbose:
                        log_accounting_info("Creating Software Plan: %s" % software_plan.name)

                software_plan_version = SoftwarePlanVersion(role=role, plan=software_plan)

                # must save before assigning many-to-many relationship
                if hasattr(SoftwarePlanVersion, 'product_rates'):
                    software_plan_version.save()

                product_rate.save()
                if hasattr(SoftwarePlanVersion, 'product_rates'):
                    software_plan_version.product_rates.add(product_rate)
                elif hasattr(SoftwarePlanVersion, 'product_rate'):
                    software_plan_version.product_rate = product_rate
                else:
                    raise AccountingError('SoftwarePlanVersion does not have product_rate or product_rates field')

                # must save before assigning many-to-many relationship
                if hasattr(SoftwarePlanVersion, 'product_rate'):
                    software_plan_version.save()

                for feature_rate in feature_rates:
                    feature_rate.save()
                    software_plan_version.feature_rates.add(feature_rate)
                software_plan_version.save()

            default_product_plan = DefaultProductPlan(
                product_type=product.product_type, edition=edition, is_trial=is_trial
            )
            if dry_run:
                log_accounting_info(
                    "[DRY RUN] Setting plan as default for product '%s' and edition '%s'."
                    % (product.product_type, default_product_plan.edition)
                )
            else:
                try:
                    default_product_plan = DefaultProductPlan.objects.get(product_type=product.product_type,
                                                                          edition=edition, is_trial=is_trial)
                    if verbose:
                        log_accounting_info(
                            "Default for product '%s' and edition '%s' already exists."
                            % (product.product_type, default_product_plan.edition)
                        )
                except DefaultProductPlan.DoesNotExist:
                    default_product_plan.plan = software_plan
                    default_product_plan.save()
                    if verbose:
                        log_accounting_info(
                            "Setting plan as default for product '%s' and edition '%s'."
                            % (product.product_type, default_product_plan.edition)
                        )
Ejemplo n.º 43
0
def weekly_digest():
    today = datetime.date.today()
    in_forty_days = today + datetime.timedelta(days=40)

    ending_in_forty_days = filter(
        lambda sub: not sub.is_renewed,
        Subscription.objects.filter(
            date_end__lte=in_forty_days,
            date_end__gte=today,
            is_active=True,
            is_trial=False,
            account__dimagi_contact__isnull=True,
        ))

    if not ending_in_forty_days:
        log_accounting_info(
            "Did not send summary of ending subscriptions because "
            "there are none."
        )
        return

    table = [[
        "Project Space", "Account", "Plan", "Salesforce Contract ID",
        "Dimagi Contact", "Start Date", "End Date", "Receives Invoice",
        "Created By",
    ]]

    def _fmt_row(sub):
        try:
            created_by_adj = SubscriptionAdjustment.objects.filter(
                subscription=sub,
                reason=SubscriptionAdjustmentReason.CREATE
            ).order_by('date_created')[0]
            created_by = dict(SubscriptionAdjustmentMethod.CHOICES).get(
                created_by_adj.method, "Unknown")
        except (IndexError, SubscriptionAdjustment.DoesNotExist):
            created_by = "Unknown"
        return [
            sub.subscriber.domain,
            "%s (%s)" % (sub.account.name, sub.account.id),
            sub.plan_version.plan.name,
            sub.salesforce_contract_id,
            sub.account.dimagi_contact,
            sub.date_start,
            sub.date_end,
            "No" if sub.do_not_invoice else "YES",
            created_by,
        ]

    table.extend([_fmt_row(sub) for sub in ending_in_forty_days])

    file_to_attach = StringIO()
    export_from_tables(
        [['End in 40 Days', table]],
        file_to_attach,
        Format.XLS_2007
    )

    email_context = {
        'today': today.isoformat(),
        'forty_days': in_forty_days.isoformat(),
    }
    email_content = render_to_string(
        'accounting/digest_email.html', email_context)
    email_content_plaintext = render_to_string(
        'accounting/digest_email.txt', email_context)

    format_dict = Format.FORMAT_DICT[Format.XLS_2007]
    file_attachment = {
        'title': 'Subscriptions_%(start)s_%(end)s.xls' % {
            'start': today.isoformat(),
            'end': in_forty_days.isoformat(),
        },
        'mimetype': format_dict['mimetype'],
        'file_obj': file_to_attach,
    }
    from_email = "Dimagi Accounting <%s>" % settings.DEFAULT_FROM_EMAIL
    env = ("[{}] ".format(settings.SERVER_ENVIRONMENT.upper())
           if settings.SERVER_ENVIRONMENT != "production" else "")
    email_subject = "{}Subscriptions ending in 40 Days from {}".format(env, today.isoformat())
    send_HTML_email(
        email_subject,
        settings.ACCOUNTS_EMAIL,
        email_content,
        email_from=from_email,
        text_content=email_content_plaintext,
        file_attachments=[file_attachment],
    )

    log_accounting_info(
        "Sent summary of ending subscriptions from %(today)s" % {
            'today': today.isoformat(),
        })
Ejemplo n.º 44
0
def _ensure_product_and_rate(product_rate, product_type, edition, dry_run, verbose, apps):
    """
    Ensures that all the necessary SoftwareProducts and SoftwareProductRates are created for the plan.
    """
    SoftwareProduct = apps.get_model('accounting', 'SoftwareProduct')
    SoftwareProductRate = apps.get_model('accounting', 'SoftwareProductRate')

    if verbose:
        log_accounting_info('Ensuring Products and Product Rates')

    product = SoftwareProduct(name='%s %s' % (product_type, edition), product_type=product_type)
    if edition == SoftwarePlanEdition.ENTERPRISE:
        product.name = "Dimagi Only %s" % product.name

    product_rate = SoftwareProductRate(**product_rate)
    if dry_run:
        log_accounting_info("[DRY RUN] Creating Product: %s" % product)
        log_accounting_info("[DRY RUN] Corresponding product rate of $%d created." % product_rate.monthly_fee)
    else:
        try:
            product = SoftwareProduct.objects.get(name=product.name)
            if verbose:
                log_accounting_info(
                    "Product '%s' already exists. Using existing product to add rate."
                    % product.name
                )
        except SoftwareProduct.DoesNotExist:
            product.save()
            if verbose:
                log_accounting_info("Creating Product: %s" % product)
        if verbose:
            log_accounting_info("Corresponding product rate of $%d created." % product_rate.monthly_fee)
    product_rate.product = product
    return product, product_rate
Ejemplo n.º 45
0
def generate_invoices(based_on_date=None):
    """
    Generates all invoices for the past month.
    """
    today = based_on_date or datetime.date.today()
    invoice_start, invoice_end = get_previous_month_date_range(today)
    log_accounting_info("Starting up invoices for %(start)s - %(end)s" % {
        'start': invoice_start.strftime(USER_DATE_FORMAT),
        'end': invoice_end.strftime(USER_DATE_FORMAT),
    })
    all_domain_ids = [d['id'] for d in Domain.get_all(include_docs=False)]
    for domain_doc in iter_docs(Domain.get_db(), all_domain_ids):
        domain_obj = Domain.wrap(domain_doc)
        if not domain_obj.is_active:
            continue
        try:
            invoice_factory = DomainInvoiceFactory(invoice_start, invoice_end, domain_obj)
            invoice_factory.create_invoices()
            log_accounting_info("Sent invoices for domain %s" % domain_obj.name)
        except CreditLineError as e:
            log_accounting_error(
                "There was an error utilizing credits for "
                "domain %s: %s" % (domain_obj.name, e),
                show_stack_trace=True,
            )
        except InvoiceError as e:
            log_accounting_error(
                "Could not create invoice for domain %s: %s" % (domain_obj.name, e),
                show_stack_trace=True,
            )
        except Exception as e:
            log_accounting_error(
                "Error occurred while creating invoice for "
                "domain %s: %s" % (domain_obj.name, e),
                show_stack_trace=True,
            )
    all_customer_billing_accounts = BillingAccount.objects.filter(is_customer_billing_account=True)
    for account in all_customer_billing_accounts:
        try:
            if account.invoicing_plan == InvoicingPlan.QUARTERLY:
                customer_invoice_start = invoice_start - relativedelta(months=2)
            elif account.invoicing_plan == InvoicingPlan.YEARLY:
                customer_invoice_start = invoice_start - relativedelta(months=11)
            else:
                customer_invoice_start = invoice_start
            invoice_factory = CustomerAccountInvoiceFactory(
                account=account,
                date_start=customer_invoice_start,
                date_end=invoice_end
            )
            invoice_factory.create_invoice()
        except CreditLineError as e:
            log_accounting_error(
                "There was an error utilizing credits for "
                "domain %s: %s" % (domain_obj.name, e),
                show_stack_trace=True,
            )
        except InvoiceError as e:
            log_accounting_error(
                "Could not create invoice for domain %s: %s" % (domain_obj.name, e),
                show_stack_trace=True,
            )
        except Exception as e:
            log_accounting_error(
                "Error occurred while creating invoice for "
                "domain %s: %s" % (domain_obj.name, e),
                show_stack_trace=True,
            )

    if not settings.UNIT_TESTING:
        _invoicing_complete_soft_assert(False, "Invoicing is complete!")
Ejemplo n.º 46
0
def ensure_plans(config, verbose, apps):
    DefaultProductPlan = apps.get_model('accounting', 'DefaultProductPlan')
    SoftwarePlan = apps.get_model('accounting', 'SoftwarePlan')
    SoftwarePlanVersion = apps.get_model('accounting', 'SoftwarePlanVersion')
    Role = apps.get_model('django_prbac', 'Role')

    for plan_key, plan_deets in six.iteritems(config):
        edition, is_trial, is_report_builder_enabled = plan_key
        features = _ensure_features(edition, verbose, apps)
        try:
            role = _ensure_role(plan_deets['role'], apps)
        except Role.DoesNotExist:
            return

        product, product_rate = _ensure_product_rate(
            plan_deets['product_rate_monthly_fee'], edition,
            verbose=verbose, apps=apps,
        )
        feature_rates = _ensure_feature_rates(
            plan_deets['feature_rates'], features, edition,
            verbose=verbose, apps=apps,
        )

        software_plan = SoftwarePlan(
            name=(
                (('%s Trial' % product_rate.name) if is_trial else ('%s Edition' % product_rate.name))
                if product is None else product.name  # TODO - remove after squashing migrations
            ),
            edition=edition,
            visibility=SoftwarePlanVisibility.PUBLIC
        )
        if is_report_builder_enabled:
            software_plan.name = '%s - Report Builder (5 Reports)' % software_plan.name

        try:
            software_plan = SoftwarePlan.objects.get(name=software_plan.name)
            if verbose:
                log_accounting_info(
                    "Plan '%s' already exists. Using existing plan to add version." % software_plan.name
                )
        except SoftwarePlan.DoesNotExist:
            software_plan.save()
            if verbose:
                log_accounting_info("Creating Software Plan: %s" % software_plan.name)

        product_rate.save()
        software_plan_version = SoftwarePlanVersion(role=role, plan=software_plan, product_rate=product_rate)
        software_plan_version.save()

        for feature_rate in feature_rates:
            feature_rate.save()
            software_plan_version.feature_rates.add(feature_rate)
        software_plan_version.save()

        try:
            default_product_plan = DefaultProductPlan.objects.get(
                edition=edition,
                is_trial=is_trial,
                is_report_builder_enabled=is_report_builder_enabled,
            )
            if verbose:
                log_accounting_info(
                    "Default for edition '%s' with is_trial='%s' already exists."
                    % (default_product_plan.edition, is_trial)
                )
        except DefaultProductPlan.DoesNotExist:
            default_product_plan = DefaultProductPlan(
                edition=edition,
                is_trial=is_trial,
                is_report_builder_enabled=is_report_builder_enabled,
            )
        finally:
            default_product_plan.plan = software_plan
            default_product_plan.save()
            if verbose:
                log_accounting_info(
                    "Setting plan as default for edition '%s' with is_trial='%s'."
                    % (default_product_plan.edition, is_trial)
                )

    _clear_cache(SoftwarePlan.objects.all())
Ejemplo n.º 47
0
def ensure_plans(config, verbose, apps):
    DefaultProductPlan = apps.get_model('accounting', 'DefaultProductPlan')
    SoftwarePlan = apps.get_model('accounting', 'SoftwarePlan')
    SoftwarePlanVersion = apps.get_model('accounting', 'SoftwarePlanVersion')
    Role = apps.get_model('django_prbac', 'Role')

    for plan_key, plan_deets in config.items():
        edition, is_trial, is_report_builder_enabled = plan_key
        features = _ensure_features(edition, verbose, apps)
        try:
            role = _ensure_role(plan_deets['role'], apps)
        except Role.DoesNotExist:
            return

        product, product_rate = _ensure_product_rate(
            plan_deets['product_rate_monthly_fee'],
            edition,
            verbose=verbose,
            apps=apps,
        )
        feature_rates = _ensure_feature_rates(
            plan_deets['feature_rates'],
            features,
            edition,
            verbose=verbose,
            apps=apps,
        )

        software_plan = SoftwarePlan(
            name=((('%s Trial' % product_rate.name) if is_trial else
                   ('%s Edition' % product_rate.name)) if product is None else
                  product.name  # TODO - remove after squashing migrations
                  ),
            edition=edition,
            visibility=SoftwarePlanVisibility.PUBLIC)
        if is_report_builder_enabled:
            software_plan.name = '%s - Report Builder (5 Reports)' % software_plan.name

        try:
            software_plan = SoftwarePlan.objects.get(name=software_plan.name)
            if verbose:
                log_accounting_info(
                    "Plan '%s' already exists. Using existing plan to add version."
                    % software_plan.name)
        except SoftwarePlan.DoesNotExist:
            software_plan.save()
            if verbose:
                log_accounting_info("Creating Software Plan: %s" %
                                    software_plan.name)

        product_rate.save()
        software_plan_version = SoftwarePlanVersion(role=role,
                                                    plan=software_plan,
                                                    product_rate=product_rate)
        software_plan_version.save()

        for feature_rate in feature_rates:
            feature_rate.save()
            software_plan_version.feature_rates.add(feature_rate)
        software_plan_version.save()

        try:
            default_product_plan = DefaultProductPlan.objects.get(
                edition=edition,
                is_trial=is_trial,
                is_report_builder_enabled=is_report_builder_enabled,
            )
            if verbose:
                log_accounting_info(
                    "Default for edition '%s' with is_trial='%s' already exists."
                    % (default_product_plan.edition, is_trial))
        except DefaultProductPlan.DoesNotExist:
            default_product_plan = DefaultProductPlan(
                edition=edition,
                is_trial=is_trial,
                is_report_builder_enabled=is_report_builder_enabled,
            )
        finally:
            default_product_plan.plan = software_plan
            default_product_plan.save()
            if verbose:
                log_accounting_info(
                    "Setting plan as default for edition '%s' with is_trial='%s'."
                    % (default_product_plan.edition, is_trial))

    _clear_cache(SoftwarePlan.objects.all())
Ejemplo n.º 48
0
def ensure_plans(config, dry_run, verbose, apps):
    DefaultProductPlan = apps.get_model('accounting', 'DefaultProductPlan')
    SoftwarePlan = apps.get_model('accounting', 'SoftwarePlan')
    SoftwarePlanVersion = apps.get_model('accounting', 'SoftwarePlanVersion')
    Role = apps.get_model('django_prbac', 'Role')

    for plan_key, plan_deets in config.iteritems():
        edition, is_trial, is_report_builder_enabled = plan_key
        features = _ensure_features(edition, dry_run, verbose, apps)
        try:
            role = _ensure_role(plan_deets['role'], apps)
        except Role.DoesNotExist:
            return

        product, product_rate = _ensure_product_and_rate(
            plan_deets['product_rate'], edition,
            dry_run=dry_run, verbose=verbose, apps=apps,
        )
        feature_rates = _ensure_feature_rates(
            plan_deets['feature_rates'], features, edition,
            dry_run=dry_run, verbose=verbose, apps=apps,
        )

        software_plan = SoftwarePlan(
            name='%s Edition' % product.name,
            edition=edition,
            visibility=SoftwarePlanVisibility.PUBLIC
        )
        if is_report_builder_enabled:
            software_plan.name = '%s - Report Builder (5 Reports)' % software_plan.name

        if dry_run:
            log_accounting_info("[DRY RUN] Creating Software Plan: %s" % software_plan.name)
        else:
            try:
                software_plan = SoftwarePlan.objects.get(name=software_plan.name)
                if verbose:
                    log_accounting_info(
                        "Plan '%s' already exists. Using existing plan to add version." % software_plan.name
                    )
            except SoftwarePlan.DoesNotExist:
                software_plan.save()
                if verbose:
                    log_accounting_info("Creating Software Plan: %s" % software_plan.name)

            software_plan_version = SoftwarePlanVersion(role=role, plan=software_plan)

            # TODO - squash migrations and remove this
            # must save before assigning many-to-many relationship
            if hasattr(SoftwarePlanVersion, 'product_rates'):
                software_plan_version.save()

            product_rate.save()
            # TODO - squash migrations and remove this
            if hasattr(SoftwarePlanVersion, 'product_rates'):
                software_plan_version.product_rates.add(product_rate)
            elif hasattr(SoftwarePlanVersion, 'product_rate'):
                software_plan_version.product_rate = product_rate
            else:
                raise AccountingError('SoftwarePlanVersion does not have product_rate or product_rates field')

            # TODO - squash migrations and remove this
            # must save before assigning many-to-many relationship
            if hasattr(SoftwarePlanVersion, 'product_rate'):
                software_plan_version.save()

            for feature_rate in feature_rates:
                feature_rate.save()
                software_plan_version.feature_rates.add(feature_rate)
            software_plan_version.save()

        default_product_plan = DefaultProductPlan(
            edition=edition, is_trial=is_trial
        )
        if hasattr(default_product_plan, 'is_report_builder_enabled'):
            default_product_plan.is_report_builder_enabled = is_report_builder_enabled

        # TODO - squash migrations and remove this
        if hasattr(default_product_plan, 'product_type'):
            default_product_plan.product_type = SoftwareProductType.COMMCARE
        if dry_run:
            log_accounting_info(
                "[DRY RUN] Setting plan as default for edition '%s' with is_trial='%s'."
                % (default_product_plan.edition, is_trial)
            )
        else:
            try:
                if not hasattr(default_product_plan, 'product_type'):
                    if hasattr(default_product_plan, 'is_report_builder_enabled'):
                        default_product_plan = DefaultProductPlan.objects.get(
                            edition=edition,
                            is_trial=is_trial,
                            is_report_builder_enabled=is_report_builder_enabled,
                        )
                    else:
                        # TODO - squash migrations and remove this
                        default_product_plan = DefaultProductPlan.objects.get(
                            edition=edition,
                            is_trial=is_trial,
                        )
                else:
                    # TODO - squash migrations and remove this
                    default_product_plan = DefaultProductPlan.objects.get(
                        edition=edition, is_trial=is_trial, product_type=SoftwareProductType.COMMCARE
                    )
                if verbose:
                    log_accounting_info(
                        "Default for edition '%s' with is_trial='%s' already exists."
                        % (default_product_plan.edition, is_trial)
                    )
            except DefaultProductPlan.DoesNotExist:
                default_product_plan.plan = software_plan
                default_product_plan.save()
                if verbose:
                    log_accounting_info(
                        "Setting plan as default for edition '%s' with is_trial='%s'."
                        % (default_product_plan.edition, is_trial)
                    )
Ejemplo n.º 49
0
    def create_invoice_for_subscription(self, subscription):
        if subscription.is_trial:
            # Don't create invoices for trial subscriptions
            log_accounting_info(
                "Skipping invoicing for Subscription %s because it's a trial."
                % subscription.pk
            )
            return

        if subscription.date_start > self.date_start:
            invoice_start = subscription.date_start
        else:
            invoice_start = self.date_start

        if (subscription.date_end is not None
           and subscription.date_end <= self.date_end):
            # Since the Subscription is actually terminated on date_end
            # have the invoice period be until the day before date_end.
            invoice_end = subscription.date_end - datetime.timedelta(days=1)
        else:
            invoice_end = self.date_end

        with transaction.atomic():
            invoice, is_new_invoice = Invoice.objects.get_or_create(
                subscription=subscription,
                date_start=invoice_start,
                date_end=invoice_end,
                is_hidden=subscription.do_not_invoice,
            )

            if not is_new_invoice:
                raise InvoiceAlreadyCreatedError("invoice id: {id}".format(id=invoice.id))

            if subscription.subscriptionadjustment_set.count() == 0:
                # record that the subscription was created
                SubscriptionAdjustment.record_adjustment(
                    subscription,
                    method=SubscriptionAdjustmentMethod.TASK,
                    invoice=invoice,
                )

            self.generate_line_items(invoice, subscription)
            invoice.calculate_credit_adjustments()
            invoice.update_balance()
            invoice.save()
            total_balance = sum(invoice.balance for invoice in Invoice.objects.filter(
                is_hidden=False,
                subscription__subscriber__domain=invoice.get_domain(),
            ))

            should_set_date_due = ((total_balance > SMALL_INVOICE_THRESHOLD) or
                                   (invoice.account.auto_pay_enabled and total_balance > Decimal(0)))
            if should_set_date_due:
                days_until_due = DEFAULT_DAYS_UNTIL_DUE
                if subscription.date_delay_invoicing is not None:
                    td = subscription.date_delay_invoicing - self.date_end
                    days_until_due = max(days_until_due, td.days)
                invoice.date_due = self.date_end + datetime.timedelta(days_until_due)
            invoice.save()

            record = BillingRecord.generate_record(invoice)
        try:
            record.send_email()
        except InvoiceEmailThrottledError as e:
            if not self.logged_throttle_error:
                log_accounting_error(e.message)
                self.logged_throttle_error = True

        return invoice