def settle_services_usage_from_client_credit( client: Client, services: List[Service], tax_rules: List[TaxRule] = None): LOG.info('Settling {} services for {}'.format(len(services), client)) total_due = Decimal(0) current_date = utcnow() with db_transaction.atomic( ): # create invoice and set the service next invoice due atomically for service in services: while service.next_due_date < current_date: service_fixed_price = service.get_fixed_price( currency=client.currency) service_fixed_price = utils.cdecimal( service_fixed_price, q='0.01') # Convert to 2 decimals service_dynamic_price = InvoiceUtils.get_dynamic_price_for_service( service, service.next_due_date) service_price = service_fixed_price + service_dynamic_price total_due += service_price # calculate taxes for invoice item if service.product.taxable and not client.tax_exempt: if tax_rules: for tax_rule in tax_rules: tax_amount = (service_price * tax_rule.rate) / 100 tax_amount = utils.cdecimal(tax_amount, q='.01') total_due += tax_amount prev_due_date = service.next_due_date InvoiceUtils.settle_dynamic_price_for_service( service, service.next_due_date) service.update_next_due_date() if prev_due_date == service.next_due_date: LOG.error('Next due date is not increasing, aborting') break if total_due > 0: # settle only if total due is above zero client.withdraw_credit(total_due, client.currency) client_credit_account = client.credits.get( client=client, currency=client.currency) Journal.objects.create(client_credit=client_credit_account, transaction=None, source_currency=client.currency, destination_currency=client.currency, source=JournalSources.credit, destination=JournalSources.settlement, source_amount=total_due, destination_amount=total_due)
def validate(self, attrs): # NOTE(tomo): Validate cycle is unique value = attrs.get('value') option = attrs.get('option') cycle = attrs.get('cycle') cycle_multiplier = attrs.get('cycle_multiplier') currency = attrs.get('currency') if not currency: raise serializers.ValidationError(detail=_('Currency is required')) extra_attrs = {} if value: extra_attrs['value'] = value elif option: extra_attrs['option'] = option else: extra_attrs['value__isnull'] = True extra_attrs['option__isnull'] = True exist_query = ConfigurableOptionCycle.objects if self.instance: # We edit the same instance, exclude it from query exist_query = exist_query.exclude(pk=self.instance.pk) if exist_query.filter(cycle=cycle, cycle_multiplier=cycle_multiplier, currency=currency, **extra_attrs).exists(): raise serializers.ValidationError(detail='Similar cycle already exists') # End cycle unique validation relative_pricing = attrs.get('is_relative_price', False) if relative_pricing: # Check if relative pricing is sent, modify pricing if we have a similar cycle in default currency # or just raise default_currency = get_default_currency() if attrs['currency'] != default_currency: def_cycle = ConfigurableOptionCycle.objects.filter( option=option, cycle=cycle, cycle_multiplier=cycle_multiplier, currency=default_currency ).first() if def_cycle is None: raise serializers.ValidationError(detail=_('Unable to auto calculate prices')) converted_price = utils.convert_currency(price=def_cycle.price, from_currency=def_cycle.currency, to_currency=attrs['currency']) attrs['price'] = utils.cdecimal(converted_price, q='.01') converted_setup_fee = utils.convert_currency(price=def_cycle.setup_fee, from_currency=def_cycle.currency, to_currency=attrs['currency']) attrs['setup_fee'] = utils.cdecimal(converted_setup_fee, q='.01') return attrs
def validate(self, attrs): attrs = super(StaffProductCycleSerializer, self).validate(attrs) cycle = attrs.get('cycle') cycle_multiplier = attrs.get('cycle_multiplier') auto_calculate_price = attrs.get('is_relative_price', False) cycle_currency_code = attrs.get('currency') cycle_product = attrs.get('product') if cycle_product: # do not allow one-time cycles to go with recurring cycles other_cycles = ProductCycle.objects.filter(product=cycle_product) if cycle == CyclePeriods.onetime: for other_cycle in other_cycles: # type: ProductCycle if other_cycle.cycle != CyclePeriods.onetime: raise serializers.ValidationError(detail=_( 'Cannot add one time cycle if the product has a recurring cycle.' )) else: # treat the case when the new cycle is a recurring one for other_cycle in other_cycles: # type: ProductCycle if other_cycle.cycle == CyclePeriods.onetime: raise serializers.ValidationError(detail=_( 'Cannot add recurring cycle if the product has a one time cycle.' )) if auto_calculate_price and cycle_product: # auto calculate prices default_currency = get_default_currency() if default_currency.code != cycle_currency_code: def_cycle = ProductCycle.objects.filter( product=cycle_product, cycle=cycle, cycle_multiplier=cycle_multiplier, currency=default_currency).first() if def_cycle is None: c_msg = _( 'A cycle with {} currency is required to auto calculate price' ).format(default_currency) raise serializers.ValidationError(detail=c_msg) converted_price = utils.convert_currency( price=def_cycle.fixed_price, from_currency=def_cycle.currency, to_currency=cycle_currency_code) attrs['fixed_price'] = utils.cdecimal(converted_price, q='.01') converted_setup_fee = utils.convert_currency( price=def_cycle.setup_fee, from_currency=def_cycle.currency, to_currency=cycle_currency_code) attrs['setup_fee'] = utils.cdecimal(converted_setup_fee, q='.01') return attrs
def update_usage(self, skip_collecting: bool = False, skip_compute_current: bool = False): if not skip_collecting: usage_settings = UsageSettings( billing_settings=self.billing_settings) # compute total unpaid usage for all services associated with the client LOG.debug('Updating usage for client {}'.format(self.client)) for service in self.client.services.all(): billing_module = module_factory.get_module_instance( service=service) billing_module.collect_usage(service=service, usage_settings=usage_settings) if not skip_compute_current: # compute current client usage self.__client_usage = self.__compute_client_usage() # update uptodate credit for client uptodate_credit = cdecimal( self.client.get_remaining_credit(self.client_usage.unpaid_usage, self.client.currency.code)) self.client.set_uptodate_credit(uptodate_credit=uptodate_credit) self.update_outofcredit_status() # log to summary self.summary.update_uptodate_credit(self.uptodate_credit)
def get_dynamic_price_for_service(service: Service, end_datetime: datetime) -> Decimal: LOG.info('Getting dynamic price for service {}'.format(service)) try: billing_module = module_factory.get_module_instance(service=service) unsettled_usage = billing_module.get_unsettled_usage(service, end_datetime) return cdecimal(unsettled_usage.total_cost) except ModuleNotFoundException: return Decimal(0)
def get_price_display(instance): if instance.service: return '{} {}'.format( cdecimal(instance.service.get_fixed_price()), instance.service.cycle.currency.code, ) else: return None
def get_fee(self, amount: decimal.Decimal) -> decimal.Decimal: fee = decimal.Decimal('0.0') if self.fixed_fee: fee = self.fixed_fee if self.percent_fee: fee += amount * (self.percent_fee / decimal.Decimal(100)) fee = utils.cdecimal( fee ) # call cdecimal since we require 2 decimal places for this model return fee
def get_percent_per_location(location_cost: dict, total_revenue: decimal.Decimal): revenue_per_location = { } # dict {location_name: {cost: x, percent: y, alloted: z}} number_of_locations = len(location_cost.keys()) if number_of_locations > 0: location_percent = cdecimal(100 / number_of_locations, q='0.01') else: location_percent = 100 for location, cost in location_cost.items(): if location not in revenue_per_location: revenue_per_location[location] = { 'cost': cost, 'percent': decimal.Decimal('0.00'), 'alloted': decimal.Decimal('0.00') } revenue_per_location[location]['percent'] = location_percent alloted = location_percent / 100 * total_revenue alloted = cdecimal(alloted, q='0.01') revenue_per_location[location]['alloted'] = alloted return revenue_per_location
def get_client_taxes_amount_by_price(price, client=None, taxable=False): if client is None or client.tax_exempt or not taxable: # If this is a tax exempted client, just return # or item not taxable, continue to the next one return [] taxes = [] tax_rules = TaxRule.for_country_and_state(country=client.country_name, state=client.state) for tax_rule in tax_rules: tax_amount = cdecimal(price * (tax_rule.rate / 100), q='0.01') tax_name = tax_rule.name taxes.append({'name': tax_name, 'amount': tax_amount}) return taxes
def get_invoice_items_percent(invoice: Invoice): items_percent = {} for item in invoice.items_with_taxes_amount(): item_total = item.taxes_amount + item.amount item_taxes_amount = item.taxes_amount if item_total == 0: item_percent = 0 else: item_percent = item_total * 100 / invoice.total if invoice.total == 0: tax_percent = 0 else: tax_percent = item_taxes_amount * 100 / invoice.total if item_percent < 0: # Always make the percent positive item_percent *= -1 items_percent[item.id] = { 'percent': cdecimal(item_percent, q='.01'), 'taxes_amount': cdecimal(item.taxes_amount, q='.01'), 'taxes_percent': cdecimal(tax_percent, q='.01') } return items_percent
def calculate_fixed_price_and_taxes(client: Client, price, taxable: bool): """Calculate total price with taxes and return the taxes applied""" taxes_applied = {} total_price = price if taxable: tax_rules = TaxRule.for_country_and_state( country=client.country_name, state=client.state) or [] for tax_rule in tax_rules: tax_amount = (price * tax_rule.rate) / 100 tax_amount = utils.cdecimal(tax_amount, q='.01') if tax_rule.name in taxes_applied: taxes_applied[tax_rule.name] += tax_amount else: taxes_applied[tax_rule.name] = tax_amount total_price += tax_amount return total_price, taxes_applied
def add_credit_invoice(self, request): serializer = AddCreditSerializer(data=request.data, context={'request': request}) serializer.is_valid(raise_exception=True) try: client = request.user.clients.get( pk=serializer.validated_data['client']) except Client.DoesNotExist: raise ValidationError({'client': _('Client not found')}) credit = serializer.validated_data['credit'] # Look for unpaid invoices already containing credit addition credit_invoices_unpaid = Invoice.objects.filter( client=client).unpaid().for_credit() if credit_invoices_unpaid.count() > 0: raise ValidationError( {'detail': _('An unpaid credit invoice already exists')}) item_description_msg = _('Add {} {} to credit balance').format( credit, client.currency.code) item_taxes = [] if client.billing_settings.add_tax_for_credit_invoices and not client.tax_exempt: client_tax_rules = TaxRule.for_country_and_state( country=client.country_name, state=client.state) if client_tax_rules: for tax_rule in client_tax_rules: tax_amount = (credit * tax_rule.rate) / 100 tax_amount = utils.cdecimal(tax_amount, q='.01') item_taxes.append({ 'name': tax_rule.name, 'amount': tax_amount, 'tax_rule': tax_rule.id }) invoice_id = tasks.invoice_create( client.pk, items=[{ 'item_type': BillingItemTypes.credit, 'amount': credit, 'description': item_description_msg, 'taxed': True if len(item_taxes) else False, 'taxes': item_taxes, }], currency=client.currency.code, issue_date=now().isoformat(), due_date=now().isoformat()) return Response({'id': invoice_id})
def generate_report(self) -> dict: entire_report = { 'revenue_report': [], 'total_revenue_per_location': [], 'total_revenue': decimal.Decimal('0.00'), 'currency_code': self.get_report_currency(), 'start_date': str(self.start_date), 'end_date': str(self.end_date) } locations_totals = {} for client in self.clients_queryset(): client_report = self.generate_client_report(client=client) entire_report['revenue_report'].append(client_report) for revenue_per_location in client_report.get( 'revenue_per_location'): location_name = revenue_per_location.get('name') if location_name not in locations_totals: locations_totals[location_name] = revenue_per_location.get( 'revenue') else: locations_totals[ location_name] += revenue_per_location.get('revenue') for location_name, revenue in locations_totals.items(): # Go through each location and set the final revenue per location # necessary since we used a dict prior to allow for faster calculation of totals entire_report['total_revenue_per_location'].append({ 'name': location_name, 'revenue': revenue }) # Calculate the total revenue entire_report['total_revenue'] += revenue entire_report['total_revenue'] = cdecimal( entire_report['total_revenue']) return entire_report
def get_price_by_cycle_quantity_and_choice(self, cycle_name, cycle_multiplier, quantity, currency, choice_value=None, option_value=None): zero = decimal.Decimal('0.00') if self.widget == ConfigurableOptionWidget.yesno and option_value != 'yes': return zero, zero, zero if self.widget in ConfigurableOptionWidget.WITHOUT_CHOICES: cycle = self.cycles.filter(cycle=cycle_name, cycle_multiplier=cycle_multiplier, currency=currency).first() if cycle: price = cycle.price * quantity if cycle.setup_fee_entire_quantity: setup_fee = cycle.setup_fee else: setup_fee = quantity * cycle.setup_fee return (utils.cdecimal(cycle.price, q='0.01'), utils.cdecimal(price, q='0.01'), utils.cdecimal(setup_fee, q='0.01')) elif self.widget in ConfigurableOptionWidget.WITH_CHOICES: value = self.choices.filter(choice=choice_value).first() if value: cycle = value.cycles.filter(cycle=cycle_name, cycle_multiplier=cycle_multiplier, currency=currency).first() if cycle is None: return zero, zero, zero else: price = cycle.price * quantity if cycle.setup_fee_entire_quantity: setup_fee = cycle.setup_fee else: setup_fee = quantity * cycle.setup_fee return (utils.cdecimal(cycle.price, q='0.01'), utils.cdecimal(price, q='0.01'), utils.cdecimal(setup_fee, q='0.01')) return zero, zero, zero
def estimate_new_service_cycle_cost(service: Service, product: Product, cycle: ProductCycle, start_date, configurable_options=None): client = service.client zero = decimal.Decimal('0.00') opt_sum = [] seconds_estimate = ServiceManager.estimate_new_service_cycle_seconds( service=service, product=product, cycle=cycle, start_date=start_date) if cycle and not product.is_free: new_cycle_cost = utils.convert_currency(cycle.fixed_price, cycle.currency, client.currency) if seconds_estimate['new_cycle_seconds'] > 0: new_cycle_cost_per_second = new_cycle_cost / seconds_estimate[ 'new_cycle_seconds'] new_cycle_remaining_cost = new_cycle_cost_per_second * seconds_estimate[ 'new_cycle_remaining_seconds'] else: new_cycle_remaining_cost = zero new_cycle_cost = zero else: new_cycle_cost = zero new_cycle_remaining_cost = zero if service.product.is_free or service.cycle and service.cycle.cycle == CyclePeriods.onetime: remaining_cost = zero elif not service.product.is_free and service.cycle: current_cycle_cost = service.get_fixed_price_without_configurable_options( currency=client.currency) if seconds_estimate['old_cycle_seconds'] > 0: current_cycle_cost_per_second = current_cycle_cost / seconds_estimate[ 'old_cycle_seconds'] remaining_cost = current_cycle_cost_per_second * seconds_estimate[ 'old_cycle_remaining_seconds'] else: remaining_cost = zero # Handle configurable options if configurable_options: opt_sum = ServiceManager.estimate_new_config_options_cost( service=service, cycle=cycle, configurable_options=configurable_options, seconds_estimate=seconds_estimate) else: remaining_cost = zero prod_upgrade_cost = utils.cdecimal(new_cycle_remaining_cost - remaining_cost, q='.01') total_upgrade_cost = prod_upgrade_cost for opt in opt_sum: total_upgrade_cost += opt['upgrade_cost'] taxable = not client.tax_exempt and product.taxable total_price, taxes_applied = SettlementManager.calculate_fixed_price_and_taxes( client=client, price=total_upgrade_cost, taxable=taxable) new_cycle_cost = utils.cdecimal(new_cycle_cost, q='.01') remaining_cost = utils.cdecimal(remaining_cost, q='.01') return { 'service_remaining_cost': remaining_cost, 'new_product_price': new_cycle_cost, 'upgrade_price': total_upgrade_cost, 'product_upgrade_price': prod_upgrade_cost, 'taxes_applied': taxes_applied, 'total_due': total_price, 'service_id': service.pk, 'product_id': product.pk, 'cycle_id': cycle.pk, 'configurable_options': opt_sum, 'display_name': '{} => {} ({} {} / {})'.format(service.display_name, product.name, new_cycle_cost, client.currency, cycle.display_name), 'currency': client.currency.code }
def get_service_report(self, service, start_date, end_date): # TODO: see what to do with this for reseller report = { 'name': 'OpenStack resources report', 'locations': {}, 'service': None, 'location_cost': {} } location_details = {} default_region = plugin_settings.DEFAULT_REGION client = service.client try: from fleio.osbilling.bin.collectorlib import service_usage from fleio.osbilling.bin.collectorlib import add_pricing from fleio.osbilling.bin.collectorlib import collect_project_metrics from fleio.osbilling.bin.collectorlib import collect_internal_usage except ImportError: return report usage_settings = UsageSettings( billing_settings=client.billing_settings) try: usage = service_usage( start_date=start_date, end_date=end_date, service_dynamic_usage=service.service_dynamic_usage, ) except ObjectDoesNotExist: return report usage['metrics_details'] = collect_project_metrics( start_date, end_date, service_dynamic_usage=service.service_dynamic_usage, ) if staff_active_features.is_enabled('openstack.instances.traffic'): # TODO: check for feature inside the collect method collect_internal_usage( usage_data=usage, start=start_date, end=end_date, service_dynamic_usage=service.service_dynamic_usage, ) add_pricing(usage, client, usage_settings=usage_settings) project = usage.get('project', None) if not project: return report report['service'] = service.id total_cost = usage.get('price', Decimal(0)) report['total_cost'] = total_cost for usage_detail in usage.get('usage_details', []): resource_type = usage_detail.get('resource_type') resource_name = usage_detail.get('resource_name') for rtype_usage in usage_detail.get('usage', []): region = rtype_usage.get('region', default_region) region = region or default_region price = Decimal(rtype_usage.get('price', 0)) if region not in location_details: location_details[region] = { resource_type: { 'resource_name': resource_name, 'price': price, 'num_resources': 1 } } report['location_cost'][region] = price elif resource_type not in location_details[region]: location_details[region][resource_type] = { 'resource_name': resource_name, 'price': price, 'num_resources': 1 } report['location_cost'][region] += price else: location_details[region][resource_type]['price'] += price location_details[region][resource_type][ 'num_resources'] += 1 report['location_cost'][region] += price report['locations'] = location_details report['total_cost'] = cdecimal(report['total_cost'], q='.01') return report
def get_domain_registrar_prices(domain: Domain, registrar: Registrar, years=None) -> SimpleNamespace or None: """Get the registrar prices for a domain""" # TODO(tomo): Check if domain is premium if years is None: years = domain.registration_period tld_name = domain.tld.name default_currency = get_default_currency() register_price = None transfer_price = None renew_price = None response = RegistrarPrices.objects.filter( tld_name=tld_name, connector=registrar.connector) if years > 1: # If we need a higher number of years, check if we have the answer cached # otherwise we need to calculate it response = response.filter(Q(years=1) | Q(years=years)) # Prices can be in multiple currencies and with different years. At least for 1 year we should have the price price_currency_match = None for db_price in response: if db_price.currency == default_currency.code: if db_price.years == years: price_currency_match = db_price elif db_price.years == 1 and not price_currency_match: price_currency_match = db_price price_years_match = None if not price_currency_match: for db_price in response: if db_price.years == years: price_years_match = db_price elif db_price.years == 1 and not price_years_match: price_years_match = db_price if price_currency_match: if price_currency_match.years != years: register_price = cdecimal(price_currency_match.register_price * years) renew_price = cdecimal(price_currency_match.renew_price * years) transfer_price = cdecimal(price_currency_match.transfer_price ) # Transfers are on 1 year only else: register_price = cdecimal(price_currency_match.register_price) renew_price = cdecimal(price_currency_match.renew_price) transfer_price = cdecimal(price_currency_match.transfer_price) elif price_years_match: if price_years_match.years != years: pre_register_price = cdecimal( price_years_match.register_price * years) pre_renew_price = cdecimal(price_years_match.renew_price * years) pre_transfer_price = cdecimal(price_years_match.transfer_price ) # Transfers are on 1 year only else: pre_register_price = cdecimal(price_years_match.register_price) pre_renew_price = cdecimal(price_years_match.register_price) pre_transfer_price = cdecimal(price_years_match.register_price) try: tld_currency = Currency.objects.get( code=price_years_match.currency) except Currency.DoesNotExist: LOG.error( 'Registry currency {} does not exist in Fleio'.format( price_years_match.currency)) return None register_price = convert_currency(price=pre_register_price, from_currency=tld_currency, to_currency=default_currency) renew_price = convert_currency(price=pre_renew_price, from_currency=tld_currency, to_currency=default_currency) transfer_price = convert_currency(price=pre_transfer_price, from_currency=tld_currency, to_currency=default_currency) if register_price or renew_price or transfer_price: tld_prices = SimpleNamespace() tld_prices.register_price = cdecimal(register_price) tld_prices.renew_price = cdecimal(renew_price) tld_prices.transfer_price = cdecimal(transfer_price) tld_prices.currency = default_currency.code return tld_prices else: return None
def get_effective_credit_limit(obj): if obj.has_billing_agreement: return cdecimal(obj.billing_settings.credit_limit_with_agreement, q='.01') else: return cdecimal(obj.billing_settings.credit_limit, q='.01')
def convert_amount_from_api(amount, currency=None): assert currency is not None, 'Currency cannot be None' if currency.lower() in ZERO_DECIMAL_CURRENCIES: return cdecimal(amount) else: return cdecimal(amount / Decimal("100"))
def create_invoice_for_services( client: Client, services: List[Service], tax_rules: List[TaxRule] = None, manual_invoice: bool = False, ): LOG.info('Invoicing {} services for {}'.format(len(services), client)) invoice_issue_date = utcnow() invoice_due_date = invoice_issue_date with db_transaction.atomic( ): # create invoice and set the service next invoice due atomically invoice_items = list() for service in services: service_next_due = service.next_due_date if service.next_due_date else service.created_at while service.next_invoice_date is None or \ service.next_invoice_date < invoice_issue_date or manual_invoice: item_taxes = [] service_fixed_price = service.get_fixed_price( currency=client.currency) service_fixed_price = utils.cdecimal( service_fixed_price, q='0.01') # Convert to 2 decimals if service.is_price_overridden: service_dynamic_price = Decimal(0) else: service_dynamic_price = InvoiceUtils.get_dynamic_price_for_service( service, service_next_due, ) service_price = service_fixed_price + service_dynamic_price # TODO: taxes are now recalculated on invoice creation in serializer, see if we need this code # calculate taxes for invoice item if service.product.taxable and not client.tax_exempt: if tax_rules: for tax_rule in tax_rules: tax_amount = (service_price * tax_rule.rate) / 100 tax_amount = utils.cdecimal(tax_amount, q='.01') item_taxes.append({ 'name': tax_rule.name, 'amount': tax_amount, 'tax_rule': tax_rule.id }) service_prev_due = service_next_due service_next_due = service.get_next_due_date( service_next_due) prev_invoice_date = service.next_invoice_date service.update_next_invoice_date( previous_due_date=service_prev_due, manual_invoice=manual_invoice) if manual_invoice and service.next_invoice_date > invoice_issue_date: invoice_due_date = prev_invoice_date if prev_invoice_date else service.next_invoice_date if service_next_due and service_next_due != utils.DATETIME_MAX: if manual_invoice: # construct description to match invoice due date while service_prev_due <= invoice_due_date: service_prev_due = service_next_due service_next_due = service.get_next_due_date( service_next_due) if service_prev_due == service_next_due or service_next_due < invoice_due_date: # there is an issue within the loop, break to prevent infinite cycle break service_next_due_display = service_next_due service_prev_due_display = service_prev_due if service.cycle.cycle in [ CyclePeriods.month, CyclePeriods.year ]: service_next_due_display -= timedelta(days=1) datetime_fmt = '%d/%m/%Y' previous_due_formatted = service_prev_due_display.strftime( datetime_fmt) next_due_formatted = service_next_due_display.strftime( datetime_fmt) description = '{} ({} - {})'.format( service.display_name, previous_due_formatted, next_due_formatted, ) else: description = service.display_name invoice_items.append({ 'amount': service_price, 'description': description, 'item_type': BillingItemTypes.service, 'taxed': service.product.taxable, 'taxes': item_taxes, 'service': service.id }) if prev_invoice_date == service.next_invoice_date: # there is an issue within the loop, break to prevent infinite cycle LOG.error('Invoice date not increasing, aborting') break if manual_invoice: # this is a manual invoice generation, stopping break invoice_id = invoice_create( client=client.id, items=invoice_items, issue_date=invoice_issue_date, currency=client.currency.code, due_date=invoice_due_date, ) LOG.info('Invoice {} generated for client {}'.format( invoice_id, client)) return invoice_id
def get_client_revenue(client: Client, start_date: datetime.datetime, end_date: datetime.datetime): """Get all client revenue that should be included in the report""" services_report = {} report = { 'client': client.id, 'client_display_name': client.long_name, 'services_report': services_report, 'credit_in': decimal.Decimal('0.00'), 'credit_out': decimal.Decimal('0.00'), 'credit_available': decimal.Decimal('0.00') } client_main_credit_account = client.credits.filter( currency=client.currency).first() if client_main_credit_account: # set the last available credit for the given period last_journal_entry = Journal.objects.filter( Q(date_added__lt=end_date) & (Q(client_credit=client_main_credit_account) | Q(invoice__client=client))).order_by('date_added').last() if (last_journal_entry and last_journal_entry.client_credit_left and last_journal_entry.client_credit_left_currency.code == client_main_credit_account.currency.code): report[ 'credit_available'] = last_journal_entry.client_credit_left else: # TODO(manu): This conditional branch will not work when re-generating for older months if a fleio # installation exists from a longer time (there are no journal entries that report the credit_ # available after that journal entry). To fix this, calculate client_credit_left for each journal # entry since beginning of time in a migration report['credit_available'] = client_main_credit_account.amount # Add each service to the report and it's usage details given by it's billing module # if available. for service in client.services.filter( Q(terminated_at__isnull=True) | Q(terminated_at__lt=end_date)): fixed_monthly_price = cdecimal( service.get_fixed_price(), q='.01') # returns fixed or overriden price services_report[service.id] = { 'service_name': service.display_name, 'service_id': service.id, 'service_type': service.product.product_type, 'service_last_cycle': JournalReport._get_next_due_date(service=service, until_date=end_date), 'entries': [], 'fixed_monthly_price': fixed_monthly_price, # fixed or overriden price 'price_overridden': service.is_price_overridden, 'total_revenue': decimal.Decimal('0.00'), 'total_from_credit': decimal.Decimal('0.00') } # Get the report module for each service if it exists, in order to get a detailed location usage service_module = module_factory.get_module_instance( service=service) services_report[service.id][ 'usage_details'] = service_module.get_service_report( service, start_date, end_date) # Gather journal entries and split out credit and direct service payments through invoices if client_main_credit_account: # All credit entries that need to appear on the report credit_qs = Journal.objects.filter( date_added__gte=start_date, date_added__lt=end_date, client_credit=client_main_credit_account) credit_in_qs = credit_qs.filter(destination=JournalSources.credit, source__in=[ JournalSources.external, JournalSources.transaction ]) credit_in_qs = credit_in_qs.aggregate( dest_amount=Coalesce(models.Sum('destination_amount'), 0)) credit_amount_in = credit_in_qs.get('dest_amount', 0) credit_out_qs = credit_qs.filter(source=JournalSources.credit, destination__in=[ JournalSources.external, JournalSources.transaction ]) credit_out_qs = credit_out_qs.aggregate( source_amount=Coalesce(models.Sum('source_amount'), 0)) credit_amount_out = credit_out_qs.get('source_amount', 0) report['credit_in'] += credit_amount_in report['credit_out'] += credit_amount_out # Revenue from invoices: invoice_journal_qs = Journal.objects.filter( invoice__client=client, date_added__gte=start_date, date_added__lt=end_date).order_by('date_added') invoice_journal_qs = invoice_journal_qs.filter( Q(source=JournalSources.invoice, destination__in=[ JournalSources.external, JournalSources.transaction ]) | Q(destination=JournalSources.invoice, source__in=[ JournalSources.external, JournalSources.transaction, JournalSources.staff, ])).all() for journal_entry in invoice_journal_qs: invoice = journal_entry.invoice items_percent = JournalReport.get_invoice_items_percent( invoice=invoice) for item in invoice.items.all(): if item.service: if item.service.id in services_report: if items_percent[item.id]['percent'] != 0: amount = items_percent[item.id][ 'percent'] / 100 * journal_entry.destination_amount taxamt = items_percent[item.id][ 'taxes_percent'] / 100 * journal_entry.destination_amount amount -= taxamt if journal_entry.destination in [ JournalSources.transaction, JournalSources.external ]: amount = -1 * amount amount = cdecimal(amount, q='.01') taxamt = cdecimal(taxamt, q='.01') services_report[item.service.id]['entries'].append( { 'amount': amount, 'item_type': item.item_type, 'from_credit': False, 'taxes_amount': taxamt, 'taxes_percent': items_percent[item.id]['taxes_percent'], 'source': journal_entry.source, 'date': str(journal_entry.date_added) }) services_report[ item.service.id]['total_revenue'] += amount else: amount = items_percent[item.id][ 'percent'] / 100 * journal_entry.destination_amount taxamt = items_percent[item.id][ 'taxes_percent'] / 100 * journal_entry.destination_amount amount -= taxamt if journal_entry.destination in [ JournalSources.transaction, JournalSources.external ]: amount = -1 * amount amount = cdecimal(amount, q='.01') taxamt = cdecimal(taxamt, q='.01') services_report[item.service.id] = { 'entries': [{ 'amount': amount, 'item_type': item.item_type, 'from_credit': False, 'taxes_amount': taxamt, 'taxes_percent': items_percent[item.id]['taxes_percent'], 'source': journal_entry.source, 'date': str(journal_entry.date_added) }], 'service': item.service.display_name, 'total_revenue': amount, 'total_from_credit': decimal.Decimal('0.00') } # Credit entries for each service cservice_qs = Journal.objects.filter( date_added__gte=start_date, date_added__lt=end_date, client_credit=client_main_credit_account) cservice_qs = cservice_qs.filter( Q(source=JournalSources.credit, destination=JournalSources.invoice) | Q(source=JournalSources.invoice, destination=JournalSources.credit)).all() for journal_entry in cservice_qs: invoice = journal_entry.invoice items_percent = JournalReport.get_invoice_items_percent( invoice=invoice) for item in invoice.items.all(): if item.service: if item.service.id in services_report: if items_percent[item.id]['percent'] != 0: amount = items_percent[item.id][ 'percent'] / 100 * journal_entry.destination_amount taxamt = items_percent[item.id][ 'taxes_percent'] / 100 * journal_entry.destination_amount amount -= taxamt if journal_entry.destination == JournalSources.credit: amount = -1 * amount amount = cdecimal(amount, q='.01') taxamt = cdecimal(taxamt, q='.01') credit_entries = services_report[ item.service.id]['entries'] credit_entries.append({ 'amount': amount, 'item_type': item.item_type, 'from_credit': True, 'taxes_amount': taxamt, 'taxes_percent': items_percent[item.id]['taxes_percent'], 'source': journal_entry.source, 'date': str(journal_entry.date_added) }) services_report[ item.service.id]['total_revenue'] += amount services_report[ item.service.id]['total_from_credit'] += ( amount + taxamt) else: amount = items_percent[item.id][ 'percent'] / 100 * journal_entry.destination_amount taxamt = items_percent[item.id][ 'taxes_percent'] / 100 * journal_entry.destination_amount amount -= taxamt if journal_entry.destination == JournalSources.credit: amount = -1 * amount amount = cdecimal(amount, q='.01') taxamt = cdecimal(taxamt, q='.01') credit_entries = [{ 'amount': amount, 'item_type': item.item_type, 'from_credit': True, 'taxes_amount': taxamt, 'taxes_percent': items_percent[item.id]['taxes_percent'], 'source': journal_entry.source, 'date': str(journal_entry.date_added) }] services_report[item.service.id] = { 'entries': credit_entries, 'service': item.service.display_name, 'total_revenue': decimal.Decimal('0.00'), 'total_from_credit': amount + taxamt } elif item.item_type == BillingItemTypes.credit: if journal_entry.destination == JournalSources.invoice: report['credit_out'] += item.amount else: report['credit_in'] += item.amount # Gether all credit used by services # The credit is split proportional to each service, based on it's usage report client_available_credit = report['credit_available'] total_still_required_cost = decimal.Decimal('0.00') total_required_cost = decimal.Decimal('0.00') total_debt = decimal.Decimal('0.00') total_credit_alloted = decimal.Decimal('0.00') for service_id, service_report in services_report.items(): price_overridden = service_report[ 'price_overridden'] if 'price_overridden' in service_report else False total_revenue = service_report['total_revenue'] if price_overridden: # If pirice is overridden, the service total cost is the fixed price one - entries for it service_required_cost = service_report['fixed_monthly_price'] service_report['service_required_cost'] = cdecimal( service_required_cost, q='.01') cost_still_required = service_required_cost - total_revenue cost_still_required = cdecimal(cost_still_required, q='.01') service_report['cost_still_required'] = cost_still_required else: # Add here logic for dynamic but minimum fixed fixed_monthly_price = (service_report['fixed_monthly_price'] if 'fixed_monthly_price' in service_report else decimal.Decimal('0.00')) usage_details = (service_report['usage_details'] if 'usage_details' in service_report else {}) service_required_cost = ( fixed_monthly_price + usage_details.get('total_cost', decimal.Decimal('0.00'))) cost_still_required = service_required_cost - total_revenue cost_still_required = cdecimal(cost_still_required, q='.01') service_report['service_required_cost'] = cdecimal( service_required_cost, q='.01') service_report['cost_still_required'] = cost_still_required # Create a total amount so we can deduct from credit available and calculate the percentage total_required_cost += service_required_cost total_still_required_cost += cost_still_required # Calculate the percentage, debts, credit alloted of each service services_report, report = JournalReport._calculate_amount_for_services( client=client, services_report=services_report, report=report, total_still_required_cost=total_still_required_cost, client_available_credit=client_available_credit, total_debt=total_debt, total_credit_alloted=total_credit_alloted, end_date=end_date) # Go through each service to get the totals per region revenue_per_location = {} default_location = JournalReport.get_default_location() for service_id, service_report in services_report.items(): # Set an equal percent for each location usage_details = service_report.get('usage_details', {}) if type(usage_details) is dict and usage_details.keys(): # Deal with services that have usage_details location_cost = usage_details.get('location_cost') total_revenue = service_report['alloted_from_credit'] service_location_alloted = JournalReport.get_percent_per_location( location_cost, total_revenue) for location_name, costs in service_location_alloted.items(): if location_name not in revenue_per_location: revenue_per_location[location_name] = decimal.Decimal( '0.00') revenue_per_location[location_name] += costs['alloted'] else: if default_location not in revenue_per_location: revenue_per_location[default_location] = decimal.Decimal( '0.00') total_revenue = service_report['alloted_from_credit'] revenue_per_location[default_location] += total_revenue report['revenue_per_location'] = [{ 'name': name, 'revenue': cdecimal(revenue, q='.01') } for name, revenue in revenue_per_location.items()] return report
def _calculate_amount_for_services(client, services_report, report, total_still_required_cost, client_available_credit, total_debt, total_credit_alloted, end_date): try: from fleio.osbilling.bin.collectorlib import service_usage from fleio.osbilling.bin.collectorlib import add_pricing from fleio.osbilling.bin.collectorlib import collect_project_metrics from fleio.osbilling.bin.collectorlib import collect_internal_usage except ImportError: return services_report, report total_credit_alloted_for_os = decimal.Decimal('0.00') for service_id, service_report in services_report.items(): cost_still_required = service_report['cost_still_required'] service_required_cost = service_report['service_required_cost'] if service_required_cost != 0: if total_still_required_cost > 0: sr_percent = ( (service_report['cost_still_required'] * 100) / total_still_required_cost) else: sr_percent = 100 sr_percent = cdecimal(sr_percent, q='.01') service_report['cost_required_percent'] = sr_percent if service_report.get('service_type', '') == ProductType.openstack: # TODO(low priority): for multiple openstack services/client, debt and revenue should be # calculated using sr_percent # calculate client_consumed_credit which represents the credit consumed in a month that was # actually paid using the following steps: # get unpaid usage from last cycle to end_date (firs of month, when report is generated) # using that information, calculate uptodate credit at that moment: # (available_credit at that moment - unpaid usage) # after that, calculate debt using information from above and consumption in that month db_service = Service.objects.filter(id=service_id).first() if not db_service: unpaid_usage = decimal.Decimal('0.00') else: try: unpaid_usage_dict = service_usage( start_date=service_report[ 'service_last_cycle'], end_date=end_date, service_dynamic_usage=db_service. service_dynamic_usage, ) unpaid_usage_dict[ 'metrics_details'] = collect_project_metrics( start=service_report['service_last_cycle'], end=end_date, service_dynamic_usage=db_service. service_dynamic_usage, ) if staff_active_features.is_enabled( 'openstack.instances.traffic'): # TODO: check for feature inside the collect method collect_internal_usage( usage_data=unpaid_usage_dict, service_dynamic_usage=db_service. service_dynamic_usage, start=service_report['service_last_cycle'], end=end_date, ) usage_settings = UsageSettings( billing_settings=client.billing_settings) add_pricing(unpaid_usage_dict, client, usage_settings=usage_settings) unpaid_usage = unpaid_usage_dict.get( 'price', decimal.Decimal('0.00')) except (Exception, AttributeError): unpaid_usage = decimal.Decimal('0.00') if unpaid_usage is None: LOG.error( 'Something went wrong when collecting unpaid usage for the period between service ' 'last cycle date and the end of month for report ({}). Report for client {} may not ' 'reflect reality for that period.'.format( str(end_date), client.id)) unpaid_usage = decimal.Decimal('0.00') client_utd_credit_at_the_moment = client_available_credit - unpaid_usage client_consumed_credit = client_available_credit - client_utd_credit_at_the_moment # calculations for debt then revenue if client_utd_credit_at_the_moment < 0: service_debt = client_utd_credit_at_the_moment * (-1) elif client_consumed_credit >= cost_still_required: service_debt = client_consumed_credit - cost_still_required else: service_debt = client_consumed_credit if client_available_credit > 0: service_debt *= (-1) else: # handle services not related to openstack service_debt = cost_still_required - service_report[ 'total_revenue'] if service_debt > 0: service_debt = cost_still_required # invoice is not paid, thus service "debt" is cost still req # NOTE: service debt will result in: # negative value: means an old debt was paid before, will be added to alloted_from_credit (revenue) # positive value: means there is debt, for non-os services this will be however zeroed # zero value: there is no debt partial_debt = cdecimal( service_debt, q='0.01' ) # used to calculate revenue (alloted_from_credit) if service_debt > 0: # Only take into account positive debt if service_report.get('service_type', '') == ProductType.openstack: service_debt = cdecimal(service_debt, q='0.01') else: # for non-os services, debt does not exist service_debt = decimal.Decimal('0.00') else: service_debt = decimal.Decimal('0.00') service_report['debt'] = service_debt total_debt += service_debt alloted_from_credit = cost_still_required - partial_debt # recalculate alloted from credit based on other services no_of_services_with_cost = 0 latest_service_with_cost = None if service_report.get('service_type', '') == ProductType.openstack: for service_id_helper, service_report_helper in services_report.items( ): if (service_report_helper.get('usage_details', {}).get( 'total_cost', decimal.Decimal('0.00')) > decimal.Decimal('0.00')): latest_service_with_cost = service_id_helper no_of_services_with_cost = no_of_services_with_cost + 1 if service_id != latest_service_with_cost: service_percent = cdecimal(str( 100 / no_of_services_with_cost / 100), q='.001') alloted_from_credit = alloted_from_credit * service_percent else: alloted_from_credit = alloted_from_credit - total_credit_alloted_for_os # until now total_credit_alloted_for_os += alloted_from_credit service_report['alloted_from_credit'] = cdecimal( alloted_from_credit, q='.01') total_credit_alloted += alloted_from_credit else: service_report['cost_required_percent'] = decimal.Decimal( '0.00') service_report['alloted_from_credit'] = decimal.Decimal('0.00') service_report['debt'] = decimal.Decimal('0.00') report['total_debt'] = cdecimal(total_debt, q='.01') report['total_alloted_from_credit'] = cdecimal(total_credit_alloted, q='.01') return services_report, report
def estimate_new_config_options_cost(service: Service, cycle: ProductCycle, configurable_options, seconds_estimate): options_upgrade_summary = [] zero = decimal.Decimal('0.00') client = service.client for config_option in configurable_options: new_option = config_option['option'] old_option = service.configurable_options.filter( option=new_option).first() new_price_set = False choice_value = None has_price = True if config_option['option'].widget == 'text_in': has_price = False quantity = config_option.get('quantity') if config_option['option'].has_choices: choice_value = config_option['option_value'] # filter out all configurable options that do not have the product cycles if not config_option['option'].has_cycle( cycle=cycle.cycle, cycle_multiplier=cycle.cycle_multiplier, choice_value=choice_value, currency=client.currency.code, ): continue if not new_price_set and has_price: unit_price, price, setupfee = config_option[ 'option'].get_price_by_cycle_quantity_and_choice( cycle_name=cycle.cycle, cycle_multiplier=cycle.cycle_multiplier, currency=client.currency, quantity=quantity, choice_value=choice_value, option_value=config_option['option_value'], ) if seconds_estimate['new_cycle_seconds'] > 0: unit_price_per_second = unit_price / seconds_estimate[ 'new_cycle_seconds'] unit_remaining_price = ( unit_price_per_second * seconds_estimate['new_cycle_remaining_seconds']) remaining_price = unit_remaining_price * quantity else: unit_remaining_price = remaining_price = zero else: unit_price, price, setupfee = zero, zero, zero # noqa unit_remaining_price = remaining_price = zero if old_option: if has_price: old_choice_value = old_option.option_value (old_unit_price, old_price, old_setupfee ) = new_option.get_price_by_cycle_quantity_and_choice( cycle_name=service.cycle.cycle, cycle_multiplier=service.cycle.cycle_multiplier, currency=client.currency, quantity=old_option.quantity, choice_value=old_choice_value, option_value=old_choice_value, ) if seconds_estimate['old_cycle_seconds'] > 0: current_cycle_cost_per_second = old_unit_price / seconds_estimate[ 'old_cycle_seconds'] remaining_unit_cost = ( current_cycle_cost_per_second * seconds_estimate['old_cycle_remaining_seconds']) remaining_cost = remaining_unit_cost * old_option.quantity else: remaining_cost = remaining_unit_cost = zero upgrade_unit_cost = utils.cdecimal(unit_remaining_price - remaining_unit_cost, q='.01') upgrade_cost = utils.cdecimal(remaining_price - remaining_cost, q='.01') else: upgrade_cost = upgrade_unit_cost = zero if config_option['option'].widget == 'yesno' and config_option[ 'option_value'] != 'yes': display_name = '{} => {}: {}'.format( old_option.display, old_option.display, config_option['option_value']) else: display_name = '{} => {}: {}'.format( old_option.display, new_option.description, config_option['option_value']) upgrade_option = { 'display_name': display_name, 'is_free': not has_price, 'option': config_option['option'].pk, 'option_value': config_option['option_value'], 'price': upgrade_cost, 'upgrade_cost': upgrade_cost, 'has_price': has_price, 'taxable': True, 'unit_price': upgrade_unit_cost, 'quantity': quantity, 'setupfee': setupfee } options_upgrade_summary.append(upgrade_option) else: options_upgrade_summary.append({ 'display_name': '{}: {}'.format(new_option.description, config_option['option_value']), 'is_free': not has_price, 'option_value': config_option['option_value'], 'option': config_option['option'].pk, 'price': price, 'upgrade_cost': utils.cdecimal(remaining_price, q='.01'), 'has_price': has_price, 'taxable': True, 'unit_price': unit_price, 'quantity': quantity, 'setupfee': setupfee }) return options_upgrade_summary