From 9a9031fd2715ced858aae9719333831333af9457 Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Tue, 31 Mar 2026 04:11:02 +0000 Subject: [PATCH] =?UTF-8?q?feat(pos):=20add=20accounting=20blueprint=20?= =?UTF-8?q?=E2=80=94=20reports,=20entries,=20periods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- pos/blueprints/accounting_bp.py | 743 ++++++++++++++++++++++++++++++++ 1 file changed, 743 insertions(+) create mode 100644 pos/blueprints/accounting_bp.py diff --git a/pos/blueprints/accounting_bp.py b/pos/blueprints/accounting_bp.py new file mode 100644 index 0000000..be611c2 --- /dev/null +++ b/pos/blueprints/accounting_bp.py @@ -0,0 +1,743 @@ +# /home/Autopartes/pos/blueprints/accounting_bp.py +"""Accounting blueprint: chart of accounts, journal entries, financial reports. + +Financial reports (balance sheet, income statement, trial balance, aging) +are computed via SQL aggregation on journal_entry_lines. All amounts are +NUMERIC(14,2) in the database. +""" + +import json +from datetime import date, datetime +from flask import Blueprint, request, jsonify, g +from middleware import require_auth +from tenant_db import get_tenant_conn +from services.accounting_engine import create_manual_entry +from services.audit import log_action + +accounting_bp = Blueprint('accounting', __name__, url_prefix='/pos/api/accounting') + + +# ─── Chart of Accounts ───────────────────────────── + +@accounting_bp.route('/accounts', methods=['GET']) +@require_auth('accounting.view') +def list_accounts(): + """Get chart of accounts as a flat list (frontend builds tree). + + Returns all active accounts with parent_id for tree construction. + """ + conn = get_tenant_conn(g.tenant_id) + cur = conn.cursor() + + cur.execute(""" + SELECT a.id, a.code, a.name, a.parent_id, a.type, a.sat_code, + a.is_system, a.is_active, + COALESCE( + (SELECT SUM(l.debit) - SUM(l.credit) + FROM journal_entry_lines l + JOIN journal_entries e ON l.journal_entry_id = e.id + WHERE l.account_id = a.id AND e.status = 'posted'), 0 + ) as balance + FROM accounts a + WHERE a.is_active = true + ORDER BY a.code + """) + + accounts = [] + for r in cur.fetchall(): + accounts.append({ + 'id': r[0], 'code': r[1], 'name': r[2], + 'parent_id': r[3], 'type': r[4], 'sat_code': r[5], + 'is_system': r[6], 'is_active': r[7], + 'balance': float(r[8]) if r[8] else 0, + }) + + cur.close() + conn.close() + return jsonify({'data': accounts}) + + +@accounting_bp.route('/accounts', methods=['POST']) +@require_auth('accounting.create') +def create_account(): + """Create a new sub-account. + + Body: { + code: str (must be unique), + name: str, + parent_id: int (must exist), + type: 'activo' | 'pasivo' | 'capital' | 'ingreso' | 'costo' | 'gasto', + sat_code: str (optional) + } + + System accounts (is_system=true) cannot be created via API. + """ + data = request.get_json() or {} + code = data.get('code', '').strip() + name = data.get('name', '').strip() + parent_id = data.get('parent_id') + acct_type = data.get('type', '') + sat_code = data.get('sat_code') + + if not code or not name: + return jsonify({'error': 'code and name are required'}), 400 + + valid_types = ('activo', 'pasivo', 'capital', 'ingreso', 'costo', 'gasto') + if acct_type not in valid_types: + return jsonify({'error': f'type must be one of: {", ".join(valid_types)}'}), 400 + + conn = get_tenant_conn(g.tenant_id) + cur = conn.cursor() + + try: + # Check uniqueness + cur.execute("SELECT id FROM accounts WHERE code = %s", (code,)) + if cur.fetchone(): + return jsonify({'error': f'Account code {code} already exists'}), 409 + + # Validate parent exists + if parent_id: + cur.execute("SELECT id FROM accounts WHERE id = %s AND is_active = true", (parent_id,)) + if not cur.fetchone(): + return jsonify({'error': f'Parent account {parent_id} not found'}), 404 + + cur.execute(""" + INSERT INTO accounts (code, name, parent_id, type, sat_code, is_system, is_active) + VALUES (%s, %s, %s, %s, %s, false, true) + RETURNING id + """, (code, name, parent_id, acct_type, sat_code)) + acct_id = cur.fetchone()[0] + + log_action(conn, 'ACCOUNT_CREATED', 'account', acct_id, + new_value={'code': code, 'name': name, 'type': acct_type}) + + conn.commit() + cur.close() + conn.close() + + return jsonify({'id': acct_id, 'code': code, 'name': name, 'type': acct_type}), 201 + + except Exception as e: + conn.rollback() + cur.close() + conn.close() + return jsonify({'error': str(e)}), 500 + + +# ─── Journal Entries ──────────────────────────────── + +@accounting_bp.route('/entries', methods=['GET']) +@require_auth('accounting.view') +def list_entries(): + """List journal entries with filters. + + Query params: date_from, date_to, type, reference_type, is_auto, page, per_page + """ + conn = get_tenant_conn(g.tenant_id) + cur = conn.cursor() + + page = int(request.args.get('page', 1)) + per_page = min(int(request.args.get('per_page', 50)), 200) + + where_clauses = ["1=1"] + params = [] + + date_from = request.args.get('date_from') + date_to = request.args.get('date_to') + entry_type = request.args.get('type') + ref_type = request.args.get('reference_type') + is_auto = request.args.get('is_auto') + + if date_from: + where_clauses.append("je.date >= %s") + params.append(date_from) + if date_to: + where_clauses.append("je.date <= %s") + params.append(date_to) + if entry_type: + where_clauses.append("je.type = %s") + params.append(entry_type) + if ref_type: + where_clauses.append("je.reference_type = %s") + params.append(ref_type) + if is_auto is not None: + where_clauses.append("je.is_auto = %s") + params.append(is_auto.lower() in ('true', '1')) + + where = " AND ".join(where_clauses) + + cur.execute(f"SELECT count(*) FROM journal_entries je WHERE {where}", params) + total = cur.fetchone()[0] + + cur.execute(f""" + SELECT je.id, je.entry_number, je.date, je.type, je.description, + je.reference_type, je.reference_id, je.status, je.is_auto, + je.created_at, e.name as created_by_name, + (SELECT SUM(debit) FROM journal_entry_lines WHERE journal_entry_id = je.id) as total_debit + FROM journal_entries je + LEFT JOIN employees e ON je.created_by = e.id + WHERE {where} + ORDER BY je.date DESC, je.entry_number DESC + LIMIT %s OFFSET %s + """, params + [per_page, (page - 1) * per_page]) + + entries = [] + for r in cur.fetchall(): + entries.append({ + 'id': r[0], 'entry_number': r[1], + 'date': str(r[2]) if r[2] else None, + 'type': r[3], 'description': r[4], + 'reference_type': r[5], 'reference_id': r[6], + 'status': r[7], 'is_auto': r[8], + 'created_at': str(r[9]) if r[9] else None, + 'created_by_name': r[10], + 'total_amount': float(r[11]) if r[11] else 0, + }) + + cur.close() + conn.close() + + total_pages = (total + per_page - 1) // per_page + return jsonify({ + 'data': entries, + 'pagination': {'page': page, 'per_page': per_page, 'total': total, 'total_pages': total_pages} + }) + + +@accounting_bp.route('/entries/', methods=['GET']) +@require_auth('accounting.view') +def get_entry(entry_id): + """Get journal entry detail with all lines.""" + conn = get_tenant_conn(g.tenant_id) + cur = conn.cursor() + + cur.execute(""" + SELECT je.id, je.entry_number, je.date, je.type, je.description, + je.reference_type, je.reference_id, je.status, je.is_auto, + je.created_at, je.created_by, e.name as created_by_name + FROM journal_entries je + LEFT JOIN employees e ON je.created_by = e.id + WHERE je.id = %s + """, (entry_id,)) + row = cur.fetchone() + if not row: + cur.close(); conn.close() + return jsonify({'error': 'Journal entry not found'}), 404 + + entry = { + 'id': row[0], 'entry_number': row[1], + 'date': str(row[2]) if row[2] else None, + 'type': row[3], 'description': row[4], + 'reference_type': row[5], 'reference_id': row[6], + 'status': row[7], 'is_auto': row[8], + 'created_at': str(row[9]) if row[9] else None, + 'created_by': row[10], 'created_by_name': row[11], + } + + # Get lines + cur.execute(""" + SELECT l.id, l.account_id, a.code, a.name, l.debit, l.credit, l.description + FROM journal_entry_lines l + JOIN accounts a ON l.account_id = a.id + WHERE l.journal_entry_id = %s + ORDER BY l.id + """, (entry_id,)) + + entry['lines'] = [] + total_debit = 0 + total_credit = 0 + for r in cur.fetchall(): + debit = float(r[4]) if r[4] else 0 + credit = float(r[5]) if r[5] else 0 + total_debit += debit + total_credit += credit + entry['lines'].append({ + 'id': r[0], 'account_id': r[1], 'account_code': r[2], + 'account_name': r[3], 'debit': debit, 'credit': credit, + 'description': r[6], + }) + + entry['total_debit'] = round(total_debit, 2) + entry['total_credit'] = round(total_credit, 2) + + cur.close() + conn.close() + return jsonify(entry) + + +@accounting_bp.route('/entries', methods=['POST']) +@require_auth('accounting.create') +def create_entry(): + """Create a manual journal entry. + + Body: { + date: 'YYYY-MM-DD', + description: str, + lines: [{account_id: int, debit: float, credit: float, description: str}] + } + """ + data = request.get_json() or {} + + if not data.get('date'): + return jsonify({'error': 'date is required'}), 400 + if not data.get('lines') or len(data['lines']) < 2: + return jsonify({'error': 'At least 2 lines are required'}), 400 + + conn = get_tenant_conn(g.tenant_id) + + try: + entry_id = create_manual_entry(conn, data) + + log_action(conn, 'MANUAL_ENTRY', 'journal_entry', entry_id, + new_value={'date': data['date'], 'description': data.get('description', '')}) + + conn.commit() + conn.close() + return jsonify({'id': entry_id, 'status': 'posted'}), 201 + + except ValueError as e: + conn.rollback() + conn.close() + return jsonify({'error': str(e)}), 400 + except Exception as e: + conn.rollback() + conn.close() + return jsonify({'error': str(e)}), 500 + + +# ─── Financial Reports ────────────────────────────── + +@accounting_bp.route('/trial-balance', methods=['GET']) +@require_auth('accounting.view') +def trial_balance(): + """Balanza de comprobacion for a period. + + Query params: + year: int (default current year) + month: int (default current month) + + Returns each account with: saldo_inicial, cargos, abonos, saldo_final + """ + conn = get_tenant_conn(g.tenant_id) + cur = conn.cursor() + + year = int(request.args.get('year', date.today().year)) + month = int(request.args.get('month', date.today().month)) + + # Period start/end dates + period_start = f'{year}-{month:02d}-01' + if month == 12: + period_end = f'{year + 1}-01-01' + else: + period_end = f'{year}-{month + 1:02d}-01' + + cur.execute(""" + WITH initial_balances AS ( + SELECT l.account_id, + SUM(l.debit) as prev_debits, + SUM(l.credit) as prev_credits + FROM journal_entry_lines l + JOIN journal_entries e ON l.journal_entry_id = e.id + WHERE e.status = 'posted' AND e.date < %s + GROUP BY l.account_id + ), + period_movements AS ( + SELECT l.account_id, + SUM(l.debit) as period_debits, + SUM(l.credit) as period_credits + FROM journal_entry_lines l + JOIN journal_entries e ON l.journal_entry_id = e.id + WHERE e.status = 'posted' AND e.date >= %s AND e.date < %s + GROUP BY l.account_id + ) + SELECT a.id, a.code, a.name, a.type, a.parent_id, + COALESCE(ib.prev_debits, 0) as prev_debits, + COALESCE(ib.prev_credits, 0) as prev_credits, + COALESCE(pm.period_debits, 0) as period_debits, + COALESCE(pm.period_credits, 0) as period_credits + FROM accounts a + LEFT JOIN initial_balances ib ON a.id = ib.account_id + LEFT JOIN period_movements pm ON a.id = pm.account_id + WHERE a.is_active = true + AND (ib.account_id IS NOT NULL OR pm.account_id IS NOT NULL) + ORDER BY a.code + """, (period_start, period_start, period_end)) + + rows = [] + for r in cur.fetchall(): + prev_debits = float(r[5]) + prev_credits = float(r[6]) + saldo_inicial = prev_debits - prev_credits + cargos = float(r[7]) + abonos = float(r[8]) + saldo_final = saldo_inicial + cargos - abonos + + rows.append({ + 'account_id': r[0], 'code': r[1], 'name': r[2], + 'type': r[3], 'parent_id': r[4], + 'saldo_inicial': round(saldo_inicial, 2), + 'cargos': round(cargos, 2), + 'abonos': round(abonos, 2), + 'saldo_final': round(saldo_final, 2), + }) + + cur.close() + conn.close() + return jsonify({'data': rows, 'period': {'year': year, 'month': month}}) + + +@accounting_bp.route('/income-statement', methods=['GET']) +@require_auth('accounting.view') +def income_statement(): + """Estado de resultados for a period. + + Query params: year, month + + Groups: Ingresos (400), Costos (500), Gastos (600) + Result: Ingresos - Costos - Gastos = Utilidad/Perdida + """ + conn = get_tenant_conn(g.tenant_id) + cur = conn.cursor() + + year = int(request.args.get('year', date.today().year)) + month = int(request.args.get('month', date.today().month)) + + period_start = f'{year}-{month:02d}-01' + if month == 12: + period_end = f'{year + 1}-01-01' + else: + period_end = f'{year}-{month + 1:02d}-01' + + cur.execute(""" + SELECT a.id, a.code, a.name, a.type, + COALESCE(SUM(l.debit), 0) as total_debit, + COALESCE(SUM(l.credit), 0) as total_credit + FROM accounts a + LEFT JOIN journal_entry_lines l ON a.id = l.account_id + LEFT JOIN journal_entries e ON l.journal_entry_id = e.id + AND e.status = 'posted' + AND e.date >= %s AND e.date < %s + WHERE a.type IN ('ingreso', 'costo', 'gasto') + AND a.is_active = true + AND a.parent_id IS NOT NULL + GROUP BY a.id, a.code, a.name, a.type + HAVING COALESCE(SUM(l.debit), 0) != 0 OR COALESCE(SUM(l.credit), 0) != 0 + ORDER BY a.code + """, (period_start, period_end)) + + ingresos = [] + costos = [] + gastos = [] + total_ingresos = 0 + total_costos = 0 + total_gastos = 0 + + for r in cur.fetchall(): + debit = float(r[4]) + credit = float(r[5]) + # For income accounts: credit - debit = revenue + # For cost/expense accounts: debit - credit = expense + if r[3] == 'ingreso': + amount = round(credit - debit, 2) + total_ingresos += amount + ingresos.append({'code': r[1], 'name': r[2], 'amount': amount}) + elif r[3] == 'costo': + amount = round(debit - credit, 2) + total_costos += amount + costos.append({'code': r[1], 'name': r[2], 'amount': amount}) + elif r[3] == 'gasto': + amount = round(debit - credit, 2) + total_gastos += amount + gastos.append({'code': r[1], 'name': r[2], 'amount': amount}) + + utilidad_bruta = round(total_ingresos - total_costos, 2) + utilidad_neta = round(utilidad_bruta - total_gastos, 2) + + cur.close() + conn.close() + + return jsonify({ + 'period': {'year': year, 'month': month}, + 'ingresos': {'items': ingresos, 'total': round(total_ingresos, 2)}, + 'costos': {'items': costos, 'total': round(total_costos, 2)}, + 'utilidad_bruta': utilidad_bruta, + 'gastos': {'items': gastos, 'total': round(total_gastos, 2)}, + 'utilidad_neta': utilidad_neta, + }) + + +@accounting_bp.route('/balance-sheet', methods=['GET']) +@require_auth('accounting.view') +def balance_sheet(): + """Balance general as of a given date. + + Query params: date (YYYY-MM-DD, default today) + + Groups: Activo (100), Pasivo (200), Capital (300) + Validates: Activo = Pasivo + Capital + """ + conn = get_tenant_conn(g.tenant_id) + cur = conn.cursor() + + as_of = request.args.get('date', str(date.today())) + + cur.execute(""" + SELECT a.id, a.code, a.name, a.type, a.parent_id, + COALESCE(SUM(l.debit), 0) as total_debit, + COALESCE(SUM(l.credit), 0) as total_credit + FROM accounts a + LEFT JOIN journal_entry_lines l ON a.id = l.account_id + LEFT JOIN journal_entries e ON l.journal_entry_id = e.id + AND e.status = 'posted' + AND e.date <= %s + WHERE a.type IN ('activo', 'pasivo', 'capital') + AND a.is_active = true + AND a.parent_id IS NOT NULL + GROUP BY a.id, a.code, a.name, a.type, a.parent_id + HAVING COALESCE(SUM(l.debit), 0) != 0 OR COALESCE(SUM(l.credit), 0) != 0 + ORDER BY a.code + """, (as_of,)) + + activo = [] + pasivo = [] + capital_items = [] + total_activo = 0 + total_pasivo = 0 + total_capital = 0 + + for r in cur.fetchall(): + debit = float(r[5]) + credit = float(r[6]) + if r[3] == 'activo': + balance = round(debit - credit, 2) + total_activo += balance + activo.append({'code': r[1], 'name': r[2], 'balance': balance}) + elif r[3] == 'pasivo': + balance = round(credit - debit, 2) + total_pasivo += balance + pasivo.append({'code': r[1], 'name': r[2], 'balance': balance}) + elif r[3] == 'capital': + balance = round(credit - debit, 2) + total_capital += balance + capital_items.append({'code': r[1], 'name': r[2], 'balance': balance}) + + # Include current period net income in capital + # (Ingresos - Costos - Gastos for the current year) + cur.execute(""" + SELECT a.type, + COALESCE(SUM(l.debit), 0) as total_debit, + COALESCE(SUM(l.credit), 0) as total_credit + FROM accounts a + JOIN journal_entry_lines l ON a.id = l.account_id + JOIN journal_entries e ON l.journal_entry_id = e.id + WHERE e.status = 'posted' AND e.date <= %s + AND a.type IN ('ingreso', 'costo', 'gasto') + GROUP BY a.type + """, (as_of,)) + + net_income = 0 + for r in cur.fetchall(): + debit = float(r[1]) + credit = float(r[2]) + if r[0] == 'ingreso': + net_income += (credit - debit) + elif r[0] in ('costo', 'gasto'): + net_income -= (debit - credit) + + net_income = round(net_income, 2) + total_capital += net_income + capital_items.append({ + 'code': '320', 'name': 'Resultado del ejercicio', 'balance': net_income + }) + + cur.close() + conn.close() + + return jsonify({ + 'as_of': as_of, + 'activo': {'items': activo, 'total': round(total_activo, 2)}, + 'pasivo': {'items': pasivo, 'total': round(total_pasivo, 2)}, + 'capital': {'items': capital_items, 'total': round(total_capital, 2)}, + 'balanced': round(total_activo, 2) == round(total_pasivo + total_capital, 2), + }) + + +@accounting_bp.route('/aging', methods=['GET']) +@require_auth('accounting.view') +def aging_report(): + """Antiguedad de saldos (accounts receivable aging). + + Groups outstanding credit sales by age: + - Corriente (not yet due) + - 1-30 dias + - 31-60 dias + - 61-90 dias + - 90+ dias + """ + conn = get_tenant_conn(g.tenant_id) + cur = conn.cursor() + + cur.execute(""" + SELECT c.id, c.name, c.rfc, c.credit_limit, c.credit_balance, + s.id as sale_id, s.total, s.created_at, + EXTRACT(DAY FROM NOW() - s.created_at)::int as days_outstanding + FROM customers c + JOIN sales s ON s.customer_id = c.id + WHERE s.sale_type = 'credit' + AND s.status = 'completed' + AND c.credit_balance > 0 + ORDER BY c.name, s.created_at + """) + + customers = {} + for r in cur.fetchall(): + cust_id = r[0] + if cust_id not in customers: + customers[cust_id] = { + 'id': r[0], 'name': r[1], 'rfc': r[2], + 'credit_limit': float(r[3]) if r[3] else 0, + 'credit_balance': float(r[4]) if r[4] else 0, + 'corriente': 0, 'd1_30': 0, 'd31_60': 0, 'd61_90': 0, 'd90_plus': 0, + 'total': 0, + } + + amount = float(r[6]) if r[6] else 0 + days = r[8] or 0 + + if days <= 0: + customers[cust_id]['corriente'] += amount + elif days <= 30: + customers[cust_id]['d1_30'] += amount + elif days <= 60: + customers[cust_id]['d31_60'] += amount + elif days <= 90: + customers[cust_id]['d61_90'] += amount + else: + customers[cust_id]['d90_plus'] += amount + + customers[cust_id]['total'] += amount + + result = list(customers.values()) + # Round all amounts + for c in result: + for key in ('corriente', 'd1_30', 'd31_60', 'd61_90', 'd90_plus', 'total'): + c[key] = round(c[key], 2) + + # Totals row + totals = { + 'corriente': round(sum(c['corriente'] for c in result), 2), + 'd1_30': round(sum(c['d1_30'] for c in result), 2), + 'd31_60': round(sum(c['d31_60'] for c in result), 2), + 'd61_90': round(sum(c['d61_90'] for c in result), 2), + 'd90_plus': round(sum(c['d90_plus'] for c in result), 2), + 'total': round(sum(c['total'] for c in result), 2), + } + + cur.close() + conn.close() + return jsonify({'data': result, 'totals': totals}) + + +# ─── Fiscal Periods ──────────────────────────────── + +@accounting_bp.route('/periods', methods=['GET']) +@require_auth('accounting.view') +def list_periods(): + """List fiscal periods with status.""" + conn = get_tenant_conn(g.tenant_id) + cur = conn.cursor() + + cur.execute(""" + SELECT fp.id, fp.year, fp.month, fp.status, + fp.closed_by, e.name as closed_by_name, fp.closed_at + FROM fiscal_periods fp + LEFT JOIN employees e ON fp.closed_by = e.id + ORDER BY fp.year DESC, fp.month DESC + """) + + periods = [] + for r in cur.fetchall(): + periods.append({ + 'id': r[0], 'year': r[1], 'month': r[2], + 'status': r[3], 'closed_by': r[4], + 'closed_by_name': r[5], + 'closed_at': str(r[6]) if r[6] else None, + }) + + cur.close() + conn.close() + return jsonify({'data': periods}) + + +@accounting_bp.route('/periods/close', methods=['POST']) +@require_auth('accounting.create') +def close_period(): + """Close a fiscal period. Owner only. + + Body: {year: int, month: int} + + Closing a period: + 1. Validates the period is currently open + 2. Prevents future journal entries in that period + 3. Only the owner can close periods + """ + if g.employee_role != 'owner': + return jsonify({'error': 'Only the owner can close fiscal periods'}), 403 + + data = request.get_json() or {} + year = data.get('year') + month = data.get('month') + + if not year or not month: + return jsonify({'error': 'year and month are required'}), 400 + + conn = get_tenant_conn(g.tenant_id) + cur = conn.cursor() + + try: + # Check if period already exists + cur.execute(""" + SELECT id, status FROM fiscal_periods WHERE year = %s AND month = %s + """, (year, month)) + row = cur.fetchone() + + if row and row[1] == 'closed': + return jsonify({'error': f'Period {year}-{month:02d} is already closed'}), 409 + + if row: + # Update existing + cur.execute(""" + UPDATE fiscal_periods + SET status = 'closed', closed_by = %s, closed_at = NOW() + WHERE id = %s + """, (g.employee_id, row[0])) + period_id = row[0] + else: + # Create and close + cur.execute(""" + INSERT INTO fiscal_periods (year, month, status, closed_by, closed_at) + VALUES (%s, %s, 'closed', %s, NOW()) + RETURNING id + """, (year, month, g.employee_id)) + period_id = cur.fetchone()[0] + + log_action(conn, 'PERIOD_CLOSED', 'fiscal_period', period_id, + new_value={'year': year, 'month': month}) + + conn.commit() + cur.close() + conn.close() + + return jsonify({ + 'id': period_id, + 'year': year, + 'month': month, + 'status': 'closed', + 'message': f'Period {year}-{month:02d} closed successfully' + }) + + except Exception as e: + conn.rollback() + cur.close() + conn.close() + return jsonify({'error': str(e)}), 500