Kgshop commited on
Commit
62a46f0
·
verified ·
1 Parent(s): 728ec43

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +117 -7
app.py CHANGED
@@ -169,7 +169,7 @@ def generate_receipt_html(transaction):
169
  if has_discounts:
170
  table_headers = """
171
  <th style="text-align: center; width: 5%; white-space: nowrap;">№</th>
172
- <th style="text-align: left; white-space: nowrap;">Наименование</th>
173
  <th style="text-align: right; white-space: nowrap;">Кол-во</th>
174
  <th style="text-align: right; white-space: nowrap;">Цена</th>
175
  <th style="text-align: right; white-space: nowrap;">Скидка</th>
@@ -179,7 +179,7 @@ def generate_receipt_html(transaction):
179
  else:
180
  table_headers = """
181
  <th style="text-align: center; width: 5%; white-space: nowrap;">№</th>
182
- <th style="text-align: left; white-space: nowrap;">Наименование</th>
183
  <th style="text-align: right; white-space: nowrap;">Кол-во</th>
184
  <th style="text-align: right; white-space: nowrap;">Цена</th>
185
  <th style="text-align: right; white-space: nowrap;">Сумма</th>
@@ -194,7 +194,7 @@ def generate_receipt_html(transaction):
194
  items_html += f"""
195
  <tr>
196
  <td style="text-align: center; white-space: nowrap;">{i + 1}</td>
197
- <td style="white-space: nowrap;">{item['name']}</td>
198
  <td style="text-align: right; white-space: nowrap;">{item['quantity']}</td>
199
  <td style="text-align: right; white-space: nowrap;">{format_currency_py(item['price_at_sale'])}</td>
200
  {discount_cell}
@@ -266,9 +266,11 @@ def generate_receipt_html(transaction):
266
  .details-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; font-size: 14px; }}
267
  table {{ width: 100%; line-height: inherit; text-align: left; border-collapse: collapse; }}
268
  table th {{ background: #f2f2f2; font-weight: bold; padding: 8px; border-bottom: 2px solid #ddd; white-space: nowrap; }}
 
269
  table td {{ padding: 8px; border-bottom: 1px solid #eee; white-space: nowrap; }}
 
270
  table tr.total td {{ font-weight: bold; font-size: 1.1em; border-top: 2px solid #ddd; }}
271
- .footer-info {{ font-size: 14px; margin-top: 20px; }}
272
  .print-hide {{ display: block; }}
273
  @media print {{
274
  body {{ margin: 0; padding: 0; background-color: #fff; }}
@@ -307,8 +309,13 @@ def generate_receipt_html(transaction):
307
  </table>
308
  {note_html}
309
  <div class="footer-info">
310
- <p>Способ оплаты: {'Наличные' if transaction['payment_method'] == 'cash' else 'Карта'}</p>
311
- <p>Кассир: {transaction['user_name']}</p>
 
 
 
 
 
312
  </div>
313
  {links_html}
314
  </div>
@@ -1591,6 +1598,39 @@ def manage_link():
1591
  upload_db_to_hf('links')
1592
  return redirect(url_for('admin_panel'))
1593
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1594
  @app.route('/cashier_login', methods=['GET', 'POST'])
1595
  def cashier_login():
1596
  if request.method == 'POST':
@@ -2118,6 +2158,12 @@ BASE_TEMPLATE = """
2118
  <button class="btn btn-dark d-lg-none" id="sidebar-toggle"><i class="fas fa-bars"></i></button>
2119
  <h2 class="h3 mb-0">__TITLE__</h2>
2120
  <div class="d-flex align-items-center">
 
 
 
 
 
 
2121
  {% if not session.admin_logged_in %}
2122
  <a href="{{ url_for('admin_login') }}" class="btn btn-outline-primary btn-sm me-3"><i class="fas fa-user-shield me-1"></i>Войти как админ</a>
2123
  {% endif %}
@@ -2208,6 +2254,21 @@ BASE_TEMPLATE = """
2208
  </div>
2209
  </div>
2210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2211
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
2212
  <script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
2213
  <script>
@@ -2505,6 +2566,7 @@ document.addEventListener('DOMContentLoaded', () => {
2505
 
2506
  let audioCtx;
2507
  let isScannerPaused = false;
 
2508
 
2509
  const variantSelectModal = new bootstrap.Modal(document.getElementById('variantSelectModal'));
2510
  const priceSelectModal = new bootstrap.Modal(document.getElementById('priceSelectModal'));
@@ -2987,14 +3049,59 @@ document.addEventListener('DOMContentLoaded', () => {
2987
  document.getElementById('scan-btn').addEventListener('click', startScanner);
2988
  document.getElementById('stop-scan-btn').addEventListener('click', stopScanner);
2989
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2990
  const updateSessionUI = () => {
2991
  const sessionInfoEl = document.getElementById('session-info');
2992
  const shiftControlsEl = document.getElementById('shift-controls');
2993
 
 
 
2994
  if (session.shift) {
2995
  sessionInfoEl.innerHTML = `Кассир: <strong>${session.cashier.name}</strong> | Касса: <strong>${session.kassa.name}</strong>`;
2996
  shiftControlsEl.innerHTML = '<button id="end-shift-btn" class="btn btn-danger btn-sm">Закончить смену</button>';
2997
  document.getElementById('end-shift-btn').addEventListener('click', handleEndShift);
 
 
2998
  } else if (session.cashier) {
2999
  sessionInfoEl.innerHTML = `Кассир: <strong>${session.cashier.name}</strong>. Смена не начата.`;
3000
  shiftControlsEl.innerHTML = '<button id="start-shift-btn" class="btn btn-success btn-sm">Начать смену</button>';
@@ -3002,9 +3109,12 @@ document.addEventListener('DOMContentLoaded', () => {
3002
  document.getElementById('start-shift-cashier-name').textContent = session.cashier.name;
3003
  startShiftModal.show();
3004
  });
 
 
3005
  } else {
3006
  sessionInfoEl.innerHTML = 'Нет активного кассира';
3007
  shiftControlsEl.innerHTML = '';
 
3008
  }
3009
  };
3010
 
@@ -4198,7 +4308,7 @@ ITEM_MOVEMENT_CONTENT = """
4198
  {% else %}<span class="badge bg-secondary">{{ m.type }}</span>{% endif %}
4199
  </td>
4200
  <td>{{ m.variant_name }}</td>
4201
- <td>{{ m.doc_id }}</td>
4202
  <td>{{ m.user }}</td>
4203
  <td class="text-end">{{ format_currency_py(m.price) }} ₸</td>
4204
  <td class="text-end fw-bold {% if m.qty > 0 %}text-success{% elif m.qty < 0 %}text-danger{% endif %}">
 
169
  if has_discounts:
170
  table_headers = """
171
  <th style="text-align: center; width: 5%; white-space: nowrap;">№</th>
172
+ <th class="name-col" style="text-align: left;">Наименование</th>
173
  <th style="text-align: right; white-space: nowrap;">Кол-во</th>
174
  <th style="text-align: right; white-space: nowrap;">Цена</th>
175
  <th style="text-align: right; white-space: nowrap;">Скидка</th>
 
179
  else:
180
  table_headers = """
181
  <th style="text-align: center; width: 5%; white-space: nowrap;">№</th>
182
+ <th class="name-col" style="text-align: left;">Наименование</th>
183
  <th style="text-align: right; white-space: nowrap;">Кол-во</th>
184
  <th style="text-align: right; white-space: nowrap;">Цена</th>
185
  <th style="text-align: right; white-space: nowrap;">Сумма</th>
 
194
  items_html += f"""
195
  <tr>
196
  <td style="text-align: center; white-space: nowrap;">{i + 1}</td>
197
+ <td class="name-col">{item['name']}</td>
198
  <td style="text-align: right; white-space: nowrap;">{item['quantity']}</td>
199
  <td style="text-align: right; white-space: nowrap;">{format_currency_py(item['price_at_sale'])}</td>
200
  {discount_cell}
 
266
  .details-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; font-size: 14px; }}
267
  table {{ width: 100%; line-height: inherit; text-align: left; border-collapse: collapse; }}
268
  table th {{ background: #f2f2f2; font-weight: bold; padding: 8px; border-bottom: 2px solid #ddd; white-space: nowrap; }}
269
+ table th.name-col {{ white-space: normal; }}
270
  table td {{ padding: 8px; border-bottom: 1px solid #eee; white-space: nowrap; }}
271
+ table td.name-col {{ white-space: normal; word-wrap: break-word; max-width: 250px; min-width: 150px; }}
272
  table tr.total td {{ font-weight: bold; font-size: 1.1em; border-top: 2px solid #ddd; }}
273
+ .footer-info {{ font-size: 14px; margin-top: 20px; display: flex; justify-content: space-between; align-items: flex-end; }}
274
  .print-hide {{ display: block; }}
275
  @media print {{
276
  body {{ margin: 0; padding: 0; background-color: #fff; }}
 
309
  </table>
310
  {note_html}
311
  <div class="footer-info">
312
+ <div>
313
+ <p style="margin: 2px 0;">Способ оплаты: {'Наличные' if transaction['payment_method'] == 'cash' else 'Карта'}</p>
314
+ <p style="margin: 2px 0;">Кассир: {transaction['user_name']}</p>
315
+ </div>
316
+ <div>
317
+ <img src="https://huggingface.co/spaces/metastore/tech/resolve/main/1778729143842-019e2483-619b-7600-b6f1-03c567356039.png" style="width: 80px; height: auto;">
318
+ </div>
319
  </div>
320
  {links_html}
321
  </div>
 
1598
  upload_db_to_hf('links')
1599
  return redirect(url_for('admin_panel'))
1600
 
1601
+ @app.route('/api/notifications/stock', methods=['GET'])
1602
+ def get_stock_notifications():
1603
+ user_id = request.args.get('user_id')
1604
+ if not user_id:
1605
+ return jsonify({'success': False, 'message': 'Missing user_id'}), 400
1606
+ users = load_json_data('users')
1607
+ user = find_item_by_field(users, 'id', user_id)
1608
+ if not user:
1609
+ return jsonify({'success': False, 'message': 'User not found'}), 404
1610
+
1611
+ last_view = user.get('last_stock_view', '2000-01-01T00:00:00')
1612
+ stock_history = load_json_data('stock_history')
1613
+ new_stocks = [
1614
+ s for s in stock_history
1615
+ if s.get('type') == 'stock_in' and s.get('timestamp', '') > last_view
1616
+ ]
1617
+ return jsonify({'success': True, 'count': len(new_stocks), 'items': new_stocks[-50:]})
1618
+
1619
+ @app.route('/api/notifications/stock/view', methods=['POST'])
1620
+ def mark_stock_viewed():
1621
+ data = request.json
1622
+ user_id = data.get('user_id')
1623
+ if not user_id:
1624
+ return jsonify({'success': False})
1625
+ users = load_json_data('users')
1626
+ for i, u in enumerate(users):
1627
+ if u.get('id') == user_id:
1628
+ users[i]['last_stock_view'] = get_current_time().isoformat()
1629
+ save_json_data('users', users)
1630
+ upload_db_to_hf('users')
1631
+ return jsonify({'success': True})
1632
+ return jsonify({'success': False})
1633
+
1634
  @app.route('/cashier_login', methods=['GET', 'POST'])
1635
  def cashier_login():
1636
  if request.method == 'POST':
 
2158
  <button class="btn btn-dark d-lg-none" id="sidebar-toggle"><i class="fas fa-bars"></i></button>
2159
  <h2 class="h3 mb-0">__TITLE__</h2>
2160
  <div class="d-flex align-items-center">
2161
+ <div id="stock-notification-container" class="me-3" style="display:none;">
2162
+ <button id="stock-notification-btn" class="btn btn-outline-info btn-sm position-relative" data-bs-toggle="modal" data-bs-target="#stockNotificationModal">
2163
+ <i class="fas fa-bell"></i>
2164
+ <span id="stock-notification-badge" class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="display:none; font-size: 0.65em;">0</span>
2165
+ </button>
2166
+ </div>
2167
  {% if not session.admin_logged_in %}
2168
  <a href="{{ url_for('admin_login') }}" class="btn btn-outline-primary btn-sm me-3"><i class="fas fa-user-shield me-1"></i>Войти как админ</a>
2169
  {% endif %}
 
2254
  </div>
2255
  </div>
2256
 
2257
+ <div class="modal fade" id="stockNotificationModal" tabindex="-1">
2258
+ <div class="modal-dialog modal-dialog-scrollable">
2259
+ <div class="modal-content">
2260
+ <div class="modal-header">
2261
+ <h5 class="modal-title">Новые поступления</h5>
2262
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
2263
+ </div>
2264
+ <div class="modal-body">
2265
+ <ul id="stock-notification-list" class="list-group">
2266
+ </ul>
2267
+ </div>
2268
+ </div>
2269
+ </div>
2270
+ </div>
2271
+
2272
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
2273
  <script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
2274
  <script>
 
2566
 
2567
  let audioCtx;
2568
  let isScannerPaused = false;
2569
+ let stockInterval;
2570
 
2571
  const variantSelectModal = new bootstrap.Modal(document.getElementById('variantSelectModal'));
2572
  const priceSelectModal = new bootstrap.Modal(document.getElementById('priceSelectModal'));
 
3049
  document.getElementById('scan-btn').addEventListener('click', startScanner);
3050
  document.getElementById('stop-scan-btn').addEventListener('click', stopScanner);
3051
 
3052
+ const checkStockNotifications = () => {
3053
+ if (!session.cashier) {
3054
+ document.getElementById('stock-notification-container').style.display = 'none';
3055
+ return;
3056
+ }
3057
+ document.getElementById('stock-notification-container').style.display = 'block';
3058
+ fetch(`/api/notifications/stock?user_id=${session.cashier.id}`)
3059
+ .then(r => r.json())
3060
+ .then(data => {
3061
+ if (data.success) {
3062
+ const badge = document.getElementById('stock-notification-badge');
3063
+ const list = document.getElementById('stock-notification-list');
3064
+ if (data.count > 0) {
3065
+ badge.textContent = data.count;
3066
+ badge.style.display = 'block';
3067
+ let itemsHtml = data.items.reverse().map(item => `<li class="list-group-item"><strong>${item.product_name}</strong> (${item.variant_name}) - ${item.quantity} шт.<br><small class="text-muted">${new Date(item.timestamp).toLocaleString('ru-RU')}</small></li>`).join('');
3068
+ list.innerHTML = itemsHtml;
3069
+ } else {
3070
+ badge.style.display = 'none';
3071
+ list.innerHTML = '<li class="list-group-item text-center text-muted">Нет новых поступлений</li>';
3072
+ }
3073
+ }
3074
+ });
3075
+ };
3076
+
3077
+ const stockNotificationModalEl = document.getElementById('stockNotificationModal');
3078
+ if (stockNotificationModalEl) {
3079
+ stockNotificationModalEl.addEventListener('hidden.bs.modal', () => {
3080
+ if (session.cashier && document.getElementById('stock-notification-badge').style.display === 'block') {
3081
+ fetch('/api/notifications/stock/view', {
3082
+ method: 'POST',
3083
+ headers: {'Content-Type': 'application/json'},
3084
+ body: JSON.stringify({user_id: session.cashier.id})
3085
+ }).then(() => {
3086
+ document.getElementById('stock-notification-badge').style.display = 'none';
3087
+ checkStockNotifications();
3088
+ });
3089
+ }
3090
+ });
3091
+ }
3092
+
3093
  const updateSessionUI = () => {
3094
  const sessionInfoEl = document.getElementById('session-info');
3095
  const shiftControlsEl = document.getElementById('shift-controls');
3096
 
3097
+ clearInterval(stockInterval);
3098
+
3099
  if (session.shift) {
3100
  sessionInfoEl.innerHTML = `Кассир: <strong>${session.cashier.name}</strong> | Касса: <strong>${session.kassa.name}</strong>`;
3101
  shiftControlsEl.innerHTML = '<button id="end-shift-btn" class="btn btn-danger btn-sm">Закончить смену</button>';
3102
  document.getElementById('end-shift-btn').addEventListener('click', handleEndShift);
3103
+ checkStockNotifications();
3104
+ stockInterval = setInterval(checkStockNotifications, 30000);
3105
  } else if (session.cashier) {
3106
  sessionInfoEl.innerHTML = `Кассир: <strong>${session.cashier.name}</strong>. Смена не начата.`;
3107
  shiftControlsEl.innerHTML = '<button id="start-shift-btn" class="btn btn-success btn-sm">Начать смену</button>';
 
3109
  document.getElementById('start-shift-cashier-name').textContent = session.cashier.name;
3110
  startShiftModal.show();
3111
  });
3112
+ checkStockNotifications();
3113
+ stockInterval = setInterval(checkStockNotifications, 30000);
3114
  } else {
3115
  sessionInfoEl.innerHTML = 'Нет активного кассира';
3116
  shiftControlsEl.innerHTML = '';
3117
+ document.getElementById('stock-notification-container').style.display = 'none';
3118
  }
3119
  };
3120
 
 
4308
  {% else %}<span class="badge bg-secondary">{{ m.type }}</span>{% endif %}
4309
  </td>
4310
  <td>{{ m.variant_name }}</td>
4311
+ <td>{{ m.doc_id }}</td>
4312
  <td>{{ m.user }}</td>
4313
  <td class="text-end">{{ format_currency_py(m.price) }} ₸</td>
4314
  <td class="text-end fw-bold {% if m.qty > 0 %}text-success{% elif m.qty < 0 %}text-danger{% endif %}">