diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,3 +1,4 @@ + from flask import Flask, render_template_string, request, redirect, url_for, jsonify, flash, abort, Response, session, g import json import os @@ -168,29 +169,33 @@ def generate_receipt_html(transaction): table_headers = """ № Наименование + Тип цены Кол-во Цена Скидка Сумма """ - total_colspan = 5 + total_colspan = 6 else: table_headers = """ № Наименование + Тип цены Кол-во Цена Сумма """ - total_colspan = 4 + total_colspan = 5 items_html = "" for i, item in enumerate(transaction['items']): discount_cell = f"""{format_currency_py(item.get('discount_per_item', '0'))}""" if has_discounts else "" + price_type_display = item.get('price_type', '') items_html += f""" {i + 1} {item['name']} + {price_type_display} {item['quantity']} {format_currency_py(item['price_at_sale'])} {discount_cell} @@ -200,6 +205,23 @@ def generate_receipt_html(transaction): total_amount_str = format_currency_py(transaction['total_amount']) + delivery_html = "" + if to_decimal(transaction.get('delivery_cost', '0')) > 0: + delivery_html = f""" + + Доставка: + {format_currency_py(transaction['delivery_cost'])} ₸ + + """ + + note_html = "" + if transaction.get('note'): + note_html = f""" +
+ Заметка: {transaction['note']} +
+ """ + return f""" @@ -241,6 +263,7 @@ def generate_receipt_html(transaction): + {note_html}

Товарная накладная № {transaction['id'][:8]}

от {datetime.fromisoformat(transaction['timestamp']).strftime('%d.%m.%Y %H:%M')}

@@ -252,6 +275,7 @@ def generate_receipt_html(transaction): {items_html} + {delivery_html} @@ -321,32 +345,38 @@ def inventory_management(): variants = [] variant_names = request.form.getlist('variant_name[]') - variant_prices = request.form.getlist('variant_price[]') + variant_price_general = request.form.getlist('variant_price_general[]') + variant_price_min = request.form.getlist('variant_price_min[]') + variant_price_wholesale = request.form.getlist('variant_price_wholesale[]') variant_cost_prices = request.form.getlist('variant_cost_price[]') variant_stocks = request.form.getlist('variant_stock[]') variant_image_urls = request.form.getlist('variant_image_url[]') - variant_items_per_packs = request.form.getlist('variant_items_per_pack[]') for i in range(len(variant_names)): v_name = variant_names[i].strip() if not v_name: continue if is_admin: - v_price = str(to_decimal(variant_prices[i])) if i < len(variant_prices) else '0.00' + v_price_gen = str(to_decimal(variant_price_general[i])) if i < len(variant_price_general) else '0.00' + v_price_min = str(to_decimal(variant_price_min[i])) if i < len(variant_price_min) else '0.00' + v_price_whl = str(to_decimal(variant_price_wholesale[i])) if i < len(variant_price_wholesale) else '0.00' v_cost = str(to_decimal(variant_cost_prices[i])) if i < len(variant_cost_prices) else '0.00' else: - v_price = '0.00' + v_price_gen = '0.00' + v_price_min = '0.00' + v_price_whl = '0.00' v_cost = '0.00' variants.append({ 'id': uuid.uuid4().hex, 'option_name': "Вариант", 'option_value': v_name, - 'price': v_price, + 'price_general': v_price_gen, + 'price_min': v_price_min, + 'price_wholesale': v_price_whl, 'cost_price': v_cost, 'stock': int(to_decimal(variant_stocks[i], '0')), - 'image_url': variant_image_urls[i] if i < len(variant_image_urls) else '', - 'items_per_pack': int(variant_items_per_packs[i] if i < len(variant_items_per_packs) and variant_items_per_packs[i] else 1) + 'image_url': variant_image_urls[i] if i < len(variant_image_urls) else '' }) if not variants: @@ -382,11 +412,11 @@ def inventory_management(): for variant in product.get('variants', []): stock = variant.get('stock', 0) cost_price = to_decimal(variant.get('cost_price', '0')) - price = to_decimal(variant.get('price', '0')) + price_general = to_decimal(variant.get('price_general', '0')) total_units += stock total_cost_value += Decimal(stock) * cost_price - total_retail_value += Decimal(stock) * price + total_retail_value += Decimal(stock) * price_general potential_profit = total_retail_value - total_cost_value @@ -404,17 +434,21 @@ def inventory_management(): def update_prices(): inventory = load_json_data('inventory') variant_ids = request.form.getlist('variant_id[]') - prices = request.form.getlist('price[]') + prices_gen = request.form.getlist('price_general[]') + prices_min = request.form.getlist('price_min[]') + prices_whl = request.form.getlist('price_wholesale[]') cost_prices = request.form.getlist('cost_price[]') - updates = {vid: (prices[i], cost_prices[i]) for i, vid in enumerate(variant_ids)} + updates = {vid: (prices_gen[i], prices_min[i], prices_whl[i], cost_prices[i]) for i, vid in enumerate(variant_ids)} for p in inventory: updated = False for v in p.get('variants', []): if v.get('id') in updates: - v['price'] = str(to_decimal(updates[v['id']][0])) - v['cost_price'] = str(to_decimal(updates[v['id']][1])) + v['price_general'] = str(to_decimal(updates[v['id']][0])) + v['price_min'] = str(to_decimal(updates[v['id']][1])) + v['price_wholesale'] = str(to_decimal(updates[v['id']][2])) + v['cost_price'] = str(to_decimal(updates[v['id']][3])) updated = True if updated: p['timestamp_updated'] = get_current_time().isoformat() @@ -450,11 +484,12 @@ def edit_product(product_id): new_variants = [] variant_ids = request.form.getlist('variant_id[]') variant_names = request.form.getlist('variant_name[]') - variant_prices = request.form.getlist('variant_price[]') + variant_price_general = request.form.getlist('variant_price_general[]') + variant_price_min = request.form.getlist('variant_price_min[]') + variant_price_wholesale = request.form.getlist('variant_price_wholesale[]') variant_cost_prices = request.form.getlist('variant_cost_price[]') variant_stocks = request.form.getlist('variant_stock[]') variant_image_urls = request.form.getlist('variant_image_url[]') - variant_items_per_packs = request.form.getlist('variant_items_per_pack[]') for j in range(len(variant_ids)): v_name = variant_names[j].strip() @@ -463,11 +498,12 @@ def edit_product(product_id): 'id': variant_ids[j] or uuid.uuid4().hex, 'option_name': "Вариант", 'option_value': v_name, - 'price': str(to_decimal(variant_prices[j])), + 'price_general': str(to_decimal(variant_price_general[j])), + 'price_min': str(to_decimal(variant_price_min[j])), + 'price_wholesale': str(to_decimal(variant_price_wholesale[j])), 'cost_price': str(to_decimal(variant_cost_prices[j])), 'stock': int(to_decimal(variant_stocks[j], '0')), - 'image_url': variant_image_urls[j] if j < len(variant_image_urls) else '', - 'items_per_pack': int(variant_items_per_packs[j] if j < len(variant_items_per_packs) and variant_items_per_packs[j] else 1) + 'image_url': variant_image_urls[j] if j < len(variant_image_urls) else '' }) inventory[i]['variants'] = new_variants @@ -626,6 +662,8 @@ def complete_sale(): kassa_id = data.get('kassaId') shift_id = data.get('shiftId') payment_method = data.get('paymentMethod', 'cash') + delivery_cost = to_decimal(data.get('deliveryCost', '0')) + note = data.get('note', '').strip() if not cart or not user_id or not kassa_id or not shift_id: return jsonify({'success': False, 'message': 'Неполные данные для продажи. Начните смену.'}), 400 @@ -644,7 +682,7 @@ def complete_sale(): total_amount = Decimal('0.00') inventory_updates = {} - for item_id, cart_item in cart.items(): + for cart_key, cart_item in cart.items(): if cart_item.get('isCustom'): price_at_sale = to_decimal(cart_item.get('price', '0')) quantity_sold = cart_item.get('quantity', 1) @@ -652,11 +690,12 @@ def complete_sale(): total_amount += item_total sale_items.append({ 'product_id': None, - 'variant_id': item_id, + 'variant_id': cart_key, 'name': cart_item.get('productName', 'Товар без штрихкода'), 'barcode': 'CUSTOM', 'quantity': quantity_sold, 'price_at_sale': str(price_at_sale), + 'price_type': 'Свободная цена', 'cost_price_at_sale': '0.00', 'discount_per_item': '0.00', 'total': str(item_total), @@ -664,7 +703,7 @@ def complete_sale(): }) continue - variant_id = item_id + variant_id = cart_item['variantId'] product = find_item_by_field(inventory, 'id', cart_item['productId']) if not product: return jsonify({'success': False, 'message': f"Товар с ID {cart_item['productId']} не найден."}), 404 @@ -674,12 +713,14 @@ def complete_sale(): return jsonify({'success': False, 'message': f"Вариант товара с ID {variant_id} не найден."}), 404 quantity_sold = cart_item['quantity'] - current_stock = variant.get('stock', 0) + + current_stock_info = inventory_updates.get(variant_id, {'new_stock': variant.get('stock', 0)}) + current_stock = current_stock_info['new_stock'] if quantity_sold > current_stock: return jsonify({'success': False, 'message': f"Недостаточно товара '{product['name']} ({variant['option_value']})'. В наличии: {current_stock}"}), 400 - price_at_sale = to_decimal(variant.get('price', '0')) + price_at_sale = to_decimal(cart_item.get('price', '0')) cost_price_at_sale = to_decimal(variant.get('cost_price', '0')) discount_per_item = to_decimal(cart_item.get('discount', '0')) @@ -697,12 +738,14 @@ def complete_sale(): 'barcode': product.get('barcode'), 'quantity': quantity_sold, 'price_at_sale': str(price_at_sale), + 'price_type': cart_item.get('priceTypeLabel', ''), 'cost_price_at_sale': str(cost_price_at_sale), 'discount_per_item': str(discount_per_item), 'total': str(item_total) }) inventory_updates[variant_id] = {'product_id': product['id'], 'new_stock': current_stock - quantity_sold} + total_amount += delivery_cost now_iso = get_current_time().isoformat() new_transaction = { @@ -718,6 +761,8 @@ def complete_sale(): 'shift_id': shift_id, 'items': sale_items, 'total_amount': str(total_amount), + 'delivery_cost': str(delivery_cost), + 'note': note, 'payment_method': payment_method } @@ -857,6 +902,8 @@ def edit_transaction(transaction_id): item['total'] = str(item_total) new_total_amount += to_decimal(item['total']) + + new_total_amount += to_decimal(original_transaction.get('delivery_cost', '0')) transactions[transaction_index]['total_amount'] = str(new_total_amount) @@ -1141,7 +1188,6 @@ def item_movement_report(): html = BASE_TEMPLATE.replace('__TITLE__', "Движение товаров").replace('__CONTENT__', ITEM_MOVEMENT_CONTENT).replace('__SCRIPTS__', ITEM_MOVEMENT_SCRIPTS) return render_template_string(html, inventory=inventory, movements=movements, product_id=product_id, variant_id=variant_id, selected_product=selected_product, selected_variant=selected_variant) - @app.route('/reports/product_roi') @admin_required def product_roi_report(): @@ -1885,13 +1931,13 @@ BASE_TEMPLATE = """ :root { --sidebar-width: 250px; } body { background-color: #f8f9fa; } .sidebar { position: fixed; top: 0; left: 0; width: var(--sidebar-width); height: 100vh; background-color: #343a40; padding-top: 1rem; } - .sidebar .nav-link { color: rgba(255,255,255,.75); } - .sidebar .nav-link:hover, .sidebar .nav-link.active { color: #fff; } + .sidebar .nav-link { color: rgba(255,255,255,.75); font-size: 1.1rem; padding: 12px 20px;} + .sidebar .nav-link:hover, .sidebar .nav-link.active { color: #fff; background-color: rgba(255,255,255,0.1); } .main-content { margin-left: var(--sidebar-width); padding: 1.5rem; } @media (max-width: 992px) { .sidebar { transform: translateX(calc(-1 * var(--sidebar-width))); transition: transform 0.3s ease-in-out; z-index: 1040; } .sidebar.active { transform: translateX(0); } - .main-content { margin-left: 0; } + .main-content { margin-left: 0; padding: 10px; } } [data-bs-theme="dark"] body { background-color: #212529; color: #dee2e6; } [data-bs-theme="dark"] .card, [data-bs-theme="dark"] .modal-content, [data-bs-theme="dark"] .list-group-item, [data-bs-theme="dark"] .table, [data-bs-theme="dark"] .accordion-item { background-color: #343a40; } @@ -1900,11 +1946,13 @@ BASE_TEMPLATE = """ [data-bs-theme="dark"] .accordion-button::after { filter: invert(1) grayscale(100) brightness(200%); } [data-bs-theme="dark"] .table-hover>tbody>tr:hover>* { color: var(--bs-table-hover-color); background-color: rgba(255, 255, 255, 0.075); } [data-bs-theme="dark"] .text-dark { color: #dee2e6 !important; } - .product-card { cursor: pointer; } - .product-card:hover { border-color: var(--bs-primary); } + .product-card { cursor: pointer; padding: 15px; margin-bottom: 10px; border-radius: 10px;} + .product-card:hover { border-color: var(--bs-primary); box-shadow: 0 4px 8px rgba(0,0,0,0.1); } .payback-positive { color: var(--bs-success); } .payback-negative { color: var(--bs-danger); } .payback-zero { color: var(--bs-secondary); } + .btn, .form-control, .form-select, .input-group-text { min-height: 48px; font-size: 16px; } + .list-group-item { padding: 15px 20px; } @@ -1990,9 +2038,9 @@ BASE_TEMPLATE = """
-
- - +
+ +
+ +
Итого к оплате: {total_amount_str} ₸