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
|