- Introduced tests for mail templates including ranking, rendering, and attachment handling. - Added tests for time billing selection and approval processes, ensuring proper validation and state management. - Implemented tests for case types in JavaScript, focusing on loading and editing functionality. - Created tests for contact mail AI integration, ensuring proper parsing and error handling. - Developed tests for dark theme CSS contrast and loading order. - Added tests for invoice fee service, covering various scenarios including exemptions and invalid inputs. - Implemented vendor resource tests, ensuring proper file handling and query validation.
113 lines
5.8 KiB
Python
113 lines
5.8 KiB
Python
"""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
|