bmc_hub/app/services/invoice_fee_service.py

113 lines
5.8 KiB
Python
Raw Permalink Normal View History

"""Snapshot the customer invoice fee at order creation, never on update/export."""
from decimal import Decimal, InvalidOperation
from uuid import uuid4
from fastapi import HTTPException
from app.core.database import execute_query_single
from app.core.config import settings
def _one(sql, params, cursor=None):
if cursor is not None:
cursor.execute(sql, params)
row = cursor.fetchone()
if row is not None and not isinstance(row, dict):
return {column[0]: value for column, value in zip(cursor.description, row)}
return row
return execute_query_single(sql, params)
def validate_fee_product(product_id, cursor=None):
try:
product_id = int(product_id)
if product_id <= 0:
raise ValueError()
except (TypeError, ValueError):
raise HTTPException(422, 'Vælg en gyldig vare til faktureringsgebyr')
product = _one('SELECT * FROM products WHERE id = %s AND deleted_at IS NULL', (product_id,), cursor)
if not product or product.get('is_active') is False or product.get('status', 'active') != 'active' or product.get('lifecycle_status') in ('inactive', 'phasing_out') or product.get('is_active_in_economic') is False:
raise HTTPException(409, 'Standardvaren til faktureringsgebyr findes ikke eller er inaktiv')
amount = _amount(product.get('sales_price'))
if amount is None or amount <= 0:
raise HTTPException(422, 'Gebyrvaren skal have en positiv salgspris')
if not (product.get('economic_product_number') or product.get('sku_internal')):
raise HTTPException(422, 'Gebyrvaren mangler varenummer til eksport')
return product
def _amount(value):
if value is None:
return None
try:
amount = Decimal(str(value))
if not amount.is_finite() or amount < 0:
raise ValueError()
return amount.quantize(Decimal('0.01'))
except (InvalidOperation, ValueError):
raise HTTPException(422, 'Faktureringsgebyret skal være et gyldigt beløb på mindst 0')
def invoice_fee_line(customer_id, *, cursor=None, legacy=False, customer=None, currency='DKK', _setting=None):
setting = _setting if _setting is not None else _one('SELECT value FROM settings WHERE key = %s', ('invoice_fee_product_id',), cursor)
configured = (setting or {}).get('value')
active = configured not in (None, '', 'null')
if not active and not legacy:
return None
customer = customer or _one('SELECT * FROM customers WHERE id = %s', (customer_id,), cursor)
if not customer:
raise HTTPException(409, 'Ordren mangler en gyldig Hub-kunde til beregning af faktureringsgebyr')
if customer.get('invoice_fee_exempt') or customer.get('supplier_service_enrolled'):
return None
override = _amount(customer.get('invoice_fee_amount'))
if override == 0:
return None
if active:
product = validate_fee_product(configured, cursor)
product_currency = product.get('sales_currency') or 'DKK'
if currency != product_currency:
raise HTTPException(422, f'Gebyrvarens pris er i {product_currency}, men ordren er i {currency}')
amount = override if override is not None else _amount(product['sales_price'])
else:
product = {}
amount = override if override is not None else _amount(settings.CUSTOMER_DEFAULT_INVOICE_FEE)
if not amount:
return None
product_number = product.get('economic_product_number') or product.get('sku_internal')
description = product.get('name') or 'Faktureringsgebyr'
return {
'line_key': f'invoice_fee:{uuid4().hex}', 'source_type': 'invoice_fee',
'source_id': None, 'product_id': product.get('id'), 'product_number': product_number,
'description': description, 'quantity': 1, 'unit_price': float(amount),
'discount_percentage': 0, 'unit': 'stk', 'selected': True,
'currency': currency, 'amount': float(amount), 'line_total': float(amount),
'economic_product_number': product_number,
'customer_id': customer_id, 'customer_name': customer.get('name'),
'sag_id': None, 'case_id': None, 'time_entry_ids': [], 'time_date': None,
'fee_snapshot': {'product_id': product.get('id'), 'product_number': product_number,
'amount': str(amount), 'currency': currency},
}
def with_invoice_fee(lines, customer_id, *, cursor=None, legacy=False, customer=None, native=False):
"""Called only for a new order. Ignore incoming fee lines; derive fee server-side."""
setting = _one('SELECT value FROM settings WHERE key = %s', ('invoice_fee_product_id',), cursor)
active = (setting or {}).get('value') not in (None, '', 'null')
if not active and not legacy:
return list(lines)
business = [dict(line) for line in lines if line.get('source_type') != 'invoice_fee']
if not business:
return business
currencies = {line.get('currency') or 'DKK' for line in business}
if len(currencies) != 1:
raise HTTPException(422, 'En ordre med faktureringsgebyr skal have én fælles valuta')
# Do not charge credits. Positive new orders are charged once.
total = sum(Decimal(str(line.get('quantity', 1))) * Decimal(str(line.get('unit_price', line.get('unitNetPrice', 0)))) * (1 - Decimal(str(line.get('discount_percentage', line.get('discountPercentage', 0)))) / 100) for line in business if line.get('selected', True))
if total <= 0:
return business
fee = invoice_fee_line(customer_id, cursor=cursor, legacy=legacy, customer=customer, currency=currencies.pop(), _setting=setting or {})
if fee:
if native:
fee.update(product={'productNumber': fee['product_number'], 'description': fee['description']},
unitNetPrice=fee['unit_price'], totalNetAmount=fee['amount'], discountPercentage=0)
business.append(fee)
return business