{"id":481191,"date":"2026-05-27T07:31:25","date_gmt":"2026-05-27T07:31:25","guid":{"rendered":"https:\/\/savepearlharbor.com\/?p=481191"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=481191","title":{"rendered":"\u041a\u0430\u043a \u0437\u0430 \u043e\u0434\u0438\u043d \u0432\u0435\u0447\u0435\u0440 \u044f \u043d\u0430\u043f\u0438\u0441\u0430\u043b \u0441\u0435\u0440\u0432\u0438\u0441 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u043e\u0440\u0433\u0442\u0435\u0445\u043d\u0438\u043a\u0438 \u0434\u043b\u044f \u0444\u0438\u043b\u0438\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438 \u0438\u0437 16 \u043b\u043e\u043a\u0430\u0446\u0438\u0439"},"content":{"rendered":"<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u0418\u0441\u0442\u043e\u0440\u0438\u044f \u043e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u0440\u043e\u0441\u044c\u0431\u0430 \u00ab\u0433\u043b\u044f\u043d\u044c, \u0447\u0442\u043e \u0443 \u043c\u0435\u043d\u044f \u0442\u0443\u0442 \u0441 \u0442\u0430\u0431\u043b\u0438\u0447\u043a\u0430\u043c\u0438\u00bb \u043f\u0440\u0435\u0432\u0440\u0430\u0442\u0438\u043b\u0430\u0441\u044c \u0432 production-\u0441\u0435\u0440\u0432\u0438\u0441 \u0441 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u043d\u0438\u0435\u043c \u043f\u043e \u0444\u043e\u0442\u043e, \u0437\u0430\u0449\u0438\u0449\u0451\u043d\u043d\u043e\u0439 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0435\u0439 \u0430\u0434\u043c\u0438\u043d\u043a\u043e\u0439 \u0438 \u0430\u0432\u0442\u043e\u043f\u0443\u0448\u0435\u043c \u0432 Google Sheets. \u0421\u043e \u0432\u0441\u0435\u043c\u0438 \u0433\u0440\u0430\u0431\u043b\u044f\u043c\u0438, \u0444\u0435\u0439\u043b\u0430\u043c\u0438 \u0438 \u0438\u043d\u0441\u0430\u0439\u0442\u0430\u043c\u0438.<\/p>\n<hr\/>\n<h3>\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442<\/h3>\n<p>\u041c\u043e\u044f \u0434\u0435\u0432\u0443\u0448\u043a\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0432 IT-\u0434\u0435\u043f\u0430\u0440\u0442\u0430\u043c\u0435\u043d\u0442\u0435 \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u0438 \u0441 16 \u0442\u0435\u0440\u0440\u0438\u0442\u043e\u0440\u0438\u0430\u043b\u044c\u043d\u044b\u043c\u0438 \u0444\u0438\u043b\u0438\u0430\u043b\u0430\u043c\u0438, \u0441\u043a\u043b\u0430\u0434\u043e\u043c \u0438 \u043f\u0430\u0440\u043e\u0439 \u043a\u043b\u0438\u043d\u0438\u043a. \u041d\u0430 \u0431\u0430\u043b\u0430\u043d\u0441\u0435 \u043f\u0440\u0438\u043c\u0435\u0440\u043d\u043e 5000 \u0435\u0434\u0438\u043d\u0438\u0446 \u043e\u0440\u0433\u0442\u0435\u0445\u043d\u0438\u043a\u0438: \u043c\u043e\u043d\u043e\u0431\u043b\u043e\u043a\u0438, \u041c\u0424\u0423, \u043d\u043e\u0443\u0442\u0431\u0443\u043a\u0438, \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u044b, \u0421\u041a\u0417\u0418. \u0420\u0430\u0437 \u0432 \u0433\u043e\u0434 \u2014 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u0438\u0437\u0430\u0446\u0438\u044f.<\/p>\n<p>\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435:<\/p>\n<ul>\n<li>\n<p>\u0413\u043b\u0430\u0432\u043d\u0430\u044f \u0432\u044b\u0433\u0440\u0443\u0437\u043a\u0430 \u0438\u0437 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u2014 <code>\u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0430.xlsx<\/code>, 4792 \u0441\u0442\u0440\u043e\u043a\u0438, 77 \u043a\u043e\u043b\u043e\u043d\u043e\u043a.<\/p>\n<\/li>\n<li>\n<p>6 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0445 \u043a\u043d\u0438\u0433 \u043f\u043e \u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b\u044c\u043d\u043e-\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u043c \u043b\u0438\u0446\u0430\u043c (\u041c\u041e\u041b\u0430\u043c) \u2014 \u0444\u043e\u0440\u043c\u0430\u0442 <code>.xls<\/code>, \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0430\u044f \u0444\u043e\u0440\u043c\u0430. \u0417\u0430\u0434\u0430\u0447\u0430 \u0437\u0432\u0443\u0447\u0430\u043b\u0430 \u0442\u0430\u043a: \u00ab\u0421\u0434\u0435\u043b\u0430\u0439 \u0441\u0435\u0440\u0432\u0438\u0441, \u0447\u0442\u043e\u0431\u044b \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u043e\u0442\u043a\u0443 \u0448\u0438\u043b\u044c\u0434\u0438\u043a\u0430, \u0438 \u043e\u043d \u043c\u043d\u0435 \u0441\u043a\u0430\u0437\u0430\u043b, \u0443 \u043a\u043e\u0433\u043e \u044d\u0442\u0430 \u0436\u0435\u043b\u0435\u0437\u043a\u0430 \u0441\u0442\u043e\u0438\u0442\u00bb. \u0417\u0432\u0443\u0447\u0438\u0442 \u043f\u0440\u043e\u0441\u0442\u043e. \u041d\u0430 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0435 \u044d\u0442\u043e \u043f\u043e\u043b\u043e\u0432\u0438\u043d\u0430 \u0437\u0430\u0434\u0430\u0447\u0438. \u0412\u0442\u043e\u0440\u0430\u044f \u043f\u043e\u043b\u043e\u0432\u0438\u043d\u0430 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u043b\u0430\u0441\u044c \u043f\u043e \u0445\u043e\u0434\u0443.<\/p>\n<\/li>\n<\/ul>\n<hr\/>\n<h3>\u0421\u0442\u0435\u043a<\/h3>\n<ul>\n<li>\n<p><strong>Backend:<\/strong> Python 3.12, FastAPI, SQLAlchemy 2, SQLite (\u043d\u0430 \u0441\u0442\u0430\u0440\u0442\u0435 \u2014 \u043e\u043a\u0430\u0437\u0430\u043b\u0441\u044f \u0443\u043c\u0435\u0441\u0442\u0435\u043d \u0438 \u043f\u043e\u0442\u043e\u043c).<\/p>\n<\/li>\n<li>\n<p><strong>ETL:<\/strong> pandas + openpyxl \u0434\u043b\u044f <code>.xlsx<\/code>, xlrd 1.2 \u0434\u043b\u044f <code>.xls<\/code> (\u043f\u0440\u043e \u044d\u0442\u0443 \u0431\u043e\u043b\u044c \u2014 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e).<\/p>\n<\/li>\n<li>\n<p><strong>AI:<\/strong> Claude Opus 4.6 \u0447\u0435\u0440\u0435\u0437 OpenRouter (vision-API).<\/p>\n<\/li>\n<li>\n<p><strong>\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438:<\/strong> gspread + Google Service Account \u0434\u043b\u044f \u043f\u0443\u0448\u0430 \u0432 Google Sheets.<\/p>\n<\/li>\n<li>\n<p><strong>Frontend:<\/strong> Jinja2 + \u0432\u0430\u043d\u0438\u043b\u044c\u043d\u044b\u0439 CSS \u0441 \u0434\u0438\u0437\u0430\u0439\u043d-\u0442\u043e\u043a\u0435\u043d\u0430\u043c\u0438 (\u0431\u0435\u0437 React\/Vue \u2014 overkill \u0434\u043b\u044f \u0430\u0434\u043c\u0438\u043d\u043a\u0438).<\/p>\n<\/li>\n<li>\n<p><strong>\u0414\u0435\u043f\u043b\u043e\u0439:<\/strong> Docker Compose, single container. \u041d\u0438\u043a\u0430\u043a\u0438\u0445 \u043c\u0438\u043a\u0440\u043e\u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432, \u043d\u0438\u043a\u0430\u043a\u043e\u0433\u043e Postgres, \u043d\u0438\u043a\u0430\u043a\u043e\u0433\u043e Kubernetes. \u041e\u0434\u0438\u043d \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440, \u043e\u0434\u043d\u0430 SQLite, \u043e\u0434\u0438\u043d FastAPI.<\/p>\n<\/li>\n<\/ul>\n<hr\/>\n<h3>\u0411\u043e\u043b\u044c \u21161: .xls \u043f\u0440\u043e\u0442\u0438\u0432 pandas 2.x<\/h3>\n<p>\u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u043c \u0444\u0430\u0439\u043b\u044b. <code>\u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0430.xlsx<\/code> \u2014 \u0441\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442, \u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044f <code>pandas + openpyxl<\/code> \u0431\u0435\u0437 \u043f\u0440\u043e\u0431\u043b\u0435\u043c. \u0410 \u0432\u043e\u0442 6 \u043a\u043d\u0438\u0433 \u041c\u041e\u041b\u043e\u0432 \u2014 \u0441\u0442\u0430\u0440\u044b\u0439 <code>.xls<\/code>, \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u044d\u043a\u0441\u043f\u043e\u0440\u0442 \u0438\u0437 1\u0421.<\/p>\n<pre><code class=\"python\">import pandas as pddf = pd.read_excel(\"mol_1.xls\", engine=\"xlrd\")<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:87px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c:<\/p>\n<pre><code>ImportError: Pandas requires version '2.0.1' or newer of 'xlrd'(version '1.2.0' currently installed).<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0421\u0442\u0430\u0432\u0438\u043c \u0441\u0432\u0435\u0436\u0438\u0439 xlrd:<\/p>\n<pre><code class=\"bash\">pip install -U xlrd<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0417\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u0441\u043d\u043e\u0432\u0430:<\/p>\n<pre><code>xlrd.biffh.XLRDError: Excel xlsx file; not supported<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0421\u044e\u0440\u043f\u0440\u0438\u0437: xlrd 2.0 \u0432\u044b\u043f\u0438\u043b\u0438\u043b\u0438 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 \u0441\u0442\u0430\u0440\u043e\u0433\u043e <code>.xls<\/code>. \u0410 pandas 2.x \u0442\u0440\u0435\u0431\u0443\u0435\u0442 xlrd \u2265 2.0. \u041f\u043e\u043b\u0443\u0447\u0438\u043b\u0441\u044f deadlock: \u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043b\u0438\u0431\u043e pandas 1.x, \u043b\u0438\u0431\u043e \u043e\u0431\u0445\u043e\u0434\u0438\u0442\u044c pandas.<\/p>\n<p>\u0412\u044b\u0431\u0440\u0430\u043b \u0432\u0442\u043e\u0440\u043e\u0435 \u2014 \u0447\u0438\u0442\u0430\u0442\u044c <code>.xls<\/code> \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e \u0447\u0435\u0440\u0435\u0437 xlrd 1.2:<\/p>\n<pre><code class=\"python\">import xlrd def parse_book(xls_path):    wb = xlrd.open_workbook(str(xls_path))    sh = wb.sheet_by_name(\"\u0418\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u0430\u044f \u043a\u043d\u0438\u0433\u0430\")    rows = []    for r in range(sh.nrows):        first = sh.cell(r, 0).value        if not first or first.lower().startswith((\"\u0441\u0447\u0435\u0442\", \"\u043c\u043e\u043b\", \"\u0438\u0442\u043e\u0433\u043e\", \"\u0438\u043d\u0432.\")):            continue        rows.append({            \"inv_number\": first,            \"serial_number\": sh.cell(r, 19).value,            \"book_initial_cost\": _to_float(sh.cell(r, 32).value),            # ...        })    return rows<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0412 \u0442\u0430\u0431\u043b\u0438\u0446\u0430\u0445 \u043e\u043a\u0430\u0437\u0430\u043b\u0438\u0441\u044c \u0441\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438-\u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u0438 (<code>\u0421\u0447\u0451\u0442 0901\u2026<\/code>, <code>\u041c\u041e\u041b \u2026-\u043e\u0441<\/code>, <code>\u0418\u0442\u043e\u0433\u043e \u043f\u043e \u041c\u041e\u041b\u0443\u2026<\/code>), \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0430\u0434\u043e \u0431\u044b\u043b\u043e \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u0442\u044c. \u0418 \u0441\u0442\u0440\u0451\u043c\u043d\u044b\u0435 \u044f\u0447\u0435\u0439\u043a\u0438 \u0442\u0438\u043f\u0430 <code>\u041a\u0410\u0414\u0420\u042b                       \u0418\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u0430\u044f \u043a\u043d\u0438\u0433\u0430 (\u043f\u043e\u043b\u043d\u0430\u044f) \u043f\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044e \u043d\u0430 \u2026<\/code> \u0432 \u043f\u0435\u0440\u0432\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u0435 (\u043a\u043e\u0433\u0434\u0430 \u0432 <code>.xls<\/code> \u043f\u043b\u043e\u0445\u043e \u0440\u0430\u0441\u043f\u0430\u0440\u0441\u0438\u043b\u0438\u0441\u044c merged cells).<\/p>\n<p><strong>\u0423\u0440\u043e\u043a:<\/strong> \u0434\u043b\u044f \u0441\u0442\u0430\u0440\u044b\u0445 \u0444\u043e\u0440\u043c\u0430\u0442\u043e\u0432 \u043e\u0444\u0438\u0441\u0430 \u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u0435\u0434\u0443\u0448\u043a\u0438\u043d xlrd 1.2 \u0438 \u0447\u0438\u0442\u0430\u0439\u0442\u0435 \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e. \u041d\u0435 \u043f\u044b\u0442\u0430\u0439\u0442\u0435\u0441\u044c \u043f\u043e\u0434\u0440\u0443\u0436\u0438\u0442\u044c \u0435\u0433\u043e \u0441 \u0441\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u043c pandas \u2014 \u0434\u0430\u0436\u0435 \u043d\u0435 \u043f\u044b\u0442\u0430\u0439\u0442\u0435\u0441\u044c.<\/p>\n<hr\/>\n<h3>\u0411\u043e\u043b\u044c \u21162: \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043d\u043e\u043c\u0435\u0440\u043e\u0432 (\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u00ab\u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c \u0431\u044b \u043f\u0440\u043e\u0441\u0442\u0430\u044f\u00bb)<\/h3>\n<p>\u042f \u043d\u0430\u0438\u0432\u043d\u043e \u0434\u0443\u043c\u0430\u043b, \u0447\u0442\u043e \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0435 \u0438 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u0435 \u043d\u043e\u043c\u0435\u0440\u0430 \u2014 \u044d\u0442\u043e \u043f\u0440\u043e\u0441\u0442\u043e \u0441\u0442\u0440\u043e\u043a\u0438. \u0421\u0432\u0435\u0440\u044f\u0442\u044c \u0438\u0445 \u0447\u0435\u0440\u0435\u0437 <code>==<\/code> \u0438 \u0432\u0441\u0451. \u0425\u0430\u0445.<\/p>\n<p>\u0420\u0435\u0430\u043b\u044c\u043d\u0430\u044f \u0432\u044b\u0431\u043e\u0440\u043a\u0430 \u0438\u0437 \u043a\u043d\u0438\u0433\u0438 (\u043f\u0440\u0438\u0432\u043e\u0436\u0443 \u043e\u0431\u0435\u0437\u043b\u0438\u0447\u0435\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043c\u0435\u0440\u044b \u0432 \u0442\u043e\u043c \u0436\u0435 \u0444\u043e\u0440\u043c\u0430\u0442\u0435, \u0447\u0442\u043e \u0432\u0441\u0442\u0440\u0435\u0447\u0430\u043b\u0438\u0441\u044c):<\/p>\n<pre><code>002410124-001287    \u2190 \u043e\u0431\u0440\u0430\u0437\u0446\u043e\u0432\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c002410124-0003750.  \u2190 \u043b\u0438\u0448\u043d\u0438\u0435 \u043d\u0443\u043b\u0438 \u0438 \u0442\u043e\u0447\u043a\u0430 \u0432 \u043a\u043e\u043d\u0446\u0435002410124--003917   \u2190 \u0434\u0432\u043e\u0439\u043d\u043e\u0439 \u0434\u0435\u0444\u0438\u0441 (\u043e\u043f\u0435\u0447\u0430\u0442\u043a\u0430)002410134003663.    \u2190 \u0432\u043e\u043e\u0431\u0449\u0435 \u0431\u0435\u0437 \u0434\u0435\u0444\u0438\u0441\u0430210134-12           \u2190 \u043a\u043e\u0440\u043e\u0442\u043a\u0438\u0439 \u0444\u043e\u0440\u043c\u0430\u0442110134-_001146      \u2190 \u043f\u043e\u0434\u0447\u0451\u0440\u043a\u0438\u0432\u0430\u043d\u0438\u0435-\u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u044c<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0410 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a \u0432\u043e\u043e\u0431\u0449\u0435:<\/p>\n<pre><code>DQVRZER1380030794C3000              \u2190 \u043d\u043e\u0440\u043cDQVUYER00K144003B43000 555-2358     \u2190 \u0441 \u0445\u0432\u043e\u0441\u0442\u043e\u043c \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0431\u0435\u043b410134-1482\/DQVRZER1380040856C3000  \u2190 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0439 \u043a\u043e\u0434 \u0441\u043a\u043b\u0435\u0435\u043d \u0447\u0435\u0440\u0435\u0437 \u0441\u043b\u0435\u0448CNDRPBV9RQ.\/555-1929                \u2190 \u043c\u0443\u0441\u043e\u0440 \u043f\u043e\u0441\u043b\u0435 \u0442\u043e\u0447\u043a\u0438 \u0438 \u0441\u043b\u0435\u0448\u0430`-                                  \u2190 \u0432\u043e\u043e\u0431\u0449\u0435 \u043d\u0435 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a, \u043c\u0443\u0441\u043e\u0440\u043d\u044b\u0439 \u043f\u043b\u0435\u0439\u0441\u0445\u043e\u043b\u0434\u0435\u0440<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u043d\u0430\u043f\u0438\u0441\u0430\u043b \u0444\u0443\u043d\u043a\u0446\u0438\u044e \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438:<\/p>\n<pre><code class=\"python\">def norm_key(value):    s = str(value).strip().upper()    # \u0445\u0432\u043e\u0441\u0442 \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0431\u0435\u043b\/\u0441\u043b\u0435\u0448 \u2014 \u0431\u0435\u0440\u0451\u043c \u043f\u0435\u0440\u0432\u044b\u0439 \u0442\u043e\u043a\u0435\u043d    for sep in (\" \", \"\/\", \"\\\\\"):        if sep in s:            s = s.split(sep, 1)[0]    # \u0432\u044b\u043a\u0438\u0434\u044b\u0432\u0430\u0435\u043c \u043c\u0443\u0441\u043e\u0440, \u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c \u0431\u0443\u043a\u0432\u044b, \u0446\u0438\u0444\u0440\u044b, \u0434\u0435\u0444\u0438\u0441    s = re.sub(r\"[^A-Z0-9-]\", \"\", s)    # \u0441\u0445\u043b\u043e\u043f\u044b\u0432\u0430\u0435\u043c \u0434\u0432\u043e\u0439\u043d\u044b\u0435 \u0434\u0435\u0444\u0438\u0441\u044b    while \"--\" in s:        s = s.replace(\"--\", \"-\")    # \u0431\u0435\u0437 \u0434\u0435\u0444\u0438\u0441\u0430 \u0438 \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u0434\u043b\u0438\u043d\u043d\u044b\u0439 \u2014 \u0432\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c \u0434\u0435\u0444\u0438\u0441 \u043f\u043e\u0441\u043b\u0435 \u043f\u0435\u0440\u0432\u044b\u0445 9 \u0446\u0438\u0444\u0440    if \"-\" not in s and s.isdigit() and len(s) &gt;= 12:        s = s[:9] + \"-\" + s[9:]    # \u0432\u0435\u0434\u0443\u0449\u0438\u0435 \u043d\u0443\u043b\u0438 \u043f\u043e\u0441\u043b\u0435 \u0434\u0435\u0444\u0438\u0441\u0430    parts = s.split(\"-\")    if len(parts) == 2 and parts[1].isdigit():        parts[1] = parts[1].lstrip(\"0\") or \"0\"        s = \"-\".join(parts)    return s or None<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u042d\u0442\u043e\u0433\u043e \u0445\u0432\u0430\u0442\u0438\u043b\u043e \u0434\u043b\u044f 80% \u0441\u043b\u0443\u0447\u0430\u0435\u0432. \u041d\u043e \u043d\u0435 \u0434\u043b\u044f \u0432\u0441\u0435\u0445. \u041a\u0430\u043a\u0438\u0435-\u0442\u043e \u043d\u043e\u043c\u0435\u0440\u0430 \u0432 \u043a\u043d\u0438\u0433\u0435 \u043f\u0438\u0441\u0430\u043b\u0438 \u0441 \u0434\u0432\u043e\u0439\u043d\u044b\u043c \u0434\u0435\u0444\u0438\u0441\u043e\u043c, \u043a\u0430\u043a\u0438\u0435-\u0442\u043e \u0431\u0435\u0437 \u0434\u0435\u0444\u0438\u0441\u0430 \u0432\u043e\u043e\u0431\u0449\u0435. \u0414\u043e\u043f\u0438\u0441\u0430\u043b fallback \u043f\u043e \u0447\u0438\u0441\u0442\u044b\u043c \u0446\u0438\u0444\u0440\u0430\u043c:<\/p>\n<pre><code class=\"python\">def digits_only(value):    s = str(value).strip().upper()    for sep in (\" \", \"\/\", \"\\\\\"):        if sep in s:            s = s.split(sep, 1)[0]    digs = \"\".join(ch for ch in s if ch.isdigit())    return digs.lstrip(\"0\") or digs or None<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041f\u0440\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0435 \u0434\u0435\u043b\u0430\u044e \u0447\u0435\u0442\u044b\u0440\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0438: \u0442\u043e\u0447\u043d\u044b\u0439 \u043c\u0430\u0442\u0447 \u043f\u043e <code>inv_norm<\/code>, \u043f\u043e\u0442\u043e\u043c \u043f\u043e <code>serial_norm<\/code>, \u043f\u043e\u0442\u043e\u043c \u043f\u043e <code>inv_digits<\/code>, \u043f\u043e\u0442\u043e\u043c \u043f\u043e <code>serial_digits<\/code>. \u042d\u0442\u0438\u043c \u043e\u0431\u0445\u043e\u0434\u043e\u043c \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0441\u043a\u043b\u0435\u0438\u0442\u044c \u0435\u0449\u0451 \u043e\u043a\u043e\u043b\u043e 50 \u0437\u0430\u043f\u0438\u0441\u0435\u0439.<\/p>\n<h4>\u041a\u043b\u044e\u0447\u0435\u0432\u043e\u0439 \u0431\u0430\u0433, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u044f \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u043b \u043f\u043e\u0437\u0434\u043d\u043e<\/h4>\n<p>\u0421\u0434\u0435\u043b\u0430\u043b \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u0443\u044e <code>values_match()<\/code>, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u0441\u043e\u0432\u043f\u0430\u0434\u0435\u043d\u0438\u0435, \u0435\u0441\u043b\u0438 \u0445\u043e\u0442\u044c \u043a\u0430\u043a\u0430\u044f-\u0442\u043e \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u0444\u043e\u0440\u043c\u0430 \u043f\u0435\u0440\u0435\u0441\u0435\u043a\u0430\u0435\u0442\u0441\u044f. \u041f\u0440\u0438\u043c\u0435\u043d\u0438\u043b \u0434\u043b\u044f \u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u044f \u0438 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0445 \u043d\u043e\u043c\u0435\u0440\u043e\u0432, \u0438 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a\u043e\u0432.<\/p>\n<p>\u0418 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u043b, \u0447\u0442\u043e \u0437\u0430\u043f\u0438\u0441\u0438 <code>Z78VBJACB002YJA<\/code> \u0438 <code>Z78VBZJACB002TLX<\/code> (\u0440\u0430\u0437\u043d\u044b\u0435 \u0436\u0435\u043b\u0435\u0437\u043a\u0438) \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u043e\u0434\u043d\u043e\u0439.<\/p>\n<p>\u0420\u0430\u0437\u0431\u043e\u0440:<\/p>\n<div>\n<div class=\"table\">\n<table>\n<tbody>\n<tr>\n<th>\n<p align=\"left\">\u0421\u0435\u0440\u0438\u0439\u043d\u0438\u043a<\/p>\n<\/th>\n<th>\n<p align=\"left\">\u0422\u043e\u043b\u044c\u043a\u043e \u0446\u0438\u0444\u0440\u044b<\/p>\n<\/th>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><code>Z78VBJACB002YJA<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\"><code>78002<\/code><\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\"><code>Z78VBZJACB002TLX<\/code><\/p>\n<\/td>\n<td>\n<p align=\"left\"><code>78002<\/code><\/p>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<\/div>\n<p>\u041e\u0431\u0430 \u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u044e\u0442\u0441\u044f \u0432 <code>78002<\/code>. \u0421\u043e\u0432\u043f\u0430\u043b\u043e.<\/p>\n<p><strong>\u0423\u0440\u043e\u043a:<\/strong> \u0434\u043b\u044f \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0445 \u043d\u043e\u043c\u0435\u0440\u043e\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0430 <code>XXXNNN-NNNNNN<\/code> \u0446\u0438\u0444\u0440\u043e\u0432\u043e\u0439 fallback \u043d\u0443\u0436\u0435\u043d \u2014 \u0438\u043d\u0430\u0447\u0435 \u043d\u0435 \u0441\u0448\u0438\u0442\u044c \u0440\u0430\u0437\u043d\u044b\u0435 \u0444\u043e\u0440\u043c\u0430\u0442\u044b \u0437\u0430\u043f\u0438\u0441\u0438. \u0414\u043b\u044f \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a\u043e\u0432 \u043e\u043d \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0447\u0435\u0441\u043a\u0438 \u0432\u0440\u0435\u0434\u0435\u043d \u2014 \u0432 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a\u0430\u0445 \u0431\u0443\u043a\u0432\u044b \u0437\u043d\u0430\u0447\u0438\u043c\u044b.<\/p>\n<p>\u041f\u0440\u0438\u0448\u043b\u043e\u0441\u044c \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u044c \u043b\u043e\u0433\u0438\u043a\u0443:<\/p>\n<pre><code class=\"python\">def inv_match(a, b):    \"\"\"\u0414\u043b\u044f \u0438\u043d\u0432.\u043d\u043e\u043c\u0435\u0440\u043e\u0432: \u043d\u043e\u0440\u043c + digit-fallback.\"\"\"    ka = all_keys(a, with_digits=True)    kb = all_keys(b, with_digits=True)    return bool(ka and kb and (ka &amp; kb)) def serial_match(a, b):    \"\"\"\u0414\u043b\u044f \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a\u043e\u0432: \u0422\u041e\u041b\u042c\u041a\u041e \u043f\u043e\u043b\u043d\u0430\u044f alphanumeric \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f.\"\"\"    ka = all_keys(a, with_digits=False)    kb = all_keys(b, with_digits=False)    return bool(ka and kb and (ka &amp; kb))<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041e\u0434\u0438\u043d \u0438\u0437 \u043b\u0443\u0447\u0448\u0438\u0445 \u043f\u0440\u0438\u043c\u0435\u0440\u043e\u0432 \u0442\u043e\u0433\u043e, \u043a\u0430\u043a \u043f\u043e\u0445\u043e\u0436\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u043c\u0435\u044e\u0442 \u0440\u0430\u0437\u043d\u044b\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430, \u0438 \u043d\u0435 \u0441\u0442\u043e\u0438\u0442 \u043f\u0438\u0441\u0430\u0442\u044c \u00ab\u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u044b\u0439\u00bb \u0445\u0435\u043b\u043f\u0435\u0440, \u0435\u0441\u043b\u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u043d\u0435\u043e\u0434\u043d\u043e\u0440\u043e\u0434\u043d\u044b\u0435.<\/p>\n<hr\/>\n<h3>\u0411\u043e\u043b\u044c \u21163: \u00ab\u0433\u043b\u0430\u0432\u043d\u0430\u044f \u0442\u0430\u0431\u043b\u0438\u0446\u0430 \u2014 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0430\u00bb<\/h3>\n<p>\u0412 \u043f\u0435\u0440\u0432\u043e\u043d\u0430\u0447\u0430\u043b\u044c\u043d\u043e\u043c \u0422\u0417 \u044f \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0438\u043b: \u043a\u043d\u0438\u0433\u0438 \u041c\u041e\u041b\u043e\u0432 \u2014 \u044d\u0442\u043e \u0434\u0435\u0442\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0438, \u043f\u043b\u044e\u0441 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u0440\u043e\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0432 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0435 \u043d\u0435\u0442. \u0420\u0435\u0448\u0435\u043d\u0438\u0435 \u043e\u0447\u0435\u0432\u0438\u0434\u043d\u043e\u0435: \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0438 \u0442\u043e, \u0438 \u0434\u0440\u0443\u0433\u043e\u0435, \u0441\u043c\u0430\u0442\u0447\u0438\u0442\u044c \u043f\u043e \u043a\u043b\u044e\u0447\u0430\u043c, \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u043e\u0435 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u043c\u0438 \u0437\u0430\u043f\u0438\u0441\u044f\u043c\u0438.<\/p>\n<p>\u041f\u043e\u0441\u043b\u0435 \u043f\u0435\u0440\u0432\u043e\u0433\u043e \u043f\u0440\u043e\u0433\u043e\u043d\u0430:<\/p>\n<ul>\n<li>\n<p>Master \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0430: 4792 \u0430\u043a\u0442\u0438\u0432\u0430<\/p>\n<\/li>\n<li>\n<p>\u041a\u043d\u0438\u0433\u0438 \u041c\u041e\u041b\u043e\u0432: 432 \u0441\u0442\u0440\u043e\u043a\u0438<\/p>\n<\/li>\n<li>\n<p>\u0421\u043c\u0430\u0442\u0447\u0435\u043d\u043e: 305<\/p>\n<\/li>\n<li>\n<p>\u0421\u043e\u0437\u0434\u0430\u043d\u043e \u043d\u043e\u0432\u044b\u0445 \u0438\u0437 \u043a\u043d\u0438\u0433: 113 \u041f\u043e\u043a\u0430\u0437\u0430\u043b \u0437\u0430\u043a\u0430\u0437\u0447\u0438\u0446\u0435. \u0420\u0435\u0430\u043a\u0446\u0438\u044f: \u00ab\u0417\u0430\u0447\u0435\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0445? \u0413\u043b\u0430\u0432\u043d\u0430\u044f \u0442\u0430\u0431\u043b\u0438\u0446\u0430 \u2014 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0430. \u0415\u0441\u043b\u0438 \u0432 \u043a\u043d\u0438\u0433\u0435 \u041c\u041e\u041b\u0430 \u0447\u0442\u043e-\u0442\u043e \u0435\u0441\u0442\u044c, \u0430 \u0432 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0435 \u043d\u0435\u0442 \u2014 \u044d\u0442\u043e \u043f\u0440\u043e\u0441\u0442\u043e \u043e\u0448\u0438\u0431\u043a\u0430 \u0432 \u043a\u043d\u0438\u0433\u0435, \u043d\u0430\u0434\u043e \u0432 \u043e\u0442\u0447\u0451\u0442, \u0430 \u043d\u0435 \u0432 \u0411\u0414\u00bb.<\/p>\n<\/li>\n<\/ul>\n<p>\u041f\u0435\u0440\u0435\u043f\u0438\u0441\u044b\u0432\u0430\u044e \u043b\u043e\u0433\u0438\u043a\u0443. \u0422\u0435\u043f\u0435\u0440\u044c \u043a\u043d\u0438\u0433\u0438 \u2014 \u0442\u043e\u043b\u044c\u043a\u043e \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u043e\u0431\u043e\u0433\u0430\u0449\u0435\u043d\u0438\u044f. \u041e\u043d\u0438 \u043f\u0440\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u044e\u0442 \u041c\u041e\u041b, \u0444\u0438\u043b\u0438\u0430\u043b, \u0437\u0430\u043f\u043e\u043b\u043d\u044f\u044e\u0442 <code>book_*<\/code> \u043f\u043e\u043b\u044f. \u0415\u0441\u043b\u0438 \u0441\u0442\u0440\u043e\u043a\u0430 \u0438\u0437 \u043a\u043d\u0438\u0433\u0438 \u043d\u0435 \u043d\u0430\u0445\u043e\u0434\u0438\u0442 \u043f\u0430\u0440\u044b \u0432 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0435 \u2014 \u043e\u043d\u0430 \u043f\u0438\u0448\u0435\u0442\u0441\u044f \u0432 <code>reconciliation_issue<\/code> \u0441\u043e \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u043c <code>only_in_book<\/code> \u0434\u043b\u044f \u043e\u0442\u0447\u0451\u0442\u0430, \u043d\u043e \u0432 \u0411\u0414 \u0430\u043a\u0442\u0438\u0432\u043e\u0432 \u0435\u0451 \u043d\u0435\u0442.<\/p>\n<pre><code class=\"python\">def import_book(db, xls_path):    rows = parse_book(xls_path)    run = ReconciliationRun(started_at=datetime.utcnow())    db.add(run)    db.flush()     for r in rows:        asset = find_match(db, r)  # 4 \u043f\u043e\u043f\u044b\u0442\u043a\u0438 \u043c\u0430\u0442\u0447\u0430        if asset is None:            db.add(ReconciliationIssue(                run_id=run.id, kind=\"only_in_book\",                inv_number=r[\"inv_number\"],                serial_number=r[\"serial_number\"],                description=\"\u0415\u0441\u0442\u044c \u0432 \u043a\u043d\u0438\u0433\u0435, \u043d\u0435\u0442 \u0432 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0435\",            ))            continue        # \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c book_* \u043f\u043e\u043b\u044f        asset.book_inv_number = r[\"inv_number\"]        asset.book_serial_number = r[\"serial_number\"]        asset.book_row_no = r[\"book_row_no\"]        asset.mol_id = mol.id        # ...<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><strong>\u0423\u0440\u043e\u043a:<\/strong> \u0435\u0441\u043b\u0438 \u0432\u0430\u0448 \u043f\u0440\u043e\u0434\u0443\u043a\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u043c \u0438\u0441\u0442\u0438\u043d\u044b \u2014 \u043d\u0435 \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u043f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u044c\u043d\u0443\u044e \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c. \u041b\u0443\u0447\u0448\u0435 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u043e\u0442\u0447\u0451\u0442-\u0436\u0443\u0440\u043d\u0430\u043b \u0442\u043e\u0433\u043e, \u0447\u0442\u043e \u043e\u0442\u043a\u043b\u043e\u043d\u044f\u0435\u0442\u0441\u044f \u043e\u0442 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u0430.<\/p>\n<hr\/>\n<h3>\u0411\u043e\u043b\u044c \u21164: \u0437\u0430\u0434\u0432\u043e\u0435\u043d\u0438\u0435 \u043e\u0431\u043e\u0440\u0443\u0434\u043e\u0432\u0430\u043d\u0438\u044f<\/h3>\n<p>\u0412 \u043f\u0440\u0438\u043c\u0435\u0447\u0430\u043d\u0438\u044f\u0445 \u043a \u0430\u043a\u0442\u0438\u0432\u0430\u043c \u0432 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0435 \u0432\u0441\u0442\u0440\u0435\u0447\u0430\u044e\u0442\u0441\u044f \u0437\u0430\u043f\u0438\u0441\u0438 \u0432\u0438\u0434\u0430:<\/p>\n<ul>\n<li>\n<p><code>\u0424\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u0437\u0430\u0434\u0432\u043e\u0435\u043d\u0438\u0435 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u0438\u043a\u0430 (\u043a\u0430\u0431. 105)<\/code><\/p>\n<\/li>\n<li>\n<p><code>\u0437\u0430\u043f\u0438\u0441\u0430\u043d \u043a\u0430\u043a 002410124-003000, \u043d\u043e \u0434\u0443\u0431\u043b\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0441 DQVRZER138017016AA3000<\/code><\/p>\n<\/li>\n<li>\n<p><code>002410124-003740 \u0437\u0430\u0434\u0432\u043e\u0435\u043d \u0441 RBU0X16179<\/code> \u0422\u043e \u0435\u0441\u0442\u044c \u0432 \u0443\u0447\u0451\u0442\u0435 \u0443 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0438\u0438 \u043e\u0434\u043d\u0430 \u0438 \u0442\u0430 \u0436\u0435 \u0444\u0438\u0437\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0436\u0435\u043b\u0435\u0437\u043a\u0430 \u0438\u043d\u043e\u0433\u0434\u0430 \u0447\u0438\u0441\u043b\u0438\u0442\u0441\u044f \u0434\u0432\u0430\u0436\u0434\u044b \u043f\u043e\u0434 \u0440\u0430\u0437\u043d\u044b\u043c\u0438 \u043d\u043e\u043c\u0435\u0440\u0430\u043c\u0438. \u0421\u0434\u0435\u043b\u0430\u043b \u0434\u0435\u0442\u0435\u043a\u0442\u043e\u0440:<\/p>\n<\/li>\n<\/ul>\n<pre><code class=\"python\">def is_duplicated(notes):    if not notes:        return False    return any(k in notes.lower() for k in (\"\u0437\u0430\u0434\u0432\u043e\", \"\u0434\u0443\u0431\u043b\", \"\u043f\u043e\u0432\u0442\u043e\u0440\")) def extract_candidates(notes):    \"\"\"\u0418\u0437\u0432\u043b\u0435\u043a\u0430\u0435\u0442 \u043f\u043e\u0445\u043e\u0436\u0438\u0435 \u043d\u0430 \u0438\u043d\u0432.\u2116 \u0438 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a\u0438 \u0441\u0442\u0440\u043e\u043a\u0438 \u0438\u0437 \u043f\u0440\u0438\u043c\u0435\u0447\u0430\u043d\u0438\u044f.\"\"\"    out = set()    for m in re.finditer(r\"\\b\\d{5,9}-_?\\d+\\b\", notes):        out.add(m.group())    for m in re.finditer(r\"\\b[A-Z][A-Z0-9]{5,}\\b\", notes, re.I):        out.add(m.group())    return list(out)<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0412 \u043a\u0430\u0440\u0442\u043e\u0447\u043a\u0435 \u043f\u043e\u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043a\u0440\u0430\u0441\u043d\u044b\u0439 \u0431\u0430\u043d\u043d\u0435\u0440 \u00ab\u0417\u0430\u0434\u0432\u043e\u0435\u043d\u0438\u0435\u00bb \u0441\u043e \u0441\u0441\u044b\u043b\u043a\u0430\u043c\u0438 \u043d\u0430 \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u043f\u0430\u0440\u043d\u044b\u0435 \u0430\u043a\u0442\u0438\u0432\u044b.<\/p>\n<p>\u041f\u043e\u0442\u043e\u043c \u0437\u0430\u043a\u0430\u0437\u0447\u0438\u0446\u0430 \u043f\u043e\u043a\u0430\u0437\u0430\u043b\u0430 \u043f\u0440\u0438\u043c\u0435\u0440: \u0443 \u0430\u043a\u0442\u0438\u0432\u0430 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0438 <code>002410134-002722<\/code>, \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a <code>PHCLK09100<\/code>. \u0410 \u0432 \u043a\u043d\u0438\u0433\u0435 \u041c\u041e\u041b\u0430 \u0442\u043e\u0442 \u0436\u0435 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440, \u043d\u043e \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a <code>PHCL628191<\/code>. \u0422\u043e \u0435\u0441\u0442\u044c \u044d\u0442\u043e \u0440\u0430\u0437\u043d\u044b\u0435 \u0436\u0435\u043b\u0435\u0437\u043a\u0438 \u043f\u043e\u0434 \u043e\u0434\u043d\u0438\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c. \u0422\u0435\u043a\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u00ab\u0437\u0430\u0434\u0432\u043e\u0435\u043d\u00bb \u0432 \u043f\u0440\u0438\u043c\u0435\u0447\u0430\u043d\u0438\u0438 \u043d\u0435\u0442, \u043d\u043e \u0444\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u044d\u0442\u043e \u0438 \u0435\u0441\u0442\u044c \u0437\u0430\u0434\u0432\u043e\u0435\u043d\u0438\u0435.<\/p>\n<p>\u0414\u043e\u043f\u0438\u0441\u0430\u043b \u0434\u0435\u0442\u0435\u043a\u0442\u043e\u0440 \u00ab\u043c\u043e\u043b\u0447\u0430\u043b\u0438\u0432\u043e\u0433\u043e\u00bb \u0437\u0430\u0434\u0432\u043e\u0435\u043d\u0438\u044f: \u0438\u0449\u0435\u043c \u0432 \u0411\u0414 \u0434\u0440\u0443\u0433\u0438\u0435 \u0430\u043a\u0442\u0438\u0432\u044b, \u0443 \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0445\u043e\u0442\u044c \u043a\u0430\u043a\u043e\u0439-\u0442\u043e \u043a\u043b\u044e\u0447 (inv_norm, serial_norm, book_inv_digits, book_serial_digits) \u043f\u0435\u0440\u0435\u0441\u0435\u043a\u0430\u0435\u0442\u0441\u044f \u0441 \u0442\u0435\u043a\u0443\u0449\u0438\u043c:<\/p>\n<pre><code class=\"python\">def find_duplicates_by_data(db, asset):    \"\"\"\u0410\u043a\u0442\u0438\u0432\u044b \u0441 \u043e\u0431\u0449\u0438\u043c\u0438 \u043a\u043b\u044e\u0447\u0430\u043c\u0438 \u2014 \u043f\u043e\u0442\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0435 \u0437\u0430\u0434\u0432\u043e\u0435\u043d\u0438\u044f.\"\"\"    pairs = {}    for column, value, reason in build_checks(asset):        for other in db.query(Asset).filter(            Asset.id != asset.id, column == value        ).limit(10):            pairs.setdefault(other.id, (other, reason))    return list(pairs.values())<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0412 \u0431\u0430\u0437\u0435 \u0438\u0437 4792 \u0430\u043a\u0442\u0438\u0432\u043e\u0432 747 \u0438\u043c\u0435\u044e\u0442 \u043f\u0435\u0440\u0435\u0441\u0435\u0447\u0435\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0435\u0439 \u0441 \u0434\u0440\u0443\u0433\u0438\u043c\u0438. \u042d\u0442\u043e \u043c\u043d\u043e\u0433\u043e. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u044d\u0442\u043e \u0447\u0430\u0441\u0442\u044c \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u0438 (\u043e\u0434\u043d\u0430 \u043f\u043e\u0437\u0438\u0446\u0438\u044f \u043f\u0435\u0440\u0435\u0432\u0435\u0434\u0435\u043d\u0430 \u0441 \u043e\u0434\u043d\u043e\u0433\u043e \u0431\u0430\u043b\u0430\u043d\u0441\u0430 \u043d\u0430 \u0434\u0440\u0443\u0433\u043e\u0439, \u043d\u043e \u043d\u0435 \u0441\u043f\u0438\u0441\u0430\u043d\u0430 \u0441\u043e \u0441\u0442\u0430\u0440\u043e\u0433\u043e), \u043d\u043e \u0432 \u043b\u044e\u0431\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043e\u0431 \u044d\u0442\u043e\u043c \u043d\u0443\u0436\u043d\u043e \u0437\u043d\u0430\u0442\u044c.<\/p>\n<hr\/>\n<h3>\u0420\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u043d\u0438\u0435 \u043f\u043e \u0444\u043e\u0442\u043e<\/h3>\n<p>\u0421\u0430\u043c\u0430\u044f \u00ab\u0432\u0430\u0443-\u0444\u0438\u0447\u0430\u00bb \u0432 \u0422\u0417. \u0420\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043b \u0447\u0435\u0440\u0435\u0437 OpenRouter \u2014 \u0443 \u043d\u0438\u0445 \u0435\u0434\u0438\u043d\u044b\u0439 API \u043a\u043e \u0432\u0441\u0435\u043c \u043c\u043e\u0434\u0435\u043b\u044f\u043c, \u0443\u0434\u043e\u0431\u043d\u043e. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b <code>anthropic\/claude-opus-4.6<\/code> \u0441 vision.<\/p>\n<pre><code class=\"python\">SYSTEM_PROMPT = (    \"\u0422\u044b \u043f\u043e\u043c\u043e\u0449\u043d\u0438\u043a \u043f\u043e \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u0438\u0437\u0430\u0446\u0438\u0438. \u041d\u0430 \u0444\u043e\u0442\u043e \u2014 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 (S\/N, \"    \"Serial, P\/N, \u0437\u0430\u0432\u043e\u0434\u0441\u043a\u043e\u0439) \u043b\u0438\u0431\u043e \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u043d\u0430 \u043a\u043e\u0440\u043f\u0443\u0441\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \"    \"\u0418\u0437\u0432\u043b\u0435\u043a\u0438 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 (\u0442\u0438\u043f\u0438\u0447\u043d\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442 '002410124-XXXXXX' \u0438\u043b\u0438 \"    \"'210134-12') \u0438\/\u0438\u043b\u0438 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440. \"    '\u0412\u0435\u0440\u043d\u0438 \u0421\u0422\u0420\u041e\u0413\u041e JSON \u0431\u0435\u0437 \u043f\u043e\u044f\u0441\u043d\u0435\u043d\u0438\u0439: {\"inv_number\": \"...\", '    '\"serial_number\": \"...\", \"confidence\": 0.0, \"notes\": \"...\"}.') async def recognize_image(image_bytes, content_type=\"image\/jpeg\"):    b64 = base64.b64encode(image_bytes).decode()    payload = {        \"model\": \"anthropic\/claude-opus-4.6\",        \"messages\": [            {\"role\": \"system\", \"content\": SYSTEM_PROMPT},            {\"role\": \"user\", \"content\": [                {\"type\": \"text\", \"text\": \"\u0418\u0437\u0432\u043b\u0435\u043a\u0438 \u043d\u043e\u043c\u0435\u0440\u0430. \u0412\u0435\u0440\u043d\u0438 JSON.\"},                {\"type\": \"image_url\", \"image_url\": {                    \"url\": f\"data:{content_type};base64,{b64}\"                }},            ]},        ],        \"temperature\": 0,        \"max_tokens\": 400,    }    async with httpx.AsyncClient(timeout=60) as c:        r = await c.post(f\"{BASE}\/chat\/completions\",                         headers={\"Authorization\": f\"Bearer {KEY}\"},                         json=payload)        return _extract_json(r.json()[\"choices\"][0][\"message\"][\"content\"])<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0426\u0435\u043d\u0430 \u0437\u0430 \u0437\u0430\u043f\u0440\u043e\u0441 \u043d\u0430 Opus 4.6 \u0432 \u0440\u0430\u0439\u043e\u043d\u0435 1-3 \u0446\u0435\u043d\u0442\u043e\u0432. \u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u0447\u0451\u0442\u043a\u0438\u0445 \u0444\u043e\u0442\u043e \u0448\u0438\u043b\u044c\u0434\u0438\u043a\u043e\u0432 HP\/Acer \u2014 \u043f\u0440\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438 100%. \u0414\u043b\u044f \u0437\u0430\u0442\u0451\u0440\u0442\u044b\u0445 \u043d\u0430\u043a\u043b\u0435\u0435\u043a \u0421\u041a\u0417\u0418 \u0445\u0443\u0436\u0435, \u043d\u043e \u0438 \u0447\u0435\u043b\u043e\u0432\u0435\u043a \u0442\u0430\u043c \u0447\u0430\u0441\u0442\u043e \u043d\u0435 \u0440\u0430\u0437\u0431\u0435\u0440\u0451\u0442.<\/p>\n<p>\u0414\u0430\u043b\u044c\u0448\u0435 \u043f\u043e\u0442\u043e\u043a \u0442\u0430\u043a\u043e\u0439: \u0444\u043e\u0442\u043e \u2192 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u043d\u044b\u0435 <code>inv_number<\/code> \u0438 <code>serial_number<\/code> \u2192 \u0438\u0449\u0435\u043c \u0432 \u0411\u0414 \u0447\u0435\u0440\u0435\u0437 <code>find_by_inv_or_serial<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0443\u0435\u0442 \u043a\u043b\u044e\u0447 \u0438 \u043f\u0440\u043e\u0431\u0443\u0435\u0442 \u0442\u043e\u0447\u043d\u043e\u0435 \u0441\u043e\u0432\u043f\u0430\u0434\u0435\u043d\u0438\u0435, fallback \u043f\u043e \u0446\u0438\u0444\u0440\u0430\u043c, fallback \u043f\u043e <code>book_inv\/book_serial<\/code>. \u0415\u0441\u043b\u0438 \u043d\u0430\u0448\u043b\u0438 \u2014 \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u043a\u0430\u0440\u0442\u043e\u0447\u043a\u0430. \u0415\u0441\u043b\u0438 \u043d\u0435\u0442 \u2014 \u043a\u043d\u043e\u043f\u043a\u0430 \u00ab\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u0430\u043a\u0442\u0438\u0432\u00bb \u0441 \u043f\u0440\u0435\u0434\u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u044b\u043c\u0438 \u043f\u043e\u043b\u044f\u043c\u0438.<\/p>\n<p>UI \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u043d\u0438\u044f \u043f\u0435\u0440\u0435\u043f\u0438\u0441\u0430\u043b \u0441 \u043f\u0440\u0438\u043c\u0438\u0442\u0438\u0432\u043d\u043e\u0433\u043e <code>&lt;input type=\"file\"&gt;<\/code> \u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0441\u044b\u0440\u043e\u0433\u043e JSON \u043d\u0430 \u043f\u043e\u043b\u043d\u043e\u0446\u0435\u043d\u043d\u044b\u0439 drag&amp;drop \u0441 preview \u0438 \u0430\u043a\u043a\u0443\u0440\u0430\u0442\u043d\u044b\u043c\u0438 \u043a\u0430\u0440\u0442\u043e\u0447\u043a\u0430\u043c\u0438 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430. \u041f\u043e\u043b\u0443\u0447\u0438\u043b\u043e\u0441\u044c \u043f\u043e\u0445\u043e\u0436\u0435 \u043d\u0430 \u0445\u043e\u0440\u043e\u0448\u0438\u0439 SaaS.<\/p>\n<hr\/>\n<h3>\u041a\u043d\u043e\u043f\u043a\u0438 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 Google Sheets<\/h3>\n<p>\u0417\u0430\u043a\u0430\u0437\u0447\u0438\u0446\u0430: \u00ab\u041d\u0443\u0436\u043d\u043e \u0434\u0432\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u043a\u0430\u0440\u0442\u043e\u0447\u043a\u0435: \u0421\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0438 \u041d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442. \u0418 \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438 \u0432\u044b\u0431\u043e\u0440\u0435 \u0432\u0442\u043e\u0440\u043e\u0439 \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u043e\u043f\u0438\u0441\u0430\u0442\u044c, \u0447\u0442\u043e \u0438\u043c\u0435\u043d\u043d\u043e \u043d\u0435 \u0441\u043e\u0448\u043b\u043e\u0441\u044c. \u0418 \u0432 Google-\u0442\u0430\u0431\u043b\u0438\u0446\u0443 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0441\u043a\u043b\u0430\u0434\u044b\u0432\u0430\u0442\u044c\u00bb.<\/p>\n<p>\u041f\u0440\u043e\u0441\u0442\u0430\u044f \u0441\u0445\u0435\u043c\u0430:<\/p>\n<pre><code class=\"python\">@app.post(\"\/asset\/{asset_id}\/verify\")async def asset_verify(asset_id, request, db):    form = await request.form()    status = form.get(\"status\")  # 'ok' | 'fail'    comment = form.get(\"comment\", \"\").strip() or None     asset = db.get(Asset, asset_id)    record = Verification(asset_id=asset.id, status=status, comment=comment)    db.add(record)    asset.verification_status = status    asset.verification_comment = comment    asset.verified_at = datetime.utcnow()     try:        push_verification(asset, record)  # \u2192 Google Sheets        record.pushed_to_sheets_at = datetime.utcnow()    except Exception:        logging.exception(\"Sheets push failed\")        # \u043d\u0435 \u0432\u0430\u043b\u0438\u043c \u0437\u0430\u043f\u0440\u043e\u0441 \u2014 \u0432 \u0411\u0414 \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u043b\u0438     db.commit()<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0414\u043b\u044f Google Sheets \u2014 gspread + service account. \u0421\u0435\u0440\u0432\u0438\u0441\u043d\u044b\u0439 \u0430\u043a\u043a\u0430\u0443\u043d\u0442 \u043d\u0443\u0436\u043d\u043e \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u00ab\u0440\u0430\u0441\u0448\u0430\u0440\u0438\u0442\u044c\u00bb \u0432 \u0442\u0430\u0431\u043b\u0438\u0446\u0443 \u043a\u0430\u043a \u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440\u0430:<\/p>\n<pre><code class=\"python\">def push_verification(asset, verification):    if not settings.google_credentials_path:        return    creds = Credentials.from_service_account_file(        settings.google_credentials_path,        scopes=[\"https:\/\/www.googleapis.com\/auth\/spreadsheets\",                \"https:\/\/www.googleapis.com\/auth\/drive\"])    client = gspread.authorize(creds)    ws = client.open_by_key(settings.google_spreadsheet_id).worksheet(\"\u041f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u043e\")    next_row = len(ws.col_values(1)) + 1    ws.append_row(asset_to_row(asset), value_input_option=\"USER_ENTERED\")    # \u043f\u043e\u043a\u0440\u0430\u0441\u043a\u0430 \u043f\u043e \u0441\u0442\u0430\u0442\u0443\u0441\u0443    color = GREEN if asset.verification_status == \"ok\" else RED    ws.format(f\"A{next_row}:L{next_row}\", {\"backgroundColor\": color})<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0421\u0442\u0440\u043e\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043a\u0440\u0430\u0441\u044f\u0442\u0441\u044f: \u0437\u0435\u043b\u0451\u043d\u044b\u043c \u00ab\u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442\u00bb, \u043a\u0440\u0430\u0441\u043d\u044b\u043c \u00ab\u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442\u00bb. \u041f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u044c\u043d\u043e \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f \u0432 <code>verification<\/code> (\u043f\u043e\u043b\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043e\u043a) \u0438 \u0432 <code>asset.verification_status<\/code> (\u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0441\u0442\u0430\u0442\u0443\u0441).<\/p>\n<hr\/>\n<h3>\u0414\u0435\u043f\u043b\u043e\u0439 \u0438 \u043d\u0435\u043e\u0436\u0438\u0434\u0430\u043d\u043d\u043e\u0441\u0442\u0438 \u0441 SSH<\/h3>\n<p>\u0417\u0430\u043a\u0430\u0437\u0447\u0438\u0446\u0430 \u0434\u0430\u043b\u0430 \u0441\u0435\u0440\u0432\u0435\u0440 \u0441 \u0440\u0443\u0442\u043e\u0432\u044b\u043c \u0434\u043e\u0441\u0442\u0443\u043f\u043e\u043c \u043f\u043e \u043f\u0430\u0440\u043e\u043b\u044e. \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u044e\u0441\u044c:<\/p>\n<pre><code>$ ssh root@XX.XX.XX.XXConnection closed by XX.XX.XX.XX port 22<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0421\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0440\u0432\u0451\u0442\u0441\u044f \u0435\u0449\u0451 \u0434\u043e \u0431\u0430\u043d\u043d\u0435\u0440\u0430. \u041f\u0440\u043e\u0432\u0435\u0440\u044f\u044e TCP \u2014 \u043e\u0442\u043a\u0440\u044b\u0442. nmap \u043d\u0430 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0435 \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0435 SSH-\u043f\u043e\u0440\u0442\u044b \u2014 \u0432\u0441\u0435 \u00ab\u043e\u0442\u043a\u0440\u044b\u0442\u044b\u00bb, \u043d\u043e \u044d\u0442\u043e \u0438\u043b\u043b\u044e\u0437\u0438\u044f (cloud-\u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u043c\u0430\u0441\u043a\u0438\u0440\u0443\u0435\u0442 closed \u0432 open). \u041d\u0430 <code>https:\/\/<\/code> \u0442\u043e\u0439 \u0436\u0435 \u043c\u0430\u0448\u0438\u043d\u044b \u0442\u0435\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0435\u043c \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043a\u0430\u043a\u043e\u0435-\u0442\u043e \u0434\u0440\u0443\u0433\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \u0422\u043e \u0435\u0441\u0442\u044c SSH-\u0434\u0435\u043c\u043e\u043d \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u043e \u0440\u0435\u0436\u0435\u0442 \u0432\u043d\u0435\u0448\u043d\u0438\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f.<\/p>\n<p>\u0421\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u044e \u0437\u0430\u043a\u0430\u0437\u0447\u0438\u0446\u0443. \u00ab\u0410, \u043f\u0435\u0440\u0435\u043f\u0443\u0442\u0430\u043b\u0430, \u0432\u043e\u0442 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0435\u0440\u0432\u0435\u0440\u00bb. \u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u044e\u0441\u044c \u2014 \u043f\u043e\u0440\u044f\u0434\u043e\u043a. Ubuntu 24.04, \u0447\u0438\u0441\u0442\u0430\u044f, \u0431\u0435\u0437 Docker. \u041f\u0430\u043c\u044f\u0442\u044c 1.9 \u0413\u0411. \u041a\u043b\u0430\u0441\u0441.<\/p>\n<pre><code class=\"bash\"># \u0421\u0442\u0430\u0432\u0438\u043c Dockercurl -fsSL https:\/\/get.docker.com | sh # \u0417\u0430\u043b\u0438\u0432\u0430\u0435\u043c \u043f\u0440\u043e\u0435\u043a\u0442 (1.1 \u041c\u0411 \u0430\u0440\u0445\u0438\u0432)scp inventory-service.tar.gz root@SERVER:\/opt\/ssh root@SERVER 'cd \/opt &amp;&amp; tar xzf inventory-service.tar.gz \\  &amp;&amp; cd inventory-service \\  &amp;&amp; docker compose -f docker-compose.prod.yml up -d --build'<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0427\u0435\u0440\u0435\u0437 3 \u043c\u0438\u043d\u0443\u0442\u044b \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u0440\u0443\u0442\u0438\u0442\u0441\u044f \u043d\u0430 80 \u043f\u043e\u0440\u0442\u0443. \u0417\u0430\u043a\u0430\u0437\u0447\u0438\u0446\u0430 \u0437\u0430\u0445\u043e\u0434\u0438\u0442, \u0433\u043e\u0432\u043e\u0440\u0438\u0442: \u00ab\u0410 \u043f\u0430\u0440\u043e\u043b\u044c \u0433\u0434\u0435?\u00bb<\/p>\n<p>\u0422\u043e\u0447\u043d\u043e. \u041e\u0442\u043a\u0440\u044b\u0442\u043e \u0432\u0441\u0435\u043c\u0443 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0431\u0435\u0437 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438. \u0421\u0440\u043e\u0447\u043d\u043e \u0434\u0435\u043b\u0430\u044e Basic Auth middleware:<\/p>\n<pre><code class=\"python\">@app.middleware(\"http\")async def basic_auth_middleware(request, call_next):    if not (settings.app_username and settings.app_password):        return await call_next(request)    auth = request.headers.get(\"authorization\", \"\")    if auth.startswith(\"Basic \"):        try:            decoded = base64.b64decode(auth[6:]).decode()            user, _, pwd = decoded.partition(\":\")            if (secrets.compare_digest(user, settings.app_username)                    and secrets.compare_digest(pwd, settings.app_password)):                return await call_next(request)        except Exception:            pass    return Response(status_code=401,                    headers={\"WWW-Authenticate\": 'Basic realm=\"Inventory\"'})<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041f\u0435\u0440\u0432\u0430\u044f \u043f\u043e\u043f\u044b\u0442\u043a\u0430 \u0434\u0435\u043f\u043b\u043e\u044f \u0443\u043f\u0430\u043b\u0430 \u0441 <code>UnicodeEncodeError: 'latin-1' codec can't encode characters<\/code> \u2014 \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u044f \u0437\u0430\u0431\u043e\u0442\u043b\u0438\u0432\u043e \u043d\u0430\u043f\u0438\u0441\u0430\u043b <code>realm=\"\u0418\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u044c\"<\/code> (\u043f\u043e-\u0440\u0443\u0441\u0441\u043a\u0438). HTTP-\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438 \u0442\u043e\u043b\u044c\u043a\u043e ASCII. \u041f\u043e\u043f\u0440\u0430\u0432\u0438\u043b, \u043f\u0435\u0440\u0435\u0434\u0435\u043f\u043b\u043e\u0438\u043b \u2014 \u0437\u0430 3 \u043c\u0438\u043d\u0443\u0442\u044b \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440 \u0441\u043e \u0441\u0432\u0435\u0436\u0435\u0439 \u0441\u0431\u043e\u0440\u043a\u043e\u0439 Docker.<\/p>\n<hr\/>\n<h3>\u0426\u0438\u0444\u0440\u044b \u0432 \u0444\u0438\u043d\u0430\u043b\u0435<\/h3>\n<p>\u041f\u043e\u0441\u043b\u0435 \u0432\u0441\u0435\u0445 \u0438\u0442\u0435\u0440\u0430\u0446\u0438\u0439:<\/p>\n<div>\n<div class=\"table\">\n<table>\n<tbody>\n<tr>\n<th>\n<p align=\"left\">\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440<\/p>\n<\/th>\n<th>\n<p align=\"left\">\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435<\/p>\n<\/th>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u0410\u043a\u0442\u0438\u0432\u043e\u0432 \u0432 \u0411\u0414<\/p>\n<\/td>\n<td>\n<p align=\"left\">4792<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u0421 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d\u043d\u044b\u043c\u0438 \u041c\u041e\u041b\u043e\u043c \u0438 \u0444\u0438\u043b\u0438\u0430\u043b\u043e\u043c<\/p>\n<\/td>\n<td>\n<p align=\"left\">302<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u0417\u0430\u0433\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0445 \u043a\u043d\u0438\u0433 \u041c\u041e\u041b\u043e\u0432<\/p>\n<\/td>\n<td>\n<p align=\"left\">6<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u0417\u0430\u043f\u0438\u0441\u0435\u0439 \u00ab\u0435\u0441\u0442\u044c \u0432 \u043a\u043d\u0438\u0433\u0435, \u043d\u0435\u0442 \u0432 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0435\u00bb<\/p>\n<\/td>\n<td>\n<p align=\"left\">116<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u041f\u043e\u043b\u043d\u043e\u0435 \u0441\u043e\u0432\u043f\u0430\u0434\u0435\u043d\u0438\u0435 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0430 \u2194 \u043a\u043d\u0438\u0433\u0430<\/p>\n<\/td>\n<td>\n<p align=\"left\">217 \u0438\u0437 302<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u0420\u0435\u0430\u043b\u044c\u043d\u044b\u0435 \u0440\u0430\u0441\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u044f \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0445 \u043d\u043e\u043c\u0435\u0440\u043e\u0432<\/p>\n<\/td>\n<td>\n<p align=\"left\">5<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u0420\u0435\u0430\u043b\u044c\u043d\u044b\u0435 \u0440\u0430\u0441\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u044f \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u0445<\/p>\n<\/td>\n<td>\n<p align=\"left\">82<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u041f\u043e\u0442\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0435 \u0437\u0430\u0434\u0432\u043e\u0435\u043d\u0438\u0435 \u043f\u043e \u0434\u0430\u043d\u043d\u044b\u043c<\/p>\n<\/td>\n<td>\n<p align=\"left\">747<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u0424\u0430\u0439\u043b\u043e\u0432 \u043a\u043e\u0434\u0430<\/p>\n<\/td>\n<td>\n<p align=\"left\">~25<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u0421\u0442\u0440\u043e\u043a \u043a\u043e\u0434\u0430<\/p>\n<\/td>\n<td>\n<p align=\"left\">~3500<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u0420\u0430\u0437\u043c\u0435\u0440 Docker-\u043e\u0431\u0440\u0430\u0437\u0430<\/p>\n<\/td>\n<td>\n<p align=\"left\">~600 \u041c\u0411<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p align=\"left\">\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u043e \u043f\u0430\u043c\u044f\u0442\u0438 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u043e\u043c<\/p>\n<\/td>\n<td>\n<p align=\"left\">~120 \u041c\u0411<\/p>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<\/div>\n<hr\/>\n<h3>\u0427\u0435\u043c\u0443 \u043c\u0435\u043d\u044f \u044d\u0442\u043e \u043d\u0430\u0443\u0447\u0438\u043b\u043e<\/h3>\n<p><strong>\u00ab\u0421\u0434\u0435\u043b\u0430\u0439 \u043f\u043e \u0422\u0417\u00bb \u2014 \u044d\u0442\u043e \u043d\u0435 \u0444\u0438\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u043b\u0430\u043d, \u0430 \u043e\u0442\u043f\u0440\u0430\u0432\u043d\u0430\u044f \u0442\u043e\u0447\u043a\u0430.<\/strong> \u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u0438\u0437 11 \u0440\u0430\u0437\u0434\u0435\u043b\u043e\u0432 \u043d\u0430 250 \u0441\u0442\u0440\u043e\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u044f \u043d\u0430\u043f\u0438\u0441\u0430\u043b \u0432 \u043d\u0430\u0447\u0430\u043b\u0435, \u0438\u0437\u043c\u0435\u043d\u0438\u043b\u0441\u044f \u043a \u0444\u0438\u043d\u0430\u043b\u0443 \u043f\u0440\u0438\u043c\u0435\u0440\u043d\u043e \u043d\u0430 60%. \u0418 \u044d\u0442\u043e \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e. \u0413\u043b\u0430\u0432\u043d\u043e\u0435 \u2014 \u0433\u0438\u0431\u043a\u0430\u044f \u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430.<\/p>\n<p><strong>\u042d\u043a\u0441\u0435\u043b\u044c\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432\u0441\u0435\u0433\u0434\u0430 \u0433\u0440\u044f\u0437\u043d\u0435\u0435, \u0447\u0435\u043c \u0432\u044b\u0433\u043b\u044f\u0434\u044f\u0442.<\/strong> \u0414\u0432\u043e\u0439\u043d\u044b\u0435 \u0434\u0435\u0444\u0438\u0441\u044b, \u043d\u0435\u0440\u0430\u0437\u0440\u044b\u0432\u043d\u044b\u0435 \u043f\u0440\u043e\u0431\u0435\u043b\u044b, \u043c\u0430\u0441\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 NULL-\u044b (<code>.<\/code>, <code>`-<\/code>, <code>nan<\/code>), merge-\u044f\u0447\u0435\u0439\u043a\u0438, \u043e\u0448\u0438\u0431\u043e\u0447\u043d\u044b\u0435 \u0432\u0435\u0434\u0443\u0449\u0438\u0435 \u043d\u0443\u043b\u0438. \u041a \u043c\u043e\u043c\u0435\u043d\u0442\u0443, \u043a\u043e\u0433\u0434\u0430 \u044f \u0437\u0430\u043a\u043e\u043d\u0447\u0438\u043b \u043f\u0438\u0441\u0430\u0442\u044c <code>normalize.py<\/code>, \u0442\u0430\u043c \u0431\u044b\u043b\u043e 5 \u0444\u0443\u043d\u043a\u0446\u0438\u0439 \u0438 80 \u0441\u0442\u0440\u043e\u043a \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u043e\u0434\u043d\u043e\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u00ab\u043f\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u043a \u043a\u0430\u043d\u043e\u043d\u0443\u00bb.<\/p>\n<p><strong>\u041d\u0435 \u043f\u0438\u0448\u0438\u0442\u0435 \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u044b\u0435 \u0445\u0435\u043b\u043f\u0435\u0440\u044b \u0434\u043b\u044f \u043d\u0435\u043e\u0434\u043d\u043e\u0440\u043e\u0434\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445.<\/strong> \u0418\u0441\u0442\u043e\u0440\u0438\u044f \u0441 <code>values_match<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043b\u043e\u043c\u0430\u043b \u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u0435 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a\u043e\u0432 \u0438\u0437-\u0437\u0430 \u0446\u0438\u0444\u0440\u043e\u0432\u043e\u0433\u043e fallback \u2014 \u043a\u043b\u0430\u0441\u0441\u0438\u043a\u0430. \u0415\u0441\u043b\u0438 \u0432 \u043e\u0441\u043d\u043e\u0432\u0435 \u0432\u0430\u0448\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u00ab\u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u0441 \u0431\u0443\u043a\u0432\u0435\u043d\u043d\u044b\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c\u00bb \u0438 \u00ab\u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a \u0441 \u043b\u044e\u0431\u044b\u043c \u0430\u043b\u0444\u0430\u0432\u0438\u0442\u043e\u043c\u00bb \u2014 \u044d\u0442\u043e \u0440\u0430\u0437\u043d\u044b\u0435 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 \u0441 \u0440\u0430\u0437\u043d\u044b\u043c\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430\u043c\u0438 \u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u044f.<\/p>\n<p><strong>SQLite \u2014 \u043d\u0435 \u0438\u0433\u0440\u0443\u0448\u043a\u0430.<\/strong> \u0414\u043b\u044f 5000 \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u2014 \u0438\u0434\u0435\u0430\u043b\u044c\u043d\u043e. \u041e\u0434\u0438\u043d \u0444\u0430\u0439\u043b, \u043d\u0438\u043a\u0430\u043a\u043e\u0439 \u0438\u043d\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u044b, \u043d\u0438\u043a\u0430\u043a\u0438\u0445 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0439 \u043c\u0435\u0436\u0434\u0443 \u0441\u0440\u0435\u0434\u0430\u043c\u0438. \u041f\u0435\u0440\u0435\u043d\u0435\u0441\u043b\u0438 \u2014 \u043f\u043e\u0435\u0445\u0430\u043b\u043e. \u0412 80% \u0430\u0434\u043c\u0438\u043d\u043e\u043a Postgres \u043d\u0435 \u043d\u0443\u0436\u0435\u043d.<\/p>\n<p><strong>Basic Auth + Docker \u2014 \u0440\u0430\u0431\u043e\u0447\u0438\u0439 \u043c\u0438\u043d\u0438\u043c\u0443\u043c.<\/strong> \u0415\u0441\u043b\u0438 \u0432\u043d\u0443\u0442\u0440\u0438\u043a\u043e\u0440\u043f\u043e\u0440\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0441\u0435\u0440\u0432\u0438\u0441 \u043d\u0430 5 \u0447\u0435\u043b\u043e\u0432\u0435\u043a, \u043d\u0435 \u043d\u0443\u0436\u043d\u044b OAuth, JWT, refresh-\u0442\u043e\u043a\u0435\u043d\u044b \u0438 \u0440\u043e\u043b\u0438. \u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0439 Basic Auth + HTTPS-\u043f\u0440\u043e\u043a\u0441\u0438 \u0441\u0432\u0435\u0440\u0445\u0443 \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442 95% \u0443\u0433\u0440\u043e\u0437 \u0438 \u043f\u0438\u0448\u0435\u0442\u0441\u044f \u0437\u0430 30 \u0441\u0442\u0440\u043e\u043a.<\/p>\n<p><strong>AI vision \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442.<\/strong> Claude Opus 4.6 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0451\u0442 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0435 \u0438 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u0435 \u043d\u043e\u043c\u0435\u0440\u0430 \u0441 \u0444\u043e\u0442\u043e \u0448\u0438\u043b\u044c\u0434\u0438\u043a\u043e\u0432 \u0432 \u043f\u043e\u043b\u0435\u0432\u044b\u0445 \u0443\u0441\u043b\u043e\u0432\u0438\u044f\u0445 (\u043f\u043b\u043e\u0445\u043e\u0435 \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u0435, \u0443\u0433\u043e\u043b, \u043f\u043e\u0442\u0451\u0440\u0442\u043e\u0441\u0442\u0438) \u0441 \u0442\u043e\u0447\u043d\u043e\u0441\u0442\u044c\u044e 90%+. \u0421\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e \u0442\u0440\u0435\u043d\u0438\u0440\u043e\u0432\u0430\u0442\u044c OCR-\u043c\u043e\u0434\u0435\u043b\u044c \u043f\u043e\u0434 \u044d\u0442\u043e \u2014 \u043c\u0438\u043b\u043b\u0438\u043e\u043d \u0434\u043e\u043b\u043b\u0430\u0440\u043e\u0432 \u0438 6 \u043c\u0435\u0441\u044f\u0446\u0435\u0432. OpenRouter \u2014 \u043f\u0430\u0440\u0430 \u0446\u0435\u043d\u0442\u043e\u0432 \u0437\u0430 \u0432\u044b\u0437\u043e\u0432 \u0438 5 \u043c\u0438\u043d\u0443\u0442 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e.<\/p>\n<hr\/>\n<h3>\u0427\u0442\u043e \u0431\u044b \u044f \u0441\u0434\u0435\u043b\u0430\u043b \u043f\u043e-\u0434\u0440\u0443\u0433\u043e\u043c\u0443<\/h3>\n<ul>\n<li>\n<p>\u0421\u0440\u0430\u0437\u0443 \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u043b \u0431\u044b \u043c\u0430\u0442\u0447\u0438\u043d\u0433 \u043f\u043e \u0442\u0438\u043f\u0430\u043c \u043a\u043b\u044e\u0447\u0435\u0439 (\u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u0438\u0432 \u0441\u0435\u0440\u0438\u0439\u043d\u043e\u0433\u043e), \u0430 \u043d\u0435 \u0434\u0435\u043b\u0430\u043b \u00ab\u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u0443\u044e \u0444\u0443\u043d\u043a\u0446\u0438\u044e\u00bb, \u0432 \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043f\u043e\u0442\u043e\u043c \u0432\u043a\u0440\u0443\u0447\u0438\u0432\u0430\u043b \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.<\/p>\n<\/li>\n<li>\n<p>\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u043b \u0431\u044b Pydantic \u0434\u043b\u044f \u043f\u0430\u0440\u0441\u0438\u043d\u0433\u0430 \u043a\u043d\u0438\u0433 \u041c\u041e\u041b\u043e\u0432 \u2014 \u043a\u043e\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043b \u0431\u044b \u0441\u0442\u0440\u043e\u043a\u0438 \u0432 dataclass \u0441 \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u0435\u0439, \u0431\u044b\u043b\u043e \u0431\u044b \u043f\u0440\u043e\u0449\u0435 \u043b\u043e\u0432\u0438\u0442\u044c \u0434\u0440\u0435\u0431\u0435\u0437\u0433 \u0434\u0430\u043d\u043d\u044b\u0445.<\/p>\n<\/li>\n<li>\n<p>\u0421\u0440\u0430\u0437\u0443 \u043d\u0430\u043f\u0438\u0441\u0430\u043b \u0431\u044b \u0442\u0435\u0441\u0442\u044b \u043d\u0430 \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e. \u042f \u043f\u043e\u043b\u0430\u0433\u0430\u043b\u0441\u044f \u043d\u0430 \u00ab\u043f\u0440\u043e\u0433\u043e\u043d \u043d\u0430 \u0431\u043e\u0435\u0432\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445\u00bb \u2014 \u044d\u0442\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442, \u043d\u043e \u0431\u0430\u0433 \u0441 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a\u0430\u043c\u0438 \u044f \u0431\u044b \u043f\u043e\u0439\u043c\u0430\u043b \u0435\u0449\u0451 \u043d\u0430 \u0434\u0435\u0432-\u043c\u0430\u0448\u0438\u043d\u0435, \u0435\u0441\u043b\u0438 \u0431\u044b \u0431\u044b\u043b \u0445\u043e\u0442\u044f \u0431\u044b \u043e\u0434\u0438\u043d pytest-\u0441\u043d\u0438\u043c\u043e\u043a \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0445 \u043f\u0430\u0440.<\/p>\n<\/li>\n<\/ul>\n<hr\/>\n<h3>\u0417\u0430\u043a\u0440\u044b\u0442\u044b\u0435 \u0434\u0432\u0435\u0440\u0438 \u0438 \u043e\u0442\u043a\u0440\u044b\u0442\u044b\u0435 \u0437\u0430\u0434\u0430\u0447\u0438<\/h3>\n<p>\u0427\u0442\u043e \u043d\u0435 \u0441\u0434\u0435\u043b\u0430\u043b \u0438 \u043e\u0441\u0442\u0430\u0432\u0438\u043b \u043d\u0430 \u043f\u043e\u0442\u043e\u043c:<\/p>\n<ul>\n<li>\n<p>Telegram-\u0431\u043e\u0442 \u0434\u043b\u044f \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u043d\u0438\u044f (\u0437\u0430\u0433\u043e\u0442\u043e\u0432\u043a\u0430 \u0432 \u043a\u043e\u0434\u0435 \u0435\u0441\u0442\u044c, \u0436\u0434\u0451\u0442 \u0442\u043e\u043a\u0435\u043d).<\/p>\n<\/li>\n<li>\n<p>HTTPS \u2014 \u0441\u0435\u0439\u0447\u0430\u0441 Basic Auth \u0447\u0435\u0440\u0435\u0437 HTTP, \u0447\u0442\u043e \u043d\u0430\u0438\u0432\u043d\u043e. \u041d\u0430 \u043e\u0447\u0435\u0440\u0435\u0434\u0438 nginx + Let\u2019s Encrypt \u043b\u0438\u0431\u043e Cloudflare Tunnel.<\/p>\n<\/li>\n<li>\n<p>\u041c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u044e\u0449\u0438\u0435 \u2014 \u0441\u0435\u0439\u0447\u0430\u0441 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0430\u043d\u043e\u043d\u0438\u043c\u043d\u0430\u044f (\u0442\u043e\u043b\u044c\u043a\u043e \u0434\u0430\u0442\u0430).<\/p>\n<\/li>\n<li>\n<p>\u0418\u043c\u043f\u043e\u0440\u0442 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0445 \u0444\u0438\u043b\u0438\u0430\u043b\u043e\u0432 \u2014 \u0443 \u0437\u0430\u043a\u0430\u0437\u0447\u0438\u0446\u044b \u0435\u0441\u0442\u044c \u043a\u043d\u0438\u0433\u0438 \u041c\u041e\u041b\u043e\u0432 \u043f\u043e\u043a\u0430 \u043f\u043e \u043e\u0434\u043d\u043e\u043c\u0443 \u0444\u0438\u043b\u0438\u0430\u043b\u0443, \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u043e\u0431\u0435\u0449\u0430\u044e\u0442 \u043f\u043e\u0434\u0432\u0435\u0437\u0442\u0438.<\/p>\n<\/li>\n<\/ul>\n<hr\/>\n<p>\u0415\u0441\u043b\u0438 \u0432\u0430\u043c \u043f\u0440\u0438\u0441\u044b\u043b\u0430\u044e\u0442 \u00ab\u043f\u043e\u043c\u043e\u0433\u0438 \u0441 \u0442\u0430\u0431\u043b\u0438\u0447\u043a\u043e\u0439 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u044f \u0432 Excel\u00bb \u2014 \u044d\u0442\u043e \u0432\u0441\u0435\u0433\u0434\u0430 \u043f\u0440\u043e\u0435\u043a\u0442 \u043d\u0430 2 \u043d\u0435\u0434\u0435\u043b\u0438, \u0430 \u043d\u0435 \u00ab\u043d\u0430\u0440\u0438\u0441\u0443\u0439 \u043f\u0438\u0432\u043e\u0442\u00bb. \u0420\u0435\u0430\u043b\u044c\u043d\u0430\u044f \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0439 \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u0438 \u2014 \u044d\u0442\u043e \u043c\u043d\u043e\u0433\u043e\u0441\u043b\u043e\u0439\u043d\u044b\u0439 \u043f\u0438\u0440\u043e\u0433 \u0438\u0437 \u0438\u0441\u0442\u043e\u0440\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u0444\u043e\u0440\u043c\u0430\u0442\u043e\u0432, \u043d\u0435\u043a\u043e\u043d\u0441\u0438\u0441\u0442\u0435\u043d\u0442\u043d\u044b\u0445 \u043f\u0440\u0430\u0432\u0438\u043b \u0438 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0445 \u0430\u0440\u0442\u0435\u0444\u0430\u043a\u0442\u043e\u0432. \u0418 \u0438\u043c\u0435\u043d\u043d\u043e \u0432 \u044d\u0442\u043e\u043c \u043e\u043d\u0430 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u0430.<\/p>\n<p>P.S. \u0417\u0430\u043a\u0430\u0437\u0447\u0438\u0446\u0430 \u0434\u043e\u0432\u043e\u043b\u044c\u043d\u0430. \u041d\u0430 \u0433\u043b\u0430\u0432\u043d\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 \u2014 \u0432\u0441\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u044b, \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u0430\u044f \u0432\u0451\u0440\u0441\u0442\u043a\u0430, \u043a\u043d\u043e\u043f\u043a\u0438 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u044f, \u0430\u0432\u0442\u043e\u043f\u0443\u0448 \u0432 Google-\u0442\u0430\u0431\u043b\u0438\u0446\u0443 \u0441 \u0446\u0432\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043b\u0438\u0432\u043a\u043e\u0439. \u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442 \u0441 \u043f\u043b\u0430\u043d\u0448\u0435\u0442\u0430 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u0438\u0437\u0430\u0446\u0438\u0438, \u0444\u043e\u0442\u043a\u0430\u0435\u0442 \u0448\u0438\u043b\u044c\u0434\u0438\u043a, \u0432\u0438\u0434\u0438\u0442 \u043a\u0430\u0440\u0442\u043e\u0447\u043a\u0443, \u0441\u0442\u0430\u0432\u0438\u0442 \u0433\u0430\u043b\u043e\u0447\u043a\u0443. \u041d\u0435 \u043f\u0440\u0438\u0448\u043b\u043e\u0441\u044c \u043d\u0438 \u0440\u0430\u0437\u0443 \u043b\u0435\u0437\u0442\u044c \u0432 Excel.<\/p>\n<\/div>\n<p>\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1039940\/\">https:\/\/habr.com\/ru\/articles\/1039940\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u0418\u0441\u0442\u043e\u0440\u0438\u044f \u043e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u0440\u043e\u0441\u044c\u0431\u0430 \u00ab\u0433\u043b\u044f\u043d\u044c, \u0447\u0442\u043e \u0443 \u043c\u0435\u043d\u044f \u0442\u0443\u0442 \u0441 \u0442\u0430\u0431\u043b\u0438\u0447\u043a\u0430\u043c\u0438\u00bb \u043f\u0440\u0435\u0432\u0440\u0430\u0442\u0438\u043b\u0430\u0441\u044c \u0432 production-\u0441\u0435\u0440\u0432\u0438\u0441 \u0441 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u043d\u0438\u0435\u043c \u043f\u043e \u0444\u043e\u0442\u043e, \u0437\u0430\u0449\u0438\u0449\u0451\u043d\u043d\u043e\u0439 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0435\u0439 \u0430\u0434\u043c\u0438\u043d\u043a\u043e\u0439 \u0438 \u0430\u0432\u0442\u043e\u043f\u0443\u0448\u0435\u043c \u0432 Google Sheets. \u0421\u043e \u0432\u0441\u0435\u043c\u0438 \u0433\u0440\u0430\u0431\u043b\u044f\u043c\u0438, \u0444\u0435\u0439\u043b\u0430\u043c\u0438 \u0438 \u0438\u043d\u0441\u0430\u0439\u0442\u0430\u043c\u0438.\u041a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u041c\u043e\u044f \u0434\u0435\u0432\u0443\u0448\u043a\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0432 IT-\u0434\u0435\u043f\u0430\u0440\u0442\u0430\u043c\u0435\u043d\u0442\u0435 \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u0438 \u0441 16 \u0442\u0435\u0440\u0440\u0438\u0442\u043e\u0440\u0438\u0430\u043b\u044c\u043d\u044b\u043c\u0438 \u0444\u0438\u043b\u0438\u0430\u043b\u0430\u043c\u0438, \u0441\u043a\u043b\u0430\u0434\u043e\u043c \u0438 \u043f\u0430\u0440\u043e\u0439 \u043a\u043b\u0438\u043d\u0438\u043a. \u041d\u0430 \u0431\u0430\u043b\u0430\u043d\u0441\u0435 \u043f\u0440\u0438\u043c\u0435\u0440\u043d\u043e 5000 \u0435\u0434\u0438\u043d\u0438\u0446 \u043e\u0440\u0433\u0442\u0435\u0445\u043d\u0438\u043a\u0438: \u043c\u043e\u043d\u043e\u0431\u043b\u043e\u043a\u0438, \u041c\u0424\u0423, \u043d\u043e\u0443\u0442\u0431\u0443\u043a\u0438, \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u044b, \u0421\u041a\u0417\u0418. \u0420\u0430\u0437 \u0432 \u0433\u043e\u0434 \u2014 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u0438\u0437\u0430\u0446\u0438\u044f.\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435:\u0413\u043b\u0430\u0432\u043d\u0430\u044f \u0432\u044b\u0433\u0440\u0443\u0437\u043a\u0430 \u0438\u0437 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u2014 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0430.xlsx, 4792 \u0441\u0442\u0440\u043e\u043a\u0438, 77 \u043a\u043e\u043b\u043e\u043d\u043e\u043a.6 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0445 \u043a\u043d\u0438\u0433 \u043f\u043e \u043c\u0430\u0442\u0435\u0440\u0438\u0430\u043b\u044c\u043d\u043e-\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u043c \u043b\u0438\u0446\u0430\u043c (\u041c\u041e\u041b\u0430\u043c) \u2014 \u0444\u043e\u0440\u043c\u0430\u0442 .xls, \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0430\u044f \u0444\u043e\u0440\u043c\u0430. \u0417\u0430\u0434\u0430\u0447\u0430 \u0437\u0432\u0443\u0447\u0430\u043b\u0430 \u0442\u0430\u043a: \u00ab\u0421\u0434\u0435\u043b\u0430\u0439 \u0441\u0435\u0440\u0432\u0438\u0441, \u0447\u0442\u043e\u0431\u044b \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u043e\u0442\u043a\u0443 \u0448\u0438\u043b\u044c\u0434\u0438\u043a\u0430, \u0438 \u043e\u043d \u043c\u043d\u0435 \u0441\u043a\u0430\u0437\u0430\u043b, \u0443 \u043a\u043e\u0433\u043e \u044d\u0442\u0430 \u0436\u0435\u043b\u0435\u0437\u043a\u0430 \u0441\u0442\u043e\u0438\u0442\u00bb. \u0417\u0432\u0443\u0447\u0438\u0442 \u043f\u0440\u043e\u0441\u0442\u043e. \u041d\u0430 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0435 \u044d\u0442\u043e \u043f\u043e\u043b\u043e\u0432\u0438\u043d\u0430 \u0437\u0430\u0434\u0430\u0447\u0438. \u0412\u0442\u043e\u0440\u0430\u044f \u043f\u043e\u043b\u043e\u0432\u0438\u043d\u0430 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u043b\u0430\u0441\u044c \u043f\u043e \u0445\u043e\u0434\u0443.\u0421\u0442\u0435\u043aBackend: Python 3.12, FastAPI, SQLAlchemy 2, SQLite (\u043d\u0430 \u0441\u0442\u0430\u0440\u0442\u0435 \u2014 \u043e\u043a\u0430\u0437\u0430\u043b\u0441\u044f \u0443\u043c\u0435\u0441\u0442\u0435\u043d \u0438 \u043f\u043e\u0442\u043e\u043c).ETL: pandas + openpyxl \u0434\u043b\u044f .xlsx, xlrd 1.2 \u0434\u043b\u044f .xls (\u043f\u0440\u043e \u044d\u0442\u0443 \u0431\u043e\u043b\u044c \u2014 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e).AI: Claude Opus 4.6 \u0447\u0435\u0440\u0435\u0437 OpenRouter (vision-API).\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438: gspread + Google Service Account \u0434\u043b\u044f \u043f\u0443\u0448\u0430 \u0432 Google Sheets.Frontend: Jinja2 + \u0432\u0430\u043d\u0438\u043b\u044c\u043d\u044b\u0439 CSS \u0441 \u0434\u0438\u0437\u0430\u0439\u043d-\u0442\u043e\u043a\u0435\u043d\u0430\u043c\u0438 (\u0431\u0435\u0437 React\/Vue \u2014 overkill \u0434\u043b\u044f \u0430\u0434\u043c\u0438\u043d\u043a\u0438).\u0414\u0435\u043f\u043b\u043e\u0439: Docker Compose, single container. \u041d\u0438\u043a\u0430\u043a\u0438\u0445 \u043c\u0438\u043a\u0440\u043e\u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432, \u043d\u0438\u043a\u0430\u043a\u043e\u0433\u043e Postgres, \u043d\u0438\u043a\u0430\u043a\u043e\u0433\u043e Kubernetes. \u041e\u0434\u0438\u043d \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440, \u043e\u0434\u043d\u0430 SQLite, \u043e\u0434\u0438\u043d FastAPI.\u0411\u043e\u043b\u044c \u21161: .xls \u043f\u0440\u043e\u0442\u0438\u0432 pandas 2.x\u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u043c \u0444\u0430\u0439\u043b\u044b. \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0430.xlsx \u2014 \u0441\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442, \u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044f pandas + openpyxl \u0431\u0435\u0437 \u043f\u0440\u043e\u0431\u043b\u0435\u043c. \u0410 \u0432\u043e\u0442 6 \u043a\u043d\u0438\u0433 \u041c\u041e\u041b\u043e\u0432 \u2014 \u0441\u0442\u0430\u0440\u044b\u0439 .xls, \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u044d\u043a\u0441\u043f\u043e\u0440\u0442 \u0438\u0437 1\u0421.import pandas as pddf = pd.read_excel(&#171;mol_1.xls&#187;, engine=&#187;xlrd&#187;)\u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c:ImportError: Pandas requires version &#8216;2.0.1&#8217; or newer of &#8216;xlrd'(version &#8216;1.2.0&#8217; currently installed).\u0421\u0442\u0430\u0432\u0438\u043c \u0441\u0432\u0435\u0436\u0438\u0439 xlrd:pip install -U xlrd\u0417\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u0441\u043d\u043e\u0432\u0430:xlrd.biffh.XLRDError: Excel xlsx file; not supported\u0421\u044e\u0440\u043f\u0440\u0438\u0437: xlrd 2.0 \u0432\u044b\u043f\u0438\u043b\u0438\u043b\u0438 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 \u0441\u0442\u0430\u0440\u043e\u0433\u043e .xls. \u0410 pandas 2.x \u0442\u0440\u0435\u0431\u0443\u0435\u0442 xlrd \u2265 2.0. \u041f\u043e\u043b\u0443\u0447\u0438\u043b\u0441\u044f deadlock: \u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043b\u0438\u0431\u043e pandas 1.x, \u043b\u0438\u0431\u043e \u043e\u0431\u0445\u043e\u0434\u0438\u0442\u044c pandas.\u0412\u044b\u0431\u0440\u0430\u043b \u0432\u0442\u043e\u0440\u043e\u0435 \u2014 \u0447\u0438\u0442\u0430\u0442\u044c .xls \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e \u0447\u0435\u0440\u0435\u0437 xlrd 1.2:import xlrd def parse_book(xls_path):    wb = xlrd.open_workbook(str(xls_path))    sh = wb.sheet_by_name(&#171;\u0418\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u0430\u044f \u043a\u043d\u0438\u0433\u0430&#187;)    rows = []    for r in range(sh.nrows):        first = sh.cell(r, 0).value        if not first or first.lower().startswith((&#171;\u0441\u0447\u0435\u0442&#187;, &#171;\u043c\u043e\u043b&#187;, &#171;\u0438\u0442\u043e\u0433\u043e&#187;, &#171;\u0438\u043d\u0432.&#187;)):            continue        rows.append({            &#171;inv_number&#187;: first,            &#171;serial_number&#187;: sh.cell(r, 19).value,            &#171;book_initial_cost&#187;: _to_float(sh.cell(r, 32).value),            # &#8230;        })    return rows\u0412 \u0442\u0430\u0431\u043b\u0438\u0446\u0430\u0445 \u043e\u043a\u0430\u0437\u0430\u043b\u0438\u0441\u044c \u0441\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438-\u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u0438 (\u0421\u0447\u0451\u0442 0901\u2026, \u041c\u041e\u041b \u2026-\u043e\u0441, \u0418\u0442\u043e\u0433\u043e \u043f\u043e \u041c\u041e\u041b\u0443\u2026), \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0430\u0434\u043e \u0431\u044b\u043b\u043e \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u0442\u044c. \u0418 \u0441\u0442\u0440\u0451\u043c\u043d\u044b\u0435 \u044f\u0447\u0435\u0439\u043a\u0438 \u0442\u0438\u043f\u0430 \u041a\u0410\u0414\u0420\u042b                       \u0418\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u0430\u044f \u043a\u043d\u0438\u0433\u0430 (\u043f\u043e\u043b\u043d\u0430\u044f) \u043f\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044e \u043d\u0430 \u2026 \u0432 \u043f\u0435\u0440\u0432\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u0435 (\u043a\u043e\u0433\u0434\u0430 \u0432 .xls \u043f\u043b\u043e\u0445\u043e \u0440\u0430\u0441\u043f\u0430\u0440\u0441\u0438\u043b\u0438\u0441\u044c merged cells).\u0423\u0440\u043e\u043a: \u0434\u043b\u044f \u0441\u0442\u0430\u0440\u044b\u0445 \u0444\u043e\u0440\u043c\u0430\u0442\u043e\u0432 \u043e\u0444\u0438\u0441\u0430 \u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u0435\u0434\u0443\u0448\u043a\u0438\u043d xlrd 1.2 \u0438 \u0447\u0438\u0442\u0430\u0439\u0442\u0435 \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e. \u041d\u0435 \u043f\u044b\u0442\u0430\u0439\u0442\u0435\u0441\u044c \u043f\u043e\u0434\u0440\u0443\u0436\u0438\u0442\u044c \u0435\u0433\u043e \u0441 \u0441\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u043c pandas \u2014 \u0434\u0430\u0436\u0435 \u043d\u0435 \u043f\u044b\u0442\u0430\u0439\u0442\u0435\u0441\u044c.\u0411\u043e\u043b\u044c \u21162: \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043d\u043e\u043c\u0435\u0440\u043e\u0432 (\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u00ab\u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c \u0431\u044b \u043f\u0440\u043e\u0441\u0442\u0430\u044f\u00bb)\u042f \u043d\u0430\u0438\u0432\u043d\u043e \u0434\u0443\u043c\u0430\u043b, \u0447\u0442\u043e \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0435 \u0438 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u0435 \u043d\u043e\u043c\u0435\u0440\u0430 \u2014 \u044d\u0442\u043e \u043f\u0440\u043e\u0441\u0442\u043e \u0441\u0442\u0440\u043e\u043a\u0438. \u0421\u0432\u0435\u0440\u044f\u0442\u044c \u0438\u0445 \u0447\u0435\u0440\u0435\u0437 == \u0438 \u0432\u0441\u0451. \u0425\u0430\u0445.\u0420\u0435\u0430\u043b\u044c\u043d\u0430\u044f \u0432\u044b\u0431\u043e\u0440\u043a\u0430 \u0438\u0437 \u043a\u043d\u0438\u0433\u0438 (\u043f\u0440\u0438\u0432\u043e\u0436\u0443 \u043e\u0431\u0435\u0437\u043b\u0438\u0447\u0435\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043c\u0435\u0440\u044b \u0432 \u0442\u043e\u043c \u0436\u0435 \u0444\u043e\u0440\u043c\u0430\u0442\u0435, \u0447\u0442\u043e \u0432\u0441\u0442\u0440\u0435\u0447\u0430\u043b\u0438\u0441\u044c):002410124-001287    \u2190 \u043e\u0431\u0440\u0430\u0437\u0446\u043e\u0432\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c002410124-0003750.  \u2190 \u043b\u0438\u0448\u043d\u0438\u0435 \u043d\u0443\u043b\u0438 \u0438 \u0442\u043e\u0447\u043a\u0430 \u0432 \u043a\u043e\u043d\u0446\u0435002410124&#8212;003917   \u2190 \u0434\u0432\u043e\u0439\u043d\u043e\u0439 \u0434\u0435\u0444\u0438\u0441 (\u043e\u043f\u0435\u0447\u0430\u0442\u043a\u0430)002410134003663.    \u2190 \u0432\u043e\u043e\u0431\u0449\u0435 \u0431\u0435\u0437 \u0434\u0435\u0444\u0438\u0441\u0430210134-12           \u2190 \u043a\u043e\u0440\u043e\u0442\u043a\u0438\u0439 \u0444\u043e\u0440\u043c\u0430\u0442110134-_001146      \u2190 \u043f\u043e\u0434\u0447\u0451\u0440\u043a\u0438\u0432\u0430\u043d\u0438\u0435-\u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u0435\u043b\u044c\u0410 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a \u0432\u043e\u043e\u0431\u0449\u0435:DQVRZER1380030794C3000              \u2190 \u043d\u043e\u0440\u043cDQVUYER00K144003B43000 555-2358     \u2190 \u0441 \u0445\u0432\u043e\u0441\u0442\u043e\u043c \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0431\u0435\u043b410134-1482\/DQVRZER1380040856C3000  \u2190 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0439 \u043a\u043e\u0434 \u0441\u043a\u043b\u0435\u0435\u043d \u0447\u0435\u0440\u0435\u0437 \u0441\u043b\u0435\u0448CNDRPBV9RQ.\/555-1929                \u2190 \u043c\u0443\u0441\u043e\u0440 \u043f\u043e\u0441\u043b\u0435 \u0442\u043e\u0447\u043a\u0438 \u0438 \u0441\u043b\u0435\u0448\u0430`-                                  \u2190 \u0432\u043e\u043e\u0431\u0449\u0435 \u043d\u0435 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a, \u043c\u0443\u0441\u043e\u0440\u043d\u044b\u0439 \u043f\u043b\u0435\u0439\u0441\u0445\u043e\u043b\u0434\u0435\u0440\u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u043d\u0430\u043f\u0438\u0441\u0430\u043b \u0444\u0443\u043d\u043a\u0446\u0438\u044e \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438:def norm_key(value):    s = str(value).strip().upper()    # \u0445\u0432\u043e\u0441\u0442 \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0431\u0435\u043b\/\u0441\u043b\u0435\u0448 \u2014 \u0431\u0435\u0440\u0451\u043c \u043f\u0435\u0440\u0432\u044b\u0439 \u0442\u043e\u043a\u0435\u043d    for sep in (&#187; &#171;, &#171;\/&#187;, &#171;\\\\&#187;):        if sep in s:            s = s.split(sep, 1)[0]    # \u0432\u044b\u043a\u0438\u0434\u044b\u0432\u0430\u0435\u043c \u043c\u0443\u0441\u043e\u0440, \u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c \u0431\u0443\u043a\u0432\u044b, \u0446\u0438\u0444\u0440\u044b, \u0434\u0435\u0444\u0438\u0441    s = re.sub(r&#187;[^A-Z0-9-]&#187;, &#171;&#187;, s)    # \u0441\u0445\u043b\u043e\u043f\u044b\u0432\u0430\u0435\u043c \u0434\u0432\u043e\u0439\u043d\u044b\u0435 \u0434\u0435\u0444\u0438\u0441\u044b    while &#171;&#8212;&#187; in s:        s = s.replace(&#171;&#8212;&#171;, &#171;-&#171;)    # \u0431\u0435\u0437 \u0434\u0435\u0444\u0438\u0441\u0430 \u0438 \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u0434\u043b\u0438\u043d\u043d\u044b\u0439 \u2014 \u0432\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c \u0434\u0435\u0444\u0438\u0441 \u043f\u043e\u0441\u043b\u0435 \u043f\u0435\u0440\u0432\u044b\u0445 9 \u0446\u0438\u0444\u0440    if &#171;-&#187; not in s and s.isdigit() and len(s) &gt;= 12:        s = s[:9] + &#171;-&#187; + s[9:]    # \u0432\u0435\u0434\u0443\u0449\u0438\u0435 \u043d\u0443\u043b\u0438 \u043f\u043e\u0441\u043b\u0435 \u0434\u0435\u0444\u0438\u0441\u0430    parts = s.split(&#171;-&#171;)    if len(parts) == 2 and parts[1].isdigit():        parts[1] = parts[1].lstrip(&#171;0&#187;) or &#171;0&#187;        s = &#171;-&#171;.join(parts)    return s or None\u042d\u0442\u043e\u0433\u043e \u0445\u0432\u0430\u0442\u0438\u043b\u043e \u0434\u043b\u044f 80% \u0441\u043b\u0443\u0447\u0430\u0435\u0432. \u041d\u043e \u043d\u0435 \u0434\u043b\u044f \u0432\u0441\u0435\u0445. \u041a\u0430\u043a\u0438\u0435-\u0442\u043e \u043d\u043e\u043c\u0435\u0440\u0430 \u0432 \u043a\u043d\u0438\u0433\u0435 \u043f\u0438\u0441\u0430\u043b\u0438 \u0441 \u0434\u0432\u043e\u0439\u043d\u044b\u043c \u0434\u0435\u0444\u0438\u0441\u043e\u043c, \u043a\u0430\u043a\u0438\u0435-\u0442\u043e \u0431\u0435\u0437 \u0434\u0435\u0444\u0438\u0441\u0430 \u0432\u043e\u043e\u0431\u0449\u0435. \u0414\u043e\u043f\u0438\u0441\u0430\u043b fallback \u043f\u043e \u0447\u0438\u0441\u0442\u044b\u043c \u0446\u0438\u0444\u0440\u0430\u043c:def digits_only(value):    s = str(value).strip().upper()    for sep in (&#187; &#171;, &#171;\/&#187;, &#171;\\\\&#187;):        if sep in s:            s = s.split(sep, 1)[0]    digs = &#171;&#187;.join(ch for ch in s if ch.isdigit())    return digs.lstrip(&#171;0&#187;) or digs or None\u041f\u0440\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0435 \u0434\u0435\u043b\u0430\u044e \u0447\u0435\u0442\u044b\u0440\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0438: \u0442\u043e\u0447\u043d\u044b\u0439 \u043c\u0430\u0442\u0447 \u043f\u043e inv_norm, \u043f\u043e\u0442\u043e\u043c \u043f\u043e serial_norm, \u043f\u043e\u0442\u043e\u043c \u043f\u043e inv_digits, \u043f\u043e\u0442\u043e\u043c \u043f\u043e serial_digits. \u042d\u0442\u0438\u043c \u043e\u0431\u0445\u043e\u0434\u043e\u043c \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0441\u043a\u043b\u0435\u0438\u0442\u044c \u0435\u0449\u0451 \u043e\u043a\u043e\u043b\u043e 50 \u0437\u0430\u043f\u0438\u0441\u0435\u0439.\u041a\u043b\u044e\u0447\u0435\u0432\u043e\u0439 \u0431\u0430\u0433, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u044f \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u043b \u043f\u043e\u0437\u0434\u043d\u043e\u0421\u0434\u0435\u043b\u0430\u043b \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u0443\u044e values_match(), \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u0441\u043e\u0432\u043f\u0430\u0434\u0435\u043d\u0438\u0435, \u0435\u0441\u043b\u0438 \u0445\u043e\u0442\u044c \u043a\u0430\u043a\u0430\u044f-\u0442\u043e \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u0444\u043e\u0440\u043c\u0430 \u043f\u0435\u0440\u0435\u0441\u0435\u043a\u0430\u0435\u0442\u0441\u044f. \u041f\u0440\u0438\u043c\u0435\u043d\u0438\u043b \u0434\u043b\u044f \u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u044f \u0438 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0445 \u043d\u043e\u043c\u0435\u0440\u043e\u0432, \u0438 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a\u043e\u0432.\u0418 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u043b, \u0447\u0442\u043e \u0437\u0430\u043f\u0438\u0441\u0438 Z78VBJACB002YJA \u0438 Z78VBZJACB002TLX (\u0440\u0430\u0437\u043d\u044b\u0435 \u0436\u0435\u043b\u0435\u0437\u043a\u0438) \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u043e\u0434\u043d\u043e\u0439.\u0420\u0430\u0437\u0431\u043e\u0440:\u0421\u0435\u0440\u0438\u0439\u043d\u0438\u043a\u0422\u043e\u043b\u044c\u043a\u043e \u0446\u0438\u0444\u0440\u044bZ78VBJACB002YJA78002Z78VBZJACB002TLX78002\u041e\u0431\u0430 \u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u044e\u0442\u0441\u044f \u0432 78002. \u0421\u043e\u0432\u043f\u0430\u043b\u043e.\u0423\u0440\u043e\u043a: \u0434\u043b\u044f \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0445 \u043d\u043e\u043c\u0435\u0440\u043e\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0430 XXXNNN-NNNNNN \u0446\u0438\u0444\u0440\u043e\u0432\u043e\u0439 fallback \u043d\u0443\u0436\u0435\u043d \u2014 \u0438\u043d\u0430\u0447\u0435 \u043d\u0435 \u0441\u0448\u0438\u0442\u044c \u0440\u0430\u0437\u043d\u044b\u0435 \u0444\u043e\u0440\u043c\u0430\u0442\u044b \u0437\u0430\u043f\u0438\u0441\u0438. \u0414\u043b\u044f \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a\u043e\u0432 \u043e\u043d \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0447\u0435\u0441\u043a\u0438 \u0432\u0440\u0435\u0434\u0435\u043d \u2014 \u0432 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a\u0430\u0445 \u0431\u0443\u043a\u0432\u044b \u0437\u043d\u0430\u0447\u0438\u043c\u044b.\u041f\u0440\u0438\u0448\u043b\u043e\u0441\u044c \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u044c \u043b\u043e\u0433\u0438\u043a\u0443:def inv_match(a, b):    &#171;&#187;&#187;\u0414\u043b\u044f \u0438\u043d\u0432.\u043d\u043e\u043c\u0435\u0440\u043e\u0432: \u043d\u043e\u0440\u043c + digit-fallback.&#187;&#187;&#187;    ka = all_keys(a, with_digits=True)    kb = all_keys(b, with_digits=True)    return bool(ka and kb and (ka &amp; kb)) def serial_match(a, b):    &#171;&#187;&#187;\u0414\u043b\u044f \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a\u043e\u0432: \u0422\u041e\u041b\u042c\u041a\u041e \u043f\u043e\u043b\u043d\u0430\u044f alphanumeric \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f.&#187;&#187;&#187;    ka = all_keys(a, with_digits=False)    kb = all_keys(b, with_digits=False)    return bool(ka and kb and (ka &amp; kb))\u041e\u0434\u0438\u043d \u0438\u0437 \u043b\u0443\u0447\u0448\u0438\u0445 \u043f\u0440\u0438\u043c\u0435\u0440\u043e\u0432 \u0442\u043e\u0433\u043e, \u043a\u0430\u043a \u043f\u043e\u0445\u043e\u0436\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u043c\u0435\u044e\u0442 \u0440\u0430\u0437\u043d\u044b\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430, \u0438 \u043d\u0435 \u0441\u0442\u043e\u0438\u0442 \u043f\u0438\u0441\u0430\u0442\u044c \u00ab\u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u044b\u0439\u00bb \u0445\u0435\u043b\u043f\u0435\u0440, \u0435\u0441\u043b\u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u043d\u0435\u043e\u0434\u043d\u043e\u0440\u043e\u0434\u043d\u044b\u0435.\u0411\u043e\u043b\u044c \u21163: \u00ab\u0433\u043b\u0430\u0432\u043d\u0430\u044f \u0442\u0430\u0431\u043b\u0438\u0446\u0430 \u2014 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0430\u00bb\u0412 \u043f\u0435\u0440\u0432\u043e\u043d\u0430\u0447\u0430\u043b\u044c\u043d\u043e\u043c \u0422\u0417 \u044f \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0438\u043b: \u043a\u043d\u0438\u0433\u0438 \u041c\u041e\u041b\u043e\u0432 \u2014 \u044d\u0442\u043e \u0434\u0435\u0442\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0438, \u043f\u043b\u044e\u0441 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u0440\u043e\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0432 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0435 \u043d\u0435\u0442. \u0420\u0435\u0448\u0435\u043d\u0438\u0435 \u043e\u0447\u0435\u0432\u0438\u0434\u043d\u043e\u0435: \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0438 \u0442\u043e, \u0438 \u0434\u0440\u0443\u0433\u043e\u0435, \u0441\u043c\u0430\u0442\u0447\u0438\u0442\u044c \u043f\u043e \u043a\u043b\u044e\u0447\u0430\u043c, \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u043e\u0435 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u043c\u0438 \u0437\u0430\u043f\u0438\u0441\u044f\u043c\u0438.\u041f\u043e\u0441\u043b\u0435 \u043f\u0435\u0440\u0432\u043e\u0433\u043e \u043f\u0440\u043e\u0433\u043e\u043d\u0430:Master \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0430: 4792 \u0430\u043a\u0442\u0438\u0432\u0430\u041a\u043d\u0438\u0433\u0438 \u041c\u041e\u041b\u043e\u0432: 432 \u0441\u0442\u0440\u043e\u043a\u0438\u0421\u043c\u0430\u0442\u0447\u0435\u043d\u043e: 305\u0421\u043e\u0437\u0434\u0430\u043d\u043e \u043d\u043e\u0432\u044b\u0445 \u0438\u0437 \u043a\u043d\u0438\u0433: 113 \u041f\u043e\u043a\u0430\u0437\u0430\u043b \u0437\u0430\u043a\u0430\u0437\u0447\u0438\u0446\u0435. \u0420\u0435\u0430\u043a\u0446\u0438\u044f: \u00ab\u0417\u0430\u0447\u0435\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0445? \u0413\u043b\u0430\u0432\u043d\u0430\u044f \u0442\u0430\u0431\u043b\u0438\u0446\u0430 \u2014 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0430. \u0415\u0441\u043b\u0438 \u0432 \u043a\u043d\u0438\u0433\u0435 \u041c\u041e\u041b\u0430 \u0447\u0442\u043e-\u0442\u043e \u0435\u0441\u0442\u044c, \u0430 \u0432 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0435 \u043d\u0435\u0442 \u2014 \u044d\u0442\u043e \u043f\u0440\u043e\u0441\u0442\u043e \u043e\u0448\u0438\u0431\u043a\u0430 \u0432 \u043a\u043d\u0438\u0433\u0435, \u043d\u0430\u0434\u043e \u0432 \u043e\u0442\u0447\u0451\u0442, \u0430 \u043d\u0435 \u0432 \u0411\u0414\u00bb.\u041f\u0435\u0440\u0435\u043f\u0438\u0441\u044b\u0432\u0430\u044e \u043b\u043e\u0433\u0438\u043a\u0443. \u0422\u0435\u043f\u0435\u0440\u044c \u043a\u043d\u0438\u0433\u0438 \u2014 \u0442\u043e\u043b\u044c\u043a\u043e \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u043e\u0431\u043e\u0433\u0430\u0449\u0435\u043d\u0438\u044f. \u041e\u043d\u0438 \u043f\u0440\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u044e\u0442 \u041c\u041e\u041b, \u0444\u0438\u043b\u0438\u0430\u043b, \u0437\u0430\u043f\u043e\u043b\u043d\u044f\u044e\u0442 book_* \u043f\u043e\u043b\u044f. \u0415\u0441\u043b\u0438 \u0441\u0442\u0440\u043e\u043a\u0430 \u0438\u0437 \u043a\u043d\u0438\u0433\u0438 \u043d\u0435 \u043d\u0430\u0445\u043e\u0434\u0438\u0442 \u043f\u0430\u0440\u044b \u0432 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0435 \u2014 \u043e\u043d\u0430 \u043f\u0438\u0448\u0435\u0442\u0441\u044f \u0432 reconciliation_issue \u0441\u043e \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u043c only_in_book \u0434\u043b\u044f \u043e\u0442\u0447\u0451\u0442\u0430, \u043d\u043e \u0432 \u0411\u0414 \u0430\u043a\u0442\u0438\u0432\u043e\u0432 \u0435\u0451 \u043d\u0435\u0442.def import_book(db, xls_path):    rows = parse_book(xls_path)    run = ReconciliationRun(started_at=datetime.utcnow())    db.add(run)    db.flush()     for r in rows:        asset = find_match(db, r)  # 4 \u043f\u043e\u043f\u044b\u0442\u043a\u0438 \u043c\u0430\u0442\u0447\u0430        if asset is None:            db.add(ReconciliationIssue(                run_id=run.id, kind=&#187;only_in_book&#187;,                inv_number=r[&#171;inv_number&#187;],                serial_number=r[&#171;serial_number&#187;],                description=&#187;\u0415\u0441\u0442\u044c \u0432 \u043a\u043d\u0438\u0433\u0435, \u043d\u0435\u0442 \u0432 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0435&#187;,            ))            continue        # \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c book_* \u043f\u043e\u043b\u044f        asset.book_inv_number = r[&#171;inv_number&#187;]        asset.book_serial_number = r[&#171;serial_number&#187;]        asset.book_row_no = r[&#171;book_row_no&#187;]        asset.mol_id = mol.id        # &#8230;\u0423\u0440\u043e\u043a: \u0435\u0441\u043b\u0438 \u0432\u0430\u0448 \u043f\u0440\u043e\u0434\u0443\u043a\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0441 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u043c \u0438\u0441\u0442\u0438\u043d\u044b \u2014 \u043d\u0435 \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u043f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u044c\u043d\u0443\u044e \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c. \u041b\u0443\u0447\u0448\u0435 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u043e\u0442\u0447\u0451\u0442-\u0436\u0443\u0440\u043d\u0430\u043b \u0442\u043e\u0433\u043e, \u0447\u0442\u043e \u043e\u0442\u043a\u043b\u043e\u043d\u044f\u0435\u0442\u0441\u044f \u043e\u0442 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u0430.\u0411\u043e\u043b\u044c \u21164: \u0437\u0430\u0434\u0432\u043e\u0435\u043d\u0438\u0435 \u043e\u0431\u043e\u0440\u0443\u0434\u043e\u0432\u0430\u043d\u0438\u044f\u0412 \u043f\u0440\u0438\u043c\u0435\u0447\u0430\u043d\u0438\u044f\u0445 \u043a \u0430\u043a\u0442\u0438\u0432\u0430\u043c \u0432 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0435 \u0432\u0441\u0442\u0440\u0435\u0447\u0430\u044e\u0442\u0441\u044f \u0437\u0430\u043f\u0438\u0441\u0438 \u0432\u0438\u0434\u0430:\u0424\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u0437\u0430\u0434\u0432\u043e\u0435\u043d\u0438\u0435 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u0438\u043a\u0430 (\u043a\u0430\u0431. 105)\u0437\u0430\u043f\u0438\u0441\u0430\u043d \u043a\u0430\u043a 002410124-003000, \u043d\u043e \u0434\u0443\u0431\u043b\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0441 DQVRZER138017016AA3000002410124-003740 \u0437\u0430\u0434\u0432\u043e\u0435\u043d \u0441 RBU0X16179 \u0422\u043e \u0435\u0441\u0442\u044c \u0432 \u0443\u0447\u0451\u0442\u0435 \u0443 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0438\u0438 \u043e\u0434\u043d\u0430 \u0438 \u0442\u0430 \u0436\u0435 \u0444\u0438\u0437\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0436\u0435\u043b\u0435\u0437\u043a\u0430 \u0438\u043d\u043e\u0433\u0434\u0430 \u0447\u0438\u0441\u043b\u0438\u0442\u0441\u044f \u0434\u0432\u0430\u0436\u0434\u044b \u043f\u043e\u0434 \u0440\u0430\u0437\u043d\u044b\u043c\u0438 \u043d\u043e\u043c\u0435\u0440\u0430\u043c\u0438. \u0421\u0434\u0435\u043b\u0430\u043b \u0434\u0435\u0442\u0435\u043a\u0442\u043e\u0440:def is_duplicated(notes):    if not notes:        return False    return any(k in notes.lower() for k in (&#171;\u0437\u0430\u0434\u0432\u043e&#187;, &#171;\u0434\u0443\u0431\u043b&#187;, &#171;\u043f\u043e\u0432\u0442\u043e\u0440&#187;)) def extract_candidates(notes):    &#171;&#187;&#187;\u0418\u0437\u0432\u043b\u0435\u043a\u0430\u0435\u0442 \u043f\u043e\u0445\u043e\u0436\u0438\u0435 \u043d\u0430 \u0438\u043d\u0432.\u2116 \u0438 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a\u0438 \u0441\u0442\u0440\u043e\u043a\u0438 \u0438\u0437 \u043f\u0440\u0438\u043c\u0435\u0447\u0430\u043d\u0438\u044f.&#187;&#187;&#187;    out = set()    for m in re.finditer(r&#187;\\b\\d{5,9}-_?\\d+\\b&#187;, notes):        out.add(m.group())    for m in re.finditer(r&#187;\\b[A-Z][A-Z0-9]{5,}\\b&#187;, notes, re.I):        out.add(m.group())    return list(out)\u0412 \u043a\u0430\u0440\u0442\u043e\u0447\u043a\u0435 \u043f\u043e\u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043a\u0440\u0430\u0441\u043d\u044b\u0439 \u0431\u0430\u043d\u043d\u0435\u0440 \u00ab\u0417\u0430\u0434\u0432\u043e\u0435\u043d\u0438\u0435\u00bb \u0441\u043e \u0441\u0441\u044b\u043b\u043a\u0430\u043c\u0438 \u043d\u0430 \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u043f\u0430\u0440\u043d\u044b\u0435 \u0430\u043a\u0442\u0438\u0432\u044b.\u041f\u043e\u0442\u043e\u043c \u0437\u0430\u043a\u0430\u0437\u0447\u0438\u0446\u0430 \u043f\u043e\u043a\u0430\u0437\u0430\u043b\u0430 \u043f\u0440\u0438\u043c\u0435\u0440: \u0443 \u0430\u043a\u0442\u0438\u0432\u0430 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u0412\u044b\u0433\u0440\u0443\u0437\u043a\u0438 002410134-002722, \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a PHCLK09100. \u0410 \u0432 \u043a\u043d\u0438\u0433\u0435 \u041c\u041e\u041b\u0430 \u0442\u043e\u0442 \u0436\u0435 \u0438\u043d\u0432\u0435\u043d\u0442\u0430\u0440\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440, \u043d\u043e \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u043a PHCL628191. \u0422\u043e \u0435\u0441\u0442\u044c \u044d\u0442\u043e \u0440\u0430\u0437\u043d\u044b\u0435 \u0436\u0435\u043b\u0435\u0437\u043a\u0438 \u043f\u043e\u0434 \u043e\u0434\u043d\u0438\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c. \u0422\u0435\u043a\u0441\u0442\u043e\u0432\u043e\u0433\u043e \u00ab\u0437\u0430\u0434\u0432\u043e\u0435\u043d\u00bb \u0432 \u043f\u0440\u0438\u043c\u0435\u0447\u0430\u043d\u0438\u0438 \u043d\u0435\u0442, \u043d\u043e \u0444\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u044d\u0442\u043e \u0438 \u0435\u0441\u0442\u044c \u0437\u0430\u0434\u0432\u043e\u0435\u043d\u0438\u0435.\u0414\u043e\u043f\u0438\u0441\u0430\u043b \u0434\u0435\u0442\u0435\u043a\u0442\u043e\u0440 \u00ab\u043c\u043e\u043b\u0447\u0430\u043b\u0438\u0432\u043e\u0433\u043e\u00bb \u0437\u0430\u0434\u0432\u043e\u0435\u043d\u0438\u044f: \u0438\u0449\u0435\u043c \u0432 \u0411\u0414 \u0434\u0440\u0443\u0433\u0438\u0435 \u0430\u043a\u0442\u0438\u0432\u044b, \u0443 \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0445\u043e\u0442\u044c \u043a\u0430\u043a\u043e\u0439-\u0442\u043e \u043a\u043b\u044e\u0447 (inv_norm, serial_norm, book_inv_digits, book_serial_digits) \u043f\u0435\u0440\u0435\u0441\u0435\u043a\u0430\u0435\u0442\u0441\u044f \u0441 \u0442\u0435\u043a\u0443\u0449\u0438\u043c:def find_duplicates_by_data(db, asset):    &#171;&#187;&#187;\u0410\u043a\u0442\u0438\u0432\u044b \u0441 \u043e\u0431\u0449\u0438\u043c\u0438 \u043a\u043b\u044e\u0447\u0430\u043c\u0438 \u2014 \u043f\u043e\u0442\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0435 \u0437\u0430\u0434\u0432\u043e\u0435\u043d\u0438\u044f.&#187;&#187;&#187;    pairs = {}    for column, value, reason in build_checks(asset):        for other in db.query(Asset).filter(            Asset.id != asset.id, column == value        ).limit(10):            pairs.setdefault(other.id, (other, reason))    return list(pairs.values())\u0412 \u0431\u0430\u0437\u0435 \u0438\u0437 4792 \u0430\u043a\u0442\u0438\u0432\u043e\u0432 747 \u0438\u043c\u0435\u044e\u0442 \u043f\u0435\u0440\u0435\u0441\u0435\u0447\u0435\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0435\u0439 \u0441 \u0434\u0440\u0443\u0433\u0438\u043c\u0438. \u042d\u0442\u043e \u043c\u043d\u043e\u0433\u043e. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u044d\u0442\u043e \u0447\u0430\u0441\u0442\u044c \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u0438 (\u043e\u0434\u043d\u0430 \u043f\u043e\u0437\u0438\u0446\u0438\u044f \u043f\u0435\u0440\u0435\u0432\u0435\u0434\u0435\u043d\u0430 \u0441 \u043e\u0434\u043d\u043e\u0433\u043e \u0431\u0430\u043b\u0430\u043d\u0441\u0430 \u043d\u0430 \u0434\u0440\u0443\u0433\u043e\u0439, \u043d\u043e \u043d\u0435 \u0441\u043f\u0438\u0441\u0430\u043d\u0430 \u0441\u043e \u0441\u0442\u0430\u0440\u043e\u0433\u043e), \u043d\u043e \u0432 \u043b\u044e\u0431\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u043e\u0431 \u044d\u0442\u043e\u043c&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-481191","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/481191","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=481191"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/481191\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=481191"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=481191"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=481191"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}